|
|
|
|
@@ -8,7 +8,7 @@ Cache drivers and shared counters for SummerCMS: memory and Redis caches with a
|
|
|
|
|
|
|
|
|
|
## Overview
|
|
|
|
|
|
|
|
|
|
cooler holds the state that several application instances share. `cooler.Cache` is a Laravel-core cache (get, set, has, forget, remember, increment, flush) with per-entry TTLs under a per-application key prefix, kept in process by an otter cache or shared through Redis. The second piece is a Redis-backed rate-limit store: with `http.rate_limit.store: redis`, the `serve` command of [surf](../surf/README.md) calls `cooler.Install`, which builds a Redis client from `redis.url`, and publishes the store so the `throttle` middleware counts in Redis instead of in the process. Two instances behind one load balancer then enforce one limit together. The third piece is the site-cache purge: with `site_cache.purge_url` and `site_cache.purge_token` set, an admin save in a plugin sends one bearer-token POST to the host site's purge route after the save commits, so a cached page shows the change on its next view. It is the counterpart of Laravel's `Cache` facade with its array and Redis stores, and of the Redis store behind `RateLimiter` and the `throttle` middleware.
|
|
|
|
|
cooler holds the state that several application instances share. `cooler.Cache` is a Laravel-core cache (get, pull, set, has, forget, remember, increment, flush) with per-entry TTLs under a per-application key prefix, kept in process by an otter cache or shared through Redis. The second piece is a Redis-backed rate-limit store: with `http.rate_limit.store: redis`, the `serve` command of [surf](../surf/README.md) calls `cooler.Install`, which builds a Redis client from `redis.url`, and publishes the store so the `throttle` middleware counts in Redis instead of in the process. Two instances behind one load balancer then enforce one limit together. The third piece is the site-cache purge: with `site_cache.purge_url` and `site_cache.purge_token` set, an admin save in a plugin sends one bearer-token POST to the host site's purge route after the save commits, so a cached page shows the change on its next view. It is the counterpart of Laravel's `Cache` facade with its array and Redis stores, and of the Redis store behind `RateLimiter` and the `throttle` middleware.
|
|
|
|
|
|
|
|
|
|
## Role in the framework
|
|
|
|
|
|
|
|
|
|
@@ -23,10 +23,11 @@ Use cases:
|
|
|
|
|
|
|
|
|
|
## Features
|
|
|
|
|
|
|
|
|
|
- `cooler.Cache` methods: `cooler.Cache.Get`, `cooler.Cache.Set`, `cooler.Cache.Has`, `cooler.Cache.Forget`, `cooler.Cache.Remember`, `cooler.Cache.Increment` and `cooler.Cache.Flush`, with a TTL per entry (0 or less: no expiry). No tags and no locks.
|
|
|
|
|
- `cooler.Cache` methods: `cooler.Cache.Get`, `cooler.Cache.Pull`, `cooler.Cache.Set`, `cooler.Cache.Has`, `cooler.Cache.Forget`, `cooler.Cache.Remember`, `cooler.Cache.Increment` and `cooler.Cache.Flush`, with a TTL per entry (0 or less: no expiry). No tags and no locks.
|
|
|
|
|
- Memory driver (`cache.driver: memory`, the default): `cooler.NewMemoryCache` on otter v2, bounded by `cache.memory.max_entries` (default `cooler.DefaultMaxEntries`, 10000) so request-derived keys cannot grow memory without limit. Values are copied in and out, and `cooler.Cache.Increment` is atomic per key.
|
|
|
|
|
- Redis driver (`cache.driver: redis`): `cooler.NewRedisCache` with keys `<prefix>cache:<key>`, `SET … PX` for TTLs, `EXISTS`, `UNLINK` and `INCRBY`. `cooler.Cache.Flush` walks `SCAN MATCH <prefix>cache:*` in batches of 500 and removes each batch with `UNLINK`; it never empties the whole database, so rate-limit counters and other applications' keys stay.
|
|
|
|
|
- `cooler.Cache.Increment` starts a missing key at 0 without a TTL and keeps an existing key's expiry, as Redis `INCRBY` does.
|
|
|
|
|
- `cooler.Cache.Pull` returns a value and removes it in one atomic step, as Laravel's `Cache::pull`: Redis runs `GETDEL` (Redis 6.2 or newer), the memory driver removes the entry under otter's per-key lock. Of two concurrent callers at most one gets the value, and an expired key is reported as not found, which makes it the primitive for one-time codes. The memory driver is per process, so one-time values that another instance must read need the Redis driver.
|
|
|
|
|
- `cooler.Cache.Remember` degrades instead of failing the page: a failed read still runs the callback and returns its value, a failed write is skipped, and both are logged at most once per minute. The other methods return their errors.
|
|
|
|
|
- `cooler.Install` reads `cache.driver` and `http.rate_limit.store` (each `memory`, the default, or `redis`), publishes the Cache on the app (reusing one `cooler.FromApp` published earlier) and, when either selects Redis, builds one client from `redis.url`, shared by both, and pings it within `cooler.PingTimeout` (3 s).
|
|
|
|
|
- `cooler.FromApp` gives commands that do not run `serve` (a dedicated job worker, console commands) the published Cache, or builds the configured one and publishes it, without a ping: go-redis dials on the first command.
|
|
|
|
|
@@ -154,7 +155,8 @@ func AllowExport(ctx context.Context, app *backpack.App, fallback cooler.Attempt
|
|
|
|
|
|
|
|
|
|
| Identifier | Description |
|
|
|
|
|
|------------|-------------|
|
|
|
|
|
| `cooler.Cache` | The cache: `Get`, `Set`, `Has`, `Forget`, `Remember`, `Increment` and `Flush` with per-entry TTLs under the app's key prefix. |
|
|
|
|
|
| `cooler.Cache` | The cache: `Get`, `Pull`, `Set`, `Has`, `Forget`, `Remember`, `Increment` and `Flush` with per-entry TTLs under the app's key prefix. |
|
|
|
|
|
| `cooler.Cache.Pull` | Returns the value at a key and removes it atomically; at most one concurrent caller gets it. |
|
|
|
|
|
| `cooler.Cache.Remember` | Returns the cached value or computes, stores and returns it; degrades on cache errors. |
|
|
|
|
|
| `cooler.Cache.Increment` | Adds to an integer value; a missing key starts at 0 without a TTL. |
|
|
|
|
|
| `cooler.Cache.Flush` | Removes this application's cache keys only. |
|
|
|
|
|
@@ -222,6 +224,6 @@ go test -short ./modules/cooler/... # no Docker: memory driver, miniredis, htt
|
|
|
|
|
go test ./modules/cooler/... # also the Redis and Postgres containers (needs Docker)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The `-short` suites need no external services. `TestMemoryCache`, `TestMemoryCacheBounded` and `TestMemoryIncrementConcurrent` cover the memory driver; `TestRedisCache` and `TestRedisFlushOnlyPrefix` run the Redis driver on miniredis (TTLs through `FastForward`). `TestRateLimitParity` runs one table of attempts through `surf.NewMemoryStore` and the Redis store and expects the same answers; `TestRateLimitFallbackOnError` stops Redis and checks the fallback store answers with one warning. `TestInstallConfigErrors`, `TestInstallPingWarning` and `TestFromAppLazy` cover start-up; `TestSiteCachePurgeConfig` and `TestSiteCachePurgeRequest` the purge configuration and request against an `httptest` receiver.
|
|
|
|
|
The `-short` suites need no external services. `TestMemoryCache`, `TestMemoryCacheBounded` and `TestMemoryIncrementConcurrent` cover the memory driver; `TestPullSmoke` takes a value exactly once through `cooler.Cache.Pull` on the memory driver and on miniredis, and checks that an expired key is not found; `TestRedisCache` and `TestRedisFlushOnlyPrefix` run the Redis driver on miniredis (TTLs through `FastForward`). `TestRateLimitParity` runs one table of attempts through `surf.NewMemoryStore` and the Redis store and expects the same answers; `TestRateLimitFallbackOnError` stops Redis and checks the fallback store answers with one warning. `TestInstallConfigErrors`, `TestInstallPingWarning` and `TestFromAppLazy` cover start-up; `TestSiteCachePurgeConfig` and `TestSiteCachePurgeRequest` the purge configuration and request against an `httptest` receiver.
|
|
|
|
|
|
|
|
|
|
The Docker suites skip under `-short`: `TestRateLimitParityDocker` and `TestRedisCacheDocker` run the same checks on a `redis:7-alpine` container, `TestSiteCacheAfterCommitOnly` and `TestSiteCacheFailureNeverFails` run the after-commit purge in Postgres transactions, and `TestRedisCacheSmoke` is the original Redis smoke test. The cross-instance rate-limit test lives in surf (`go test ./modules/surf -run TestRateLimitAcrossInstancesSmoke`).
|
|
|
|
|
|