Skip to content

Redrive runs whose workflow code upserted memos or search attributes - #1001

Merged
rmcdaniel merged 5 commits into
mainfrom
fix/864-redrive-visibility-upserts
Nov 3, 2026
Merged

rmcdaniel merged 5 commits into
mainfrom
fix/864-redrive-visibility-upserts

Conversation

@rmcdaniel

Copy link
Copy Markdown
Member

A2d of #864.

Problem

Redrive refused any failed run whose workflow code called upsertMemo() or upsertSearchAttributes() before the failed step. The plan saw MemoUpserted / SearchAttributesUpserted in the completed prefix and refused with unsupported_history, and eligibility separately refused any memo or search attribute row with upserted_at_sequence > 0 as unsupported_visibility_metadata.

Related gap: A1 copied start-time rows to the successor as inherited rows at step 1, so a successor that failed again was refused for its own memos.

Change

  • Plan. Both upsert events are reusable, sequence-keyed steps, handled exactly like side effects and version markers: step-order checks, duplicate checks, and recording context (task, command) stripped.
  • Successor history. The control plane's existing generic copy records them with reused_from_run_id and reused_recorded_at. A step reused twice keeps its first time.
  • Replay time. WorkflowExecutor, QueryStateReplayer and WorkflowFiberRunner send RedriveReplayClock::recordedAt() for these events, so now() after an upsert matches the source run.
  • Rows. New MemoUpsertService::copyForRedrive() / SearchAttributeUpsertService::copyForRedrive() copy rows unchanged, keeping upserted_at_sequence and inherited_from_parent, so the successor's rows match its reused history. Continue-as-new still uses inheritFromParent().
  • Eligibility. A row is refused (unsupported_visibility_metadata) only when workflow code wrote it (upserted_at_sequence > 0, not inherited) and no reused upsert step of that type has that sequence. The existing refusal test for a row with no history behind it still passes.
  • Payload contract and docs/api-stability.md. Both events accept the two reuse keys. The SearchAttributesUpserted row now also lists its existing attribute_types key.
  • CHANGELOG Unreleased updated.

SDK replay parity for these keys follows the (C) issues General filed (sdk-python#232, sdk-php#240, sdk-rust#164). Native parity stays with server#980.

Tests

Local runs, PHP 8.4, SQLite + Redis:

  • V2FailedRunRedriveTest (13 tests), with two new tests:
    • testRedriveReusesWorkflowCodeMemoAndSearchAttributeUpserts: the redrive happens 5 minutes later. The successor completes without rerunning the first activity, rows match the source exactly (values, sequence, inherited flag), reused payloads equal the source minus recording context, and workflow time after each upsert matches the source in the live run, export replay and stored-history replay.
    • testARedriveSuccessorWithUpsertsCanBeRedrivenAgain: source → successor fails → second redrive completes, keeping the first run's time.
  • DefaultWorkflowControlPlaneRedriveRejectionTest, FailedRunRedrivePlanTest, V2FailedRunRedrivePlanTest, HistoryEventPayloadContractTest, SurfaceStabilityContractTest and ReplayRegressionCorpusTest pass. One DefaultWorkflowControlPlaneRedriveRejectionTest case fails locally on a cache-store difference; it fails the same way on main and passes in CI.
  • Replay corpus: new redrive-reused-upserts-clock fixture plus a scenario test. validate-regression-corpus.py --base-ref <main> reports replay counterfactual_fixture_paths: 1: it fails on main and passes here.
  • ecs and phpstan are clean on the changed files.

🤖 Generated with Claude Code

rmcdaniel and others added 2 commits November 3, 2026 11:35
…utes (#864)

A2d of #864. Any MemoUpserted or SearchAttributesUpserted event in the
completed prefix refused redrive with unsupported_history.

- The redrive plan treats both upsert events as reusable steps, like side
  effects and version markers. The successor records them with
  reused_from_run_id and reused_recorded_at, and the executor, query
  replayer and fiber runner replay them at the original time.
- Memo and search attribute rows carry to the successor unchanged,
  keeping upserted_at_sequence and inherited_from_parent, so the
  successor's rows match its reused history and a successor that fails
  again can be redriven again.
- A row written by workflow code with no matching reused upsert step is
  still refused with unsupported_visibility_metadata.

Adds feature coverage (single redrive and a redrive chain) and replay
corpus evidence redrive-reused-upserts-clock.

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

# Conflicts:
#	CHANGELOG.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rmcdaniel
rmcdaniel marked this pull request as ready for review November 3, 2026 18:07
@rmcdaniel

Copy link
Copy Markdown
Member Author

Security full source review at 84ac448292d505e1e2a9fdaafdc1c404b9bedd19: no product-code finding; exact-head merge clearance is intentionally deferred until the 2.18.0 release cut lands.

Accepted source behavior:

  • MemoUpserted and SearchAttributesUpserted enter the reusable step plan with the existing positive-integer, duplicate-step, gap and failed-boundary checks. Source task/command recording context is stripped; step payload/provenance is retained.
  • Eligibility permits start-time and inherited projection rows, but a row written by this run’s workflow code must have a reusable upsert of the same type and sequence. The existing no-history projection mutation remains refused as unsupported_visibility_metadata.
  • copyForRedrive() preserves memo values, all typed search-attribute fields, upserted_at_sequence and inherited_from_parent, then re-applies count/size limits inside the redrive transaction. Tests bind typed source/successor equality.
  • Live execution, query replay and stored/export replay use the first reused_recorded_at; single-redrive and redrive-chain tests prove no activity re-execution and preserve first-run time/projection state.
  • The new corpus fixture has a verified fail-before counterfactual; payload contract/API-stability updates include the two provenance keys and the pre-existing attribute_types field.

Remaining merge gate:

This is source acceptance, not permission to merge 84ac4482 ahead of the release cut.

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

sonarqubecloud Bot commented Nov 3, 2026

Copy link
Copy Markdown

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security exact-head readback of c0179ed405b60c32c04eb3ea4dba226257d47562: accepted, provided this SHA remains the merge head.

  • Relative to the fully reviewed 84ac4482, the refresh changes only CHANGELOG.md and composer.json for the already accepted Workflow 2.18.0 release merge. docs/, src/, and tests/ are byte-identical between the two heads.
  • Against current main 36dc304d, the released 2.18.0 section is untouched and the PR adds only its Redrive runs whose workflow code upserted memos or search attributes #1001 entry under Unreleased.
  • The hosted MySQL feature leg, regression corpus, repository contracts, quality, tracing, static analysis/CodeQL, and public-boundary checks pass; the final unit-contract job also passes.

This lifts the earlier release-cut refresh hold and grants merge clearance for this exact head. No clearance carries to a changed head. #1002 remains separately held/draft until it is refreshed after this merge and returns with its full exact-head evidence.

@rmcdaniel
rmcdaniel merged commit 6185ea4 into main Nov 3, 2026
23 checks passed
@rmcdaniel
rmcdaniel deleted the fix/864-redrive-visibility-upserts branch November 3, 2026 18:35
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