cicadel

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

Go
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

ResultMeaning
(nil, false, nil)A miss.
An errorAn 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 Memory and Store APIs 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

  • Delete removes one logical key. Deleting a missing key succeeds.
  • Changing Version selects 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

Text
Read https://cicadel.fauzaanu.com/en/0.2.8/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:

Go
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.ErrLoaderBusy immediately. Check it with errors.Is and 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:

  • TestBoundedLoaderAdmissionAndAbandonedExecution checks capacity rejection, abandoned-write prevention and slot reuse.
  • TestBoundedLoaderDeadline checks deadline propagation and limit validation.

Type to search this version.