Caching
The cache package stores expensive results as bytes for a limited time. Like
Django’s CACHES, you name each cache and pick a backend for it.
cache.Backend is the context-aware interface that backends implement. You can
replace the backend without changing the code that uses it.
#Cache a value
backend, err := cache.NewMemoryBackend(1000, 64*1024)
if err != nil { return err }
registry, err := cache.NewRegistry(map[string]cache.Alias{
"articles": {Backend: backend, Namespace: "production:blog", Version: "v1"},
})
if err != nil { return err }
articles, err := registry.Lookup("articles")
if err != nil { return err }
err = articles.Set(ctx, "public:42", []byte("cached payload"), time.Minute)Build the registry at startup. Then inject the clients into your apps.
#Misses and errors
| Result | Meaning |
|---|---|
(nil, false, nil) | A miss. |
| An error | An outage. Check it with errors.Is. |
On an error, you choose whether to fall back to the authoritative data source. Cicadel never decides that for you.
Warning
Never use cache contents for session revocation, authorization decisions or distributed locking.
#The memory backend
NewMemoryBackend(capacity, maxValueBytes) is a bounded, concurrent backend that
lives in one process.
- It copies values in and out.
- Entries expire at the TTL boundary.
- When full, it evicts the oldest written entry.
- The existing
MemoryandStoreAPIs remain compatible.
Each instance is local to one application process. It does not coordinate with other processes. Use a suitable shared backend when several instances must share entries.
#Keys
The registry copies the alias map. Each alias hashes its name, namespace and version together with your key into a length-framed SHA-256 key.
- Raw keys never reach the backend.
- Keys must be 1–1024 bytes.
- TTLs must be positive.
- Hashing does not encrypt values.
Put every variation into the logical key: tenant, locale, permission and representation, where they apply.
#Invalidate entries
Deleteremoves one logical key. Deleting a missing key succeeds.- Changing
Versionselects a fresh key space. Old entries expire on their own. Every process must adopt the new version to agree on the active generation. - There is deliberately no global flush.
Readers and writers in flight can race with a delete. Invalidation is not transactional.
#Write a backend
A backend must:
- be safe for concurrent use;
- preserve byte ownership;
- implement positive TTLs;
- report operational errors.
Cancellation can race with a completed write. An error does not prove the change was rolled back. The built-in backend checks cancellation before its short in-memory operation. Network backends should pass the context to their transport.
#Ask your agent
Read https://cicadel.fauzaanu.com/en/dev/topics/cache/index.md.
Cache [expensive read] in this application. Include tenant and permission
variation in the key, set a TTL, define invalidation after writes, and keep
the database authoritative. Test hits, misses and backend failures.#Coalescing expensive loads
Create one cache.NewLoader(client) and reuse it. Then call
loader.Get(ctx, key, ttl, load). The load function takes a context and returns
([]byte, error).
- Hits skip the function.
- Concurrent misses for the same key share the first caller’s function and TTL. Different keys load independently.
- The loader rechecks the cache before loading, which closes the miss/join race.
- Successful loads fill the cache. Load and backend failures propagate, and callers can retry.
- Every caller gets its own copy of the result.
#Cancellation
Cancelling a caller stops only that caller’s wait. The load keeps the first caller’s context values. It ends when every waiter cancels or the work finishes.
#Rules for load functions
- Honor cancellation.
- Never call back into the same loader and key.
- Use keys that include every authorization and tenant variation.
- Keep one load policy per loader.
Coordination is local to one loader instance, not across processes. Deletes and concurrent writes keep the best-effort consistency described above.
#Bounded load execution
For request-serving apps, cap concurrent loads and give each one a deadline:
loader := cache.NewBoundedLoader(client, 32, 5*time.Second)Both limits must be positive.
- Hits still work at capacity.
- Callers that join an existing load share its slot.
- New misses at capacity get
cache.ErrLoaderBusyimmediately. Check it witherrors.Isand choose your own retry or fallback.
The deadline covers the backend recheck, the load and the write. Functions and backends must cooperate with cancellation.
An abandoned load keeps its slot until it actually returns. This stops repeated cancelled requests from starting unlimited replacement goroutines.
Warning
A load function that ignores cancellation can use up every slot. Go cannot force it to stop.
NewLoader stays compatible and unbounded. Neither constructor provides
distributed admission control or transactional invalidation.
Race-tested evidence:
TestBoundedLoaderAdmissionAndAbandonedExecutionchecks capacity rejection, abandoned-write prevention and slot reuse.TestBoundedLoaderDeadlinechecks deadline propagation and limit validation.