Files
summercms/docs/services/caching.md
Jakub Zych fd7f204979 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
2026-10-15 17:40:23 +02:00

5.5 KiB

title, description, section, order
title description section order
Caching Cache values with TTLs through cooler.Cache, in process or shared through Redis, under a per-application key prefix. services 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: 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:

// 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), both use one connection pool.
# config/cache.yaml
driver: redis
# 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.