Skip to content

Commit cb035bf

Browse files
ADR-56: Add design for linearizable reads
Signed-off-by: Maurice van Veen <github@mauricevanveen.com>
1 parent a86241f commit cb035bf

1 file changed

Lines changed: 103 additions & 9 deletions

File tree

adr/ADR-56.md

Lines changed: 103 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
|----------|------------|-----------------------------|---------------------------------------------------|
1212
| 1 | 2025-09-12 | @ripienaar, @MauriceVanVeen | Initial document for R1 `async` persistence model |
1313
| 2 | 2025-10-28 | @MauriceVanVeen | Add read consistencies |
14+
| 3 | 2025-12-05 | @MauriceVanVeen | Add design for linearizable reads |
1415

1516
## Context and Problem Statement
1617

@@ -56,11 +57,11 @@ The interactions between `PersistMode:async` and `sync:always` are as follows:
5657
The table below describes the current read consistencies supported by the JetStream API, from the highest consistency
5758
level to lowest.
5859

59-
| Stream configuration | JetStream API | Description | Level of consistency |
60-
|:-----------------------------------------|:----------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
61-
| `AllowDirect` disabled. | `$JS.API.STREAM.MSG.GET.<stream>` | An API meant only for management outside of the hot path. The request goes to every server, but is normally only answered by the stream leader. | Current highest level of read consistency. Only the stream leader answers, but stale reads are technically possible after leader changes or during network partitions since an old leader could still answer before the current leader does. |
62-
| `AllowDirect` enabled. | `$JS.API.DIRECT.GET.<stream>` | If the stream is replicated, the followers will also answer read requests. | Higher availability read responses but with lower consistency. A read request will be randomly served by a server hosting the stream. Recently written data is not guaranteed to be returned on a subsequent read request. |
63-
| `MirrorDirect` enabled on mirror stream. | `$JS.API.DIRECT.GET.<stream>` | If the stream is mirrored, the mirror can also answer read requests. For example a mirror stream in a different cluster or on a leaf node. | Higher availability with potential of fast local read responses but with lowest consistency. Mirrors can be in any relative state to the source. |
60+
| Stream configuration | JetStream API | Description | Level of consistency |
61+
|:-----------------------------------------|:----------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
62+
| `AllowDirect` disabled. | `$JS.API.STREAM.MSG.GET.<stream>` | An API meant only for management outside of the hot path. The request goes to every server, but is normally only answered by the stream leader. | Current highest level of read consistency. Only the stream leader answers, but stale reads are technically possible after leader changes or during network partitions since an old leader could still answer before the current leader does. |
63+
| `AllowDirect` enabled. | `$JS.API.DIRECT.GET.<stream>` | If the stream is replicated, the followers will also answer read requests. | Higher availability read responses but with lower consistency. A read request will be randomly served by a server hosting the stream. Recently written data is not guaranteed to be returned on a subsequent read request. |
64+
| `MirrorDirect` enabled on mirror stream. | `$JS.API.DIRECT.GET.<stream>` | If the stream is mirrored, the mirror can also answer read requests. For example a mirror stream in a different cluster or on a leaf node. | Higher availability with potential of fast local read responses but with lowest consistency. Mirrors can be in any relative state to the source. Although mirrors will initially wait with responding to read requests until they're _largely_ up-to-date, they don't offer a way to stop responding to reads if contact with the upstream was lost for a long period. |
6465

6566
Additionally, if a stream is replicated and a consumer is created, there is no guarantee that the consumer can
6667
immediately observe all the written messages at that time. For example, if a R1 consumer is created on a follower not up
@@ -69,10 +70,10 @@ they come in.
6970

7071
## Proposal to add linearizability
7172

72-
Newer server versions, like for 2.14+, should support more configurability or in general higher levels of consistency as
73-
opt-in. For example, higher read consistency for consumers can be achieved by having consumer CRUD operations go through
74-
the stream's Raft log instead of the Meta Raft log, which ensures that a consumer created at time X in the stream log
75-
can observe all the stream writes up to time X.
73+
Newer server versions should support more configurability or in general higher levels of consistency as opt-in. For
74+
example, higher read consistency for consumers can be achieved by having consumer CRUD operations go through the
75+
stream's Raft log instead of the Meta Raft log, which ensures that a consumer created at time X in the stream log can
76+
observe all the stream writes up to time X.
7677

7778
Specifically, higher level consistency for message read requests would roughly require:
7879

@@ -150,3 +151,96 @@ consider:
150151

151152
> etcd ensures linearizability for all other operations by default. Linearizability comes with a cost, however, because linearized requests must go through the Raft consensus process. To obtain lower latencies and higher throughput for read requests, clients can configure a request’s consistency mode to serializable, which may access stale data with respect to quorum, but removes the performance penalty of linearized accesses’ reliance on live consensus.
152153
> - https://etcd.io/docs/v3.5/learning/api_guarantees/
154+
155+
### Design
156+
157+
The design introduces 'linearizable reads' to JetStream in two ways:
158+
159+
- Stream-level opt-in; an easy 'toggle' to get high-consistency reads, with some notes of caution for specific
160+
topologies.
161+
- A new API that's specifically used for this purpose and guarantees high-consistency reads from anywhere a client is
162+
connected.
163+
164+
#### Stream-level opt-in
165+
166+
The stream configuration will be extended with a new setting: `ReadConsistency`. This setting will be a string that
167+
can be set to different values depending on the desired consistency level. Specifically, when the `default` value is
168+
used, the `AllowDirect` and `MirrorDirect` can be manually specified. If `ReadConsistency` is not set to `default`, the
169+
read consistency level will take control over these fields, not allowing them to be manually set. The `ReadConsistency`
170+
setting will require API Level 3.
171+
172+
| Value | Description |
173+
|:----------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
174+
| `default` | Used when the consistency is not explicitly specified. Used for backward-compatibility, can be used to allow manually specifying `AllowDirect` and `MirrorDirect`. |
175+
| `weak` | Weak read consistency valuing availability and fast responses over consistency. If the stream is replicated, followers can answer read requests. If the stream is mirrored, mirrors can also answer read requests. |
176+
| `strong` | Strong read consistency valuing consistency over availability. The stream will guarantee linearizable consistency. If set on a stream that's acting as a mirror, it will guarantee sequential consistency. |
177+
178+
The addition of `ReadConsistency` allows for various levels of read consistencies (from weakest to strongest):
179+
180+
- Leader/Follower/Mirror reads, high availability with potential of fast local read responses by a mirror with no
181+
cross-request/session consistency guarantees, weakest consistency: `ReadConsistency: weak` on both the stream and the
182+
mirror.
183+
- Leader/Follower reads, high availability with no cross-request/session consistency guarantees: `ReadConsistency: weak`
184+
with no mirrors.
185+
- Leader/Mirror reads, higher consistency with potential of fast local read responses by a replicated mirror with
186+
cross-request/session consistency guarantees (when using a connection to the cluster/server hosting either the stream
187+
or mirror, but not both). Both the stream and mirror are set to `ReadConsistency: strong`. The stream itself will
188+
guarantee linearizable consistency as specified below, the mirror will guarantee sequential consistency.
189+
- Linearizable reads, highest level of consistency: `ReadConsistency: strong` with no mirrors. A read request which is
190+
only answered by the stream leader if it can guarantee linearizability.
191+
192+
A note of caution when using `ReadConsistency: strong` for a mirrored stream; the mirror's consistency level will not be
193+
linearizable, it will be sequential. This can be a desirable guarantee when mirroring a stream on a local leaf node. The
194+
difference of consistency will be clear since the leaf node is likely meant to be "loosely" connected to the cluster,
195+
and the client will be guaranteed to always connect to the leaf node and not the cluster. However, if a mirror is
196+
created in a cluster part of a super cluster setup, this could be problematic depending on the use case. If clients are
197+
allowed to reconnect between clusters, this could result in the consistency levels changing between linearizable to
198+
sequential. For example, if the client was first connected to the cluster containing the stream and then reconnected to
199+
the cluster containing the mirror. This will depend on how the client is configured and what the desired use case is.
200+
Please keep this in mind when designing your topology.
201+
202+
Additionally, if the `ReadConsistency` setting is set to anything other than `default`, the 'Linearizable reads API'
203+
specified in the next section will be enabled. This can be used to guarantee linearizable reads even if the stream is
204+
configured to `ReadConsistency: weak`, as well as guaranteeing linearizable reads if a read would otherwise be served by
205+
a local mirror.
206+
207+
#### Linearizable reads API
208+
209+
This linearizable reads API allows location transparent access; it doesn't matter if a client is connected via a leaf
210+
node and several hops to the stream leader, if it requires linearizable reads, it can use this new API to get this
211+
guarantee. Additionally, this API is enabled through setting a non-default value for `ReadConsistency`. If enabled, the
212+
leader will also respond to `$JS.API.DIRECT.GET.<stream>`, not requiring `AllowDirect`. This allows clients to migrate
213+
away from using the `$JS.API.STREAM.MSG.GET.<stream>` API, since it's primarily meant for management purposes only.
214+
215+
- Introduce a new API for linearizable reads: `$JS.API.DIRECT_LEADER.GET.<stream>`.
216+
- The new API will be similar to `$JS.API.DIRECT.GET.<stream>` but will go to the stream leader only. If it's a
217+
replicated stream, the read will need to "go through Raft" to ensure linearizability.
218+
- The new API will be enabled by the `ReadConsistency` setting on the stream. Once enabled, the new API will be active,
219+
and the leader will also respond to `$JS.API.DIRECT.GET.<stream>`, not requiring `AllowDirect`. This allows clients to
220+
use the DirectGet API instead of the MsgGet API:
221+
- If the user specifies requiring linearizable reads:
222+
- If `ReadConsistency` is NOT set, then the client should return an error that 'linearizable reads are not
223+
enabled for this stream'.
224+
- If `ReadConsistency` is set, then the client should use the new `$JS.API.DIRECT_LEADER.GET.<stream>` API.
225+
- If the client does not know the current value of `ReadConsistency` (since it might not have access to the
226+
stream info), then the client should use the new `$JS.API.DIRECT_LEADER.GET.<stream>` API anyway. A '503 No
227+
Responders' error will be returned to the user, which will either mean there's temporarily no leader
228+
available, or the stream is not configured to allow linearizable reads, but the client can't differentiate
229+
between these two cases.
230+
- If `AllowDirect` is set, or if `ReadConsistency` is non-`default`, the client should use
231+
`$JS.API.DIRECT.GET.<stream>`.
232+
- If none are specified, the client falls back to the `$JS.API.STREAM.MSG.GET.<stream>` API.
233+
234+
This design makes linearizability an opt-in and a conscious choice by a user. The server provides all the tools required
235+
for various consistency levels. Clients can ease the user experience by offering:
236+
237+
- Per-request linearizable read opt-in. For example: `js.GetMsg("my-stream", 1, nats.Linearizable()` and
238+
`js.GetLastMsg("stream", "subject", nats.Linearizable())`. This allows the user to value availability by default, but
239+
opt in to linearizable reads for the requests that need it.
240+
- Per-object linearizable read opt-in. Opt-in to linearizable reads for a specific stream, KV or Object Store. All reads
241+
to that 'object' will use linearizable read requests by default, without needing the user to specify this on a
242+
per-request basis. For example:
243+
`js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: "TEST", Replicas: 3, LinearizableReads: true})`.
244+
245+
The clients are free to implement this in a way that's best for the given language, but should generally provide both
246+
the per-request and per-object options.

0 commit comments

Comments
 (0)