Skip to content

RRDCached backend: good enough (fixes, real tests, documented differences) - #48

Merged
fungiboletus merged 8 commits into
mainfrom
rrdcached-good-enough
Oct 3, 2026
Merged

fungiboletus merged 8 commits into
mainfrom
rrdcached-good-enough

Conversation

@fungiboletus

@fungiboletus fungiboletus commented Oct 3, 2026 •

Copy link
Copy Markdown
Member

Why

RRDCached is a side-track backend, but "keep Prometheus' data in RRDtool" is a story worth keeping true. Probing a real rrdcached found that it was not: see done/rrdcached-good-enough.md for the audit table.

What was wrong, and is fixed

  • Samples more than 20 s apart were lost (data source heartbeat of 20 s): five samples a minute apart returned one point. Now an hour, ?heartbeat= sets it.
  • A restart erased the history: CREATE replaces an existing file and SensApp only remembered created files in memory (same for a second instance or two concurrent requests). Files are created with no_overwrite (rrdcached-client 0.4, -O on each CREATE): the daemon refuses to replace one that exists, whatever options it was started with, and "File exists" is accepted.
  • A retried or out-of-order write failed with a 500 (Prometheus then retried forever): batches are sorted, one sample per second, and the daemon's refusal of times that are not after the last update is not a failed write.
  • A request cancelled by the HTTP timeout left its answer on the shared connection for the next request: the connection is dropped instead; 15 s operation timeout; an unreachable daemon is a 503.
  • FLUSHALL on every write, read and health check is gone (health check is a PING).
  • Reads: first row of a window, end alone, backwards windows, errors reported as errors (not "not found"), limit, empty vs missing series, and the freshest rows of long windows (a coarse archive does not have them yet).
  • Listing: sorted so the bookmark pagination works, only files named like a series (others got a random UUID per call), no fake fallback when the daemon fails.
  • Selectors read each series once instead of twice.
  • rrdcached+unix:// was never routed by the storage factory ("Unsupported storage type").

Documentation

docs/RRDCACHED.md is rewritten: what is different from the other backends (a table), what it means for Prometheus (remote write works, remote read only by UUID), presets and file sizes, how time, gaps and consolidation behave, how reads pick an archive, operating the daemon (-O), performance. BACKENDS.md and CONFIGURATION.md follow.

Tests

  • 54 unit tests with a scripted daemon (cancellation, concurrent creation, stale refusals, listing, selectors, fresh-tail search...).
  • 26 integration tests against a real daemon (the 12 old ones mostly printed): 20 of the first 24 fail on the previous code. Includes Prometheus remote write sent twice then read through the HTTP API.
  • Passing against rrdcached 1.11 (the CI image, built from the upstream release) and 1.7.2 (Debian's package); Unix socket checked by hand with 1.11.

Performance (debug build, daemon in Docker, 2 runs each)

before after
Bulk load 190k samples/s 188-191k
Small write (50 series) 1.95-2.09 ms 1.28-1.35 ms
Scrape of 1000 series 8.0-9.3 ms 7.7-8.0 ms
First write of 1000 series 0.72-0.82 s 0.72-1.0 s
Reads same same
list_series 74-96 ms 35-49 ms

Notes

  • Files created by older versions keep their 20 s heartbeat; rrdtool tune <file> --heartbeat sensapp:3600 fixes them (in the docs).
  • Left out on purpose (ideas/rrdcached-follow-ups.md): a metadata sidecar, a connection pool, and using no_overwrite of rrdcached-client once released (a local, unpushed branch create-no-overwrite of that crate adds -O to CREATE, which would remove the need for the daemon's -O).

🤖 Generated with Claude Code

Update

  • Uses rrdcached-client 0.4.0 (no_overwrite). No existence check before a creation, no lock, and the daemon does not need -O: the 26 integration tests pass on a daemon started without it.
  • The test image is a plain copy of the one of rrdcached-client: latest upstream rrdcached (1.11.0) built from source, non-root. CI builds it with buildx and a layer cache; the compose service and its health check follow.

🤖 Generated with Claude Code

fungiboletus and others added 8 commits October 3, 2026 19:28
Task file with what probing a real rrdcached found, and an ignored benchmark
test that prints timings of bulk writes, small writes, reads and the health
check.
Found by probing a real rrdcached (see current_tasks/rrdcached-good-enough.md):

- The data source had a 20 s heartbeat: samples further apart than that
  became unknown. It is an hour now, and `?heartbeat=<seconds>` sets it.
- `CREATE` replaces an existing file, so a restart (or a second instance, or
  two concurrent requests) erased the history of the series it wrote to. The
  daemon is asked about the file first (`LAST`), one creator at a time, and a
  "File exists" answer of a daemon started with `-O` is fine.
- A batch is sorted, one sample per second, and the daemon's refusal of an
  update that is not after the last one (a retried request, a late sample) is
  not a failed write any more: Prometheus retried those forever.
- A request that is cancelled (HTTP timeout) drops the connection instead of
  leaving its answer for the next request; requests time out after 15 s; an
  unreachable daemon is `Unavailable` (503).
- No more `FLUSHALL` on every write, read and health check: the daemon flushes
  what a fetch needs, the health check is a `PING`.
- Reads: the first row of a window is included, rows are filtered to the
  window, `end` alone or an open window work, a window with nothing in it is
  empty and a missing file is "not found", errors are errors, `limit` keeps the
  first rows, the aggregation happens before the limit, and the newest rows
  that a coarse archive does not have yet are read from finer archives.
- Listing: sorted so that the bookmark pagination works, only the files named
  like a series, no guessing when the daemon fails.
- Selectors read each series once instead of twice, and a name that is a UUID
  is looked up without listing the files.

The connection, the preset, the preparation of the updates and the tests with
a scripted daemon are in their own files.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The 12 old tests mostly printed what they found. They are replaced by 24 that
assert: values round trip, sparse samples, the heartbeat setting, types,
refused and unsorted writes, a new instance that must not erase the files of
the previous one, several instances creating one series at once, windows and
their boundaries, the limit, open and backwards windows, the freshest rows of a
long window, the Munin preset, pagination, foreign files, selectors.

20 of them fail against the previous implementation. The daemon of the tests
now runs with `-O` (refuse to overwrite a file), as the documentation will
recommend.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Prometheus remote write of a series sampled every minute, sent twice as
Prometheus does after a timeout, read back by UUID and as the last sample;
SenML samples an hour apart; not found, not implemented and the health
check.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…he backend

The storage factory only recognised "rrdcached:", so the Unix socket scheme
that the backend implements was reported as "Unsupported storage type".
Also: documentation of the backend (differences, presets, time and gaps,
operations, performance), CI runs the factory tests, the task is closed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e tests

The test image is the one of rrdcached-client: the latest upstream rrdcached built
from source (Debian's package is 1.7.2), non-root, with -O. CI builds it with
buildx and a layer cache, the compose service follows its volumes and health check.
Unit and integration tests pass on 1.11.0.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Each CREATE carries -O, so the daemon refuses to replace a file that exists,
whatever options it was started with. SensApp no longer asks the daemon with
LAST before creating, nor needs a lock for it: a file that exists is a
"File exists" answer, which is fine. The daemon of the tests and the
documentation do not need -O any more, and the test image is a plain copy of
the one of rrdcached-client.

The 26 integration tests pass on a daemon started without -O. A first write of
many series is back to the 0.7 ms per file it had before.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@fungiboletus
fungiboletus merged commit 68916de into main Oct 3, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant