-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathplugin.go
More file actions
170 lines (152 loc) · 4.09 KB
/
Copy pathplugin.go
File metadata and controls
170 lines (152 loc) · 4.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
package main
import (
"context"
"sync"
"time"
)
type SyncOp string
const (
SyncOpCreated SyncOp = "created"
SyncOpUpdated SyncOp = "updated"
)
// SyncEvent is emitted after a successful target-LDAP write. Plugins receive
// it via Registry.Dispatch and may take side effects (e.g. PVC creation).
// Content is the resolved entry written to target LDAP. Plugins must treat
// it as read-only — the same map may be passed to multiple plugins.
type SyncEvent struct {
SearchID string
DN string
Content map[string]interface{}
Op SyncOp
Timestamp time.Time
}
// Plugin runs out-of-band side effects in response to successful target-LDAP
// writes. Implementations must be safe for concurrent use: Apply may be
// invoked from many goroutines simultaneously.
type Plugin interface {
Name() string
Match(SyncEvent) bool
Apply(context.Context, SyncEvent) error
}
// PluginRetry controls per-plugin retry behavior in the registry.
type PluginRetry struct {
MaxAttempts int
InitialDelayMs int
MaxDelayMs int
}
func (r PluginRetry) withDefaults() PluginRetry {
if r.MaxAttempts <= 0 {
r.MaxAttempts = 5
}
if r.InitialDelayMs <= 0 {
r.InitialDelayMs = 500
}
if r.MaxDelayMs <= 0 {
r.MaxDelayMs = 60000
}
return r
}
// Registry holds the active plugins and dispatches events to them.
// Dispatch is non-blocking: each plugin runs in its own goroutine with a
// bounded retry loop. Plugin failures are logged but never affect the
// caller (in particular, never block markSyncedAndRelease or LDAP sync).
type Registry struct {
mu sync.RWMutex
plugins []Plugin
retry PluginRetry
wg sync.WaitGroup // tracks in-flight Apply goroutines, used by tests
}
func NewRegistry(retry PluginRetry) *Registry {
return &Registry{retry: retry.withDefaults()}
}
func (r *Registry) Register(p Plugin) {
r.mu.Lock()
defer r.mu.Unlock()
r.plugins = append(r.plugins, p)
}
// Dispatch fans the event out to every matching plugin in its own goroutine.
// Returns immediately. The caller does not need to (and must not) hold any
// dependency-state mutex while invoking this.
func (r *Registry) Dispatch(event SyncEvent) {
r.mu.RLock()
matching := make([]Plugin, 0, len(r.plugins))
for _, p := range r.plugins {
if p.Match(event) {
matching = append(matching, p)
}
}
retry := r.retry
r.mu.RUnlock()
if len(matching) == 0 {
return
}
for _, p := range matching {
p := p
r.wg.Add(1)
go func() {
defer r.wg.Done()
r.runWithRetry(p, event, retry)
}()
}
}
// Wait blocks until all in-flight plugin goroutines complete. Test-only.
func (r *Registry) Wait() {
r.wg.Wait()
}
func (r *Registry) runWithRetry(p Plugin, event SyncEvent, retry PluginRetry) {
delay := time.Duration(retry.InitialDelayMs) * time.Millisecond
maxDelay := time.Duration(retry.MaxDelayMs) * time.Millisecond
for attempt := 1; attempt <= retry.MaxAttempts; attempt++ {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
err := p.Apply(ctx, event)
cancel()
if err == nil {
if logger != nil {
logger.Debug("Plugin applied",
"Plugin", p.Name(),
"DN", event.DN,
"Op", event.Op,
"Attempt", attempt,
)
}
return
}
if logger != nil {
logger.Warn("Plugin apply failed",
"Plugin", p.Name(),
"DN", event.DN,
"Op", event.Op,
"Attempt", attempt,
"MaxAttempts", retry.MaxAttempts,
"Err", err,
)
}
if attempt == retry.MaxAttempts {
if logger != nil {
logger.Error("Plugin apply exhausted retries",
"Plugin", p.Name(),
"DN", event.DN,
"Op", event.Op,
"Err", err,
)
}
return
}
time.Sleep(delay)
delay *= 2
if delay > maxDelay {
delay = maxDelay
}
}
}
// pluginRegistry is the package-level registry consulted by markSyncedAndRelease.
// nil means "no plugins configured" — Dispatch is a no-op.
var pluginRegistry *Registry
// dispatchSyncEvent is the indirection point used by markSyncedAndRelease so
// tests can swap the registry behavior without touching the global.
var dispatchSyncEvent = func(event SyncEvent) {
if pluginRegistry == nil {
return
}
pluginRegistry.Dispatch(event)
}