Skip to content

fix(io): prevent self-destruction of in-memory durable object alarm task - #7442

Open
spideydotjs wants to merge 1 commit into
cloudflare:mainfrom
spideydotjs:fix/in-memory-durable-object-alarm-crash
Open

spideydotjs wants to merge 1 commit into
cloudflare:mainfrom
spideydotjs:fix/in-memory-durable-object-alarm-crash

Conversation

@spideydotjs

Copy link
Copy Markdown

Overview

Fixes #7190 and fixes #7191.

When a Durable Object alarm fires under durableObjectStorage = (inMemory = void), workerd previously terminated with:

*** Fatal uncaught kj::Exception: kj/async.c++:2202: failed: Promise callback destroyed itself.

Root Cause

  1. For in-memory storage, neverFlush is true in ActorCache.
  2. When the alarm handler finishes execution, deferred alarm deletion in ActorCache marks the alarm deleted and immediately calls hooks.updateAlarmInMemory(kj::none) synchronously.
  3. HooksImpl::updateAlarmInMemory() was unconditionally resetting maybeAlarmPreviewTask = kj::none;.
  4. However, runAlarm() was called by the coroutine inside maybeAlarmPreviewTask. Clearing maybeAlarmPreviewTask while it was suspended waiting on runAlarm() caused the coroutine to destroy its own promise node from within its own execution stack, tripping KJ's assertion.

Solution

  1. Track isRunningAlarm in HooksImpl.
  2. While an alarm is actively executing, updateAlarmInMemory() stores the updated scheduledAlarmTime without destroying maybeAlarmPreviewTask.
  3. After runAlarm() completes, the coroutine inspects scheduledAlarmTime to either schedule the next alarm or exit cleanly.
  4. Added actor-alarms-in-memory-test.wd-test to test in-memory alarm firing and prevent future regressions.

When an alarm fires in a Durable Object configured with in-memory storage (durableObjectStorage = (inMemory = void)), completing the alarm handler triggers deferred alarm deletion in ActorCache. Under in-memory storage, neverFlush is true, which immediately and synchronously invokes HooksImpl::updateAlarmInMemory(kj::none).

Previously, updateAlarmInMemory() unconditionally set maybeAlarmPreviewTask = kj::none. Because runAlarm() was invoked by the coroutine running inside maybeAlarmPreviewTask, clearing the task while it was suspended awaiting runAlarm() destroyed the coroutine frame from within its own callback stack, crashing workerd with "Promise callback destroyed itself".

Fix this by tracking whether an alarm execution is currently in flight. When an alarm is running, updateAlarmInMemory() records the updated alarm time without destroying the active task. After runAlarm() finishes, the task loop inspects the scheduled alarm time to determine whether to schedule a subsequent alarm or exit cleanly.

Fixes cloudflare#7190, fixes cloudflare#7191
@spideydotjs
spideydotjs requested review from a team as code owners September 20, 2026 08:46
@spideydotjs

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

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

Labels

None yet

Projects

None yet

1 participant