Skip to content

Archive cache on an edge node

A node that serves someone else's archive rather than its own — over remotes or a legacy source — goes to that source for every single fragment. The archive cache changes this: a fragment that has been read settles on a local cache disk, and the next request for the same part of the archive is served locally, with no network round trip.

That is how a CDN edge is built: the origin keeps the archive, the edges keep whatever the viewers actually watch.

The cache is transparent. It changes where the bytes come from, not what is served: fragment URLs, playlists and archive boundaries stay exactly the same for the viewer.

What makes a cache disk different from an archive disk

A cache disk is a disk role, not a flag on a disk:

  • live recording never picks it — the archive is written only to disks listed in disks;
  • stream retention does not govern it — a stream's max_depth and max_bytes do not apply to the cache;
  • space is freed by eviction — by how long ago a blob was last accessed, not by how old its content is;
  • it is isolated in the catalog — cache blobs live in their own tables, and archive traversals (cleanup, backfill, the split of used space into buckets) do not see them at all.

That is why an edge's cache does not show up on the dashboard as an ownerless archive, and why cache eviction does not compete with archive recording for the same mutex.

Configuration

The cache is configured in two places: the disks on the node, the switch on the stream.

dvr:
  root: /storage
  cache_disks:
  - path: ssd1              # relative to root, same as an archive disk
    max_bytes: 536870912000 # cap on the cache on this disk, bytes
    min_free_bytes: 10737418240
  - path: ssd2

streams:
  - name: cam1
    remotes:
    - url: http://origin:5080
    cache: {}               # turns caching of archive reads on

Cache disk parameters:

Parameter Description
path disk path relative to root
max_bytes how much the cache may occupy on this disk; empty — bounded only by free space
min_free_bytes how much free space to leave on the volume

The limits are independent: eviction runs until the disk fits within both. A cache disk has no modeDegraded describes a member of the archive RAID, and the cache replicates nothing.

The stream's cache section is a top-level section of the document, a sibling of dvr, not a field inside it: caching and recording are independent properties. An edge caches someone else's archive while recording none of its own. The section has no fields yet — its mere presence enables caching.

Config validation rejects:

  • a path listed both in disks and in cache_disks;
  • a duplicate within cache_disks;
  • a cache disk whose path equals root — eviction would wipe the catalog's home.

Different paths on the same physical volume are a valid configuration, but a warning is logged at startup: eviction frees space that the archive immediately takes back.

A cache section on a node that has no cache disks is a valid no-op with a warning in the log. That is normal in a cluster, where one stream document spreads across different nodes.

A pure edge

Storage made of root and cache disks only, with no archive disks, is a valid configuration: there is a catalog, there is no live recording, and the archive is pulled in from remotes.

dvr:
  root: /storage
  cache_disks:
  - path: ssd1
    max_bytes: 536870912000

streams:
  - name: cam1
    remotes:
    - url: http://origin:5080
    cache: {}

Warning

root is the catalog's home, not a write disk. An empty disks list used to mean "write straight into root"; now it is a write error visible in the statistics instead of a silent write past the RAID. If the node is supposed to record an archive, list the disks explicitly in disks.

Cache admission

Caching everything on first request means evicting the popular in favour of the random: a single seek deep into the archive would push out what everybody is watching. Admission therefore has two axes, configured on the node for the cache as a whole:

Parameter Description
cache_fresh_age_secs a fragment younger than this depth (seconds back from now) is cached on the very first request; one day by default
cache_admission_hits which repeat request starts caching a fragment older than the freshness boundary; the second by default, 1 disables the barrier
dvr:
  root: /storage
  cache_fresh_age_secs: 86400
  cache_admission_hits: 2
  cache_disks:
  - path: ssd1

Almost everybody watches the fresh tail of the archive, so it enters the cache straight away. The depth of the archive interests a handful of viewers — it enters the cache only when asked for again.

Read source order

An archive fragment is looked up along a chain, and the first source to answer closes the request:

  1. the live stream's segmenter;
  2. the cache;
  3. the local archive;
  4. the cluster (remotes);
  5. the legacy old_m4f source;
  6. the on-disk legacy archive.

A cache hit answers without touching anything below it. If a fragment is present both on an archive disk and on a cache disk, the read is served by the cache: an SSD cache beats spindles. A stream without a cache section has no cache step in the chain at all.

What a miss puts into the cache

A successful read from a source below the cache is written to a cache disk together with its init fragment. The differences from archive recording:

  • the max_depth cutoff does not apply — what the viewer asked for goes into the cache, even if it is deeper than any configured archive depth;
  • a multi-track segment is stored whole, with every track — so a viewer's bitrate switch is served by hits rather than misses;
  • the write is idempotent — several viewers missing on the same fragment at once leave exactly one copy in the cache;
  • a cache write error does not affect the client's response — it already has the bytes; the error is only logged and counted in metrics.

Read-ahead

Archive viewing is sequential, so after serving a read Sapsan fetches the next fragment of the same track in the background if it is not in the cache yet. Read-ahead works on a pure edge and when reading from a remote source, is limited to one fragment ahead, and never delays the client's response.

This is not warm-up: there is no scheduled or EPG-driven cache filling here.

Eviction

Space on a cache disk is freed by whole hourly blobs in order of last access — the least recently used goes first — until the disk is back within both limits.

  • Content age is not a criterion: yesterday's popular show outlives today's unpopular one, and the current hour is evicted on equal terms with the rest.
  • The single exception is a blob a writer is appending to right now: it stays, and the next least recently used one goes instead.
  • Archive cleanup (a stream's max_depth and max_bytes) and episode protection do not consider cache disks at all.

The eviction order is kept by its own catalog index and survives a restart. The access mark is flushed to the catalog lazily — at most about once per 10 minutes per blob — so that reading does not turn into writing.

Cache accounting (used volume, access times) is restored at startup by a catalog range scan, without walking the data files. A wiped cache disk is a valid cold state: the server starts, the cache fills from scratch, the archive is untouched.

Timeline and depth on an edge

The rewind depth and archive ranges of a cached stream are computed as the union of the local catalog (archive and cache) and every remote source — and announced in rewind playlists and in archive range responses. A viewer on the edge sees the same depth as on the origin.

Source windows are merged as a list, preserving the gaps between them: a solid interval from a source's start to its end must not stand in for that list — otherwise the timeline paints intervals green where there is no data, and a click on them lands in a 404. Gaps no larger than the requested resolution are still merged by the usual rule.

If the origin is unreachable but the requested range is fully cached, the playlist is built from the local catalog and the fragments are served from the cache — playback continues.

Observability

Every archive read is attributed to the link of the chain that served it:

  • cache — served by the node's cache disk (a hit);
  • local — served by the node's archive disk;
  • remote — a remote source had to be queried (a miss).

Only a client's fragment read counts as an archive read. Output from the live segmenter and read-ahead fetches are not attributed — otherwise read-ahead would inflate the hit rate the better it worked; fetches are counted separately.

Cumulative counters go to Prometheus and local-prom:

Metric Meaning
dvr_archive_reads_total{source} archive reads per cache / local / remote axis
dvr_archive_read_bytes_total{source} bytes of those reads
dvr_read_ahead_fetches_total read-ahead fetches
dvr_disk_reads_total{disk_id}, dvr_disk_read_bytes_total{disk_id} disk accesses; on a cache disk these are its hits
dvr_disk_cache_bytes{disk_id} cache volume on the disk

The local GET /streamer/api-v4/dvr endpoint reports each cache disk's role, volume and limit, and the node's read rates along the three axes. Cumulative counters are not exposed in the management API: it carries rates and gauges.

The hit rate is never delivered as a ready-made number — it is cache / (cache + local + remote), and the consumer computes it: add the rates up, divide afterwards. An average of averages lies when nodes carry unequal weight.

The console screens that show this are described in the Catena documentation: DVR capacity across the cluster and node settings.

How this differs from lazy replication over remotes

Remotes have a similar behaviour: a fragment read from a remote server is appended to the local archive. The difference is fundamental:

Lazy replication over remotes Archive cache
Written where to archive disks to cache disks
What for to move someone else's history here for good to serve repeat requests locally
Freed when by cleanup according to the stream's retention by eviction according to last access
Archive disks required yes no, an edge can run without them

Replication is about relocating an archive; the cache is about saving network on what is popular.

What's next