Skip to content

feat: purge the CDN when the registry evicts its own caches - #2215

Open
netomi wants to merge 2 commits into
feat/surrogate-keysfrom
feat/cdn-purge
Open

netomi wants to merge 2 commits into
feat/surrogate-keysfrom
feat/cdn-purge

Conversation

@netomi

@netomi netomi commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Second step towards #1889. Stacked on #2214 — review that first; this PR's diff against it is the purge machinery alone.

What sends the purge

CacheService already knows every point at which a cached answer stopped being true: a version published, an extension deprecated or deleted, a namespace changed, a review posted. A CDN is one more cache, so the purge goes out from there, in the terms the responses are already tagged with (ext/<namespace>/<extension>, ns/<namespace>).

Two things CdnPurgeService takes care of

After the commit, never inside it. Those eviction points run inside @Transactional methods. A purge sent there races the commit: the CDN refetches, gets the row as it was, and caches that — leaving it staler than if nothing had been purged at all. The keys are collected on the transaction and sent from afterCompletion, and a transaction that rolled back purges nothing, because whatever it was going to change did not happen.

One purge per transaction. Publishing a version evicts the extension JSONs, the latest version and the namespace details separately — three calls naming two keys. They are collected into a set and sent once.

Delivery

The purge is a JobRunr job, following the pattern already used for mail and migrations:

  • a CDN that is briefly unreachable is retried rather than losing the purge;
  • a slow purge API never holds up a publish;
  • a purge that fails for good is counted (openvsx_cdn_purge_failed_keys_total), because it is invisible from the outside — the registry answers correctly while the CDN keeps serving what it had, and the only symptom is a reader seeing an old version until the cached response expires.

Fastly's bulk purge takes at most 256 keys, so larger sets are split. Soft purge is on by default: it marks the cached responses stale instead of dropping them, so the CDN keeps serving while it refetches and publishing a popular extension does not send everyone asking for it to the origin at once.

Configuration

Everything is off unless a provider is named, so a registry with no CDN in front of it does no extra work and needs no configuration. A half-configured provider is treated as no provider rather than as a startup failure — the registry serves fine without purging, and failing to boot over a CDN credential would be a poor trade.

property default
ovsx.cdn.purge.provider (empty — purges nothing)
ovsx.cdn.purge.fastly.service-id
ovsx.cdn.purge.fastly.api-token (needs the purge_select scope)
ovsx.cdn.purge.fastly.soft true
ovsx.cdn.purge.fastly.api-url https://api.fastly.com

All five documented in doc/configuration.md, under a new CDN Cache Purging section.

Testing

  • CdnPurgeServiceTest — one purge for everything a transaction changed, nothing before the commit, nothing at all for a rollback, straight through outside a transaction, nothing without a configured provider, and no leakage of one transaction's keys into the next.
  • FastlyCdnPurgeClientTest — the request Fastly actually receives (URL, Fastly-Key, Fastly-Soft-Purge, body), soft purge off, batching past 256 keys, and a failure surfacing rather than being swallowed.
  • CdnPurgeJobRequestHandlerTest — counters on success and failure, the rethrow that lets JobRunr retry, and the case where the provider was unconfigured between a job being enqueued and it running.

Full server suite green (1443 tests); pre-commit hooks pass, including the configuration-documentation check.

Next

The TTLs this makes safe to raise — Surrogate-Control for the CDN kept separate from the short Cache-Control browsers get, since a browser cache cannot be purged. Files stay out of it: they are served by redirect to storage, so their cached response carries no key and is not purgeable yet.

🤖 Generated with Claude Code

netomi and others added 2 commits September 15, 2026 20:37
First step towards #1889: a CDN in front of the registry needs to be told
what to drop when something changes, and telling it by URL does not scale -
one published version changes the extension JSON, its target-platform and
version variants, the version lists, the gallery's view of it, the namespace
and the files, each a different URL and several of them carrying query
parameters.

Name the thing instead. Every response about a namespace or an extension now
carries the keys it is made of - ns/<namespace> and ext/<namespace>/<name> -
so a purge can name what changed and every cached response carrying that key
goes, whatever its URL. The vocabulary matches what CacheService already
evicts internally: extensions and namespaces, never single versions.

Nothing acts on the header yet, and nothing here decides whether a response
may be cached: that stays with the Cache-Control each endpoint sets. Keys are
lower-cased because names are matched that way, and tagged in preHandle
because a response that streams its body is committed before postHandle runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Second step of #1889, on top of the surrogate keys: something has to send the
purge. CacheService already knows every point at which a cached answer stopped
being true - a version published, an extension deprecated or deleted, a
namespace changed - and a CDN is one more cache, so the purge goes out from
there, in the same terms the responses are tagged with.

Two things the service takes care of for its callers:

- After the commit, never inside it. Those eviction points run inside a
  transaction, and a purge sent there races the commit: the CDN refetches, gets
  the row as it was, and caches that - leaving it staler than if nothing had
  been purged. Rolled back transactions purge nothing.
- One purge per transaction. Publishing evicts the extension JSONs, the latest
  version and the namespace details separately: three calls naming two keys,
  sent once.

The purge itself is a JobRunr job, so a CDN that is briefly unreachable is
retried rather than losing the purge, and a slow API never holds up a publish.
A purge that fails for good is counted, because it is invisible from outside:
the registry answers correctly while the CDN keeps serving what it had.

Only Fastly is implemented, and only where a service id and token are
configured. Everything is off by default, so a registry with no CDN in front
of it does no extra work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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