Cache Something All Replicas Share
The catalog converts prices with a currency rate it gets from an external service. Asking for the rate on every request is slow and costs money; asking once an hour is enough, whichever replica happens to ask. The stash is a cache in Redis shared by all replicas of a component.
Put there only what can be lost and recomputed. Anything the application must keep is entity state.
§Turn it on
Declare the extension in the manifest, with no value:
# components/store.catalog/manifest.toa.yaml
name: catalog
namespace: store
stash: ~
operations:
rate:
input:
type: object
properties:
currency: { type: string }
required: [currency] The Context says where Redis is. toa create wrote the address of the one npm run dock starts;
give production its own:
# context.toa.yaml
stash: redis://localhost
stash@production: redis://redis.example.com §Read and write
context.stash exposes the commands of the ioredis client as methods:
// components/store.catalog/operations/rate.ts
import type { Context, RateInput } from '../types/index.d.ts'
export async function computation (input: RateInput, context: Context) {
const cached = await context.stash.get(`rate:${input.currency}`)
if (cached !== null) return Number(cached)
const rate = await lookup(input.currency, context)
await context.stash.set(`rate:${input.currency}`, rate, 'EX', 3600)
return rate
} lookup stands for the request to the rate service; see Call an External Service.
§Try it
$ npm run restart $ npx toa call store.catalog.rate "{ input: { currency: 'EUR' } }"
$ npx toa call store.catalog.rate "{ input: { currency: 'EUR' } }" The first call asks the external service; the second answers from the stash, and keeps doing so
after another npm run restart.
§Other commands
Any Redis command is available the same way: del, incr, expire, hset, sadd, and so on.
const views = await context.stash.incr(`views:${input.product}`) multi and pipeline return the client’s chain, executed with exec():
await context.stash.multi()
.incr(`views:${input.product}`)
.expire(`views:${input.product}`, 86400)
.exec() §Store an object
Plain commands store strings. Two methods added by the extension store and read an object or an array:
await context.stash.store('featured', { products: ['a1', 'b2'], since: context.now() })
const featured = await context.stash.fetch('featured') // the object, or null store(key, value, ...args) takes the same extra arguments as set after the value, so an
expiry is written the same way:
await context.stash.store('featured', featured, 'EX', 600) Read with fetch what was written with store, and with get what was written with set; the
two are not interchangeable.
§Keys belong to the component
featured written by store.catalog and featured written by store.orders are different
keys: one component cannot read the stash of another, and applications that share one Redis do
not share keys either. To give data to another component, use an operation or an event.
§A different Redis for one component
The annotation is an address, or addresses per component:
# context.toa.yaml
stash:
.: redis://redis.example.com
store.catalog: redis://catalog.redis.example.com . is the default for every component that has no key of its own, and stash: <address> is the
short way to write only the default. Keys, namespaces and the @environment suffix work as
described in Addresses and Credentials.
Limits:
- A component uses one Redis. Where a key resolves to several addresses, the first is used and a warning is logged.
- Password authentication and TLS are not supported: the address carries no credentials and none are supplied as a secret.
§When Redis is away
The client reconnects on its own. The outage is logged once (Stash is unreachable), and so is
its end (Stash reconnected). What a command does meanwhile is up to the client. An operation
that treats the stash as a cache should catch the exception a command throws and go to the
source; one that cannot do without the stash fails with it.
§What you see in telemetry
Every command is a span named <command> stash in the trace of the operation that
issued it and, with metrics on, an observation of toa.stash.command.duration labeled by command.
§What the stash is not for
- Locks and limits. To let one replica at a time do something, or to count usage across
replicas against a limit, use
context.atom. - Objects of one process, such as a client of an external system created once and reused. That
is
context.state: Keep Something in a Process.