April 3, 2024·6 min read·engineeringdragonflyhttpsearchbloom

Dragonfly adds an HTTP command API with a limited scope

Dragonfly 1.16 exposes simple commands over HTTP and adds basic search aggregation. Its new Bloom filters also demonstrate why command availability and persistence support need separate treatment.

An environment that can make HTTP requests cannot necessarily open a Redis protocol connection. Dragonfly's April release addresses that mismatch with an optional HTTP command endpoint. The implementation proposal defines a deliberately small interface for simple commands. It explicitly excludes pipelining and blocking commands, which makes the intended scope more precise than a general promise to expose the whole database over HTTP.

Dragonfly 1.16.0, published on April 3, attributes the addition to requests from the edge developer community. It also adds basic FT.AGGREGATE support and the first Bloom-filter commands. Together, these changes make more application operations available through the engine, but they do not all have the same completeness or operational properties.

I find the HTTP addition useful because it addresses an application constraint without requiring a separate protocol translation service. Dragonfly's appeal to me remains the simpler deployment available when one machine can handle the cache. Keeping an additional intermediary out of that deployment is consistent with that preference, provided the endpoint supports the application's actual command pattern.

HTTP carries the command arguments

The request format is a JSON array of strings. Its first element names the command, and the remaining elements hold its arguments. The handler parses the array and passes the resulting arguments into Dragonfly's command dispatcher. It captures the reply and formats it as JSON. The implementation therefore adds a transport and reply representation around the command machinery.

The server registers /api only when --expose_http_api is enabled. The release provides a PING request as its minimal example. A pair of string operations makes the format clearer, including the fact that the expiry duration is a string argument in the array.

curl -X POST -d '["SET", "example", "value", "EX", "100"]' \
  http://localhost:6379/api
curl -X POST -d '["GET", "example"]' \
  http://localhost:6379/api

The HTTP regression test checks this sequence with a Unicode value. It expects the write to return a JSON object containing a result of OK, and the read to return the stored string in the same field. It also checks the remaining key lifetime through a Redis client. That last assertion connects the HTTP request to the underlying command semantics, including its expiry option.

The parser requires a nonempty array whose elements are strings. Sending a JSON number for the expiry duration is therefore different from sending its textual representation. This is an argument-array interface, so the JSON types do not replace the command's own argument grammar. A client still needs to supply the arguments that the named command accepts.

Successful command execution and complete response formatting are separate questions. The proposal says its initial formatting does not cover every command. It describes replies using a result field and errors using an error field. The release also includes a subsequent error-response correction. A simple string-operation example consequently supports a narrower claim than saying any Redis command can be substituted into the request unchanged.

Connection-oriented operations remain outside the initial goal

Pipelining and blocking commands depend on behaviour that a single simple request example does not describe. The proposal explicitly says those operations will not work in this initial implementation. That boundary matters to applications that already depend on a client library's use of persistent Redis connections, even when their individual command names are familiar.

This makes the endpoint most directly relevant to isolated command calls that fit its request and reply handling. An application cannot infer support for a blocking queue consumer from support for SET and GET. Nor does accepting a JSON array mean the server accepts a batch of command arrays. The array represents one command's arguments.

The implementation's small scope also makes error handling part of the client contract. A caller has to interpret the command reply object, not only the fact that an HTTP exchange completed. Parsing the transport response and interpreting the database result remain distinct steps. The sources establish the initial shape of that contract without providing a comprehensive protocol specification for every command family.

Aggregation changes where result reduction happens

The basic FT.AGGREGATE change adds grouping, sorting, and limiting. Its tests also exercise reductions for counts, distinct counts, minima, and maxima. This allows an indexed query to produce grouped results inside Dragonfly instead of requiring the client to retrieve matching documents and perform the same grouping itself.

The test data contains 101 hashes, with a tag indicating whether each numeric value is even. The aggregation groups on that tag and counts each group. It expects 51 even entries and 50 odd entries. Distinct counts and extrema check that the reducer operates on the corresponding group, rather than merely applying an operation to the entire result set.

That example is useful because it makes the operation's meaning visible without a performance claim. The client asks for a summary whose cardinality differs from the number of matching documents. Returning the grouped rows changes how much document-level work the application must perform. The source does not attach a universal latency improvement to that change.

The syntax exercised by the tests uses plain field names for the grouping and reducer arguments. This shortened example follows that tested form rather than assuming a broader aggregation dialect.

FT.CREATE items SCHEMA kind TAG SORTABLE value NUMERIC SORTABLE
FT.AGGREGATE items * GROUPBY 1 kind REDUCE count 0 AS count

Another test uses LOAD to obtain a field that is not part of the index schema before grouping on it. This distinguishes indexed fields from fields needed by the aggregation. The release adds basic aggregation support, and the tested operations define a useful subset. They do not establish that every aggregation operator supported by another search implementation is available.

I see this as a practical compatibility improvement for applications whose search results already need simple grouping. It can remove a client-side reduction step. The value depends on the particular operators the application uses, so the supported subset is more informative than the command name by itself.

Bloom filters have a different completeness boundary

The Bloom-command implementation adds single-item and multi-item insertion and membership checks. Its tests exercise BF.ADD, BF.MADD, BF.EXISTS, and BF.MEXISTS. They also show that adding an item can create a filter. A separate reservation operation supplies an explicit initial capacity and false-positive probability.

The versioned Bloom implementation describes membership in terms of hashed fingerprints and a bit array. Its insertion method can report that an item, or a collision, was already present. A positive result therefore does not establish exact membership. The configurable false-positive probability is part of the data structure's contract.

That probabilistic behaviour is separate from persistence. The release explicitly says the new Bloom functionality does not yet work with snapshots or replication. The insertion pull request independently lists persistence as missing. An application can exercise the commands successfully and still lack a way to recover or replicate the filter through those mechanisms.

These are different engineering decisions. Accepting probabilistic membership may be reasonable when a positive answer triggers an exact lookup elsewhere. Accepting a filter without snapshot or replication support requires a way to reconstruct the filter when the process is replaced. The release makes the second limitation explicit, so presenting the commands as general-purpose durable database functionality would misrepresent their current state.

Compatibility includes value metadata

The Memcached flag propagation correction supplies another example of behaviour surrounding a value. Its test stores a value with flags set to 42 and checks that a Memcached read includes those flags in the returned value header. Retaining the payload alone would not preserve that response. The release identifies both flag updates and propagation to replicas as corrected areas. This matters to clients that attach meaning to the flags, because a byte-for-byte match of the stored value does not establish that the complete Memcached response remains equivalent after replication.

Memory accounting includes encrypted connections

The release also extends accounting for client connections. The TLS metrics change sums thread-local TLS memory usage and exposes tls_bytes through /metrics and INFO MEMORY. This gives operators a specific counter for memory outside the stored values themselves.

The change is relevant when comparing two deployments with similar datasets but different connection arrangements. An engine's capacity includes the memory associated with its connections, and encrypted connections introduce allocations worth accounting for. The patch improves visibility into that cost without claiming that adding the metric reduces it. In this release, the new interfaces expand what clients can request, while the accounting work makes more of the memory supporting those requests observable.