|
6 | 6 | "fmt" |
7 | 7 | "reflect" |
8 | 8 | "runtime" |
| 9 | + "strings" |
9 | 10 | "sync" |
10 | 11 | "sync/atomic" |
11 | 12 | "testing" |
@@ -353,6 +354,179 @@ func TestWorkflowsRegistration(t *testing.T) { |
353 | 354 | }() |
354 | 355 | RegisterWorkflow(freshCtx, simpleWorkflow) |
355 | 356 | }) |
| 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 | + }) |
356 | 530 | } |
357 | 531 |
|
358 | 532 | func stepWithinAStep(ctx context.Context) (string, error) { |
|
0 commit comments