tako.Cache — per-instance TTL cache
A typed, in-memory cache. v0 is per-instance: no shared backend, no cross-instance invalidation — which is also why it costs nothing.
Declare
From examples/gallery/app.go:
var feed = tako.Cache[[]Photo]("feed", time.Minute)
The name must be a string constant (analyzer-enforced). The TTL is a second parameter, not an option — a cache without an expiry policy is a decision, not a default worth making silently — so there is no way to declare a cache without answering the question. A zero TTL is the way to say "never expires", out loud.
Use
Keyed like every keyed store: Get, Put, Delete, Exists — an
absent or expired entry reports through tako.IsNotFound.
GetOrFill covers the cold start:
return feed.GetOrFill(ctx, feedKey(owner), func(ctx tako.Ctx) ([]Photo, error) {
return readyPhotos(ctx, owner)
})
A fill error is returned and nothing is cached; concurrent misses may
each fill — it's a cache, the last Put wins. Gallery keeps the feed
warm by Putting from an event subscriber (warmFeed), so readers
rarely pay the query.
Local
In-process and in-memory; entries expire on read past their deadline.
tako dev's whole app is one instance, so locally the cache behaves
like a shared one — the cloud is where the per-instance semantics show.
Cloud
Each Cloud Run instance carries its own copy. A Put on one instance
is invisible on the others; after a deploy or scale-to-zero the cache
starts empty. Staleness is bounded only by the TTL — pick it as your
answer to "how stale may this read be?". The honest upside: tako deploy provisions nothing for a cache — no Redis, no IAM, no line in
the plan, no bill. A shared backend becomes a later opt-in without
changing call sites.
When NOT to use it: anything correctness depends on. Session or
auth state, counters, idempotency ledgers, "claimed" flags — those are
tako.Entity's job (transactions), and cross-instance mutual
exclusion is tako.Lock's. If you'd need to invalidate it everywhere
at once, it isn't cache material.
Test it
From examples/gallery/app_test.go: upload,
app.DrainQueues() to let warmFeed run, then assert the served feed —
the cache is exercised through the endpoints, never poked directly.
Errors you'll meet
tako.Cache takes no options at all — the TTL is its second
parameter, required, because a cache with no expiry policy is a
decision rather than a default worth making silently. So the mistakes
here are ones the compiler catches: a missing TTL is a wrong arity, a
wrong-typed one is a type error.
A non-constant name is the same tako/const-name analysis error every
resource gets (see entity.md). Outside a wired app:
tako: no cache backend wired — run via tako dev, a generated service, or tako.NewTestApp; a tool that reaches a deployed app calls runtime.BootGCPWith first.
Not yet
- No shared/cross-instance backend (Memorystore-class) — planned as an opt-in behind the same call sites.
- No explicit cross-instance invalidation, no size limits or eviction policy beyond TTL, no hit/miss metrics.