Skip to content

Commit 163e45b

Browse files
Fix panic in defer after cache eviction. (#1048)
We have rather rare but annoying panic that hapens when workflow contains certain api calls within a defer function. This only happens when workflow is being blocked for some time and gets evicted from the cache. At this point the dispatcher is being closed and all its coroutines are exited with runtime.Goexit(). This in turn calls deffered function. If user calls ExecuteActivity (among others) it receives context that is no longer valid, since it is used not from within ExecuteUntilAllBlocked loop. A safety check in getState is executed which causes rather cryptic panic message: "illegal access from outside of workflow context". This panic is not harmful. Once unblocked workflow will be recreated in the cache and replayed. When finished it will call its defer again this time with a valid context. Nevertheless it is annoying an causes confusion for the users. The fix is rather simple. Within getState function, we check for closed dispatcher. If it is the case we terminate current goroutine again with runtime.GoExit() which prevent further execution of the defer.
1 parent 0256258 commit 163e45b

File tree

2 files changed

+15
-0
lines changed

2 files changed

+15
-0
lines changed

internal/internal_coroutines_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,11 @@ func TestDispatchClose(t *testing.T) {
626626
for i := 0; i < 10; i++ {
627627
ii := i
628628
GoNamed(ctx, fmt.Sprintf("c-%v", i), func(ctx Context) {
629+
defer func(){
630+
// Trigger no-longer-valid context access within deferred function.
631+
// At this point deferred function should not continue and will be exited.
632+
getState(ctx)
633+
}()
629634
c.Receive(ctx, nil) // blocked forever
630635
history = append(history, fmt.Sprintf("child-%v", ii))
631636
})
@@ -652,6 +657,10 @@ func TestDispatchClose(t *testing.T) {
652657
"root",
653658
}
654659
require.EqualValues(t, expected, history)
660+
for _, c := range d.coroutines {
661+
// Ensure that coroutines did not panic during dispatcher closing procedure
662+
require.Nil(t, c.panicError)
663+
}
655664
}
656665

657666
func TestPanic(t *testing.T) {

internal/internal_workflow.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,12 @@ func getState(ctx Context) *coroutineState {
564564
panic("getState: not workflow context")
565565
}
566566
state := s.(*coroutineState)
567+
// When workflow gets evicted from cache is closes the dispatcher and exits all its coroutines.
568+
// However if workflow function have a defer, it will be executed. Many workflow API calls will end up here.
569+
// The following check prevents coroutine executing further. It would panic otherwise as context is no longer valid.
570+
if state.dispatcher.closed {
571+
runtime.Goexit()
572+
}
567573
if !state.dispatcher.executing {
568574
panic(panicIllegalAccessCoroutinueState)
569575
}

0 commit comments

Comments
 (0)