Skip to content

Reuse side effects and version markers in redrive (#980) - #1175

Merged
rmcdaniel merged 1 commit into
mainfrom
feat/980-redrive-reused-markers
Nov 12, 2026
Merged

rmcdaniel merged 1 commit into
mainfrom
feat/980-redrive-reused-markers

Conversation

@rmcdaniel

Copy link
Copy Markdown
Member

Refs #980. Slice 3 (Core workflow#991, fixture redrive-reused-markers-clock), after #1162 and #1164.

Native redrive refused any failed run whose history had a side effect, version marker or workflow-code upsert, with unsupported_history. Core's FailedRunRedrivePlan reuses those steps when they come before the failed step.

Change (rust/src/runtime/redrive.rs)

  • The plan accepts SideEffectRecorded, VersionMarkerRecorded, MemoUpserted and SearchAttributesUpserted as one-step markers, as Core's plan does. It refuses with Core's reasons:
    • invalid_step_sequence when a marker has no valid sequence or comes before the start.
    • duplicate_step_sequence when two markers share a step, or a marker shares one with an activity.
    • external_side_effect_result_not_supported when a side-effect result is stored externally (Core's external_storage or a stored reference, plus native's external_payload envelope).
    • invalid_failed_step when a marker is at or after the failed step.
    • activity_sequence_gap when activities and markers don't fill every step before the failed one.
  • The successor records each reused step in step order. It keeps the source payload without its task and command, and adds reused_from_run_id and reused_recorded_at. That is when the step first ran, kept across a chain of redrives (successorEvents). Reused activities are unchanged.
  • Visibility follows FailedRunRedriveEligibility::rowsMatchReusedUpserts. A memo or search-attribute row that workflow code upserted must match a reused upsert step; otherwise the run refuses with unsupported_visibility_metadata.
  • Reused upserts refuse with service_redrive_history_not_qualified. Core does this for a run a service worker started (workflow_definition_fingerprint_source = worker; Core fixture redrive-service-unqualified-history) because the SDKs can't replay a reused upsert yet. Native applies the check at the same point in Core's order, and again after the definition check. Native runs every workflow on a service worker, so it never accepts a reused upsert.
  • Describe's can_redrive/redrive_blocked_reason (Report the redrive decision in describe (#980) #1164) report the same decision with no further change.

Core parity evidence (workflow origin/main 2ce626d4, 2.26.0+)

  • src/V2/Support/FailedRunRedrivePlan.php: the marker branch of forRun, the ordering of marker/activity checks after the boundary, and successorEvents for steps that aren't activities.
  • src/V2/Support/FailedRunRedriveEligibility.php: rowsMatchReusedUpserts, then the service qualification check (reusesServiceUnqualifiedSteps: memo/search-attribute upserts, signals, children and updates are not qualified; side effects and version markers are).
  • Fixtures: redrive-reused-markers-clock (side effect at step 1, version marker at step 2, resume at 4, reused times are the source's) and redrive-service-unqualified-history. Core test testRedriveReusesSideEffectsAndVersionMarkersAtTheirRecordedTime.
  • There is no PHP-parity fixture: the parity reference is pinned to published PHP 2.15.12, which predates marker reuse. This change doesn't touch describe's shape, so the parity contracts don't change.

Known divergence (safe direction): for a run that Core would treat as embedded-or-unknown (no worker fingerprint source) with a matching definition, Core accepts reused upserts. Native refuses them because it has no embedded replay.

Tests

  • New redrive_reuses_side_effects_and_version_markers_at_their_recorded_time, after the Core fixture. The source records a side effect and a version marker, then activity a, and fails at activity b (step 4); its marker times are backdated. The redrive resumes at 4. The successor's history and the worker's poll replay SideEffectRecorded, VersionMarkerRecorded and ActivityCompleted at steps 1–3, each with the source payload (no task/command), reused_from_run_id and the source's original reused_recorded_at. The worker then schedules b at step 4. The successor fails again, and its own redrive keeps the first-run times while reused_from_run_id names the successor.
  • redrive_carries_start_time_memo_and_search_attributes: a code-upserted memo now reports and refuses service_redrive_history_not_qualified, in both describe and redrive, before anything is written.
  • New unit test plan_reuses_marker_steps_like_core covers step order, task/command stripping and each new refusal reason.
  • SQLite: lib 485 passed, execution 785 passed. MySQL 8.4 and PostgreSQL 16: the redrive, replay-clock and describe-actions execution tests (9) pass. The toolchain is Rust 1.99.0.

Release notes (for the next native release): Redrive now reuses side effects and version markers recorded before the failed step, as Workflow does. The new run records them with the original payload, reused_from_run_id and the time they first ran (reused_recorded_at), so workflow time after those steps matches the failed run. A run whose workflow code upserted a memo or search attribute is refused with service_redrive_history_not_qualified (previously unsupported_history) until the SDKs can replay reused upserts. A side effect stored externally is refused with external_side_effect_result_not_supported.

Remaining #980 slices: fired plain timers (workflow#992, unsupported_timer/incomplete_timer_step, fixture redrive-reused-timer-clock). Then, following Core's service-mode qualification: signal waits, updates and completed child workflows (workflow#1109), with their own validation and then the service refusal, plus MessageCursorAdvanced carryover and the workflow_code/structural_limit failure kinds.

🤖 Generated with Claude Code

Native redrive refused any failed run whose history held a side effect,
version marker or workflow-code upsert (unsupported_history). Core's
FailedRunRedrivePlan (workflow#991) reuses those steps before the failed
one: the successor records each in step order with the source payload,
minus its task and command, plus reused_from_run_id and the time the step
first ran as reused_recorded_at.

The plan now takes SideEffectRecorded, VersionMarkerRecorded, MemoUpserted
and SearchAttributesUpserted steps with Core's refusals
(invalid_step_sequence, duplicate_step_sequence,
external_side_effect_result_not_supported, invalid_failed_step,
activity_sequence_gap). Upserted visibility rows must match a reused
upsert step (rowsMatchReusedUpserts). A reused upsert refuses with
service_redrive_history_not_qualified, as Core refuses it for a service
worker's run, since native runs every workflow on a service worker.

Refs #980

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@rmcdaniel
rmcdaniel added this pull request to the merge queue Nov 12, 2026
Merged via the queue into main with commit ade46df Nov 12, 2026
63 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant