fix handling of restart events
3 files changed, 96 insertions(+), 34 deletions(-)
changed files
M main.go → main.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.go → state/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.go → state/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 {