August 2, 2017·4 min read·engineeringvalkeyredismodulesconcurrency

Redis module contexts let long queries yield the server lock

RediSearch uses thread-safe contexts to interleave expensive queries with other work. Redis data access remains serialized, and yielding changes the atomicity of a search.

A long search query can delay unrelated Redis operations even when its data structures and algorithms are efficient. Thread-safe module contexts offer a way to divide that delay. A module can run work in its own threads, acquire the server lock when accessing Redis, and release it periodically so other work can proceed.

Dvir Dukhan's 2 August explanation describes this approach in RediSearch. Its purpose is to let short queries finish while longer ones remain in progress. Access to Redis data is still serialized by a global lock, so the design does not make every query execute simultaneously on several cores.

I like Redis's direct execution model, but a search command can contain substantially more work than a simple key lookup. Allowing a long operation to yield is useful when latency depends on what other clients submit. The resulting consistency model needs to be explicit because another operation can change data while the first query waits to resume.

The module moves execution to a worker pool

The article describes parsing an incoming search request on the main thread, constructing a request object, and queuing it to a thread pool. A worker acquires the global lock before executing the query. The search loop samples elapsed time after several iterations and releases the lock when enough time has passed.

The worker then attempts to acquire the lock again. During the gap, another query thread or the Redis main thread can run. After reacquiring it, the query reopens Redis resources it held and continues from its previous execution state. The article reports yielding about 5,000 times per second in this design.

Reopening resources is necessary because releasing the lock allows other operations to alter the data. The worker cannot assume that every object it previously examined remains unchanged. Progress through the query and validity of references to server resources become separate concerns.

Short queries may complete without yielding. Longer queries span several intervals, giving other work opportunities to run between those intervals. This improves scheduling under a mixed workload without reducing the amount of search work required by every expensive query.

The API makes locking explicit

The Redis 4.0 module implementation creates a thread-safe context with its own client object. The source explains that a worker cannot safely access the blocked client directly, so this separate client retains context such as the selected database. When associated with a blocked client, it selects that client's database.

The API's lock and unlock functions call moduleAcquireGIL and moduleReleaseGIL. Those functions operate a pthread mutex. The implementation therefore supplies a concrete exclusion mechanism around server access, rather than declaring ordinary module API calls safe from arbitrary threads.

The comments require acquiring the server lock before thread-safe API calls, with a documented exception for reply calls associated with a blocked client. That distinction separates working with Redis state from sending a result through the blocked-client mechanism. A thread-safe context supplies the interface, while the caller still observes its locking rules.

The article also identifies work that can run in parallel outside Redis access. Tokenizing and normalizing a large document can use multiple cores during indexing. The global-lock restriction applies to the relevant shared server operations, so it does not prohibit every kind of useful background computation.

Yielding permits intervening updates

The concurrency model changes what a search can observe. Its example follows a query that identifies a document, yields, and then finds another thread has deleted or changed that document. The resumed query cannot retrieve the same document state simply because it had selected it earlier.

This is a loss of whole-operation atomicity for those concurrent searches. Protecting each interval of Redis access does not make the entire query one uninterrupted observation. The lock can preserve safe access while still allowing the dataset to change between intervals.

I regard this as the central tradeoff. A latency improvement for short requests comes with a different observation boundary for longer searches. The implementation has to handle missing or changed resources when it resumes, and applications need to understand the behavior of the search mode they select.

Safe mode retains uninterrupted execution

The article documents a SAFEMODE load-time option for RediSearch. It disables query concurrency and makes searches and updates atomic according to the described mode. That restores the uninterrupted behavior while also restoring the blocking cost of a long operation.

The reported benchmark uses about one million Reddit comments and compares a common-term query with a much smaller-result query. Both clients and the server run on the author's laptop. The article reports that the lightweight query runs much faster under contention in concurrent mode, while also noting overhead when contention is absent.

Those results support the scheduling explanation without proving a universal throughput improvement. The mechanism gives other clients intervals in which to run. Redis data access remains protected by one lock, while module work outside that access can use separate threads. RediSearch's mode choice determines whether a search yields and accepts intervening changes or retains the atomic execution boundary described for safe mode.