diff --git a/internal/state/state.go b/internal/state/state.go index 839c4b5fce..7ff88bd54e 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -215,8 +215,9 @@ func (ts *ThreadState) RequestSafeStateChange(nextState State) bool { } ts.mu.Unlock() - // wait for the state to change to a stable state - ts.WaitFor(Ready, Inactive, Reserved) + // Done too: a thread that ends on its own goes ShuttingDown then Done without + // being stable again, and only Done means its C side is gone + ts.WaitFor(Ready, Inactive, Reserved, Done) return ts.RequestSafeStateChange(nextState) } diff --git a/internal/state/state_test.go b/internal/state/state_test.go index 767a27606e..26863a6d8d 100644 --- a/internal/state/state_test.go +++ b/internal/state/state_test.go @@ -53,6 +53,25 @@ func TestWaitForStateWithTimeoutGivesUpAndDropsItsSubscriber(t *testing.T) { }) } +func TestRequestSafeStateChangeRefusesAThreadThatEndedOnItsOwn(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + threadState := &ThreadState{currentState: TransitionComplete} + + refused := make(chan bool, 1) + go func() { + refused <- threadState.RequestSafeStateChange(ShuttingDown) + }() + + // once the request is parked, the thread ends by itself without passing + // through a stable state, the way a worker that fails to boot does + synctest.Wait() + threadState.Set(ShuttingDown) + threadState.Set(Done) + + assert.False(t, <-refused, "a thread that is already done cannot be asked to shut down") + }) +} + func assertNumberOfSubscribers(t *testing.T, threadState *ThreadState, expected int) { t.Helper() diff --git a/testdata/worker-reset-then-fail.php b/testdata/worker-reset-then-fail.php new file mode 100644 index 0000000000..a4cdfbf3bd --- /dev/null +++ b/testdata/worker-reset-then-fail.php @@ -0,0 +1,7 @@ +