Skip to content

Conversation

@deathbyknowledge
Copy link
Contributor

@deathbyknowledge deathbyknowledge commented Nov 17, 2025

this.destroy() in handlers

Currently, calling this.destroy inside a schedule won't clean up the Agent properly. There are 2 issues at play:

  • this.destroy deletes all tables from SQLITE (and all other storage). This causes an error in the alarm handler since the Agent alarm tries to update the cf_agent_schedules table after the schedule callback is run.
  • this.ctx.abort throws an uncatchable error to force DO eviction. When this happens inside the alarm handler the runtime attempts a re-try.

To address both, this PR:

  1. Sets an in-memory flag this.destroyed = true from within this.destroy to skip SQLITE updates in the alarm handler whenever it's set.
  2. Now yields this.ctx.abort to the event loop so we don't throw the error in the alarm handler's context. Since we're always in a single-threaded runtime we ensure that the handler completes before the this.ctx.abort task in the event loop is executed.
    (IO ops after this.destroy could see this.ctx.abort being called before completion but adding anything after this.destroy is an anti-pattern anyway so I think it's fine)

Fixes #616.

this.ctx.blockConcurrencyWhile(...) in constructor

Inside the Agent there's a manual call to await this.alarm() (for which we use the bCW) to run due schedules. Since we're using this.ctx.storage.setAlarm internally, the alarm method is called by the runtime after the constructor anyway, so we can get rid of our manual call.

So basically:
BEFORE: constructor() -> await this.alarm() (called manually) -> alarm() (called by the runtime)
NOW: constructor() -> alarm() (called by the runtime)

Fixes #600

PS: There are 2 seemingly unrelated changes included:

  1. this._scheduleNextAlarm wasn't taking into account schedules for now (e.g. this.schedule(0, "destroy")).
  2. Set observability = undefined in the test DOs to reduce stdout noise.

@changeset-bot
Copy link

changeset-bot bot commented Nov 17, 2025

🦋 Changeset detected

Latest commit: 8dffadb

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
agents Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@claude
Copy link

claude bot commented Nov 17, 2025

Claude Code Review

Issues found:

  1. Race condition in destroy flag checks (packages/agents/src/index.ts:1317, 1326, 1334)

    • The _destroyed checks are placed after SQL operations that can fail when tables are dropped
    • If destroy() runs between executing a schedule callback and the SQL UPDATE/DELETE, those operations will throw
    • Move the if (this._destroyed) return; check to the beginning of the alarm loop (after line 1275) to short-circuit before any DB operations
  2. Test reliability issue (packages/agents/src/tests/alarms.test.ts:22)

    • 50ms timeout is arbitrary and may cause flaky tests on slower CI environments
    • Consider using vi.advanceTimersByTime() with fake timers, or increase timeout to 200-500ms
  3. Missing test coverage:

    • No test verifying that cron schedules are properly updated (not deleted) when _destroyed=true
    • No test for the blockConcurrencyWhile removal fix (issue Schedules can't process long AI requests #600)
    • No test for immediate schedules (this.schedule(0, ...))

Suggested fix for #1:

// After line 1275, add early return:
if (result && Array.isArray(result)) {
  for (const row of result) {
    if (this._destroyed) return; // Check at loop start
    
    const callback = this[row.callback as keyof Agent<Env>];
    // ... rest of loop
    
    // Remove the three other _destroyed checks since we check at loop start

Otherwise the approach is sound and properly addresses the reported issues.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Nov 17, 2025

Open in StackBlitz

npm i https://pkg.pr.new/cloudflare/agents@653

commit: 8dffadb

threepointone pushed a commit to cloudflare/cloudflare-docs that referenced this pull request Nov 17, 2025
Updates documentation to reflect that destroy() can now be safely called
within scheduled task callbacks. The Agent properly handles cleanup by:
- Setting an internal flag to skip remaining database updates
- Yielding ctx.abort() to the event loop for clean alarm handler completion

This addresses the fix in cloudflare/agents#653 where calling destroy()
inside a schedule previously caused errors.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@threepointone
Copy link
Contributor

📚 Documentation has been updated to reflect these changes.

Documentation PR: cloudflare/cloudflare-docs#26558

The following documentation pages have been updated:

  • /agents/concepts/agent-class/ - Updated destroy() method documentation to note it can be safely called within scheduled tasks
  • /agents/api-reference/schedule-tasks/ - Added note about calling destroy() in scheduled task callbacks

The documentation now explains that when destroy() is called from within a schedule callback:

  • An internal flag is set to skip remaining database updates
  • ctx.abort() is yielded to the event loop to ensure clean alarm handler completion

threepointone pushed a commit to cloudflare/cloudflare-docs that referenced this pull request Nov 17, 2025
Updates documentation to reflect that destroy() can now be safely called
from within scheduled task callbacks. The Agent SDK now:
- Sets an internal flag to prevent database updates after destruction
- Defers ctx.abort() to the event loop to allow handlers to complete

Related to cloudflare/agents#653

Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@threepointone
Copy link
Contributor

Documentation has been updated to reflect this change in cloudflare-docs#26558.

threepointone pushed a commit to cloudflare/cloudflare-docs that referenced this pull request Nov 17, 2025
Updates schedule-tasks.mdx to reflect changes from cloudflare/agents#653:

- Add example showing immediate task scheduling (delay = 0)
- Document safe usage of destroy() within scheduled callbacks
- Explain that Agent SDK defers destruction to ensure scheduled tasks complete

Addresses the fix for issue #616 where calling destroy() inside a schedule
would cause errors and retry loops.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@threepointone
Copy link
Contributor

📚 Documentation Updated

The documentation has been updated to reflect the changes in this PR.

Updated documentation:

  • Schedule tasks - Added section on lifecycle management with scheduled tasks and immediate scheduling examples

Cloudflare Docs PR: cloudflare/cloudflare-docs#26558

Changes include:

  • Documentation of safe destroy() usage within scheduled callbacks
  • Example showing immediate task scheduling (delay = 0)
  • Explanation of how the Agent SDK defers destruction to ensure scheduled tasks complete successfully

This comment was automatically generated by the documentation sync workflow.

@threepointone
Copy link
Contributor

Documentation Sync

📚 Documentation has been synced to cloudflare-docs

Docs PR: cloudflare/cloudflare-docs#26558

Changes synced:

  • Updated destroy() method documentation to clarify behavior when called within scheduled tasks
  • Added example showing destroy() working correctly in scheduled callbacks
  • Documented support for immediate schedules (0 second delay)
  • Added lifecycle management section with self-destructing agent example

The documentation now reflects the fixes for calling destroy() in scheduled contexts and the support for immediate schedule execution.

@deathbyknowledge deathbyknowledge marked this pull request as ready for review November 17, 2025 13:12
@deathbyknowledge deathbyknowledge changed the title add destroyed flag and yield ctx.abort Schedule fixes Nov 17, 2025
@threepointone
Copy link
Contributor

Documentation for this PR has been updated in cloudflare/cloudflare-docs#26558

@threepointone
Copy link
Contributor

📚 Documentation update: cloudflare/cloudflare-docs#26558

This PR documents the schedule bug fixes, including:

  • Support for immediate schedule execution (delay=0)
  • Safe destroy() calls within scheduled callbacks
  • Internal implementation details (destroyed flag and event loop yielding)

Copy link
Contributor

@mattzcarey mattzcarey left a comment

Choose a reason for hiding this comment

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

looks good. I was seeing this all the time for the playground :))

Copy link
Contributor

@threepointone threepointone left a comment

Choose a reason for hiding this comment

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

some notes, will stamp after clarification.

"agents": patch
---

add destroyed flag and yield ctx.abort
Copy link
Contributor

Choose a reason for hiding this comment

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

this isn't clear, can you make it just a bit clearer what it does?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated

}

export class TestCaseSensitiveAgent extends Agent<Env> {
observability = undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

why removing observability on all these agents?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

They flood stdout with logs about schedules, connections etc. I've never found them particularly useful (at least in the test runs).

There's still a lot of workerd internal logs but I appreciate less noise unless others find them useful

Copy link
Contributor

Choose a reason for hiding this comment

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

fair. tbh I'm very unhappy with our observability stuff anyway, I want to rip it all out.

`;

// execute any pending alarms and schedule the next alarm
await this.alarm();
Copy link
Contributor

Choose a reason for hiding this comment

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

really wish I could remember why we did this. but I'll keep an.eye out for it.

@deathbyknowledge deathbyknowledge merged commit 412321b into main Nov 18, 2025
12 checks passed
@deathbyknowledge deathbyknowledge deleted the allow-destroy-in-schedules branch November 18, 2025 10:55
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.

Impossible to fully delete agent via a schedule Schedules can't process long AI requests

3 participants