Skip to content

Preserve legacy patch decisions and memoize repeated change IDs #103

Closed

Description

@rmcdaniel

Verified problem

Reproduced with a fresh installation of published durable-workflow==2.5.0 on Python 3.13. Current main is 04790ae42e8da0a83f65026f096572b547b7baa8.

The public WorkflowContext.patched() documentation promises False for an old history that passed the insertion point without a marker. Inserting a patch before a recorded activity instead raises NonDeterministicReplayError. This happens with both pending and completed activities.

@workflow.defn(name="patch-example")
class Example:
    def run(self, ctx):
        if (yield ctx.patched("new-step")):
            yield ctx.schedule_activity("new", [])
        return (yield ctx.schedule_activity("old", []))

Replaying an ActivityScheduled event for old at sequence 1, optionally followed by its ActivityCompleted event, rejects the version-marker command instead of choosing the old branch.

Two calls to yield ctx.patched("same") on a new run also emit two record_version_marker commands. A history containing those two identical markers at sequences 1 and 2 successfully replays today and returns [True, True]. The repair must preserve those existing histories.

PHP SDK 2.2.6 was independently exercised as a control and chooses the legacy branch and emits one marker for repeated calls.

Required outcome

  • Select version -1 without consuming the next recorded command when an unmarked old history has already passed this patch site.
  • Freeze one decision per change ID across repeated calls, replay and cold replacement.
  • Preserve supported ranges and reject actual incompatible or malformed markers.
  • Define and test get_version, patched and deprecate_patch together, including insertion before pending and completed operations.
  • Support retained histories written by the current duplicate-marker implementation, or provide an explicit safe migration. Do not silently discard markers or unfinished runs.
  • Add minimal replay-corpus regressions and shared semantic cases across PHP, Python, Rust and embedded mode. Coordinate shared-runner changes with Server #325.
  • Verify a published successor with an existing run that continues after a worker deployment.

The verification used isolated SDK replay APIs. It made no requests to a deployed Server.

Implementation: workflow.py at 2.5.0. Existing marker contract: Workflow API stability.

Activity

  1. rmcdaniel commented on Oct 10, 2026

    @rmcdaniel
    MemberAuthor

    Python 2.5.1 is published and its repair is verified from a fresh PyPI installation. PR #107 merged as c5508bce97effaac379ebc2e481f6543d436fbd1, with all 13 PR checks, main qualification and publication run 38068557889 passing.

    Published-artifact results:

    • All 57 focused patch/replay-corpus tests pass against the installed wheel. Its replay source is byte-identical to the qualified source.
    • Published Python 2.5.0 writes an original unmarked run and an original run with two markers for one patch ID. After actual SIGKILL, a distinct published 2.5.1 worker completes both original runs once.
    • Complete parked history prefixes, original activity sequences 1/3 and marker counts 0/2 remain unchanged. The old unmarked run schedules no new activity. The retained duplicate history also works after removing the redundant authored patch call.
    • A fresh installed 2.5.1 client and worker execute two patch calls, recording one marker and returning both decisions true.

    Server is the untouched published 2.5.16 image, digest sha256:d0035cbffa5b904e5f9f7f43f84f6caf03beefc93bab8731c7e960b7a344e771. PyPI wheel SHA-256 is 5bf872c64b1490f6e6232ac35de727693e78ef516fc0c2fe07b903d51863ddf8, sdist a8ba627502a16423fd0bf461b6671ddecb8eea622326102ee096ab69c614c756. The patch guide is live. All local test resources are removed.

    Published downstream follow-through is complete. Website #197 merged, all main checks passed, and the live install pointer is 2.5.1. Sample App #187 merged as 1fc639c0f85d1ac11f5022a2eec6533dbf5c8673. All PR and ten main workflows passed, including image publication. Its retained activity-recovery artifact passes 45 cases across all nine PHP/Python/Rust workflow/activity directions, with actual Python 2.5.1 workers and observer.

    Shared semantic cases remain with Server #325, including SDK Rust #80 and the independently confirmed embedded Workflow #744. #103 stays open until the required shared cases are complete. PHP/Rust consumption of Python's historical duplicate-marker format remains unqualified.

  2. rmcdaniel commented on Oct 13, 2026

    @rmcdaniel
    MemberAuthor

    Python SDK ownership is active for #103, then #123 and the existing #116 source/qualification work. Current PyPI stable is 2.5.9; main includes unpublished Nexus corrections and will not be released wholesale as a stable patch.

    The #107 repair and recorded 2.5.1 original-run worker replacement are existing evidence. The remaining check is one frozen version across all three public helpers and current shared history consumption. I am independently exercising the installed 2.5.9 wheel before changing code, while coordinating portable semantics with Workflow #744 and Rust SDK #80. Shared fixtures and the Server differential runner remain with their owner.

  3. rmcdaniel commented on Oct 13, 2026

    @rmcdaniel
    MemberAuthor

    Shared marker contract agreed with the Workflow and Rust SDK owners:

    • Freeze the numeric version per change ID across get_version, patched and deprecate_patch. Each helper keeps its numeric, boolean or void result projection.
    • Every current supported range must contain that frozen version. Compatible helper mixing is valid; an unsupported version remains a replay error.
    • Consistent retained duplicate markers stay at their original physical positions and clocks. Their recorded ranges may differ, but each present range must have integer bounds, minimum at most maximum, and contain the same version.
    • Keep concretely supported legacy payloads with absent bounds; this does not authorize malformed canonical records. Identity and conflicting-version failures remain errors.
    • An unmarked old history chooses supported legacy version -1 without consuming its next recorded command.

    The unchanged PyPI 2.5.9 wheel independently fails all 42 mixed-helper fresh/cold/pending/completed regressions while passing the 18 existing Python controls. Python correction and corpus are in #136; shared Server reader/runner changes stay with Server #325. Stable publication and original-run replacement remain pending.

  4. rmcdaniel commented on Oct 13, 2026

    @rmcdaniel
    MemberAuthor

    Agreed marker contract with the SDK owners:

    • Freeze the numeric version once per change ID. getVersion, patched and deprecation share that decision and keep their integer, boolean and void projections. Each current invocation must support the frozen version.
    • Retained duplicate markers keep their physical positions and clocks. Their recorded bounds may differ, provided each range is individually valid and contains that same version; equality with current authored bounds is not required.
    • Canonical markers have integer min_supported and max_supported, with min_supported <= version <= max_supported. Malformed present bounds must be refused. Missing historical fields need evidence of an actually supported producer or normalization format, rather than being assumed to be legacy.

    A separate published Workflow 2.5.12 probe confirms the embedded reader currently accepts inverted bounds 3..2, recorded version 1 outside 2..5, string/boolean bounds and a partial bound. These are deliberately constructed reader inputs, not claims that a normal producer emitted corrupt history. The normally executed mixed-helper queue case independently completes one original run through separate author/activity/replacement processes, recording one marker and activity sequence 2, with one invocation and exact int64/null/Unicode output. Cold offline replay preserves the history.

    The current package's loaded marker/executor bytes match tag 19a7a1cf33604c66f1f838e8b3db22563041f6b2; the published Server 2.6.1 launcher has index digest sha256:ef043e5c582761e486a64a2ab4226476e17ae4f71682ef59d6b9ddd784d246b6. Its local Composer path reference is recorded separately and is not treated as the package Git commit. Historical shared tuples and original failed observations stay unchanged. Next is a focused canonical reader correction and fail-before/pass-after corpus evidence; publication and wider connected qualification remain ahead.

  5. rmcdaniel commented on Oct 13, 2026

    @rmcdaniel
    MemberAuthor

    Current installed PyPI 2.5.9 accepts VersionMarkerRecorded with a valid change ID/version but both supported-range fields missing. It already rejects partial, noninteger, inverted and out-of-range bounds. The missing-both acceptance is an isolated replay-reader finding, not evidence that a real Server wrote such a record.

    All immutable Python marker fixtures inspected, including 0.3.1 and 2.0.0-rc.30, contain both bounds. The real published 2.5.0 author checkpoints also contain both. No concrete supported sparse-marker producer or normalization has been found. Following the agreed canonical contract, the reader will require both integer bounds and a version inside them. Recorded duplicate ranges may still differ and authored ranges may still evolve around the same supported frozen version.

    This small follow-up is being qualified before the pending stable 2.5.10 publication. Existing immutable histories stay unchanged; shared readers stay with their owners.

  6. rmcdaniel commented on Oct 13, 2026

    @rmcdaniel
    MemberAuthor

    Both corrections are merged on main: #136 and #138. All normal checks pass. The narrow stable backport #137 is merged as caf006be741ad928f4f5e5d7283dce37c2e99f02; its tree exactly matches reviewed candidate 7675465ed1242326cd01b2933dc98554243d1df6. Independent stable source qualification passes all ten jobs, including actual HTTP integration.

    The final installed candidate passes 128 focused replay/corpus tests and seven real consumer cases against unchanged published Server 2.6.1 / Workflow 2.5.12 / MySQL 8.0 / Redis 7. Published 2.5.0 authors create original unmarked/two-marker histories; after SIGKILL, distinct cold candidate readers resume those runs once under default 1.20 and explicit 1.19. Original prefixes, marker counts 0/2 and activity sequences 1/3 survive. Fresh helpers produce one marker and activity sequence 2. A separate published 2.5.9 author is killed at a marker/timer checkpoint; cold cooperative cleanup preserves delivery sequence 2, the original request and 60-second deadline, and its committed Avro result independently verifies the numeric/boolean/void projections. Canonical terminal cancellation remains cancellation.

    Tag 2.5.10 now points to the reviewed stable backport and publication has started. Registry and normally installed successor verification remain pending. This issue stays open for publication, pinned consumer follow-through and remaining shared qualification; no candidate result is claimed as published delivery.

  7. rmcdaniel commented on Oct 13, 2026

    @rmcdaniel
    MemberAuthor

    Python 2.5.10 is published from the reviewed stable backport caf006be741ad928f4f5e5d7283dce37c2e99f02. Publication and registry checks pass. The source, independently qualified stable tree and published installation agree.

    Fresh normal PyPI installation passes all 128 focused replay/corpus controls. The installed workflow.py, worker.py and client.py match the tag byte for byte. Both pinned and unpinned index resolution select the same wheel, SHA-256 ea2535c2ddb96a2891b4f9dbafd8504a9d91205ae6852f3ae9a8bca3d8ddd5c7; sdist SHA-256 is 2b9d7c483f105e0e29835c0d800d06f93be7408341396c8b0bec395c8cbce874. An earlier installation attempt stopped at an index-availability error before any workflow admission; primary Simple-index availability and a fresh normal installation were subsequently verified.

    Published-artifact outcomes on untouched Server 2.6.1 (sha256:ef043e5c582761e486a64a2ab4226476e17ae4f71682ef59d6b9ddd784d246b6), Workflow 2.5.12, MySQL 8.0 and Redis 7:

    • Six original-run/fresh cases pass under default 1.20 and explicit minimum 1.19. Published 2.5.0 authors are killed; distinct installed 2.5.10 readers finish those same runs once. Unmarked histories retain zero markers/activity sequence 1; genuine duplicate histories retain both markers/activity sequence 3 and their full checkpoint prefix. Fresh helpers retain one marker/activity sequence 2.
    • Actual cooperative cleanup passes after killing a published 2.5.9 author. The original request, 60-second deadline and committed delivery at sequence 2 survive. The committed Avro cleanup result independently verifies integer 1, boolean true and null helper projections. The terminal result remains canonical cancellation.
    • The unchanged six-case ordinary task-family driver passes in 21.24 seconds. Full HTTP capture includes cloned query clients, eight successful registrations, eight successful deregistrations and 158 query-poll replies, with no HTTP error. API histories and final task/registration state are retained. This does not explain Explain intermittent first-task startup stalls with published Python workers #123's historical stalls.

    Task containers, database storage and network are removed. #103 remains open for pinned consumer adoption and remaining shared/native/embedded qualification in #744, SDK Rust #80 and Server #325. Pending Nexus work from main is excluded from this stable release.

  8. rmcdaniel commented on Oct 13, 2026

    @rmcdaniel
    MemberAuthor

    Normally installed Python 2.5.10 now consumes eight copied, actual embedded histories from the Workflow 2.5.13 qualification. Its loaded workflow module hash matches exact published source caf006be741ad928f4f5e5d7283dce37c2e99f02.

    • The 2.5.12 mixed-helper author and 2.5.13 activity/replacement/cold-reader captures preserve [true,1,null,true], one physical marker at 1 and activity at 2.
    • The genuine 2.5.6 repeated-marker author, prior 2.5.12 activity/replacement and 2.5.13 cold-reader captures preserve [true,true], two physical markers at 1/2 and activity at 3, including both distinct original microsecond clocks.
    • Pending prefixes remain pending without a new command. Completed activity/history reads retain exact integer 9007199254740993, null and Unicode output. Every original file, copied file and in-memory input remains unchanged.

    These are Python cold-reader marker/typed-payload checks on actual embedded rows. Full clock-result parity and cross-language worker takeover are not qualified: embedded row clock fields differ from the Python replay clock input/API, and the observed clock output does not equal the recorded PHP result columns. No synthetic timestamp mapping was applied and no workflow was admitted or history rewritten. All test resources are retired.

    The agreed numeric decision/range/helper projection contract is unchanged. This issue remains open for the broader shared/native and affected-consumer acceptance; downstream consumer updates remain separately owned.

  9. rmcdaniel commented on Oct 13, 2026

    @rmcdaniel
    MemberAuthor

    Additional normally installed Python 2.5.11 cold-reader checks pass on all eight copied actual histories from Workflow's zero/range qualification. The loaded workflow module remains byte-identical to published 2.5.10; 2.5.11 changes reporting only.

    • Fresh Workflow 2.5.13 version 0 keeps numeric/boolean/void projections [0,false,null,0,false], one marker at 1 and activity at 2.
    • Genuine Workflow 2.5.6 version 7 keeps both recorded ranges [0,7] / [5,7] under current [0,10] / [5,20], numeric [7,7], both physical markers at 1/2, their distinct original clocks and activity at 3.
    • Pending prefixes emit no new command; completed activity/replacement/cold captures retain exact integer 9007199254740993, null and Unicode. Original/copy/in-memory hashes remain unchanged.

    Full clock-result parity and cross-language worker takeover remain unqualified; no timestamp mapping, history rewrite or admission was introduced. Rust's connected cases and the reserved shared/native cohort remain separate owner work.

    Sample's published consumer receipt now verifies its normally installed Python 2.5.10 in both published architectures. Its qualified tuple stays unchanged. This issue remains open for its remaining shared/native acceptance; the Python marker implementation and actual killed-worker successor outcomes are already published at the original delivery receipt.

  10. rmcdaniel commented on Oct 13, 2026

    @rmcdaniel
    MemberAuthor

    The existing Python producer captures contain consistent duplicate markers, but not the newly requested exact [0,1] then [1,1] pair. Published Python 2.5.0 source emits a fresh marker for each get_version call, so I will verify that unchanged normally installed producer in a separate actual Server 2.6.1 run before claiming the pair.

    The workflow calls get_version("retained-patch",0,1), then get_version("retained-patch",1,1), followed by one pending marker.v1.typed activity with an explicit separate queue, max_attempts=1, backoff_seconds=[], and 120/600-second start-to-close/schedule-to-close bounds. Intended output is {"versions":[1,1],"typed":{"integer":9007199254740993,"optional":null,"unicode":"range λ"}}. Actual fields and original task/run/lease bounds will be retained as produced. Any unsupported profile must be settled before a dependent reader; no synthetic duplicate, history rewrite, SDK behavior change or shared-cohort adoption is involved.

  11. rmcdaniel commented on Oct 13, 2026

    @rmcdaniel
    MemberAuthor

    The requested genuine differing-bound producer and normally installed cold successor now pass. Published Python 2.5.0 (04790ae42e8da0a83f65026f096572b547b7baa8, wheel 0f69fdb6e6103ab57ce6bc0f802504463b1498745e874a32ed9de58dabeb40be) actually emits numeric 1 twice with bounds [0,1] / [1,1], at command positions 1/2 and history positions 3/4, before one pending activity at command 3/history 5. Loaded Client/Worker/workflow bytes match the exact tag and wheel.

    The actual activity profile is snapshot 1, max attempts 1, empty backoff and non-retryable types, start-to-close 120s, schedule-to-close 600s, and null remaining timeouts. After the original author is killed and that activity completes once, normally installed Rust 3.5.2 cold-replays the actual original successor using deprecation, patched, then wider get_version(0,10). The Rust owner’s exact build and execution receipt records the registry identity and reader hash.

    An independent owner read verifies original run 01m4srms2kdtsc070jf3tnbg2p completed at 04:40:02.018631Z, returning {"versions":[1,1],"typed":{"integer":9007199254740993,"optional":null,"unicode":"range λ"}}. The initial five-event prefix stays field-exact, with two markers, one scheduled/completed activity, one invocation and one workflow completion. Actual stored activity deadline 04:44:32.517705Z and run/execution deadlines 04:54:30.098752Z remain unchanged. The original 30s activity-worker terminal observer expired before the independent reader completed; its log is preserved alongside the later read-only verification. No second admission or budget change replaced it.

    This new profile supplies genuine version-one author/replacement evidence. Older embedded clock/takeover limitations remain separate; the supported bridge-wire clock gap is now #146. Both owners’ runtime/build resources are retired. Shared native/cohort acceptance remains with Server #325; frozen actors, original histories and the shared runner are unchanged.

  12. rmcdaniel commented on Oct 14, 2026

    @rmcdaniel
    MemberAuthor

    The remaining shared marker gate is now delivered in Server #435, exact main 7b53fc26db4e69a8013b6f1ad991fd1d98266833 / tree b9f7e9bd652586e23a4e80aa58433c79c1e51ea6. All six original fresh-main database jobs and applicable source/boundary gates pass.

    Together with the published SDK correction and earlier normally installed cold-worker successors, this completes patch insertion before retained pending/completed operations, frozen decisions, strict range refusal/restoration, genuine duplicate-bound histories, deprecation and shared PHP/Python/Rust/embedded marker cases. The shared cohort also verifies the full explicit retry policy without trimming its unused entries. Original marker prefixes, recorded times and repair boundaries remain preserved.

    The SDK marker defect is resolved. Python's settled workflow-start clock and PHP's per-command clock remain distinct. The separate query/cancellation, runtime performance, migration and native-release work keeps its own acceptance criteria. Existing frozen actors retain their identities.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions