September 17, 2025·5 min read·engineeringdragonflystoragereplication

Dragonfly makes SSD reads and replication failures explicit

Dragonfly 1.34.0 improves SSD tiering error handling and repairs how replicas resume and replay list operations.

Moving a value out of RAM changes the meaning of reading it. A pointer lookup becomes an operation that can fail, and code that once assumed the value was available needs an error path. Several changes in Dragonfly 1.34.0, published on September 17, address that boundary. The release also repairs replication paths where an apparently reasonable shortcut could leave a replica with different data.

The common subject is how the server represents work that has already happened. An SSD read must return either bytes or an error. A resumed replication stream must begin at the receiver's actual position. A list operation must replicate the change the primary resolved, even when the replica distributes keys differently. Those requirements become visible in small changes to code that sits between larger subsystems.

I like Dragonfly when a cache has outgrown one Redis process and a larger machine remains simpler than a cluster. SSD tiering extends that capacity argument, but it also adds a storage failure model to the process. This release is interesting because it spends effort on that model, including paths used to extract and persist values.

Reading an external value can fail

The tiered read error handling change covers both the DUMP command and snapshot serialization. In the DUMP path, an external value that is not present in the cooling layer requires a tiered storage read. The old code obtained the result and proceeded as though it contained a value. The revised path checks the result and returns an I/O error when the read fails.

That matters because DUMP is a serialization operation. Its useful result is a representation of the stored value, not an acknowledgement that the key was found in the index. Finding a key whose payload lives on storage does not establish that the payload can be retrieved. Returning an error preserves that distinction for the caller.

Snapshot serialization needs the same distinction with a different destination for the failure. The revised snapshot path checks the asynchronous read result before trying to use its bytes. A failed read is reported through the snapshot error mechanism, with the affected key included in the error context, and that serialization path returns failure. The snapshot machinery can then treat the operation as unsuccessful instead of continuing through an assumption that the payload exists.

These changes do not make storage failures disappear. They give failures a defined route through operations that cross the RAM and SSD boundary. For persistence, that is a correctness property. A successful serialization result should describe retrieved data, and an unsuccessful read should remain visible to the operation responsible for producing the snapshot.

Failed growth should not become a hot loop

Another tiering change concerns the space into which values are written. The storage growth fix adds a cooldown after a failed attempt to grow the underlying file. The implementation records the time of an allocation error and suppresses another growth attempt during the following 100 milliseconds.

The existing capacity and pending-operation checks answer different questions. A maximum file size limits permitted growth. A pending operation prevents overlapping growth attempts. Neither prevents a completed but unsuccessful allocation from being retried immediately by subsequent work. The cooldown covers that third state, where the operation has finished, space was not obtained, and another request arrives before the underlying condition has meaningfully changed.

This is a local retry policy, not a claim that a full device will recover after a fixed interval. Its benefit is to bound how aggressively the server repeats a failing allocation. The ordinary capacity and concurrency checks still apply. That makes the change relevant to the behavior of the server during storage pressure, not just to the success case of adding more disk capacity.

The release also makes the minimum eligible value size configurable through tiered_min_value_size. Previously, the fixed minimum was 64 bytes. Configuration retains that default and rejects values below 64. In the tagged implementation, eligibility also requires a string value that is neither already external nor awaiting a stash operation, with sufficient room under the storage limit. Changing the threshold therefore changes which otherwise eligible values can be offloaded, rather than making every value a candidate.

I see that control as a useful expression of the tiering tradeoff. Offloading smaller values can move more payload out of memory, but it also increases the set of objects taking the storage path. The threshold exposes a choice about that set while retaining a lower bound in the implementation.

Replicate resolved list changes

The list journaling repair addresses a different boundary. Dragonfly can place keys on different internal shards on a replica than on its primary. An operation that happens to involve one shard on the primary cannot rely on the same placement at the receiver.

For a list move, the patch changes journaling to record a pop from the source and a push to the destination, including the actual moved value. The source side records the appropriate LPOP or RPOP, while the destination records LPUSH or RPUSH. Blocking moves receive corresponding treatment. Multi-pop also moves away from automatic journaling so the resolved pop can be recorded explicitly.

The primary has already decided which element moved and where it belongs. Recording those effects avoids delegating that decision to a receiver with a potentially different internal arrangement. This is particularly relevant to commands whose logical operation spans keys. The Redis-compatible command surface does not imply identical execution topology on every Dragonfly node.

A separate partial synchronization fix repairs the starting position used when resuming replication. The reported failure involved promotion and reconnecting replicas. Starting from a position that skips the receiver's missing history can leave it without writes even though synchronization appears to have resumed. The repair uses the requested replication position and checks whether the necessary journal history remains available.

A release about subsystem boundaries

There is a smaller example in the geospatial command work. The GEOSEARCH count fix rejects an explicit COUNT 0. Previously, zero could become confused with an internal representation used for an unlimited result count. An omitted limit and an invalid explicit limit need different treatment, even if an implementation uses a sentinel while processing the query.

For callers generating geospatial queries, this preserves a useful distinction between leaving the result set unbounded and providing an invalid bound. The patch includes a regression test for the explicit zero case, so the parser can retain its internal unlimited representation without accepting zero from a client.