Skip to content

workflow: fix replay failures for disposed hooks - #419

Merged
fantix merged 2 commits into
mainfrom
fantix/workflow-disposed-hook-replay
Sep 28, 2026
Merged

fantix merged 2 commits into
mainfrom
fantix/workflow-disposed-hook-replay

Conversation

@fantix

@fantix fantix commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

This PR fixes a bug where disposing a hook can incorrectly cause workflow replay to fail, as in the following example (step and hook definitions omitted):

@workflows.workflow
async def process_order(order_id):
    inventory = asyncio.create_task(check_inventory(order_id))
    order = await load_order(order_id)  # Runs alongside the inventory check.

    approval = Approval.wait(token=order.approval_token)
    if not await inventory:
        approval.dispose()  # Out of stock: approval is no longer needed.
        return await notify_out_of_stock(order_id)

    decision = await approval
    approval.dispose()
    return await apply_decision(order_id, decision)
  1. The order loads while the inventory check is still pending. The workflow creates the approval hook, then suspends at await inventory.
  2. Before the runtime persists the hook registration, the inventory check records an out-of-stock result.
  3. On replay, that result is read first. The workflow calls approval.dispose(), removing the hook from active suspensions.
  4. Replay then reaches the hook registration event. It only checks active suspensions (context.suspensions), so it incorrectly reports the hook as missing.

This PR fixes to look up disposed hooks in the hook registry (context.hooks) so their registration events can still be processed.

@fantix
fantix force-pushed the fantix/workflow-disposed-hook-replay branch from 3e00321 to f1612b1 Compare September 25, 2026 15:52
@fantix
fantix force-pushed the fantix/workflow-disposed-hook-replay branch from 17da093 to 2fe1a48 Compare September 25, 2026 21:33
@fantix fantix changed the title workflow: fix replay after early hook disposal workflow: fix replay failures for disposed hooks Sep 25, 2026
@fantix
fantix requested a review from a team September 25, 2026 21:47
@fantix
fantix added this pull request to stack #422 September 25, 2026 21:47
@fantix
fantix marked this pull request as ready for review September 25, 2026 21:47
Move the existing subscription check into the hook_received handler,
without changing behavior.
During replay, a workflow can dispose a hook before its recorded
registration is replayed. Looking only in active suspensions then
incorrectly treats that hook as missing.

Use self.hooks to find disposed hooks when replaying registration
events and checking for operation mismatches.
Comment on lines +1245 to +1247
if hook is None:
# Internal step-cancellation hooks only live in suspensions.
hook = self.suspensions[event.correlation_id]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we put the step-cancellation hooks in hooks instead/also?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes! Let me do that in a new PR.

@e0-gh-vercel-connect e0-gh-vercel-connect Bot mentioned this pull request Sep 30, 2026

This branch was successfully deployed

1 active deployment
ci — f313c9d9 Deployed Sep 27, 2026 by fantix via Test (Linux, py3.12) #1597
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.

2 participants