Skip to content

Fence Backfiller tasks by generation - #11311

Open
chaptersix wants to merge 5 commits into
temporalio:mainfrom
chaptersix:sch-074
Open

Fence Backfiller tasks by generation#11311
chaptersix wants to merge 5 commits into
temporalio:mainfrom
chaptersix:sch-074

Conversation

@chaptersix

@chaptersix chaptersix commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

What changed?

  • Fence Backfiller tasks with a persisted generation instead of comparing their wall-clock schedule with the historical range cursor.
  • Use the persisted execution attempt count to recognize generation-zero continuations created by an older binary and restore the generation invariant when a new binary executes one.
  • Add lifecycle coverage for processed historical continuations, forward-dated ranges, and mixed-version task handoff.
  • Extend the CHASM test engine with a default namespace entry and a focused driver for due persisted pure tasks.

Background

Expected CHASM pure-task lifecycle

A CHASM pure timer is a physical wake-up for due logical tasks in the component tree. For each due logical task, CHASM calls its validator immediately before execution. An invalid task is a successful no-op: Execute is skipped, the node is marked dirty, and transaction close removes logical tasks whose validators return false (ExecutePureTask, closeTransactionCleanupInvalidTasks).

After a successful Execute, the handler must change component state so the processed logical task becomes invalid. It may also schedule a valid successor. CHASM explicitly relies on the processed task becoming invalid and being removed at transaction close (pure-task iteration contract).

stateDiagram-v2
    direction LR
    [*] --> Due
    Due --> Validate: physical timer finds logical task
    Validate --> Invalid: returns false
    Invalid --> Acknowledged: skip Execute and remove logical task
    Validate --> Execute: returns true
    Validate --> Retry: returns error
    Execute --> Retry: returns error
    Execute --> Complete: deletes Backfiller
    Execute --> Successor: schedules generation N+1
    Successor --> Committed: generation N is invalid and N+1 is valid
    Complete --> Acknowledged: commit deletion
    Committed --> Acknowledged: commit state and tasks
    Retry --> Due: physical queue retry
Loading

For a normal continuation, the expected validation results are:

  • Before execution: Validate(N) == true.
  • After successful execution schedules N+1: Validate(N) == false and Validate(N+1) == true.
  • After successful completion: the Backfiller and its logical tasks are deleted.
  • After an execution error that does not commit: generation N remains retryable.

What happens when validation is wrong?

The timer executor treats an invalid task as success and commits the cleanup (active pure-timer executor). A boolean validation result therefore does not consume a retry attempt and does not enter the DLQ path.

Validator failure Result
False negative before execution Execute is skipped. Transaction close removes the logical task, the physical timer completes successfully, and there is no retry or DLQ entry. If no other task can recreate the continuation, the Backfiller stalls.
False positive after execution The processed logical task remains in the tree even though the physical delivery completed successfully. It can execute again if that physical task is redelivered. After acknowledgement, it is not continuously rescheduled through the retry system and has no retry-attempt cutoff or DLQ transition because no error occurred. The retained task may also remain the earliest logical pure task and prevent creation of a physical timer for its successor (physical pure-task generation).
Validator or executor returns an error The transaction does not commit and the physical history task is retried with queue backoff. Unexpected errors may become terminal after the configured attempt limit and be sent to the history-task DLQ when enabled (queue error handling, DLQ defaults).

Backfiller validation

LastProcessedTime is the Backfiller's cursor in schedule time. A delayed CHASM task's ScheduledTime is in wall-clock time. The current validator treats the task as valid when:

task.ScheduledTime > backfiller.LastProcessedTime

The range bounds do not directly decide validity; they determine where the cursor can move after each partial batch. The same comparison therefore fails differently depending on the cursor's position relative to wall-clock time.

Existing behavior

Range only in the past

stateDiagram-v2
    direction LR
    [*] --> Due
    Due --> Executing: Validate = true
    Executing --> CursorAdvanced: Execute partial batch
    CursorAdvanced --> Retained: post-Execute Validate = true
    Retained --> Executing: redelivery
Loading

Range from the past through now

stateDiagram-v2
    direction LR
    [*] --> Due
    Due --> Executing: Validate = true
    Executing --> Compare: cursor advances toward now
    Compare --> Retained: cursor remains before task time
    Retained --> Executing: redelivery
    Compare --> OldTaskRemoved: cursor reaches task time
    OldTaskRemoved --> NextTask: continuation remains valid
Loading

Range from the past into the future

stateDiagram-v2
    direction LR
    [*] --> Due
    Due --> Compare: Validate before Execute
    Compare --> Executing: task time is after cursor
    Executing --> CursorAdvanced: Execute partial batch
    CursorAdvanced --> Retained: cursor remains before task time
    Retained --> Executing: redelivery
    Compare --> Removed: cursor is at or after task time
    CursorAdvanced --> Removed: cursor advances past continuation time
    Removed --> Stalled: Execute skipped and Attempt unchanged
Loading
Backfill range Existing behavior after a partial batch
Entirely in the past The historical cursor remains earlier than the delayed task's wall-clock time. The task validates before execution and still validates afterward, so transaction close does not remove the processed logical task. A redelivery can execute another batch.
Starts in the past and runs through approximately now The cursor normally remains at or before wall-clock time, so it behaves like a historical range: the processed task can remain valid. The margin is smaller, and the behavior flips if the cursor crosses the task timestamp.
Starts in the past and ends in the future It behaves like a historical range while the cursor is behind wall-clock time. Once a partial batch advances the cursor beyond the delayed task timestamp, the continuation fails validation before execution, Attempt does not advance, and the range stalls.

Desired behavior

Task validity should depend only on task lifecycle state, not on where the requested range falls on the calendar.

Backfill range Desired behavior
Entirely in the past The current task is valid before execution. A successful execution either deletes the completed Backfiller or schedules a new generation; the executed task is then invalid.
Starts in the past and runs through approximately now The same generation transition applies. Moving the range cursor toward wall-clock time does not change task validity.
Starts in the past and ends in the future The same generation transition applies even when the range cursor is later than the task's wall-clock timestamp. The continuation remains valid and the range keeps making progress.

Why this change?

A generation fence enforces the lifecycle above when the task is handled by the new code. Scheduling a replacement advances the persisted generation, making the prior payload invalid without consulting the range cursor.

A plain generation equality check was not sufficient for rolling upgrades. When an old binary successfully executes a generation-bearing task, it preserves the unknown state generation, advances Attempt, and schedules a generation-zero continuation. The attempt-aware compatibility path recognizes only that successor and re-establishes the generation invariant when a new binary executes it.

That compatibility path applies only after the old binary executes the task. Historical continuations pass the old high-water-mark validator and can use this handoff. A forward-dated continuation can be rejected by the old validator before Attempt advances, so the compatibility path cannot recover it. Forward-dated partial backfills already stall this way on the current implementation; this PR does not make that behavior worse, but the fix is not guaranteed for a continuation evaluated by an old executor during a rolling deployment.

Potential risks

The rolling-upgrade compatibility path relies on the persisted Backfiller attempt count advancing once per successful task execution. Tests cover ordinary generation invalidation and a new-binary → old-binary → new-binary continuation handoff.

An old binary can still remove a forward-dated continuation before executing it. Avoiding that limitation requires preventing mixed-version CHASM task execution or separately gating the new behavior; this PR does not add recovery for a logical task already removed by the old validator.

@chaptersix

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: 7e4c3c06d2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

The Backfiller validated its delayed tasks by comparing the task's wall-clock
scheduled time against LastProcessedTime, which for a range backfill is a
schedule-time cursor into the (often historical) range. The two clocks never
reconcile: for historical ranges an already-executed continuation stays valid,
so redelivery re-runs it and advances the cursor prematurely; for forward-dated
ranges the fresh continuation is invalidated immediately and the backfill stalls
after the first batch.

Stamp a generation on each BackfillerTask and track it on BackfillerState.
scheduleTask bumps the generation, and Validate accepts a task only while it
matches the current generation, so a superseded or redelivered task is rejected.
LastProcessedTime is now used purely as the range cursor. Backward compatible:
in-flight tasks (generation 0) stay valid until superseded on their next
reschedule.

Also fix two fidelity gaps in the shared chasmtest engine that these tests
surfaced:
- Supply a default namespace entry (and WithNamespace option) so components that
  read namespace-filtered config can run under the engine.
- Make NextTransitionCount stable within a transaction, advancing once per
  commit, so newly scheduled tasks are actually persisted rather than silently
  dropped by the task-persistence gate.
Add Engine.FirePureTasks to deliver a component's due pure tasks through the
framework's real timer path, and drive the new backfiller lifecycle tests
through the actual create/patch handler.
# Conflicts:
#	chasm/chasmtest/test_engine.go
Use persisted execution attempts to recognize generation-zero continuations created by an older binary, reject the task it already processed, and restore the generation invariant on the next execution.

Exercise the persisted logical-task lifecycle on the updated CHASM test engine and keep the regression setup local to its tests.
@chaptersix chaptersix changed the title fix(scheduler): fence Backfiller tasks by generation, not range cursor Fence Backfiller tasks by generation Jul 28, 2026
Comment thread chasm/lib/scheduler/backfiller_lifecycle_test.go
@chaptersix
chaptersix marked this pull request as ready for review July 28, 2026 20:31
@chaptersix
chaptersix requested review from a team as code owners July 28, 2026 20:31
@chaptersix
chaptersix requested a review from awln-temporal July 28, 2026 20:31

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 18eea9ba96

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +65 to +67
if taskGeneration == 0 {
// An old binary schedules generation-zero tasks and advances only Attempt.
valid = currentGeneration == 0 || attempt >= currentGeneration

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep forward backfills alive across old-binary handoff

During a rolling upgrade, a forward-dated backfill can still stall before this compatibility branch is reached: if an old binary receives the generation-bearing continuation, its old Validate ignores the generation and calls validateTaskHighWaterMark; because LastProcessedTime is in the future while the delayed task uses wall-clock time, it rejects and removes the continuation without scheduling the generation-zero successor assumed here. The mixed-version test only increments Attempt and therefore skips this real old-validator path; preserve a continuation across that handoff or explicitly gate mixed-version processing.

Useful? React with 👍 / 👎.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant