Formula cache#

A formula is normally evaluated every time it is read. For a heavy formula — a deep dependency tree, many fine-granularity (sub-quarter-hour) series pulled over the whole history, or costly operators — this can make reads slow (a sluggish tsinfo). The formula cache materialises the result of such a series: its values are pre-computed and stored, so a read serves them directly instead of re-evaluating the formula.

When to use it#

Cache a formula when it is slow to display, typically because of:

  • a deep dependency tree;

  • many fine-granularity series read over a long history;

  • costly operators (resample, priority, block-staircase, or multi-site aggregates such as findseries + add).

How it works#

Three things are worth understanding.

  • Refresh — the cache is filled and kept up to date by a rework task, refresh_formula_cache, run on each policy’s schedule (schedule_rule); it walks the policy’s series in dependency order and materialises new revision dates incrementally. Its on-demand counterpart, refresh_formula_cache_now, is behind the Refresh button. Creating a policy and mapping series does not populate the cache — a refresh has to run.

  • Freshness — when a cached series is read, if its cache has not been refreshed for more than about twice its usual cadence, it is considered stale and recomputed live over the policy’s window. In normal operation the cache is served directly, giving a fast read.

  • Intermediate nodes speed up their parents — when the engine evaluates a formula, it stops expanding the dependency tree as soon as it reaches a cached sub-series and reads the cached values directly. Caching an intermediate node therefore cuts out the computation of everything below it, for every formula that depends on it.

Configuring a policy#

Caching is driven by a policy, which has five parameters:

parameter

role

initial_revdate

revision date of the first snapshot, the only one computed over the full horizon

look_before

lower bound of the materialised value window

look_after

upper bound of the materialised value window

revdate_rule

cron rule: which revision dates to snapshot

schedule_rule

cron rule: when the refresh job runs

A policy works on two time axes (see the Theory part of To the next level for an illustration). initial_revdate and revdate_rule live on the revision axis: they say when snapshots are taken. look_before and look_after live on the value axis: at each snapshot, only the value dates within [look_before, look_after] are recomputed. The first snapshot, at initial_revdate, is the exception: it materialises the whole horizon.

For instance, with initial_revdate on October 1st, one snapshot a day, look_before = (shifted now #:days -2) and look_after = (shifted now #:days 3):

                           value dates →
revision dates ↓       …  26 27 28 29 30 01 02 03 04 05 06 07 08

01/10 initial_revdate │ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
02/10                 │                ██ ██ ██ ██ ██ ██
03/10                 │                   ██ ██ ██ ██ ██ ██
04/10                 │                      ██ ██ ██ ██ ██ ██
05/10                 │                         ██ ██ ██ ██ ██ ██

read on 05/10, from   │ 01 01 01 01 01 02 03 04 05 05 05 05 05 05

Each row is a snapshot, ██ marks the recomputed value dates. The last row tells, for each value date, which snapshot the value read on October 5th comes from: everything before September 30th still comes from the initial snapshot.

A snapshot is skipped when no new data arrived upstream since the previous one, so in practice some rows may be missing.

initial_revdate, look_before and look_after are date expressions (a small lisp), evaluated at refresh time — for example (date "2025-01-01"), (today), (shifted now #:days -60) or (shifted now #:days 15). The now variable is bound to the revision date being refreshed and is only available in look_before / look_after. Do not use it in initial_revdate: it passes the policy validation but makes the refresh task fail — use a fixed date or (today)-based expressions there.

The two cron rules serve different purposes: revdate_rule selects which revision dates to snapshot (e.g. 0 * * * * — every hour on the hour), while schedule_rule decides when the refresh task actually runs (e.g. 5 * * * * — five past the hour). The small offset lets data pipelines finish before the snapshot is taken.

../_images/cache_policy_form.png

Warning

Values outside the window are frozen. Once a value date has left [look_before, look_after], the cache never recomputes it: a late correction of the source (e.g. a consolidated actual published a month later) never reaches the cache. look_before must reach back as far as the data can still change. look_after must cover the whole forecast horizon of the series.

Examples.

# Hourly cache, rebuilt from one week back, six months of value window
initial_revdate  (shifted (today) #:days -7)
look_before      (shifted now #:days -180)
look_after       (shifted now #:days 15)
revdate_rule     0 * * * *      # snapshot every hour
schedule_rule    5 * * * *      # refresh at 5 past the hour

# Once-a-day cache, fixed history start
initial_revdate  (date "2025-01-01")
look_before      (date "2025-01-01")
look_after       (shifted now #:days 15)
revdate_rule     0 6 * * *      # one snapshot a day, 06:00
schedule_rule    15 6 * * *     # refresh at 06:15

Absolute or relative initial_revdate#

initial_revdate is evaluated at each refresh, not only when the cache is created. Once the cache exists, the refresh starts from the most recent of the last snapshot and initial_revdate.

  • In normal operation, it makes no difference: the last snapshot is recent, so it wins.

  • When the cache is rebuilt (after a formula edit, for instance), a relative date such as (shifted (today) #:days -30) bounds the work to thirty days of snapshots, while an absolute date replays every snapshot since that date, a cost that grows over time.

  • Nothing is purged: a relative initial_revdate does not delete old snapshots, it only bounds how far back a rebuild goes.

  • After a long interruption (refresh task down for longer than the relative span), the missed snapshots between the last one and initial_revdate are skipped. Since the following snapshots only cover the value window, the value dates in between may never be materialised. In that case, delete the cache so that it is rebuilt from a full-horizon snapshot.

Cache operations#

From the cache management interface:

  • Refresh — triggers an immediate refresh (the refresh_formula_cache_now task) over the window. It does not purge anything and does not rebuild an emptied cache.

  • Activate / Deactivate — schedules or removes the periodic refresh_formula_cache task. A policy can only be edited while it is deactivated.

  • Delete — purges the cache of all the policy’s series and removes the policy and its series mappings (everything has to be recreated).

  • Free series — moving a series to the free list purges that series’ cache and unmaps it from the policy.

A series’ cache is emptied in three cases: deleting its policy (every series), moving the series to the free list (that series only), and editing its formula — a change to the formula invalidates its own cache and those of its dependents.

Programmatic access#

A policy can be created and populated from the API:

tsa.new_cache_policy(
    'daily',
    initial_revdate='(shifted (today) #:days -7)',
    look_before='(shifted now #:days -180)',
    look_after='(shifted now #:days 15)',
    revdate_rule='0 * * * *',
    schedule_rule='5 * * * *',
)
tsa.set_cache_policy('daily', ['my.heavy.formula'])

Related methods: cache_policies() and cache_free_series() list the policies and the formulas still available for caching; cache_series_policy and has_cache inspect a series; delete_cache clears a series’ cache; and refresh_series_policy_now triggers an immediate refresh of a policy. See Formulas (computed series) for the full cache API reference.