Skip to content

Commit effb852

Browse files
Add error/panic handling for the Startup of components
1 parent f63dc01 commit effb852

File tree

2 files changed

+71
-7
lines changed

2 files changed

+71
-7
lines changed

manager.go

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ type Manager struct {
3030
setupTimeout time.Duration
3131
closeTimeout time.Duration
3232
lifetime TerminationSignal
33+
34+
exitSignal chan syscall.Signal
3335
}
3436

3537
func NewManager(options ...managerOption) *Manager {
@@ -43,6 +45,7 @@ func NewManager(options ...managerOption) *Manager {
4345
setupTimeout: ops.setupTimeout,
4446
closeTimeout: ops.closeTimeout,
4547
lifetime: ops.lifetime,
48+
exitSignal: make(chan syscall.Signal, 1),
4649
}
4750
}
4851

@@ -63,7 +66,7 @@ func (m *Manager) Run() syscall.Signal {
6366

6467
m.startComponents()
6568

66-
signal := m.lifetime() // Wait for the exit signal
69+
signal := m.waitForSignal() // Wait for the exit signal
6770

6871
err = m.closeComponents()
6972
if errors.Is(err, errTimeout) {
@@ -95,27 +98,38 @@ func (m *Manager) setupComponents() error {
9598
return nil
9699
}
97100

98-
// TODO: startComponents could definitely use some more love
99-
// It would be great for a way for start methods to signal the manager that a start errored, this way we can make sure we close back down gracefully..
100-
// Maybe something can be done with error groups?
101-
//
102-
// TODO: We should probably also think in some panic handling... Maybe that goes for Setup and Close aswell
103101
func (m *Manager) startComponents() {
104102
for _, s := range m.components {
105103
startable, ok := s.Component.(startable)
106104
if ok {
107105
m.logger.Info(fmt.Sprintf("[UnixCycle] Starting component %q", s.name), slog.String("component_name", s.name))
108106
go func() {
107+
defer func() {
108+
if r := recover(); r != nil {
109+
m.logger.Error(fmt.Sprintf("[UnixCycle] Panic during start for component %q: %v", s.name, r), slog.String("component_name", s.name))
110+
m.exitSignal <- syscall.SIGABRT
111+
}
112+
}()
109113
err := startable.Start() // Blocking for go routine
110114
if err != nil {
111-
//TODO: We need to signal the manager somehow that a start failed...
112115
m.logger.Error(fmt.Sprintf("[UnixCycle] Failure during start for component %q: %v", s.name, err), slog.String("component_name", s.name))
116+
m.exitSignal <- syscall.SIGABRT
113117
}
114118
}()
115119
}
116120
}
117121
}
118122

123+
func (m *Manager) waitForSignal() syscall.Signal {
124+
go func() {
125+
m.exitSignal <- m.lifetime()
126+
}()
127+
128+
signal := <-m.exitSignal
129+
m.logger.Info(fmt.Sprintf("[UnixCycle] Received signal: %v", signal), slog.String("signal", signal.String()))
130+
return signal
131+
}
132+
119133
func (m *Manager) closeComponents() error {
120134
for _, s := range slices.Backward(m.components) {
121135
closable, ok := s.Component.(closable)

manager_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,56 @@ func TestManager(t *testing.T) {
121121
assert.Equal(t, uint32(0), calledCount.Load())
122122
assert.Equal(t, syscall.SIGALRM, got)
123123
})
124+
125+
t.Run("should close back down when a start function errors", func(t *testing.T) {
126+
var (
127+
m, _ = newManager()
128+
startCalled = false
129+
closedCalled = false
130+
closeable = func() error {
131+
closedCalled = true
132+
return nil
133+
}
134+
startable = func() error {
135+
startCalled = true
136+
return assert.AnError
137+
}
138+
sut = m.
139+
Add("startable func", unixcycle.Starter(startable)).
140+
Add("closable func", unixcycle.Closer(closeable))
141+
)
142+
143+
got := sut.Run()
144+
145+
assert.Equal(t, startCalled, true, "startable func should have been called")
146+
assert.Equal(t, closedCalled, true, "closable func should have been called")
147+
assert.Equal(t, syscall.SIGABRT, got)
148+
})
149+
150+
t.Run("should close back down when a start function panics", func(t *testing.T) {
151+
var (
152+
m, _ = newManager()
153+
startCalled = false
154+
closedCalled = false
155+
closeable = func() error {
156+
closedCalled = true
157+
return nil
158+
}
159+
startable = func() error {
160+
startCalled = true
161+
panic("panic")
162+
}
163+
sut = m.
164+
Add("startable func", unixcycle.Starter(startable)).
165+
Add("closable func", unixcycle.Closer(closeable))
166+
)
167+
168+
got := sut.Run()
169+
170+
assert.Equal(t, startCalled, true, "startable func should have been called")
171+
assert.Equal(t, closedCalled, true, "closable func should have been called")
172+
assert.Equal(t, syscall.SIGABRT, got)
173+
})
124174
}
125175

126176
type testComponent struct {

0 commit comments

Comments
 (0)