all repos — erl @ 5b2e1e8a6dcdc5b7201d16efb9190ff49ed39564

Execute Reload Loop

fix handling of restart events

Alan Pearce
commit

5b2e1e8a6dcdc5b7201d16efb9190ff49ed39564

parent

d29d0816680a343ee47c40fa9ace8d80862ac412

3 files changed, 96 insertions(+), 34 deletions(-)

changed files
M main.gomain.go
@@ -39,7 +39,7 @@ sm.SendEvent(state.Start)
for { select { case <-ctx.Done(): - sm.SendEvent(state.Signal) + sm.SendEvent(state.Shutdown) return case event, ok := <-events:
@@ -99,7 +99,7 @@
<-ctx.Done() log.Println("shutting down") - sm.SendEvent(state.Signal) + sm.SendEvent(state.Shutdown) wg.Wait() }
M state/state.gostate/state.go
@@ -11,6 +11,7 @@
const ( NotStarted State = iota Running + Stopping Exited )
@@ -20,6 +21,8 @@ case NotStarted:
return "NotStarted" case Running: return "Running" + case Stopping: + return "Stopping" case Exited: return "Exited" default:
@@ -31,7 +34,7 @@ type Event int
const ( Start Event = iota - Signal + Shutdown Exit Restart )
@@ -40,7 +43,7 @@ func (e Event) String() string {
switch e { case Start: return "Start" - case Signal: + case Shutdown: return "Shutdown" case Exit: return "Stopped"
@@ -72,23 +75,31 @@ sm.transitions[NotStarted] = map[Event]State{
Start: Running, } sm.transitions[Running] = map[Event]State{ - Signal: Exited, - Exit: Exited, - Restart: Running, + Exit: Exited, + Shutdown: Stopping, + Restart: Stopping, + } + sm.transitions[Stopping] = map[Event]State{ + Shutdown: Exited, + Exit: Exited, } sm.transitions[Exited] = map[Event]State{ Start: Running, Restart: Running, } + var waiting chan struct{} wait := func(cmd command.Command) { err := cmd.Wait() if err != nil { log.Printf("Error waiting for command: %v\n", err) } sm.SendEvent(Exit) + + close(waiting) } start := func() { + waiting = make(chan struct{}, 1) err := cmd.Start() if err != nil { log.Printf("Failed to start command: %v", err)
@@ -104,15 +115,16 @@ }
} restart := func() { stop() - start() + <-waiting + sm.SendEvent(Restart) } sm.actions[NotStarted] = map[Event]Action{ Start: start, } sm.actions[Running] = map[Event]Action{ - Signal: stop, - Restart: restart, + Shutdown: stop, + Restart: restart, } sm.actions[Exited] = map[Event]Action{ Start: start,
M state/state_test.gostate/state_test.go
@@ -53,11 +53,16 @@ m.mu.Lock()
defer m.mu.Unlock() m.stopCalled = true m.started = false - // Signal Wait to return - select { - case m.waitChan <- struct{}{}: - default: - } + + // Add small delay to allow tests to observe Stopping state + go func() { + time.Sleep(20 * time.Millisecond) + // Signal Wait to return + select { + case m.waitChan <- struct{}{}: + default: + } + }() return m.stopErr }
@@ -122,9 +127,22 @@ m.startErr = nil
m.waitErr = nil m.stopErr = nil m.started = false + // Signal any waiting goroutines before creating new channel + select { + case m.waitChan <- struct{}{}: + default: + } m.waitChan = make(chan struct{}) } +func (m *MockCommand) ClearCallCounts() { + m.mu.Lock() + defer m.mu.Unlock() + m.startCalled = false + m.stopCalled = false + m.waitCalled = false +} + func createTestStateMachine() (*StateMachine, *MockCommand, *bytes.Buffer) { mockCmd := NewMockCommand() var buf bytes.Buffer
@@ -144,6 +162,7 @@ expected string
}{ {NotStarted, "NotStarted"}, {Running, "Running"}, + {Stopping, "Stopping"}, {Exited, "Exited"}, {State(99), "Unknown"}, }
@@ -163,7 +182,7 @@ event Event
expected string }{ {Start, "Start"}, - {Signal, "Shutdown"}, + {Shutdown, "Shutdown"}, {Exit, "Stopped"}, {Restart, "Restart"}, {Event(99), "Unknown"},
@@ -210,7 +229,7 @@ t.Error("Wait should have been called on command")
} } -func TestSignalTransition(t *testing.T) { +func TestShutdownTransition(t *testing.T) { t.Parallel() sm, mockCmd, _ := createTestStateMachine()
@@ -219,20 +238,31 @@ // Start the state machine
sm.SendEvent(Start) time.Sleep(10 * time.Millisecond) - // Reset mock to clear start calls - mockCmd.Reset() + // Clear call counts to track shutdown calls + mockCmd.ClearCallCounts() - // Send Signal event - sm.SendEvent(Signal) + // Send Shutdown event + sm.SendEvent(Shutdown) time.Sleep(10 * time.Millisecond) - if sm.currentState != Exited { - t.Errorf("State should be Exited after Signal event from Running, got %v", sm.currentState) + if sm.currentState != Stopping { + t.Errorf( + "State should be Stopping after Shutdown event from Running, got %v", + sm.currentState, + ) } if !mockCmd.WasStopCalled() { t.Error("Stop should have been called on command") } + + // Simulate command exit to complete transition to Exited + mockCmd.SimulateExit() + time.Sleep(10 * time.Millisecond) + + if sm.currentState != Exited { + t.Errorf("State should be Exited after command exit, got %v", sm.currentState) + } } func TestExitTransition(t *testing.T) {
@@ -262,15 +292,15 @@ // Start the state machine
sm.SendEvent(Start) time.Sleep(10 * time.Millisecond) - // Reset mock to clear initial calls - mockCmd.Reset() + // Clear call counts to track restart calls + mockCmd.ClearCallCounts() // Send Restart event sm.SendEvent(Restart) - time.Sleep(10 * time.Millisecond) + time.Sleep(50 * time.Millisecond) // Give more time for restart sequence if sm.currentState != Running { - t.Errorf("State should still be Running after Restart event, got %v", sm.currentState) + t.Errorf("State should be Running after Restart event, got %v", sm.currentState) } if !mockCmd.WasStopCalled() {
@@ -346,10 +376,12 @@ initialState State
event Event description string }{ - {NotStarted, Signal, "Signal from NotStarted"}, + {NotStarted, Shutdown, "Shutdown from NotStarted"}, {NotStarted, Exit, "Exit from NotStarted"}, {NotStarted, Restart, "Restart from NotStarted"}, - {Exited, Signal, "Signal from Exited"}, + {Stopping, Start, "Start from Stopping"}, + {Stopping, Restart, "Restart from Stopping"}, + {Exited, Shutdown, "Shutdown from Exited"}, {Exited, Exit, "Exit from Exited"}, }
@@ -411,11 +443,22 @@ // Set stop to return an error
mockCmd.SetStopError(fmt.Errorf("stop error")) buf.Reset() - sm.SendEvent(Signal) + // Clear call counts to track stop calls + mockCmd.ClearCallCounts() + + sm.SendEvent(Shutdown) + time.Sleep(10 * time.Millisecond) + + if sm.currentState != Stopping { + t.Errorf("State should be Stopping even if Stop fails, got %v", sm.currentState) + } + + // Complete the transition by simulating command exit + mockCmd.SimulateExit() time.Sleep(10 * time.Millisecond) if sm.currentState != Exited { - t.Errorf("State should be Exited even if Stop fails, got %v", sm.currentState) + t.Errorf("State should be Exited after command exit, got %v", sm.currentState) } logOutput := buf.String()
@@ -459,15 +502,22 @@ if sm.currentState != Running {
t.Errorf("Expected Running, got %v", sm.currentState) } - // Signal to stop - sm.SendEvent(Signal) + // Shutdown to stop + sm.SendEvent(Shutdown) + time.Sleep(10 * time.Millisecond) + if sm.currentState != Stopping { + t.Errorf("Expected Stopping, got %v", sm.currentState) + } + + // Complete shutdown by simulating command exit + mockCmd.SimulateExit() time.Sleep(10 * time.Millisecond) if sm.currentState != Exited { t.Errorf("Expected Exited, got %v", sm.currentState) } // Start again - mockCmd.Reset() + mockCmd.ClearCallCounts() sm.SendEvent(Start) time.Sleep(10 * time.Millisecond) if sm.currentState != Running {