Skip to content

Commit 2aecce9

Browse files
Fix child workflow codec mismatch when args contain PHP-only values
`async()` produces a `SerializableClosure` which Avro cannot round-trip. The child run inherits the parent's `payload_codec` but `serializeWithCodec` silently serialized the closure-carrying array as zero-length Avro bytes, so on child start the codec tag said "avro" while the blob was empty/invalid — the v2 engine failed the run with `CodecDecodeException`. Same pattern as `a71304a` (legacy `__callStatic` path) extended to the explicit-codec `serializeWithCodec` path: - Serializer: new `chooseCodecForData(?preferred, data): string` helper that falls back to `workflow-serializer-y` when Avro is preferred but the data carries PHP-only objects. Callers that stamp `payload_codec` alongside a blob must use this helper so the row's codec tag matches the bytes. - WorkflowExecutor::scheduleChildWorkflow: pick the child run's codec via the helper before serializing, so `workflow_runs.payload_codec` reflects the actual wire format. - WorkflowExecutor::recordWorkflowStartCommand: same treatment for the `workflow_commands.payload_codec` column. Also fill in missing columns in the V2ConfiguredCoreModelsTest custom tables (`search_attributes`, `namespace`, `message_cursor_position`, `run_timeout_seconds`, `execution_deadline_at`, `run_deadline_at`, plus new `namespace`/`execution_timeout_seconds`/`last_message_sequence` on the instance-alias table and `namespace` on the task-alias table) so the test's alias models accept every column the v2 engine writes. Clears five MySQL failures on workflow v2 CI under #399: * testCoreRuntimeUsesConfiguredInstanceRunAndTaskModels * testAsyncHelperRunsStraightLineClosureAsDurableChildWorkflow * and three related cascading failures that depended on async child workflows completing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 26c587b commit 2aecce9

3 files changed

Lines changed: 53 additions & 3 deletions

File tree

src/Serializers/Serializer.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,25 @@ public static function serializeWithCodec(?string $codec, $data): string
126126
return $class::serialize($data);
127127
}
128128

129+
/**
130+
* Pick the codec name actually used to serialize a payload. If the
131+
* preferred codec is Avro but the value contains PHP-only objects
132+
* (SerializableClosure, resources, arbitrary objects Avro cannot encode),
133+
* fall back to the legacy `workflow-serializer-y` codec so the payload
134+
* round-trips. Callers that store a payload_codec column alongside the
135+
* blob must use the codec returned here, not the preferred input.
136+
*/
137+
public static function chooseCodecForData(?string $preferred, mixed $data): string
138+
{
139+
$preferredCodec = CodecRegistry::resolve($preferred);
140+
141+
if ($preferredCodec === Avro::class && self::containsPhpOnlyValue($data)) {
142+
return 'workflow-serializer-y';
143+
}
144+
145+
return CodecRegistry::canonicalize($preferred);
146+
}
147+
129148
/**
130149
* Unserialize using an explicit codec name.
131150
*/

src/V2/Support/WorkflowExecutor.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1515,9 +1515,14 @@ private function scheduleChildWorkflow(
15151515
// arguments (next line) match the codec stamped on the child run.
15161516
// Falling back to the package default when the parent has none
15171517
// keeps pre-pinned parents working.
1518-
$childCodec = is_string($run->payload_codec) && $run->payload_codec !== ''
1518+
$preferredChildCodec = is_string($run->payload_codec) && $run->payload_codec !== ''
15191519
? $run->payload_codec
15201520
: CodecRegistry::defaultCodec();
1521+
// When the child arguments carry PHP-only values (e.g. a
1522+
// SerializableClosure produced by async()), Avro cannot round-trip
1523+
// them. Pick the actually-used codec so the row's `payload_codec`
1524+
// matches what the blob was serialized with.
1525+
$childCodec = Serializer::chooseCodecForData($preferredChildCodec, $metadata->arguments);
15211526
$serializedChildArguments = Serializer::serializeWithCodec($childCodec, $metadata->arguments);
15221527
StructuralLimits::guardPayloadSize($serializedChildArguments);
15231528

@@ -2499,8 +2504,11 @@ private function recordWorkflowStartCommand(
24992504
'target_scope' => 'instance',
25002505
'status' => CommandStatus::Accepted->value,
25012506
'outcome' => CommandOutcome::StartedNew->value,
2502-
'payload_codec' => $sourceRun->payload_codec ?? CodecRegistry::defaultCodec(),
2503-
'payload' => Serializer::serializeWithCodec($sourceRun->payload_codec, $arguments),
2507+
'payload_codec' => $startCommandCodec = Serializer::chooseCodecForData(
2508+
$sourceRun->payload_codec ?? CodecRegistry::defaultCodec(),
2509+
$arguments,
2510+
),
2511+
'payload' => Serializer::serializeWithCodec($startCommandCodec, $arguments),
25042512
'accepted_at' => $recordedAt,
25052513
'applied_at' => $recordedAt,
25062514
'created_at' => $recordedAt,

tests/Feature/V2/V2ConfiguredCoreModelsTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,17 +77,24 @@ private function createConfiguredInstancesTable(): void
7777
->primary();
7878
$table->string('workflow_class');
7979
$table->string('workflow_type');
80+
$table->string('namespace')
81+
->nullable()
82+
->index();
8083
$table->string('business_key', 191)
8184
->nullable();
8285
$table->json('visibility_labels')
8386
->nullable();
8487
$table->json('memo')
8588
->nullable();
89+
$table->unsignedInteger('execution_timeout_seconds')
90+
->nullable();
8691
$table->string('current_run_id', 26)
8792
->nullable()
8893
->index();
8994
$table->unsignedInteger('run_count')
9095
->default(0);
96+
$table->unsignedInteger('last_message_sequence')
97+
->default(0);
9198
$table->timestamp('reserved_at', 6)
9299
->nullable();
93100
$table->timestamp('started_at', 6)
@@ -106,12 +113,17 @@ private function createConfiguredRunsTable(): void
106113
$table->unsignedInteger('run_number');
107114
$table->string('workflow_class');
108115
$table->string('workflow_type');
116+
$table->string('namespace')
117+
->nullable()
118+
->index();
109119
$table->string('business_key', 191)
110120
->nullable();
111121
$table->json('visibility_labels')
112122
->nullable();
113123
$table->json('memo')
114124
->nullable();
125+
$table->json('search_attributes')
126+
->nullable();
115127
$table->string('status');
116128
$table->string('closed_reason')
117129
->nullable();
@@ -131,6 +143,14 @@ private function createConfiguredRunsTable(): void
131143
->default(0);
132144
$table->unsignedInteger('last_command_sequence')
133145
->default(0);
146+
$table->unsignedInteger('message_cursor_position')
147+
->default(0);
148+
$table->unsignedInteger('run_timeout_seconds')
149+
->nullable();
150+
$table->timestamp('execution_deadline_at', 6)
151+
->nullable();
152+
$table->timestamp('run_deadline_at', 6)
153+
->nullable();
134154
$table->timestamp('started_at', 6)
135155
->nullable();
136156
$table->timestamp('closed_at', 6)
@@ -156,6 +176,9 @@ private function createConfiguredTasksTable(): void
156176
->primary();
157177
$table->string('workflow_run_id', 26)
158178
->index();
179+
$table->string('namespace')
180+
->nullable()
181+
->index();
159182
$table->string('task_type');
160183
$table->string('status');
161184
$table->json('payload')

0 commit comments

Comments
 (0)