Skip to content

fix(core): batch plugin content cache purges during scheduled tasks - #4409

Open
Stormster wants to merge 2 commits into
emdash-cms:mainfrom
Stormster:Stormster/issue-4408-fix
Open

Stormster wants to merge 2 commits into
emdash-cms:mainfrom
Stormster:Stormster/issue-4408-fix

Conversation

@Stormster

@Stormster Stormster commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do?

Batches plugin content-action cache invalidation within each runScheduledTasks() cron batch. Tags are deduplicated and flushed once in finally, including after a task or executor bookkeeping failure. This reduces the purge calls generated by plugin import jobs.

The invalidator is scoped to the scheduled invocation. Concurrent HTTP actions remain immediate, sandbox callbacks retain the batch across RPC, and late hook writes invalidate immediately after collection closes. Scheduled publishing keeps its existing per-collection onPublished callback.

Closes #4408

Type of change

  • Bug fix
  • Feature (requires an accepted feature plan or RFC)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Your connection to this change

  • I use EmDash and ran into the problem this PR addresses
  • I maintain a tool, dependency, or service that EmDash uses, and this change concerns that integration
  • A Maintainer approved this specific change before I opened the PR

I want to help resolve the problem another EmDash user reported in #4408. This contribution reproduces the repeated purge calls with real plugin cron tasks and SQLite, adds a fix, and verifies the behavior with regression tests. I am contributing as a community member; I am not claiming personal production impact, an integration-maintainer role, or prior Maintainer approval.

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable). Do not include messages.po changes except in translation PRs — a workflow extracts catalogs on merge to main.
  • I have added and reviewed the user-facing changeset (if this PR changes a published package)
  • New features link to their merged design PR: https://github.com/emdash-cms/emdash/pull/...
  • I have included screenshots below if this PR changes the UI

No admin strings or UI changes; translation and screenshots are not applicable. This fixes reproduced repeated purge calls without adding a public API; no feature plan is linked.

AI assistance disclosure

  • This PR includes AI-generated code or text — model/tool: Codex (GPT-6)

Screenshots / test output

Screenshots: Not applicable.

Before: scheduled-cache-invalidation.test.ts ran real plugin cron tasks against SQLite. Two publications made two invalidator calls; the regression failed with Number of calls: 2 where one deduplicated call was expected.

After: 60 targeted tests pass across scheduler, cron correctness, route invalidation, and sandbox action settlement. Coverage includes task failures, executor bookkeeping failures, empty and successive ticks, provider errors, concurrent HTTP writes, late hook writes, sandbox callbacks arriving outside the scheduler's async context, and trusted/sandboxed scheduled-publication hooks that write after maintenance returns.

Validation:

  • pnpm format
  • pnpm build from repository root
  • pnpm lint:quick
  • pnpm --silent lint:json — zero diagnostics
  • Root package typecheck targets, run with --workspace-concurrency=1
  • pnpm --filter emdash exec vitest run tests/integration/runtime/scheduled-cache-invalidation.test.ts tests/integration/runtime/plugin-action-settlement.test.ts tests/integration/astro/plugin-publication-cache-invalidate.test.ts tests/unit/scheduled-publish.test.ts tests/integration/runtime/plugin-cron-route.test.ts tests/integration/plugins/cron-correctness.test.ts

Local tests verify content commits and purge callback counts. Live Cloudflare rate-limit errors and the reporter's CRM feed were not exercised. Automated review found that scheduled-publication after-hooks had lost their invalidator. Maintenance now retains an immediate invalidator in its own async scope; two added regressions fail with zero purges on the previous implementation and pass with the fix. Hosted provider behavior remains unverified.

Batching trade-off: a Worker killed by a hard CPU or wall-clock limit before the final flush can leave already committed cron writes unpurged. Ordinary task/executor errors still execute the final flush; scheduled publishing keeps incremental per-collection invalidation.

@changeset-bot

changeset-bot Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 911ce98

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

This PR includes changesets to release 12 packages
Name Type
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/plugin-test Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/admin Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
create-emdash Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds 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

@pkg-pr-new

pkg-pr-new Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/admin@4409

@emdash-cms/auth

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/auth@4409

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/auth-atproto@4409

@emdash-cms/blocks

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/blocks@4409

@emdash-cms/cloudflare

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/cloudflare@4409

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/contentful-to-portable-text@4409

emdash

npm i https://pkg.pr.new/emdash-cms/emdash@4409

create-emdash

npm i https://pkg.pr.new/emdash-cms/emdash/create-emdash@4409

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/gutenberg-to-portable-text@4409

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-cli@4409

@emdash-cms/plugin-test

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-test@4409

@emdash-cms/plugin-types

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-types@4409

@emdash-cms/registry-client

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-client@4409

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-lexicons@4409

@emdash-cms/registry-loader

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-loader@4409

@emdash-cms/registry-moderation

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-moderation@4409

@emdash-cms/registry-verification

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-verification@4409

upgrade-emdash

npm i https://pkg.pr.new/emdash-cms/emdash/upgrade-emdash@4409

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/sandbox-workerd@4409

@emdash-cms/x402

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/x402@4409

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-ai-moderation@4409

@emdash-cms/plugin-ai-search

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-ai-search@4409

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-atproto@4409

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-audit-log@4409

@emdash-cms/plugin-color

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-color@4409

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-embeds@4409

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-field-kit@4409

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-forms@4409

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-webhook-notifier@4409

commit: 911ce98

@emdashbot emdashbot Bot left a comment

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.

Batching plugin content-action cache tags per cron tick is the right approach for #4408. Tags are collected in an AsyncLocalStorage scope, deduplicated, and purged once in finally. Late writes fall through to an immediate purge. This fits the shared-runtime architecture better than mutating the global invalidator. Both reviewers found the same regression outside the cron batch. The PR removes the line that stored options.invalidateContentCache as the runtime's default invalidator, and the new scope covers only runPluginCronTick. Plugin content actions triggered by scheduled publishing's content:afterPublish hooks, which run afterward in runScheduledMaintenanceTick, therefore get no invalidator and never purge on Cloudflare. A regression test for that path is needed. One reviewer also noted a non-blocking trade-off. Purges are deferred to the end of the batch, so if the Worker hits its CPU or wall-clock limit mid-tick, plugin writes that already committed get no purge. Flushing after each task would bound that exposure. Both reviewers reviewed statically.


Reviewed by anthropic/claude-opus-5-5 and openai/gpt-6.1-sol.

if (this.cronExecutor) {
try {
processed = await this.cronExecutor.tick();
processed = await this.runPluginCronTick(options.invalidateContentCache);

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.

[needs fixing] This PR removes if (options.invalidateContentCache) this.pluginContentCacheInvalidator = options.invalidateContentCache;. No production code calls setPluginContentCacheInvalidator, so that line was the only thing that set the fallback used by runPluginContentAction on Cloudflare.

The new ALS scope only covers runPluginCronTick. Scheduled publishing runs afterwards, in runScheduledMaintenanceTick → publishScheduledWithFence → handleContentPublish. That publish fires content:afterPublish hooks through runDeferredContentHook.

  • Trusted plugin hooks: content actions in these hooks (update, addEntryTerms, publish) get no explicit invalidator, no invocation invalidator, and no ALS store.
  • Sandboxed plugin hooks: invokeHook calls begin without an invalidator, so they end up in the same state.

Before this PR, those writes fell back to invalidateContentTags from createScheduledHandler. Now the invalidator is undefined, so they never purge the cache. Plugin edits made in response to scheduled publishes stay stale at the edge. onPublished cannot cover this: it receives only entries promoted by the scheduled sweep, not the hook's additional writes.

One fix is to keep the invocation's immediate (non-collecting) invalidator available during maintenance and its deferred hooks, overriding it with the collector only for the plugin cron tick. For example, run runScheduledMaintenanceTick inside an immediate scheduledContentCacheInvalidator.run(options.invalidateContentCache, …), or pass the invalidator through explicitly. That keeps the PR's goal of not mutating the shared default from a scheduled invocation.

Please add a regression test: a scheduled publication's content:afterPublish hook performs a content action on another, unscheduled entry (for example in another collection) during a scheduled-publish sweep. Verify that entry's tags reach invalidateContentCache.

@Stormster
Stormster force-pushed the Stormster/issue-4408-fix branch from f30f338 to 911ce98 Compare October 26, 2026 14:31

@emdashbot emdashbot Bot left a comment

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.

The follow-up takes the right approach: maintenance receives an invocation-scoped immediate invalidator, while plugin cron actions retain their deduplicated batch.

The earlier finding is addressed. I traced scheduled publishing through deferred hooks and invocation invalidator capture, and reviewed the added trusted and sandboxed regression tests, including callbacks arriving outside the scheduler’s async context. The tests cover hook writes after maintenance returns without relying on onPublished to invalidate those additional writes.

No further issues found in the follow-up. Reviewed statically; I did not run tests or verify live Cloudflare behavior.


Follow-up review by openai/gpt-6.1-sol, scoped to the changes since the last review.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Batch cache invalidation of plugin content actions per scheduled tick

1 participant