feat(19.3-01): add cooler.Cache with otter memory and Redis drivers
- Cache: Get, Set, Has, Forget, Remember (degrades on cache errors), Increment, Flush with per-entry TTLs under <prefix>cache: - memory driver on otter v2.3.0 bounded by cache.memory.max_entries; Redis driver with SET PX, INCRBY and a Flush limited to SCAN MATCH <prefix>cache:* plus UNLINK - Install publishes the Cache (sharing one Redis client with the rate limiter); FromApp serves jobs and commands without a dial - new docs/services/caching.md, configuration rows, cooler README
This commit is contained in:
96
docs/services/caching.md
Normal file
96
docs/services/caching.md
Normal file
@@ -0,0 +1,96 @@
|
||||
---
|
||||
title: Caching
|
||||
description: Cache values with TTLs through cooler.Cache, in process or shared through Redis, under a per-application key prefix.
|
||||
section: services
|
||||
order: 45
|
||||
---
|
||||
# Caching
|
||||
|
||||
WinterCMS and Laravel cache through the `Cache` facade: `Cache::get`, `Cache::put`, `Cache::remember` and friends, backed by the store `config/cache.php` selects. SummerCMS has the same core through [cooler](../../modules/cooler/README.md): one `cooler.Cache` per application, with an in-process driver by default and a Redis driver shared by every instance.
|
||||
|
||||
## The Cache API
|
||||
|
||||
| Laravel | cooler |
|
||||
|---------|--------|
|
||||
| `Cache::get($key)` | `cooler.Cache.Get` returns the bytes and whether the key was found. |
|
||||
| `Cache::put($key, $value, $ttl)` | `cooler.Cache.Set` stores bytes for a TTL. |
|
||||
| `Cache::forever($key, $value)` | `cooler.Cache.Set` with a TTL of 0. |
|
||||
| `Cache::has($key)` | `cooler.Cache.Has`. |
|
||||
| `Cache::forget($key)` | `cooler.Cache.Forget`. |
|
||||
| `Cache::remember($key, $ttl, $callback)` | `cooler.Cache.Remember`. |
|
||||
| `Cache::increment($key, $by)` | `cooler.Cache.Increment`. |
|
||||
| `Cache::flush()` | `cooler.Cache.Flush`, limited to this application's cache keys. |
|
||||
|
||||
Values are bytes, so encode structured values yourself, for example with `encoding/json`. There are no tags and no locks.
|
||||
|
||||
A plugin looks the cache up on the application with `app.Lookup[cooler.Cache]()`, or asks `cooler.FromApp` for it. Here the second `cooler.Cache.Remember` call is answered from the cache:
|
||||
|
||||
```go src=modules/cooler/example_test.go#ExampleFromApp
|
||||
// serve publishes the Cache at start-up; a job or console command asks
|
||||
// FromApp, which builds the configured driver (memory here) once.
|
||||
app := backpack.New(nil)
|
||||
cache, err := cooler.FromApp(app)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
ctx := context.Background()
|
||||
loads := 0
|
||||
popular := func(context.Context) ([]byte, error) {
|
||||
loads++
|
||||
return json.Marshal([]string{"Hello", "Summer"})
|
||||
}
|
||||
var titles []string
|
||||
for range 2 {
|
||||
raw, err := cache.Remember(ctx, "blog:popular", 10*time.Minute, popular)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
if err := json.Unmarshal(raw, &titles); err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
fmt.Println(titles, loads)
|
||||
// Output: [Hello Summer] 1
|
||||
```
|
||||
|
||||
## Drivers
|
||||
|
||||
`cache.driver` selects the driver:
|
||||
|
||||
- `memory` (the default) keeps values in the process, in an otter cache bounded by `cache.memory.max_entries` (default 10000; `cooler.DefaultMaxEntries`). Beyond that, the least valuable entries are evicted, so keys derived from request data cannot grow memory without limit. Each instance has its own memory cache.
|
||||
- `redis` keeps values in Redis at `redis.url`, shared by every instance. When the rate limiter also uses Redis (`http.rate_limit.store: redis`, see [Rate limiting](rate-limiting.md#sharing-limits-across-instances)), both use one connection pool.
|
||||
|
||||
```yaml
|
||||
# config/cache.yaml
|
||||
driver: redis
|
||||
```
|
||||
|
||||
```yaml
|
||||
# config/redis.yaml (or SUMMER_REDIS__URL, which keeps a password out of YAML)
|
||||
url: redis://10.0.0.7:6379/0
|
||||
```
|
||||
|
||||
A missing or malformed `redis.url`, an unknown driver name or a `cache.memory.max_entries` below 1 fails the start-up with an error that names the key; an error never shows the Redis URL. Redis being down at start-up is a warning, not a failed start.
|
||||
|
||||
## Keys and the prefix
|
||||
|
||||
Every key is stored as `<prefix>cache:<key>`. The prefix is `cache.prefix` when set; by default it is `app.name` lower-cased, with every run of characters other than letters and digits replaced by `_`, followed by `:`, so `app.name: Acme Blog` gives `acme_blog:` and the key `blog:popular` is stored as `acme_blog:cache:blog:popular`. With `app.name` empty the prefix is `summer:`. Rate-limit counters live under `<prefix>rl:`, so several applications, and the cache and the limiter of one application, can share a Redis without touching each other's keys.
|
||||
|
||||
## TTLs
|
||||
|
||||
A TTL of 0 or less keeps a value until it is forgotten, flushed or, in the memory driver, evicted. A positive TTL expires the value after that long; Redis stores it with millisecond precision. `cooler.Cache.Increment` starts a missing key at 0 without a TTL and keeps the expiry of an existing key, as Redis `INCRBY` does.
|
||||
|
||||
## Remember degrades instead of failing
|
||||
|
||||
`cooler.Cache.Remember` reads the key and, on a miss, calls the callback, stores its value for the TTL and returns it. When the cache itself fails, for example while Redis is down, it does not fail the page: a failed read still calls the callback and returns its value, a failed write is skipped, and both are logged as a warning at most once per minute. Only an error from the callback is returned. The other methods return cache errors to the caller.
|
||||
|
||||
## Jobs and console commands
|
||||
|
||||
The `serve` command installs cooler at start-up (`cooler.Install`) and publishes the cache on the application, so plugins and the in-process job worker find it with `app.Lookup[cooler.Cache]()`. Other commands, such as a dedicated `queue:work` process or a console command, do not run `serve`; they call `cooler.FromApp`, which returns the published cache or builds the configured one and publishes it. It does not connect at construction: the Redis driver dials on the first call, so a command that never touches the cache never needs Redis.
|
||||
|
||||
## 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.
|
||||
@@ -54,7 +54,8 @@ These are the keys most applications set. Each module's reference page lists all
|
||||
| `http.body_limits.default_bytes`, `http.body_limits.upload_bytes` | Request body limits. Required by `serve` and `route:list`. | [surf](../../modules/surf/README.md) |
|
||||
| `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) |
|
||||
| `redis.url` | Redis URL (`redis://`, `rediss://` or `unix://`), required when a Redis store is selected. Keep it in `SUMMER_REDIS__URL` when it carries a password. | [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) |
|
||||
| `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://` or `mem://`. Required by `serve`. | [lagoon](../../modules/lagoon/README.md) |
|
||||
| `storage.uploads.serve` | Mounts `attach.StaticHandlerPublic` under `storage.uploads.public_path_prefix` with `X-Content-Type-Options: nosniff`, a sandboxing CSP and `Content-Disposition: attachment` for anything but a raster image. Defaults to `false`. | [surf](../../modules/surf/README.md) |
|
||||
|
||||
Reference in New Issue
Block a user