Skip to content

Commit 0f4498f

Browse files
committed
handle duplicate gob registration
1 parent e1922e1 commit 0f4498f

File tree

2 files changed

+206
-0
lines changed

2 files changed

+206
-0
lines changed

dbos/serialization.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"encoding/base64"
66
"encoding/gob"
77
"fmt"
8+
"log/slog"
9+
"strings"
810
)
911

1012
func serialize(data any) (string, error) {
@@ -39,3 +41,33 @@ func deserialize(data *string) (any, error) {
3941

4042
return result, nil
4143
}
44+
45+
// safeGobRegister attempts to register a type with gob, recovering only from
46+
// panics caused by duplicate type/name registrations (e.g., registering both T and *T).
47+
// These specific conflicts don't affect encoding/decoding correctness, so they're safe to ignore.
48+
// Other panics (like register `any`) are real errors and will propagate.
49+
func safeGobRegister(value any, logger *slog.Logger) {
50+
defer func() {
51+
if r := recover(); r != nil {
52+
// Only catch specific duplicate registration errors
53+
if errStr, ok := r.(string); ok {
54+
// Check if this is one of the two specific duplicate registration errors we want to ignore
55+
// These patterns come from gob's RegisterName function
56+
// See https://cs.opensource.google/go/go/+/refs/tags/go1.25.1:src/encoding/gob/type.go;l=832
57+
if strings.Contains(errStr, "gob: registering duplicate types for") ||
58+
strings.Contains(errStr, "gob: registering duplicate names for") {
59+
// Log at debug level since these specific conflicts are expected and harmless
60+
if logger != nil {
61+
logger.Debug("gob registration conflict",
62+
"type", fmt.Sprintf("%T", value),
63+
"error", r)
64+
}
65+
return // Recover from this specific panic
66+
}
67+
}
68+
// Re-panic for any other errors
69+
panic(r)
70+
}
71+
}()
72+
gob.Register(value)
73+
}

dbos/workflows_test.go

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"reflect"
88
"runtime"
9+
"strings"
910
"sync"
1011
"sync/atomic"
1112
"testing"
@@ -353,6 +354,179 @@ func TestWorkflowsRegistration(t *testing.T) {
353354
}()
354355
RegisterWorkflow(freshCtx, simpleWorkflow)
355356
})
357+
358+
t.Run("SafeGobRegister", func(t *testing.T) {
359+
// Create a fresh DBOS context for this test
360+
freshCtx := setupDBOS(t, false, true) // Don't reset DB but do check for leaks
361+
362+
// Test 1: Basic type vs pointer conflicts
363+
type TestType struct {
364+
Value string
365+
}
366+
367+
// Register workflows that use the same type to trigger potential gob conflicts
368+
// The safeGobRegister calls within RegisterWorkflow should handle the conflicts
369+
workflow1 := func(ctx DBOSContext, input TestType) (TestType, error) {
370+
return input, nil
371+
}
372+
workflow2 := func(ctx DBOSContext, input *TestType) (*TestType, error) {
373+
return input, nil
374+
}
375+
376+
// Both registrations should succeed despite using conflicting types (T and *T)
377+
RegisterWorkflow(freshCtx, workflow1)
378+
RegisterWorkflow(freshCtx, workflow2)
379+
380+
// Test 2: Multiple workflows with the same types (duplicate registrations)
381+
workflow3 := func(ctx DBOSContext, input TestType) (TestType, error) {
382+
return TestType{Value: input.Value + "-modified"}, nil
383+
}
384+
workflow4 := func(ctx DBOSContext, input TestType) (TestType, error) {
385+
return TestType{Value: input.Value + "-another"}, nil
386+
}
387+
388+
// These should succeed even though TestType is already registered
389+
RegisterWorkflow(freshCtx, workflow3)
390+
RegisterWorkflow(freshCtx, workflow4)
391+
392+
// Test 3: Nested structs
393+
type InnerType struct {
394+
ID int
395+
}
396+
type OuterType struct {
397+
Inner InnerType
398+
Name string
399+
}
400+
401+
workflow5 := func(ctx DBOSContext, input OuterType) (OuterType, error) {
402+
return input, nil
403+
}
404+
workflow6 := func(ctx DBOSContext, input *OuterType) (*OuterType, error) {
405+
return input, nil
406+
}
407+
408+
RegisterWorkflow(freshCtx, workflow5)
409+
RegisterWorkflow(freshCtx, workflow6)
410+
411+
// Test 4: Slice and map types
412+
workflow7 := func(ctx DBOSContext, input []TestType) ([]TestType, error) {
413+
return input, nil
414+
}
415+
workflow8 := func(ctx DBOSContext, input []*TestType) ([]*TestType, error) {
416+
return input, nil
417+
}
418+
workflow9 := func(ctx DBOSContext, input map[string]TestType) (map[string]TestType, error) {
419+
return input, nil
420+
}
421+
workflow10 := func(ctx DBOSContext, input map[string]*TestType) (map[string]*TestType, error) {
422+
return input, nil
423+
}
424+
425+
RegisterWorkflow(freshCtx, workflow7)
426+
RegisterWorkflow(freshCtx, workflow8)
427+
RegisterWorkflow(freshCtx, workflow9)
428+
RegisterWorkflow(freshCtx, workflow10)
429+
430+
// Launch and verify the system still works
431+
err := Launch(freshCtx)
432+
require.NoError(t, err, "failed to launch DBOS after gob conflict handling")
433+
defer Shutdown(freshCtx, 10*time.Second)
434+
435+
// Test all registered workflows to ensure they work correctly
436+
437+
// Run workflow1 with value type
438+
testValue := TestType{Value: "test"}
439+
handle1, err := RunWorkflow(freshCtx, workflow1, testValue)
440+
require.NoError(t, err, "failed to run workflow1")
441+
result1, err := handle1.GetResult()
442+
require.NoError(t, err, "failed to get result from workflow1")
443+
assert.Equal(t, testValue, result1, "unexpected result from workflow1")
444+
445+
// Run workflow2 with pointer type
446+
testPointer := &TestType{Value: "pointer"}
447+
handle2, err := RunWorkflow(freshCtx, workflow2, testPointer)
448+
require.NoError(t, err, "failed to run workflow2")
449+
result2, err := handle2.GetResult()
450+
require.NoError(t, err, "failed to get result from workflow2")
451+
assert.Equal(t, testPointer, result2, "unexpected result from workflow2")
452+
453+
// Run workflow3 with modified output
454+
handle3, err := RunWorkflow(freshCtx, workflow3, testValue)
455+
require.NoError(t, err, "failed to run workflow3")
456+
result3, err := handle3.GetResult()
457+
require.NoError(t, err, "failed to get result from workflow3")
458+
assert.Equal(t, TestType{Value: "test-modified"}, result3, "unexpected result from workflow3")
459+
460+
// Run workflow5 with nested struct
461+
testOuter := OuterType{Inner: InnerType{ID: 42}, Name: "test"}
462+
handle5, err := RunWorkflow(freshCtx, workflow5, testOuter)
463+
require.NoError(t, err, "failed to run workflow5")
464+
result5, err := handle5.GetResult()
465+
require.NoError(t, err, "failed to get result from workflow5")
466+
assert.Equal(t, testOuter, result5, "unexpected result from workflow5")
467+
468+
// Run workflow6 with nested struct pointer
469+
testOuterPtr := &OuterType{Inner: InnerType{ID: 43}, Name: "test-ptr"}
470+
handle6, err := RunWorkflow(freshCtx, workflow6, testOuterPtr)
471+
require.NoError(t, err, "failed to run workflow6")
472+
result6, err := handle6.GetResult()
473+
require.NoError(t, err, "failed to get result from workflow6")
474+
assert.Equal(t, testOuterPtr, result6, "unexpected result from workflow6")
475+
476+
// Run workflow7 with slice type
477+
testSlice := []TestType{{Value: "a"}, {Value: "b"}}
478+
handle7, err := RunWorkflow(freshCtx, workflow7, testSlice)
479+
require.NoError(t, err, "failed to run workflow7")
480+
result7, err := handle7.GetResult()
481+
require.NoError(t, err, "failed to get result from workflow7")
482+
assert.Equal(t, testSlice, result7, "unexpected result from workflow7")
483+
484+
// Run workflow8 with pointer slice type
485+
testPtrSlice := []*TestType{{Value: "a"}, {Value: "b"}}
486+
handle8, err := RunWorkflow(freshCtx, workflow8, testPtrSlice)
487+
require.NoError(t, err, "failed to run workflow8")
488+
result8, err := handle8.GetResult()
489+
require.NoError(t, err, "failed to get result from workflow8")
490+
assert.Equal(t, testPtrSlice, result8, "unexpected result from workflow8")
491+
492+
// Run workflow9 with map type
493+
testMap := map[string]TestType{"key1": {Value: "value1"}}
494+
handle9, err := RunWorkflow(freshCtx, workflow9, testMap)
495+
require.NoError(t, err, "failed to run workflow9")
496+
result9, err := handle9.GetResult()
497+
require.NoError(t, err, "failed to get result from workflow9")
498+
assert.Equal(t, testMap, result9, "unexpected result from workflow9")
499+
500+
// Run workflow10 with pointer map type
501+
testPtrMap := map[string]*TestType{"key1": {Value: "value1"}}
502+
handle10, err := RunWorkflow(freshCtx, workflow10, testPtrMap)
503+
require.NoError(t, err, "failed to run workflow10")
504+
result10, err := handle10.GetResult()
505+
require.NoError(t, err, "failed to get result from workflow10")
506+
assert.Equal(t, testPtrMap, result10, "unexpected result from workflow10")
507+
508+
t.Run("validPanic", func(t *testing.T) {
509+
// Verify that non-duplicate registration panics are still propagated
510+
// interface{} types cause gob.Register to panic because they can't register a nil interface
511+
// This should NOT be caught by safeGobRegister since it's not a duplicate registration error
512+
workflow11 := func(ctx DBOSContext, input any) (any, error) {
513+
return input, nil
514+
}
515+
516+
// This should panic during registration because interface{} creates a nil value
517+
// which gob.Register cannot handle
518+
defer func() {
519+
r := recover()
520+
require.NotNil(t, r, "expected panic from interface{} registration but got none")
521+
// Verify it's not a duplicate registration error (which would be caught)
522+
if errStr, ok := r.(string); ok {
523+
assert.False(t, strings.Contains(errStr, "gob: registering duplicate"),
524+
"panic should not be a duplicate registration error, got: %v", r)
525+
}
526+
}()
527+
RegisterWorkflow(freshCtx, workflow11) // This should panic
528+
})
529+
})
356530
}
357531

358532
func stepWithinAStep(ctx context.Context) (string, error) {

0 commit comments

Comments
 (0)