V Platform Features

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.
Search
Search the documentation