Skip to content

Commit 73c79cd

Browse files
GitHub #92: Workflow: Migration strategy (#526)
1 parent 78f9239 commit 73c79cd

2 files changed

Lines changed: 193 additions & 1 deletion

File tree

docs/workflow/plan.md

Lines changed: 146 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,150 @@ There are no known current v1 product features left without a v2 home.
357357
Items listed as deferred are either not v1 parity requirements or are
358358
explicitly reserved for a future contract before support is advertised.
359359

360+
## Migration Strategy
361+
362+
V2 ships as a new product surface beside the v1 PHP package. The migration
363+
contract below frames how the durable kernel, the package and naming
364+
boundary, payload/config/model compatibility, and the adoption rollout fit
365+
together. The customer-facing migration guide on the docs site implements
366+
these rules; this section is the durable-kernel authority they cite.
367+
368+
### Storage compatibility
369+
370+
- V2 does not interpret v1 tables as native runtime truth. The v2 durable
371+
homes named in the [Feature Compatibility Matrix](#feature-compatibility-matrix)
372+
are the only source of replay authority for v2 runs. The v1 tables
373+
(`workflows`, `workflow_logs`, `workflow_signals`, `workflow_timers`,
374+
`workflow_exceptions`, `workflow_relationships`) remain on disk for the
375+
v1 finish-on-v1 path; v2 code MUST NOT read them as engine truth.
376+
- Active v1 executions finish on v1. They are not rewritten into v2
377+
`workflow_runs` mid-flight, and there is no automatic in-place migration
378+
of leased v1 work.
379+
- Completed v1 executions may be imported into v2 archive/history form
380+
through the same `workflow:v2:history-import` contract that handles
381+
embedded v2 imports. The importer maps v1 generic logs and
382+
`PHP_INT_MAX` sentinels into v2 event/link payloads, and writes the
383+
result into the durable rows named by the matrix above.
384+
- The importer detects pruned or partial v1 executions, normalizes v1
385+
schema inconsistencies, and marks records as partial when source rows
386+
are missing rather than fabricating durable history. Imported runs
387+
carry the `workflow_runs.import_source`, `import_id`,
388+
`import_dedupe_key`, `import_contract_version`, and `imported_at`
389+
markers so the run is visibly "imported, not natively executed."
390+
- The default upgrade path is "finish on v1, start new on v2." Importing
391+
closed v1 history is opt-in archive support, not the standard cutover
392+
path.
393+
394+
### Package and naming compatibility
395+
396+
- The Composer rename `laravel-workflow/laravel-workflow`
397+
`durable-workflow/workflow` is a packaging boundary. The v1 package
398+
keeps its name on Packagist; v2 ships under the new name with the
399+
`Workflow\V2` namespace.
400+
- Waterline reads from both v1 and v2 durable rows during the cutover
401+
window. It is not part of the standalone-server distribution and does
402+
not require its package coordinates to change for the v2 cutover.
403+
- Microservice upgrade guidance: existing PHP microservices that share
404+
the same v1 package upgrade together by adopting the v2 type-alias
405+
registry (`workflows.v2.types`), the language-neutral payload codec,
406+
and the v2 task-id transport. Stable workflow/activity type keys are
407+
the durable boundary across services; PHP fully-qualified class names
408+
are not.
409+
- Laravel embedded-to-server upgrade path: an embedded v2 deployment
410+
that wants to move new work onto the standalone server keeps the same
411+
durable identities (`workflow_instance_id`, `run_id`), the same task
412+
queue names, and the same payload codec, while pointing the
413+
application at the server's HTTP API as an explicit remote dependency.
414+
Existing embedded runs drain in place; new starts route to the server.
415+
- Remote-endpoint configuration is explicit. The server base URL,
416+
namespace, task queue, and auth material are configured directly
417+
rather than inferred from Laravel-app-local settings, so the cutover
418+
does not depend on hidden defaults.
419+
420+
### Payload, config, and model compatibility
421+
422+
- Every durable v2 payload carries a payload-envelope codec name (and,
423+
where applicable, a version) so a worker, importer, or replay tool can
424+
decode it without inspecting deployment-local config. The
425+
package-level default codec is the language-neutral `avro` codec; the
426+
legacy `workflow-serializer-y` and `workflow-serializer-base64` codecs
427+
remain resolvable only as drain/import codecs for v1 history.
428+
- The importer accepts `SerializableClosure`-wrapped payloads, mixed
429+
serializer formats inside one v1 history, `WorkflowMetadata` envelopes,
430+
and `ModelIdentifier`-only payloads. Identifier-only payloads stay
431+
identifier-only on import; the importer does not eagerly hydrate
432+
application models it cannot prove still exist.
433+
- Custom serializer classes from v1 are not a v2 runtime contract.
434+
`php artisan workflow:v2:doctor` flags any `workflows.serializer`
435+
setting that is not `avro`, `workflow-serializer-y`, or
436+
`workflow-serializer-base64` as migration debt; default-codec
437+
resolution silently falls back to `avro` for new runs so encode does
438+
not fail, and the configured custom class is never invoked.
439+
- Custom model-subclass compatibility is a frozen support matrix.
440+
Subclassing `WorkflowInstance`, `WorkflowRun`, `WorkflowTask`,
441+
history-event/projection/schedule/activity/failure/link/messages
442+
models, and the search-attribute/memo/child-call models is supported
443+
when the subclass keeps the package's column names, primary keys, and
444+
foreign keys. Custom table names with custom foreign-key column names
445+
are out of contract.
446+
- Waterline v2 adapters consume the
447+
`Workflow\V2\Contracts\OperatorObservabilityRepository` contract and
448+
the v2 projection rows. They do not depend on a configured Eloquent
449+
subclass to render run detail; swapping the subclass does not require
450+
a Waterline change as long as the package column and key contract is
451+
preserved.
452+
453+
### Adoption
454+
455+
- Cutover migration guides exist for the v1 surfaces customers actually
456+
ran in production: Laravel queues and chains, batches, scheduled
457+
workflows (cron-driven starts), webhook-driven external ingress, and
458+
PHP microservices. Each guide maps v1 entry points to the v2
459+
primitives named in the matrix and points at the durable home where
460+
the new run's truth lives.
461+
- Reference architectures cover the deployment shapes adopters actually
462+
run:
463+
- **Monolith.** One Laravel app embedding the workflow package owns
464+
durable rows, workers, and Waterline. Adoption is a same-app
465+
package upgrade plus a cutover window for active v1 runs.
466+
- **Multi-app.** Several Laravel apps share a database or a server
467+
deployment. Each app stamps stable workflow/activity type keys; one
468+
namespace owns the durable kernel, and other apps participate as
469+
starters or workers using the type-alias registry.
470+
- **Microservice.** Workers are split across services and possibly
471+
languages. The standalone server owns the durable kernel; PHP, and
472+
later Python or other SDK workers, register against the same
473+
namespace, task queue, payload codec, and worker protocol.
474+
- **Operator-heavy.** Deployments that lean on schedules, repair,
475+
archive, terminate, and replay-debug surface adopt v2 by exercising
476+
the operator command taxonomy from
477+
[`docs/architecture/webhook-and-command-taxonomy.md`](../architecture/webhook-and-command-taxonomy.md)
478+
and the deployment lifecycle controls from
479+
[`docs/architecture/worker-deployment.md`](../architecture/worker-deployment.md)
480+
rather than by editing durable rows directly.
481+
- Single-version rollout mechanics that apply inside a v2 fleet:
482+
- **Canary.** Stamp newly-started runs with a new compatibility
483+
marker on a small worker cohort before flipping the starter
484+
process's default marker.
485+
- **Drain.** `dw task-queue:drain` flips a worker cohort's drain
486+
intent; pinned runs finish on the cohort that started them or
487+
continue-as-new onto the new marker.
488+
- **Rollback.** Resume a previously drained cohort and re-point
489+
starter processes at the old marker. In-flight runs on the new
490+
cohort keep running on the new cohort; nothing is silently
491+
rerouted.
492+
- **Replay-debug.** Versioned history-export bundles plus
493+
`workflow:v2:replay-verify` reproduce a closed run on a separate
494+
process so production correctness is debugged out-of-band.
495+
- "Mixed-fleet" is not a v2-internal rollout primitive. There is no
496+
v2-alpha-to-v2 backwards-compatibility lane and no mixed-build
497+
correctness contract that lives outside the compatibility-marker
498+
story above. The only cross-generation surface where mixed-fleet
499+
language remains meaningful is the v1→v2 transition itself: while v1
500+
workers finish v1 runs and v2 workers execute v2 runs, both fleets
501+
may be live at once, and that is the only sense in which a "mixed
502+
fleet" is part of v2 adoption.
503+
360504
## Relationship To Other Contracts
361505

362506
- [`docs/api-stability.md`](../api-stability.md) freezes public API and
@@ -368,7 +512,8 @@ explicitly reserved for a future contract before support is advertised.
368512
- [`docs/architecture/scheduler-correctness.md`](../architecture/scheduler-correctness.md)
369513
defines schedule and timer correctness boundaries.
370514
- [`docs/architecture/worker-compatibility.md`](../architecture/worker-compatibility.md)
371-
defines mixed-fleet worker compatibility.
515+
defines worker build identity, compatibility markers, and routing of
516+
in-flight runs across coexisting builds.
372517
- [`docs/architecture/worker-deployment.md`](../architecture/worker-deployment.md)
373518
defines first-class deployment lifecycle and rollout blockage.
374519
- [`docs/architecture/sticky-execution.md`](../architecture/sticky-execution.md)

tests/Unit/V2/FeatureMappingDocumentationTest.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ final class FeatureMappingDocumentationTest extends TestCase
2626
'## V2.0 Defaults',
2727
'## Child Parent-Close Policy Contract',
2828
'## Gap Analysis',
29+
'## Migration Strategy',
30+
'### Storage compatibility',
31+
'### Package and naming compatibility',
32+
'### Payload, config, and model compatibility',
33+
'### Adoption',
2934
'## Relationship To Other Contracts',
3035
'## Changing This Contract',
3136
];
@@ -187,6 +192,35 @@ final class FeatureMappingDocumentationTest extends TestCase
187192
'Unsupported backend combinations',
188193
];
189194

195+
private const REQUIRED_MIGRATION_POINTS = [
196+
'V2 does not interpret v1 tables as native runtime truth',
197+
'Active v1 executions finish on v1',
198+
'workflow:v2:history-import',
199+
'PHP_INT_MAX',
200+
'finish on v1, start new on v2',
201+
'durable-workflow/workflow',
202+
'Workflow\\V2',
203+
'workflows.v2.types',
204+
'embedded-to-server',
205+
'avro',
206+
'workflow-serializer-y',
207+
'workflow-serializer-base64',
208+
'SerializableClosure',
209+
'WorkflowMetadata',
210+
'ModelIdentifier',
211+
'OperatorObservabilityRepository',
212+
'Monolith',
213+
'Multi-app',
214+
'Microservice',
215+
'Operator-heavy',
216+
'Canary',
217+
'Drain',
218+
'Rollback',
219+
'Replay-debug',
220+
'"Mixed-fleet" is not a v2-internal rollout primitive',
221+
'v1→v2 transition',
222+
];
223+
190224
private const REQUIRED_RELATED_CONTRACTS = [
191225
'docs/api-stability.md',
192226
'docs/architecture/query-and-live-debug.md',
@@ -304,6 +338,19 @@ public function testContractDocumentDeclaresGapAnalysis(): void
304338
);
305339
}
306340

341+
public function testContractDocumentDeclaresMigrationStrategy(): void
342+
{
343+
$contents = $this->documentContents();
344+
345+
foreach (self::REQUIRED_MIGRATION_POINTS as $point) {
346+
$this->assertStringContainsString(
347+
$point,
348+
$contents,
349+
sprintf('Feature mapping contract migration strategy must cover %s.', $point),
350+
);
351+
}
352+
}
353+
307354
public function testContractDocumentCitesRelatedContracts(): void
308355
{
309356
$contents = $this->documentContents();

0 commit comments

Comments
 (0)