feat(19.3-04): cooler site-cache purge service with site_cache.purge_url and purge_token
- cooler.SiteCachePurger moved from the Journal plugin: loopback-only http, 32-character token, 2 s detached request, no token in logs or errors - NewSiteCachePurgerFor names caller keys (for plugin alias keys) - PurgeSiteCacheAfterCommit through lagoon.AfterCommit, one POST per save - SiteCacheFromApp for hooks outside serve; Install publishes the purger unless a plugin already did, and fails boot on an invalid config - caching docs section, cooler README, configuration row
This commit is contained in:
@@ -94,3 +94,77 @@ The `serve` command installs cooler at start-up (`cooler.Install`) and publishes
|
||||
## Flush
|
||||
|
||||
`cooler.Cache.Flush` removes this application's cache keys and nothing else. The Redis driver walks `SCAN MATCH <prefix>cache:*` in batches of 500 and removes each batch with `UNLINK`; it never empties the whole Redis database, which would also drop rate-limit counters and other applications' keys. The memory driver empties the process cache.
|
||||
|
||||
## Site cache purge
|
||||
|
||||
A server-rendered site in front of the API usually caches its pages for a few minutes (a Nuxt `routeRules` `swr` window, for example). Without help, an edit in the admin shows on the site only when that window ends. cooler closes the gap: after an admin save commits, it sends the site one purge request, and the site drops its route cache, so the next page view renders the change.
|
||||
|
||||
Configure the target once for the application:
|
||||
|
||||
| Key | Environment | Meaning |
|
||||
|-----|-------------|---------|
|
||||
| `site_cache.purge_url` | `SUMMER_SITE_CACHE__PURGE_URL` | The site's purge route, for example `http://127.0.0.1:3000/api/__site__/cache/purge`. |
|
||||
| `site_cache.purge_token` | `SUMMER_SITE_CACHE__PURGE_TOKEN` | The bearer token, at least 32 characters (`cooler.SiteCachePurgeMinTokenLength`). |
|
||||
|
||||
Set both or neither; with neither, nothing is purged. The URL must be http or https with a host, without user info or a fragment, and plain http is accepted only for a loopback host (127.0.0.0/8, `::1`, `localhost`): the site normally runs on the same machine, and the token must not cross a network in clear. Use https for any other host. A wrong or half-set configuration stops `serve` at start-up with an error that names the key; neither errors nor log lines ever contain the token.
|
||||
|
||||
### What a purge sends
|
||||
|
||||
Each committed save sends one request, with no debounce:
|
||||
|
||||
```http
|
||||
POST /api/__site__/cache/purge
|
||||
Authorization: Bearer <site_cache.purge_token>
|
||||
Content-Type: application/json
|
||||
|
||||
{"kind":"faq","slug":"shipping"}
|
||||
```
|
||||
|
||||
The kind names what was saved (a plugin picks it, for example `faq`, `menu`, `menu_entry`, `post` or `category`) and the slug which record; the site may log them. The site answers `{"purged": <n>}`, and cooler logs `cooler: site cache purged` with the kind, slug, status and count.
|
||||
|
||||
### Timing and failures
|
||||
|
||||
`cooler.PurgeSiteCacheAfterCommit` registers the request on the save's transaction through `lagoon.AfterCommit`. It is sent only after the commit, so a page view between the purge and the commit cannot re-cache the old content, and a save that fails or is rolled back sends nothing. Outside a transaction it is sent at once.
|
||||
|
||||
A purge never fails a save. The request is bounded by `cooler.SiteCachePurgeTimeout` (2 s) and detached from the admin request's cancellation; an unreachable site, an error status or a timeout is logged as one `cooler: site cache purge failed` warning and the save answers as usual.
|
||||
|
||||
### Purging from a plugin
|
||||
|
||||
The `serve` command publishes the purger at start-up (`cooler.Install`), and `cooler.SiteCacheFromApp` returns it, or builds it from the keys in commands and tests that do not run `serve`. An admin controller calls it from its after-save hooks, which run inside the save transaction, and takes that transaction from `cabana.TxFromContext`:
|
||||
|
||||
```go src=modules/cooler/example_test.go#articlesController.purge
|
||||
// purge registers one site-cache purge for the saved article, sent after the
|
||||
// save transaction commits (at once outside one). Without a purge configured
|
||||
// the purger is nil and nothing is sent.
|
||||
func (c articlesController) purge(ctx context.Context, model any) error {
|
||||
article, ok := model.(*Article)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
tx, _ := cabana.TxFromContext(ctx)
|
||||
cooler.PurgeSiteCacheAfterCommit(ctx, tx, cooler.SiteCacheFromApp(c.app()), "article", article.Slug)
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
Each of `pact.FormAfterCreate`, `pact.FormAfterUpdate` and `pact.FormAfterDelete` calls it:
|
||||
|
||||
```go src=modules/cooler/example_test.go#articlesController.FormAfterUpdate
|
||||
// FormAfterUpdate runs inside the update's transaction.
|
||||
func (c articlesController) FormAfterUpdate(ctx context.Context, model any) error {
|
||||
return c.purge(ctx, model)
|
||||
}
|
||||
```
|
||||
|
||||
A nil purger (no purge configured) does nothing, so the hooks need no configuration check.
|
||||
|
||||
### The site side
|
||||
|
||||
The Nuxt layers of the core plugins (the Journal, menu and FAQ layers) each ship the same route, so a site that extends any of them has it:
|
||||
|
||||
- `POST /api/__site__/cache/purge` removes every entry of Nitro's route cache (pages, their `_payload.json` and feeds) and answers `{"purged": <n>}`.
|
||||
- Its token is `runtimeConfig.siteCache.purgeToken`, set through `NUXT_SITE_CACHE_PURGE_TOKEN` to the same value as `site_cache.purge_token`. With a token shorter than 32 characters the route answers 404.
|
||||
- It answers 404 to any request that came through a reverse proxy (`X-Forwarded-For`, `X-Real-IP` or `Forwarded`) and 401 to a missing or wrong bearer token, compared in constant time.
|
||||
- Each layer also ships the route rule `'/api/__site__/**': { cache: false }`. Without it, a site rule such as `'/**': { swr: 300 }` would wrap the purge route in Nitro's cached handler, which answers from the cache without running the route.
|
||||
|
||||
The three copies are byte-identical, so it does not matter which layer's copy Nitro picks.
|
||||
|
||||
@@ -55,6 +55,7 @@ These are the keys most applications set. Each module's reference page lists all
|
||||
| `http.cors.*`, `http.trusted_proxies` | CORS and the proxies whose forwarded client IP is trusted. | [surf](../../modules/surf/README.md) |
|
||||
| `http.rate_limit.store` | Where `throttle` counters live: `memory` (per instance, the default) or `redis` (shared by every instance). | [cooler](../../modules/cooler/README.md) |
|
||||
| `cache.driver`, `cache.prefix`, `cache.memory.max_entries` | The cache driver (`memory`, the default, or `redis`), the key prefix (default from `app.name`) and the memory driver's size bound (default 10000). | [cooler](../../modules/cooler/README.md) |
|
||||
| `site_cache.purge_url`, `site_cache.purge_token` | The host site's page-cache purge route and its bearer token (at least 32 characters). Set both or neither; http only to a loopback host. Keep the token in `SUMMER_SITE_CACHE__PURGE_TOKEN`. | [cooler](../../modules/cooler/README.md) |
|
||||
| `redis.url` | Redis URL (`redis://`, `rediss://` or `unix://`), required when a Redis cache or rate-limit store is selected. Keep it in `SUMMER_REDIS__URL` when it carries a password. | [cooler](../../modules/cooler/README.md) |
|
||||
| `http.access_log` | Logs one record per request with method, matched route pattern, status and duration. Defaults to `false`. | [surf](../../modules/surf/README.md) |
|
||||
| `storage.uploads.bucket_url` | Uploads bucket, `file://`, `mem://` or `s3://` (the last only in a binary built with the `summercms_s3` tag from `build.tags` in `summer.yaml`; credentials from the AWS environment). Required by `serve`. | [lagoon](../../modules/lagoon/README.md) |
|
||||
|
||||
Reference in New Issue
Block a user