July 9, 2024·6 min read·engineeringdragonflycachingnotificationssearch

Dragonfly improves the connection semantics of client-side caching

Dragonfly 1.20 improves client tracking for rueidis and adds expiry notifications. The release separates read invalidation from expiration events while extending search schemas and multi-document JSON writes.

A client-side cache depends on the relationship between a connection's reads and later invalidation messages. Supporting CLIENT TRACKING is therefore only part of compatibility. The server also has to preserve the connection's tracking state through transactions and optimized execution. Dragonfly's recommitted tracking work explicitly addresses interactions between client commands, MULTI/EXEC, and command squashing.

Dragonfly 1.20.0, released on July 9, identifies client-side caching improvements and rueidis support among its main changes. It also adds expiry notifications, FT.ALTER, and JSON.MSET. Those additions touch several ways an application learns about or changes database state, but they provide different guarantees and should not be treated as interchangeable notification mechanisms.

I am interested in Dragonfly where a cache can grow within one machine and retain familiar client interfaces. Support for an existing client library matters to that choice. A compatible command name has limited value if the library's connection-state operations fail inside the execution patterns it actually uses.

Tracking state survives transaction execution

The tracking pull request describes a previous restriction that allowed only certain client commands inside MULTI/EXEC. It removes that restriction and disables squashing for client commands. Squashing is an execution optimization, while a client command can change the connection state that gives later commands their meaning. Preserving those state changes requires more than treating the command as another key operation.

The opt-out tracking addition adds another piece of the client contract. It implements CLIENT TRACKING ON OPTOUT and corrects acceptance of CLIENT CACHING NO outside that mode. The tests check that CLIENT CACHING YES requires opt-in mode and that CLIENT CACHING NO requires opt-out mode. Invalid combinations receive corresponding error messages.

In the opt-out test, ordinary reads establish tracking relationships. Subsequent writes cause invalidations for those reads. A CLIENT CACHING NO command then excludes a read, and changing that key does not increase the invalidation count. This gives a client a way to avoid retaining tracking state for an individual read it does not intend to cache.

The following protocol sequence shows the tested mode selection and exclusion syntax. The protocol negotiation is explicit because the implementation uses RESP3 for client tracking.

HELLO 3
CLIENT TRACKING ON OPTOUT
GET frequently-read
CLIENT CACHING NO
GET one-off-read

That mode distinction lets a library choose which reads contribute tracking state without issuing the same instruction before every normally cached read. The server still needs to interpret the exclusion at the correct point in the command sequence. The transaction tests consequently exercise tracking and caching controls inside a queued block, where parsing time and execution time differ.

An optimization cannot erase connection state

The NOLOOP change also adjusts command squashing. Its condition rejects squashing both for client commands and when tracking is enabled on the connection. This makes an implementation constraint visible in the code. An optimization that combines commands must account for tracking semantics, or the server must keep those commands on a path that already preserves them.

The same source says tracking is not supported for RESP2 and directs clients to RESP3. It also contains a comment about unimplemented redirection behaviour. These details limit what the release announcement establishes. Supporting rueidis does not imply complete support for every tracking configuration available in another server.

The distinction is particularly relevant to reusable connection pools. Tracking options are connection state, so the commands emitted by a library and the protocol negotiated on its connections form part of the compatibility question. A library that enables one supported mode can work even when a different tracking arrangement remains incomplete. The release names a concrete library whose needs the changes address.

I regard that specificity as useful evidence of progress. It ties the compatibility work to actual client behaviour without requiring an unsupported claim that all caching clients now work. The pull requests explain the state transitions and execution restrictions behind the announcement.

Expiration events report a different kind of change

The expiry-notification implementation adds key-event notifications for expired keys. Its regression test enables notify_keyspace_events with Ex, subscribes to the database-zero expired-event channel, creates keys with short lifetimes, and compares the received key names with the set created by the test.

This supplies a subscription to an event category. Client tracking instead associates reads with later invalidation messages for the clients holding copies. An expiry-event subscriber and a tracking client can therefore have different reasons to receive a message. Receiving events does not by itself establish that a subscriber has tracked every value it has read.

SUBSCRIBE __keyevent@0__:expired

The subscription above follows the tested channel name. It assumes the server has enabled the corresponding notification setting. The test does not claim that merely subscribing changes the server's event configuration, and the event channel identifies the database involved.

The test also comments out an immediate-expiration case and says it is unsupported. This matters because assigning an expiry already in the past and waiting for a future expiry are distinct paths. The release supports notifications for the tested lifetime-based expirations. It would be inaccurate to turn that into a statement that every way of removing a key emits the same notification.

Applications can use this event stream when the expiry of a named key is itself useful information. The source shows delivery of key names through the subscription. It does not describe this channel as a recovery log or establish replay semantics, so those properties should not be inferred from successful notification delivery.

Search fields can be added to an existing index

The FT.ALTER tests begin with hashes that already contain color and cost fields. They create an index, add color as a tag field, and query it. They then add cost as numeric and exercise a range query. Existing documents become searchable through the newly added schema fields in those tests.

That behaviour addresses an application evolution problem. Adding a field to the stored document is different from making that field queryable through the index. The new command gives the schema an explicit update operation, and the tests show the resulting queries against data created before the alteration.

FT.ALTER items SCHEMA ADD color TAG
FT.ALTER items SCHEMA ADD cost NUMERIC

These commands assume an index named items already exists. The tests separately assert an error for an unknown index. They demonstrate adding fields, so the addition should be described as basic schema alteration. They do not establish that arbitrary removal or replacement of existing index definitions is supported.

Pipeline buffering receives an explicit limit

The pipeline backpressure change addresses another consequence of clients sending work faster than a connection executes it. The patch adds a per-I/O-thread limit for memory used by parsed pipeline requests. Before preferring asynchronous dispatch, the connection checks that limit and can wait for queued work to drain. Once it can proceed, it prefers synchronous dispatch to conserve memory.

This concerns outstanding request memory, which differs from the memory occupied by cached application values. A small dataset does not imply small request queues when clients pipeline aggressively. Bounding parsed requests gives the engine a way to slow their accumulation. The implementation also wakes waiting work when the queue drains or the connection closes, so shutdown participates in the same coordination. These details complement the tracking changes because a connection's scheduling and buffering behaviour remains relevant even when its command results are correct. Client-side caching can reduce repeated remote reads, while backpressure constrains work already arriving at the server.

Multi-document JSON writes preserve argument groups

The JSON.MSET implementation adds a command that repeats a key, path, and JSON value for each write. Its tests reject incomplete argument groups, then write four documents and read them back with JSON.MGET. This covers both command grammar and the stored results.

JSON.MSET first $ '{"enabled":true}' second $ '{"enabled":false}'

The grouped syntax makes each target path explicit. It differs from a sequence of ordinary string assignments because every value must also be valid input to the JSON operation. The same change adds an invalid-path test for JSON.MGET, so read-side parsing receives attention alongside the new write command.

These additions share a practical concern with the tracking work. Existing applications bring expectations about the order and shape of their operations, including stateful commands and repeated argument groups. Version 1.20 extends those supported patterns. The cited tests identify the cases covered, including tracking inside transactions, search over added fields, and reading back several JSON documents written by one command.