November 25, 2025·4 min read·engineeringdragonflystoragereplicationobservability

Dragonfly repairs tiering access tracking and rejects self-replication

Dragonfly 1.35.1 corrects tiering policy state, exposes storage backpressure, and checks replication identity before accepting an upstream.

An access bit can be stored on the wrong object without breaking a lookup. The key still exists and the value can still be read, while the policy deciding where that value belongs works from the wrong history. That is one of the repairs behind the tiering stability changes in Dragonfly 1.35.1, released on November 25.

The patch release also adds visibility into tiering backpressure and prevents a server from replicating from itself. Those changes affect different parts of the system, but each makes an operational decision depend on the relevant state. The tiering scan needs the value's access state. Replication setup needs the peer's identity. A slow client needs to be distinguishable from a client deliberately waiting for offloading work.

I value Dragonfly's ability to use a larger machine before a cache needs a cluster. Tiering can extend that approach beyond keeping every payload in RAM. Its usefulness depends on the policy controlling movement as much as on the storage capacity it exposes, which makes these maintenance changes relevant to that preference.

The value carries the access history

The tiering strategy fix changes the background offloading scan from checking the key object's touched bit to checking the value object's touched bit. The code also clears the bit on the value when it encounters a touched entry. An eligible value without that bit set can proceed to the stash operation.

This is a small change in the expression used by the scan, from it->first to it->second, with a direct policy consequence. Reading the wrong object's state disconnects offloading decisions from the access information the value carries. Correcting the object restores the intended opportunity for a touched value to survive a scan, with its bit cleared so that a future scan can reconsider it.

The accompanying test makes the movement policy more concrete. With the cooling layer disabled and offloading forced, it creates a value and waits for it to move to storage. A first read fetches the value without promoting it into RAM. A second read triggers promotion, after which the test waits for offloading again. It repeats the sequence and checks fetch, upload, and stash counters.

That test covers the distinction between fetching bytes to answer a request and retaining the value in memory afterwards. A single access need not justify promotion. Repeated access can justify it, and the subsequent background scan must use the correct access state when deciding whether to offload the value again. The patch also initializes the string decoder's modified_ member to false, removing an uninitialized state from that decoding path.

Backpressure becomes observable

The throttling metrics change exposes both current tiering backpressure and the accumulated number of throttling events. Its final implementation adds tiered_clients_throttled and tiered_total_clients_throttled to the server's information output. The current figure comes from the pending backpressure map, while the total increments when the stash path supplies backpressure during active offloading.

Those figures answer different questions. The current value shows pending work at the instant of observation. The total records that throttling occurred even if the wait was over before the next observation. A quiet current value therefore does not erase evidence of earlier backpressure.

I find that distinction useful for a storage-backed cache. A slower response can reflect the server regulating incoming work while values move out of memory. Exposing that regulation gives an explanation that a throughput or latency figure alone cannot provide. It also avoids interpreting every storage-related wait as an unexplained command execution slowdown.

The release's Prometheus metrics work adds tiering measurements to the metrics endpoint. It includes entry and byte gauges, event counters for movement operations, and counters that distinguish RAM, cooling-layer, and disk access paths. Overload counters expose client throttling and stash overflows. These measurements describe activity and pressure separately from occupied capacity.

The byte categories also require care. The tagged implementation exposes a cold category from the cooling memory statistic, alongside used, allocated, and capacity figures. Those are different accounting views. Adding all of them together would not produce a meaningful total amount of stored application data.

Replication compares identities

The self-replication fix checks identity during Dragonfly replication negotiation. In the shipped code, the replica reads the upstream replication ID from the capability response. If that ID equals its own ID, setup returns a connection-aborted error.

Using the negotiated ID avoids reducing this check to a hostname comparison. A server can be addressed through different names, and the patch's regression test attempts self-replication through both localhost and 127.0.0.1. Both commands are expected to return an error. The relevant equality is between the instance's replication identity and the upstream identity reported by the protocol.

The tagged response handler performs the self-identity check before recording the upstream context and continuing with the negotiated session information. That places rejection inside setup, where the peer's identity is available, rather than leaving a self-referential relationship for later replication work to encounter. It is a targeted guard against an invalid upstream configuration, alongside the tiering changes in this maintenance release.