-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlifecycle.go
More file actions
282 lines (223 loc) · 7.36 KB
/
lifecycle.go
File metadata and controls
282 lines (223 loc) · 7.36 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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
package forge
import (
"context"
"fmt"
"sync"
"github.com/xraph/forge/errors"
)
// LifecyclePhase represents a phase in the application lifecycle.
type LifecyclePhase string
const (
// PhaseBeforeStart is called before the app starts (before extensions register).
PhaseBeforeStart LifecyclePhase = "before_start"
// PhaseAfterRegister is called after extensions register but before they start.
PhaseAfterRegister LifecyclePhase = "after_register"
// PhaseAfterStart is called after the app starts (after all extensions start).
PhaseAfterStart LifecyclePhase = "after_start"
// PhaseBeforeRun is called before the HTTP server starts listening.
PhaseBeforeRun LifecyclePhase = "before_run"
// PhaseAfterRun is called after the HTTP server starts (in a goroutine, non-blocking).
PhaseAfterRun LifecyclePhase = "after_run"
// PhaseBeforeStop is called before the app stops (before graceful shutdown).
PhaseBeforeStop LifecyclePhase = "before_stop"
// PhaseAfterStop is called after the app stops (after all extensions stop).
PhaseAfterStop LifecyclePhase = "after_stop"
)
// LifecycleHook is a function called during a lifecycle phase
// Hooks receive the App instance and a context for cancellation.
type LifecycleHook func(ctx context.Context, app App) error
// LifecycleHookOptions configures a lifecycle hook.
type LifecycleHookOptions struct {
// Name is a unique identifier for this hook (for logging/debugging)
Name string
// Priority determines execution order (higher priority runs first)
// Default: 0
Priority int
// ContinueOnError determines if subsequent hooks run if this hook fails
// Default: false (stop on error)
ContinueOnError bool
}
// DefaultLifecycleHookOptions returns default hook options.
func DefaultLifecycleHookOptions(name string) LifecycleHookOptions {
return LifecycleHookOptions{
Name: name,
Priority: 0,
ContinueOnError: false,
}
}
// lifecycleHookEntry wraps a hook with its options.
type lifecycleHookEntry struct {
hook LifecycleHook
opts LifecycleHookOptions
}
// LifecycleManager manages lifecycle hooks.
type LifecycleManager interface {
// RegisterHook registers a hook for a specific lifecycle phase
RegisterHook(phase LifecyclePhase, hook LifecycleHook, opts LifecycleHookOptions) error
// RegisterHookFn is a convenience method to register a hook with default options
RegisterHookFn(phase LifecyclePhase, name string, hook LifecycleHook) error
// ExecuteHooks executes all hooks for a given phase
ExecuteHooks(ctx context.Context, phase LifecyclePhase, app App) error
// GetHooks returns all hooks for a given phase (for inspection)
GetHooks(phase LifecyclePhase) []LifecycleHookOptions
// RemoveHook removes a hook by name
RemoveHook(phase LifecyclePhase, name string) error
// ClearHooks removes all hooks for a given phase
ClearHooks(phase LifecyclePhase)
}
// lifecycleManager implements LifecycleManager.
type lifecycleManager struct {
mu sync.RWMutex
hooks map[LifecyclePhase][]lifecycleHookEntry
logger Logger
}
// NewLifecycleManager creates a new lifecycle manager.
func NewLifecycleManager(logger Logger) LifecycleManager {
if logger == nil {
logger = NewNoopLogger()
}
return &lifecycleManager{
hooks: make(map[LifecyclePhase][]lifecycleHookEntry),
logger: logger,
}
}
// RegisterHook registers a hook for a specific lifecycle phase.
func (m *lifecycleManager) RegisterHook(phase LifecyclePhase, hook LifecycleHook, opts LifecycleHookOptions) error {
if hook == nil {
return errors.New("hook cannot be nil")
}
if opts.Name == "" {
return errors.New("hook name is required")
}
m.mu.Lock()
defer m.mu.Unlock()
// Check for duplicate name in this phase
for _, entry := range m.hooks[phase] {
if entry.opts.Name == opts.Name {
return fmt.Errorf("hook with name %s already registered for phase %s", opts.Name, phase)
}
}
// Add hook
m.hooks[phase] = append(m.hooks[phase], lifecycleHookEntry{
hook: hook,
opts: opts,
})
// Sort by priority (higher priority first)
m.sortHooks(phase)
m.logger.Debug("lifecycle hook registered",
F("phase", string(phase)),
F("name", opts.Name),
F("priority", opts.Priority),
)
return nil
}
// RegisterHookFn is a convenience method to register a hook with default options.
func (m *lifecycleManager) RegisterHookFn(phase LifecyclePhase, name string, hook LifecycleHook) error {
return m.RegisterHook(phase, hook, DefaultLifecycleHookOptions(name))
}
// ExecuteHooks executes all hooks for a given phase.
func (m *lifecycleManager) ExecuteHooks(ctx context.Context, phase LifecyclePhase, app App) error {
m.mu.RLock()
hooks := m.hooks[phase]
m.mu.RUnlock()
if len(hooks) == 0 {
return nil
}
m.logger.Debug("executing lifecycle hooks",
F("phase", string(phase)),
F("count", len(hooks)),
)
var firstError error
executedCount := 0
for _, entry := range hooks {
m.logger.Debug("executing lifecycle hook",
F("phase", string(phase)),
F("name", entry.opts.Name),
F("priority", entry.opts.Priority),
)
if err := entry.hook(ctx, app); err != nil {
m.logger.Error("lifecycle hook failed",
F("phase", string(phase)),
F("name", entry.opts.Name),
F("error", err),
)
if firstError == nil {
firstError = fmt.Errorf("hook %s failed: %w", entry.opts.Name, err)
}
// Stop execution unless ContinueOnError is true
if !entry.opts.ContinueOnError {
m.logger.Warn("stopping hook execution due to error",
F("phase", string(phase)),
F("failed_hook", entry.opts.Name),
F("executed", executedCount),
F("remaining", len(hooks)-executedCount-1),
)
return firstError
}
} else {
m.logger.Debug("lifecycle hook completed",
F("phase", string(phase)),
F("name", entry.opts.Name),
)
executedCount++
}
}
m.logger.Debug("lifecycle hooks completed",
F("phase", string(phase)),
F("executed", executedCount),
F("total", len(hooks)),
)
return firstError
}
// GetHooks returns all hooks for a given phase (for inspection).
func (m *lifecycleManager) GetHooks(phase LifecyclePhase) []LifecycleHookOptions {
m.mu.RLock()
defer m.mu.RUnlock()
hooks := m.hooks[phase]
result := make([]LifecycleHookOptions, len(hooks))
for i, entry := range hooks {
result[i] = entry.opts
}
return result
}
// RemoveHook removes a hook by name.
func (m *lifecycleManager) RemoveHook(phase LifecyclePhase, name string) error {
m.mu.Lock()
defer m.mu.Unlock()
hooks := m.hooks[phase]
for i, entry := range hooks {
if entry.opts.Name == name {
// Remove hook by slicing
m.hooks[phase] = append(hooks[:i], hooks[i+1:]...)
m.logger.Debug("lifecycle hook removed",
F("phase", string(phase)),
F("name", name),
)
return nil
}
}
return fmt.Errorf("hook %s not found for phase %s", name, phase)
}
// ClearHooks removes all hooks for a given phase.
func (m *lifecycleManager) ClearHooks(phase LifecyclePhase) {
m.mu.Lock()
defer m.mu.Unlock()
count := len(m.hooks[phase])
m.hooks[phase] = nil
m.logger.Debug("lifecycle hooks cleared",
F("phase", string(phase)),
F("count", count),
)
}
// sortHooks sorts hooks by priority (higher priority first).
func (m *lifecycleManager) sortHooks(phase LifecyclePhase) {
hooks := m.hooks[phase]
// Simple bubble sort (sufficient for small lists)
for i := range hooks {
for j := i + 1; j < len(hooks); j++ {
if hooks[j].opts.Priority > hooks[i].opts.Priority {
hooks[i], hooks[j] = hooks[j], hooks[i]
}
}
}
}