February 3, 2024·4 min read·engineeringrqlitetimeoutssqlite

rqlite puts a deadline on database work

rqlite 8.19.0 adds db_timeout. Its implementation explains what the deadline covers and why a client timeout alone cannot bound database work.

A caller can stop waiting for a database request while the database continues doing its work. That distinction makes a timeout in an HTTP client an incomplete control for an expensive query. rqlite 8.19.0, released on February 3, adds a database execution timeout so a request can specify how long its SQL work may run. The useful detail is where that timeout enters the execution path.

The proposal began with issue 1657, opened on January 31. It requested a query parameter and suggested using SQLite's interrupt facility when a Go timer expired. The released implementation connects the request's duration to a Go context in the database layer. That puts the deadline alongside the operation it needs to bound, instead of treating the caller's patience as a proxy for database execution time.

I value rqlite's small operational model. Adding a bounded execution interval to an individual request fits its existing approach to read consistency, where the caller expresses the requirement for the operation. A query with a short useful lifetime should be able to carry that requirement into the database without needing a separate deployment configuration.

Two parameters for different waits

The new parameter is db_timeout. The release's query parameter parser accepts a Go duration string and validates it alongside the existing duration parameters. A value such as 500ms therefore describes the requested database interval. The default passed by the HTTP handlers is zero, and the database layer installs a deadline only for a positive duration.

The similarly named timeout parameter already exists, but it has a different destination. In the HTTP service implementation, remote query forwarding passes timeout to the cluster client. The handler puts db_timeout into the database request itself. These settings cover different work even when both happen during the same HTTP exchange.

For example, this request gives the database half a second to execute a simple query. The query is intentionally trivial so the example demonstrates syntax without manufacturing a resource-intensive workload.

curl --get 'http://localhost:4001/db/query' \
  --data-urlencode 'q=SELECT 1' \
  --data-urlencode 'db_timeout=500ms'

The query parameter parser also makes read consistency independent of the timeout. level still selects among none, weak, and strong, with weak as the fallback in this release. Asking for a deadline does not select another consistency level. Those choices answer separate questions about the read, so a client can express both when it constructs the request.

An invalid duration is a request error before SQL execution. The parser calls the duration parser while constructing the query parameters and reports the name of the invalid parameter. This is useful for distinguishing a malformed request from a valid query that consumed its allowed execution interval. Both are errors, but correcting the request and changing the SQL are different responses.

Where the clock starts

The database implementation at 8.19.0 obtains a database connection using a background context. After obtaining that connection, it creates a context with the requested timeout. This ordering matters. The database interval does not include every possible wait that preceded connection acquisition. It is therefore inaccurate to describe db_timeout as an end-to-end HTTP deadline.

The query path passes that context to QueryContext. It then reads the result rows and checks the iterator's error before returning. A failure can occur after query submission, while rows are being produced or collected. The implementation accounts for that by checking the final row-iteration error instead of assuming that a successful initial call means the whole query succeeded.

The context is created around the request's database work, outside the loop over its statements. For a request containing several queries, the statements share the request's execution budget. The code does not give every query a fresh full interval. This matters for clients that batch unrelated reads. Putting more statements into the same request also puts them under the same parent deadline.

Transaction handling uses the same context when beginning a transaction. The transaction path retains the existing rollback cleanup, while the query code collects per-statement results. A timeout therefore participates in the database operation's actual control flow. It is not merely a timer that changes the response text after all the work finishes.

The implementation translates context.DeadlineExceeded into the database layer's query timeout error. Its rewrite function is specific to that error value. This is a useful limit on the interpretation of the release announcement. A caller should inspect the returned error, since the existence of the feature does not mean every failure during execution becomes the same timeout message.

Reading the response as a database result

The HTTP query handler separates failure of the overall store call from the query rows returned by that call. The database layer can place an error in an individual query result. Client code therefore needs to interpret the response body, including result errors, to determine whether its requested SQL completed. An HTTP exchange that successfully delivers JSON is not by itself evidence of a successful query.

This distinction becomes more useful with batching. A response can describe individual statements, and the shared timeout affects work that follows an earlier expensive statement. Treating the request as one undifferentiated transport success would discard the information needed to understand which SQL returned usable results. The response structure is part of the database contract.

I prefer this explicit separation between communication and execution. A caller can choose its own network deadline while also giving the server a database budget. The two values need not be identical because the HTTP exchange includes work outside the SQL execution interval. Their relationship should follow the application's latency requirements, rather than the similarity of their parameter names.

The release changelog presents the feature as a maximum query runtime that returns an error when execution does not finish in the interval. The code supplies the operational detail behind that summary. It starts the context after connection acquisition, carries the duration through the request, and checks errors while consuming rows. Those boundaries explain the control a client gains without turning the feature into a promise about the complete lifetime of an HTTP request.