Repository navigation
Add Redis TimeSeries support - #3978
Conversation
* Add TimeSeries command types and keywords #2716 Add the TS.CREATE, TS.ALTER, TS.DEL and TS.INFO command types, the keywords their options use, and register TS.INFO as read-only. * Add TimeSeries value types #2716 Add TsAggregation, TsDuplicatePolicy and TsEncoding as interfaces with a known-values enum each and a package-private raw value. of(String) looks a name up by its wire token, ignoring case, and keeps an unknown name, so that a value a newer server sends never breaks parsing. Co-authored-by: HwangRock <peter9244@catholic.ac.kr> * Add TimeSeries series-option arguments #2716 Add TsCreateArgs and TsAlterArgs on package-private self-typed bases. One final build method emits the options in a fixed order with LABELS last, because the server stores every argument after LABELS as a label. An empty label map sends a bare LABELS, which removes all labels on ALTER. Co-authored-by: HwangRock <peter9244@catholic.ac.kr> * Add TsInfoValue and its parser #2716 TsInfoValue models the TS.INFO reply, with nested Rule and Chunk types for the compaction rules and the DEBUG chunks. TsInfoValueParser reads both RESP2 and RESP3 shapes into equal values, keeps unknown value-type names as raw values, and exposes every reported field through rawInfo. Co-authored-by: HwangRock <peter9244@catholic.ac.kr> * Add TimeSeries lifecycle commands to all API flavors #2716 Add TS.CREATE, TS.ALTER, TS.DEL and TS.INFO to the sync, async, reactive, node-selection and Kotlin coroutines APIs, with a dedicated command builder, and wire the new interfaces into the aggregate interfaces. TS.DEL takes a Range<Long>; exclusive bounds are sent as the inclusive neighbor, because timestamps are integer milliseconds. The Kotlin flavor is part of this commit so that the coroutines consistency test passes for the registered TIME_SERIES group. Co-authored-by: HwangRock <peter9244@catholic.ac.kr> * Add TimeSeries user guide #2716 Add docs/user-guide/timeseries.md with the introduction, the supported server versions, the value types and the series lifecycle commands, and link it from the navigation. * Add TimeSeries series lifecycle unit tests #2716 * Add TimeSeries series lifecycle integration tests #2716 * Add TimeSeries series lifecycle cluster tests #2716 * Return the wire token from TimeSeries value-type toString #2716 TsAggregationType, TsDuplicatePolicyType and TsEncodingType now return the token sent to the server from toString(), for example STD.P instead of the constant name STD_P. Unit tests check every constant. --------- Co-authored-by: HwangRock <peter9244@catholic.ac.kr>
* Add TimeSeries write command types and keywords #2716 Add the TS.ADD, TS.MADD, TS.INCRBY and TS.DECRBY command types and the ON_DUPLICATE and TIMESTAMP keywords. * Add TsAddArgs and TsIncrByArgs #2716 Add the TS.ADD arguments with the per-call ON_DUPLICATE override, and the TS.INCRBY/TS.DECRBY arguments with TIMESTAMP, on the shared series options. The base serializer now places TIMESTAMP first and ON_DUPLICATE after DUPLICATE_POLICY, keeping LABELS last. Co-authored-by: HwangRock <peter9244@catholic.ac.kr> * Add TsMAddValue #2716 TsMAddValue is one TS.MADD sample. of(key, timestamp, value) requires a non-negative timestamp; of(key, value) sends * so that the server clock sets the timestamp. Co-authored-by: HwangRock <peter9244@catholic.ac.kr> * Add error-tolerant integer list outputs #2716 TS.MADD replies with an array that holds an error element for each rejected sample. ErrorTolerantIntegerListOutput turns those elements into null and ErrorTolerantIntegerValueListOutput into Value.empty(), reading the reply to the end; an error reply instead of the array still fails the command. Co-authored-by: HwangRock <peter9244@catholic.ac.kr> * Add TimeSeries write commands to all API flavors #2716 Add TS.ADD, TS.MADD, TS.INCRBY and TS.DECRBY to every API flavor. The TS.ADD overloads without a timestamp send * (server clock); their Javadoc warns that a long passed as the value compiles silently. TS.MADD returns null for each rejected sample (Value.empty() on the reactive API, List<Long?> on Kotlin), so the reactive tsMAdd and the builder method tsMAddValues are registered as known deviations. Co-authored-by: HwangRock <peter9244@catholic.ac.kr> * Split TS.MADD by slot in Redis Cluster #2716 The advanced cluster API sends TS.MADD with keys in several slots as one command per slot, in parallel, and puts the results back in the caller's order. Samples are partitioned by position, because one key can appear several times. A slot whose command fails fails the whole call, as with MGET. The split is not atomic across slots. * Document TimeSeries writes #2716 Add the writes section to the TimeSeries user guide: TS.ADD with and without a timestamp and the long-to-double pitfall, IGNORE (only with duplicate policy LAST), TS.MADD partial failure and the per-slot split in Redis Cluster, and TS.INCRBY/TS.DECRBY. * Add TimeSeries writes unit tests #2716 * Add TimeSeries writes integration tests #2716 * Add TimeSeries writes cluster tests #2716 * Fix the Kotlin KDoc of tsAdd with the server clock #2716 The Java long-to-double warning does not apply in Kotlin, which does not widen a Long to a Double. Drop it from tsAdd(key, value) and tsAdd(key, value, args), and name the overloads that set the timestamp as tsAdd(key, timestamp, value) and tsAdd(key, timestamp, value, args). * Clarify where TS.MADD is split by slot #2716 The slot split happens in the Redis Cluster API only. Name it in the tsMAdd documentation of all six flavors, and state in the class documentation of both node-selection interfaces that tsMAdd runs unchanged on every selected node, so its keys must hash to slots that node serves. --------- Co-authored-by: HwangRock <peter9244@catholic.ac.kr>
* Add TimeSeries compaction rule commands #2716 Add TS.CREATERULE, with and without an alignment timestamp, and TS.DELETERULE to every API flavor. The rule takes one TsAggregation, so an aggregation this Lettuce version does not know can be passed with TsAggregation.of(String). Co-authored-by: HwangRock <peter9244@catholic.ac.kr> * Document TimeSeries compaction rules #2716 Add the compaction rules section to the TimeSeries user guide, including the requirement that source and destination share a slot in Redis Cluster. * Add TimeSeries compaction rule unit tests #2716 * Add TimeSeries compaction rule integration tests #2716 * Add TimeSeries compaction rule cluster tests #2716 * Fix cross-slot and alignment notes of TimeSeries rules #2716 Keys in different slots get CROSSSLOT only from Redis 8.4 on; Redis 8.0 and 8.2 reply "TSDB: the key does not exist" (TS.CREATERULE) or "TSDB: compaction rule does not exist" (TS.DELETERULE). State both in the Javadoc and the user guide. Reject a negative alignTimestamp with IllegalArgumentException before sending, which the server would refuse. * Add TimeSeries negative alignment tests #2716 * Reject a non-positive bucket duration in TS.CREATERULE #2716 The server rejects a bucket duration of zero or less. Check it in the client and throw IllegalArgumentException before sending, and state it in the tsCreateRule Javadoc of every flavor. The compaction-rule integration test now expects the client-side IllegalArgumentException, and a builder unit test covers 0 and -1 with and without alignment. * Document the bucket duration check of TimeSeries rules #2716 State in the compaction-rule section of the user guide that the bucket duration must be greater than 0 and that the client rejects other values before sending the command. --------- Co-authored-by: HwangRock <peter9244@catholic.ac.kr>
* Add TimeSeries read command types and keywords #2716 Add the TS.GET, TS.RANGE, TS.REVRANGE and TS.READ command types, the keywords of the range and read options, and register the four commands as read-only. * Add TsSample and sample outputs #2716 TsSample is a timestamp with one or more values, compared with Double.compare semantics so that NaN equals NaN. TsSampleOutput reads a single sample or an empty reply (null), and TsSampleListOutput streams each sample row as it is decoded. Both read RESP2 string values (NaN, Infinity, -Infinity) and RESP3 doubles. Co-authored-by: HwangRock <peter9244@catholic.ac.kr> * Add TimeSeries range arguments #2716 Add TsBucketTimestamp, the package-private BaseTsRangeArgs with the options shared by every range command, and TsRangeArgs for TS.RANGE and TS.REVRANGE. An aggregation takes one or more TsAggregation values, sent as one comma-joined argument. ALIGN, BUCKETTIMESTAMP and EMPTY without an aggregation are rejected when the arguments are built, because the server ignores the last two silently. * Add TS.GET, TS.RANGE and TS.REVRANGE to all API flavors #2716 TS.GET returns the latest sample or null (an empty Mono on the reactive API); tsGet(key, true) sends LATEST. TS.RANGE and TS.REVRANGE take a Range<Long> with the same bound rules as TS.DEL and optional TsRangeArgs, and stream the samples to the reactive subscriber. Co-authored-by: HwangRock <peter9244@catholic.ac.kr> * Add TS.READ #2716 Add TS.READ to every API flavor, with TsReadOffset for the starting point (earliest, latest, new samples or a timestamp) and TsReadArgs for BLOCK and MAX_COUNT. The Javadoc covers how BLOCK occupies the connection, the dedicated connection it needs, Redis Cluster and replicas, and the empty reply when the key is removed. Requires Redis 8.10. * Document TimeSeries single-key reads #2716 Add the single-key reads section to the TimeSeries user guide: TS.GET and LATEST, ranges with exclusive bounds, the range options and several aggregations, and TS.READ with BLOCK, including the dedicated connection it needs, Redis Cluster and replicas. * Add TimeSeries single-key read unit tests #2716 * Add TimeSeries single-key read integration tests #2716 * Add TimeSeries single-key read cluster tests #2716 * Reject a non-positive bucket duration in range arguments #2716 The server rejects a bucket duration of zero or less. The range arguments now check it when the aggregation is set and throw IllegalArgumentException; a Duration shorter than one millisecond counts as zero. The Duration unit test now expects zero to be rejected. * Drop the NaN note from the TS.GET return description #2716 The TsSample class documentation already says that values may be NaN, so the @return of tsGet(K) in all six flavors no longer repeats it. * List the exact @throws of TsRangeArgs.Builder.aggregation #2716 The @throws of Builder.aggregation(Duration, TsAggregation...) now lists the same conditions as the setter it delegates to. * Document the timeout and NaN rules of TimeSeries reads #2716 The @throws of TsReadArgs.Builder.block(Duration, long) now lists the exact timeout conditions of the setter, and the TsSample class documentation says that values may be NaN on Redis 8.6 and later. * Fix TimeSeries test compilation on JDK 9 and later #2716 assertThat((List<?>) …).containsExactly(…) does not compile on JDK 9 and later because of the wildcard capture. Read the transaction results with an explicit type argument instead. --------- Co-authored-by: HwangRock <peter9244@catholic.ac.kr>
* Add label-index command types and keywords #2716 Add the TS.MGET, TS.MRANGE, TS.MREVRANGE, TS.QUERYINDEX and TS.QUERYLABELS command types, the WITHLABELS, SELECTED_LABELS and EXCLUDEEMPTY keywords, and register the five commands as read-only. * Add TsFilter #2716 TsFilter builds one label filter expression for the label-index commands: equal, notEqual, exists, notExists, in and notIn, plus raw for syntax the factories do not cover. Its toString is the expression sent to the server. Co-authored-by: HwangRock <peter9244@catholic.ac.kr> * Add TS.MGET and TS.QUERYINDEX #2716 Add TS.MGET with TsMGetArgs (LATEST, WITHLABELS or SELECTED_LABELS) and TS.QUERYINDEX to every API flavor, both taking TsFilter expressions. TsMGetValueParser reads the RESP2 array and the RESP3 map into equal TsMGetValue lists; TsSampleParser decodes the nested samples. Co-authored-by: HwangRock <peter9244@catholic.ac.kr> * Add TsReducer and TsMRangeArgs #2716 Add TsReducer with the TsReducerType constants, and TsMRangeArgs for TS.MRANGE and TS.MREVRANGE: the range options plus labels, GROUPBY with a reducer and EXCLUDEEMPTY. build(CommandArgs, List<TsFilter>) places FILTER between the range options and GROUPBY. Conflicts the server would reject are rejected by the setter that completes them and again when the arguments are built. The getters expose what the reply parser needs. * Add TS.MRANGE and TS.MREVRANGE #2716 Add TS.MRANGE and TS.MREVRANGE to every API flavor, with TsFilter expressions and optional TsMRangeArgs. TsSeriesParser reads the RESP2 array and the RESP3 map into TsSeries; it takes the aggregations, the reducer and the GROUPBY flag from the request, so both protocols give equal results, and it moves the RESP2 __source__ label into the group's source keys. The whole reply is held in memory before parsing. * Add TS.QUERYLABELS #2716 Add tsQueryLabels (LABELS) and tsQueryLabelValues (VALUES label) to every API flavor. The overloads without a filter query all indexed series; the filter overloads require at least one filter, so an empty array is a usage error rather than an all-series query. Results are UTF-8 strings whatever the codec. Requires Redis 8.10. * Route keyless TimeSeries commands in Redis Cluster #2716 The advanced cluster API runs TS.MGET, TS.MRANGE, TS.MREVRANGE, TS.QUERYINDEX and TS.QUERYLABELS on one node, chosen as for FT.*, with routeKeyless (async) and routeKeylessMany (reactive). The commands are read-only, so the node follows ReadFrom. There is no client fan-out, because the server gathers across shards. The node-selection interfaces document that each selected node returns every shard's series on 8.10. * Document TimeSeries label-index reads #2716 Add the label-index reads section to the TimeSeries user guide: filters, TS.MGET and TS.QUERYINDEX, TS.MRANGE with labels, GROUPBY and EXCLUDEEMPTY, the in-memory reply limit, TS.QUERYLABELS, and the Redis Cluster behavior by server version, including the error fallback. * Add TimeSeries label-index read unit tests #2716 * Add TimeSeries label-index read integration tests #2716 * Add TimeSeries label-index read cluster tests #2716 * Disable the cluster async keyless argument test #2716 On the cluster async API, routeKeyless builds keyless commands inside thenCompose after picking a node, so an invalid argument fails the returned future instead of throwing from the call. Keep the test disabled until #3962 is fixed for the keyless cluster commands. * Document EXCLUDEEMPTY on servers before Redis 8.10 #2716 Servers before 8.10 accept EXCLUDEEMPTY and ignore it without an error, so series without samples in the range are still returned. State this in the Javadoc of excludeEmpty() and of TS.MRANGE/TS.MREVRANGE in every flavor, and in the user guide. * Document the bucket duration check of TsMRangeArgs #2716 TsMRangeArgs inherits the check that rejects a non-positive bucket duration. State it in the Javadoc of its aggregation methods and Builder factories, and add a unit test for the inherited check. * Return the wire token from TsReducerType toString #2716 TsReducerType now returns the token sent to the server from toString(), for example STD.P instead of the constant name STD_P, like the other TimeSeries value types. A unit test checks every constant. * Drop the REFRESHCLUSTER note from TS.QUERYLABELS #2716 TS.QUERYLABELS requires Redis 8.10, so the cluster note of the six QUERYLABELS methods no longer mentions TIMESERIES.REFRESHCLUSTER on Redis 8.0 to 8.8. All six flavors. * List the exact @throws of TsMRangeArgs.Builder.aggregation #2716 The @throws of Builder.aggregation(Duration, TsAggregation...) now lists the conditions of the setter it delegates to that can occur on new arguments. --------- Co-authored-by: HwangRock <peter9244@catholic.ac.kr>
* Add multi-key range command types #2716 Add the TS.NRANGE and TS.NREVRANGE command types and register both as read-only. * Add TsNRangeArgs and TsNRangeRow #2716 TsNRangeArgs holds the TS.NRANGE and TS.NREVRANGE options: the shared range options and an aggregation with one aggregation list per key, or aggregationForEachKey to send the same aggregations for every key. It is built with the key count, so a list count that differs from the number of keys is rejected. TsNRangeRow is one row of flat values with per-key slices, and TsNRangeRowListOutput streams the rows. * Add TS.NRANGE and TS.NREVRANGE #2716 Add TS.NRANGE and TS.NREVRANGE to every API flavor, with a key list or two keys, a Range<Long> and optional TsNRangeArgs. The builder sends numkeys and then the keys, so the first key routes the command in Redis Cluster, and builds the output with the value count of each key. Rows keep the server's order. Requires Redis 8.10. * Document TimeSeries multi-key reads #2716 Add the multi-key reads section to the TimeSeries user guide (TS.NRANGE and TS.NREVRANGE, per-key aggregation, one slot in Redis Cluster), the Lettuce 8.0 entry in docs/new-features.md, and list RedisTimeSeries as supported in docs/compatibility-roadmap.md. * Add TimeSeries multi-key read unit tests #2716 * Add TimeSeries multi-key read integration tests #2716 * Add TimeSeries multi-key read cluster tests #2716 * Reject a non-positive bucket duration in TsNRangeArgs #2716 The server rejects a bucket duration of zero or less. aggregation and aggregationForEachKey now throw IllegalArgumentException for it; their Duration overloads already reject durations shorter than one millisecond. State it in the Javadoc and add a unit test for the check. * List the exact @throws of TsNRangeArgs duration aggregations #2716 The @throws of the Duration forms of Builder.aggregation, Builder.aggregationForEachKey and aggregation(Duration, List) now list the same conditions as their long counterparts, with the Duration rules.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3197184f15
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
BaseTsSeriesArgs no longer holds TIMESTAMP, ENCODING and ON_DUPLICATE. Its final build calls the package-private hook buildOwnArgs first, then emits the shared options with LABELS last. BaseTsCreateArgs is removed: TsCreateArgs, TsAddArgs and TsIncrByArgs extend BaseTsSeriesArgs and each keeps its own encoding with an encoding(TsEncoding) setter. TsAddArgs writes ENCODING and ON_DUPLICATE, TsIncrByArgs TIMESTAMP and ENCODING, TsAlterArgs nothing. The unit tests expect the new token order and the new hierarchy.
Review finding 1.1. TsInfoValueParser.parse returns null for a null reply instead of throwing, so a TS.INFO queued in an EXEC aborted by WATCH completes cleanly. Updated test: TsInfoValueParserUnitTests.nullDataThrows, now nullDataGivesNull.
Review finding 1.3, with N2. TsAggregation.of, TsReducer.of, TsDuplicatePolicy.of and TsEncoding.of reject a name containing ',' or whitespace, which would break the wire form (min,max) or become a raw value. The Javadoc of the four interfaces says they are implemented by Lettuce, not by applications.
Review finding 1.9. The public TsNRangeArgs.getColumnCounts(int), needed only by the command builder, is removed. build(CommandArgs, int) returns the unmodifiable column counts instead, and RedisTimeSeriesCommandBuilder uses them.
Review finding 1.6. TsRangeArgs.Builder, TsMRangeArgs.Builder and TsNRangeArgs.Builder lose empty(), align(long), alignStart(), alignEnd() and bucketTimestamp(...), which always fail when used alone; the instance methods stay. Updated tests: TsRangeArgsUnitTests (aggregationOnlyOptionsWithoutAggregation, aggregationSetAfterDependentOptions, setterValidation, builderFactories), TsNRangeArgsUnitTests (aggregationOnlyOptionsWithoutAggregation, builderFactories).
Review finding 2.2. The TsAlterArgs class Javadoc and Builder.label/labels say that on TS.ALTER both send the complete label set, so labels not listed are removed. Javadoc only.
Review finding 3.1. docs/new-features.md said all 21 TS.* commands work on Redis 8.0; it now says that TS.READ, TS.QUERYLABELS, TS.NRANGE and TS.NREVRANGE need Redis 8.10.
Review finding 3.2.
The aligned tsCreateRule example in the user guide now creates its
destination series {sensor1}:max-1h first, as the rule requires.
Review finding 3.5. Test Javadoc no longer points to documents that are not in the repository (the canonical server notes, design and test-plan sections, QA reports, HLD and requirement IDs). Scenario IDs and facts stay. Comments only.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dd03cd7ae4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| for (String value : values) { | ||
| assertNoSyntax(value, "Value"); | ||
| } |
There was a problem hiding this comment.
Reject empty members in membership filters
For inputs such as TsFilter.in("area", "") or an empty member among several values, this loop accepts the empty string because it contains no syntax characters, and the factory emits area=() or a list with an empty item. That no longer represents the requested value membership and is rejected or interpreted as malformed filter syntax by Redis, whereas the single-value factories already reject empty values immediately. Validate each member with the same non-empty check used by value(String).
Useful? React with 👍 / 👎.
User decision (after the batch), following RediSearch. The TimeSeries command interfaces in every flavor, the Kotlin interface and impl, and TsInfoValueParser, TsMGetValueParser and TsSeriesParser take only the key type K; the parsers accept RedisCodec<K, ?>. The aggregates extend the TimeSeries groups with <K>. Updated declarations in RedisTimeSeriesIntegrationTests and RedisTimeSeriesClusterIntegrationTests.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1ed5498e47
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| @Override | ||
| public RedisFuture<List<TsMGetValue<K>>> tsMGet(TsFilter filter) { | ||
| return routeKeyless(() -> super.tsMGet(filter), conn -> conn.tsMGet(filter), CommandType.TS_MGET); |
There was a problem hiding this comment.
Propagate routed TimeSeries command errors
When the selected node executes this command but returns a server-side error—for example NOPERM, an unsupported command during a rolling upgrade, or a malformed raw filter—routeKeyless treats the exceptional completion as a routing failure and invokes superCall, issuing the read again through the default cluster writer. This can mask the original error or return data from a node that does not follow the configured ReadFrom policy; the added keyless TimeSeries overrides should fall back only when connection selection fails and propagate command execution errors.
Useful? React with 👍 / 👎.
Decision D7.3 (ts-docs/DECISIONS-2026-10-26.md); review status §4 items 4 and 5.
Source: review status §4.
count(n) on TsRangeArgs, TsMRangeArgs and TsNRangeArgs (and their Builder
factories) rejects n <= 0 with IllegalArgumentException; the server
answers "Invalid COUNT value" for both (checked on 8.10). A bucket
Duration longer than Long.MAX_VALUE milliseconds now throws
IllegalArgumentException ("Bucket duration is too large") from
TsArgsSupport instead of ArithmeticException.
The ALIGN check with an open range end is not in this commit: the args
don't see the range, so a client check needs an API decision (see
ts-run/STATUS.md).
Decision D7.5 (ts-docs/DECISIONS-2026-10-26.md); review status §4 item 2. Source: review status §4. The per-key aggregation(long|Duration, ...) setters and their Builder factories take List<? extends List<? extends TsAggregation>>, so a prebuilt list of TsAggregationType lists compiles. The copy and the emitted tokens are unchanged. Source-compatible widening.
Decision D1 (ts-docs/DECISIONS-2026-10-26.md); review status §1 item 1. Source: review status §1 item 1. tsAdd(key, value[, args]) and TsMAddValue.of(key, value) are renamed to tsAddNow and TsMAddValue.now in all flavors (sync, async, reactive, node selection, Kotlin), so a lone long timestamp no longer widens to the value-only overload. The widening warning is dropped from the Javadoc and the user guide, whose examples use the new names. Renamed calls in RedisTimeSeriesCommandBuilderUnitTests (A05), RedisTimeSeriesIntegrationTests (B13, null-key check) and TsMAddValueUnitTests (U14, equality).
Decision D2 (ts-docs/DECISIONS-2026-10-26.md); review status §1 item 2. Source: review status §1 item 2. TsReadOffset.latest() becomes newest(); it still sends "+". The range and MGET args keep latest(), which sends the server keyword LATEST, so the two meanings no longer share a name. The TS.READ Javadoc in every flavor, the TsReadOffset Javadoc and the user guide use the new name. Renamed calls in RedisTimeSeriesCommandBuilderUnitTests, the readOffsets integration test and TsReadOffsetUnitTests (A11).
Decision D7.4 (ts-docs/DECISIONS-2026-10-26.md); review status §4 item 3. Source: review status §4. Their constructors are already package-private, so no code outside the package can subclass them; final states it. TsApiConventionsUnitTests gets an M04 check that the three types are final.
Decision D5.4 (ts-docs/DECISIONS-2026-10-26.md); review status §2 item 4. Source: review status §2 item 4. TsSeriesParser removed the __reducer__ and __source__ labels from every MRANGE/MREVRANGE series, so a series' own labels with those names were missing under WITHLABELS. It now removes them only from a grouped reply, where RESP2 uses them for the reducer and the sources. Tests: TsSeriesParserUnitTests (RESP2 and RESP3 ungrouped replies keep both labels; the grouped tests are unchanged) and an MRANGE WITHLABELS round-trip in RedisTimeSeriesIntegrationTests.
Decision D5.5 (ts-docs/DECISIONS-2026-10-26.md); review status §2 item 5.
Source: review status §2 item 5.
Revised after review: REVIEW-FIXES-2026-10-26.md R1.
An exclusive upper bound 0 became toTimestamp -1, which the server
rejects ("wrong toTimestamp"). An inverted interval is not safe either:
with AGGREGATION ... EMPTY, Redis 8.10 fills a bucket at 0 for it. Any
upper bound below 0 now throws IllegalArgumentException on the client,
on RANGE/REVRANGE, DEL, MRANGE/MREVRANGE and NRANGE/NREVRANGE; nothing
is sent and nothing is clamped. The @throws of every Range overload in
every flavor and the user guide say so.
Tests: builder tests for exclusive 0, including(-1) and 5..excluding(0)
on every command, with exclusive 1 and an upper bound of exactly 0
still sent as 0; the A06c assertion for an exclusive upper bound
Long.MIN_VALUE + 1 now expects the exception; an integration test for
RANGE (also with EMPTY), REVRANGE and DEL, with the sample at 0 kept.
Decision D5.6 (ts-docs/DECISIONS-2026-10-26.md); review status §2 item 6.
Source: review status §2 item 6.
The TsFilter class Javadoc and the guide showed raw("room=\"living
room\""), whose quotes become part of the value, so it matches nothing.
The example is now raw("room=living room"), and the Javadoc and the
guide say that each filter is one RESP argument, so a space needs no
quotes and quotes are literal. The guide row shows a real use of raw.
Tests: TsFilterUnitTests (raw keeps the space) and an integration test:
raw("room=living room") matches through QUERYINDEX and MGET, the quoted
form matches nothing. equal() still rejects the space (finding 1.2), so
the "through equal" case of DECISIONS is asserted as a client rejection.
Decision D5.7 (ts-docs/DECISIONS-2026-10-26.md); review status §2 item 7. Source: Codex bot on #3978. in("area", "") sent area=(), which silently matches nothing, and in("area", "north", "") got a server parse error. in and notIn now check each value like equal does and throw IllegalArgumentException for an empty one; the @PARAM and @throws Javadoc say so. Test: TsFilterUnitTests, "" alone and among other values for both.
Decision D5.9 (ts-docs/DECISIONS-2026-10-26.md); review status §2 item 9. Source: Codex detailed reviewer (A1). The reactive cluster tsMAdd merged the per-slot MADDs with Flux.mergeSequential(publishers), which subscribes at most 256 at a time. With auto-flush off and samples in more than 256 slots, one flushCommands() sent only 256 and the call hung. The merge now subscribes to all slots (concurrency = number of slots), so every command is written before the flush. The result order is unchanged. Test: RedisTimeSeriesClusterIntegrationTests, 300 slots, auto-flush off, one flush, all results in caller order (times out without the fix on 8.8).
Decision D7.1 (ts-docs/DECISIONS-2026-10-26.md); review status §4 item 8. Source: lettuce-jedis re-review. TsInfoValueParser built the chunk type, the duplicate policy and the rule aggregation with the strict of(String) factories, which reject ',' and whitespace, so such a server value would throw while decoding and close the connection. A package-private TsValueTypes now holds lenient lookups (enum constant for a known name, raw value otherwise) that the parser uses; the public of() validates and then delegates to them. TsReducer.of is not used by any parser, so it is unchanged. Test: TsInfoValueParserUnitTests, RESP2 and RESP3 replies with "z std", "keep,last" and "avg,max" decode to raw values kept as given.
Decision D7.2 (ts-docs/DECISIONS-2026-10-26.md); review status §4 item 1. Source: review status §4. The public TsSeriesParser constructor kept the caller's aggregation list, so changing it later changed results already returned, and it accepted grouped = true with a null reducer. It now keeps an unmodifiable copy and throws IllegalArgumentException for a grouped parser without a reducer. The constructor Javadoc says so. Tests: TsSeriesParserUnitTests for both.
- Warn that a cancelled reactive TS.READ stays blocked - Add @SInCE 8.0 to the TimeSeries output constructors - Use RedisGraph in the roadmap custom-command example - Drop GROUPBY from the return of MRANGE without args - Add the NaN note to tsAddNow - Describe NaN on INCRBY and DECRBY as a server rule - Note that several aggregations need Redis 8.8 - State which TS.READ BLOCK is rejected in MULTI - State that node-selection tsMAdd keys share one slot
- Close the RESP2 TimeSeries test connection - Widen the TS.READ blocking margins and pause polling - Add unit tests for TimeSeriesSlotPartition - Check that all 9 TimeSeries writes are not read-only - Fail with the missing key in keyed TimeSeries results - Gate the COUNTNAN/COUNTALL reducer test on Redis 8.8
Summary
Adds native Redis TimeSeries support: all 21
TS.*commands, throughRedisTimeSeriesCommandsand its async, reactive, Kotlin coroutine, cluster and node-selection flavors. It works on Redis 8.0 and later; TS.READ, TS.QUERYLABELS and TS.NRANGE/TS.NREVRANGE need Redis 8.10.The branch has one commit per area, each reviewed as its own PR (#3967–#3972). Fixes from the final review follow as separate commits on top.
TsInfoValue, user guideTsSampleTsFilter, GROUPBY/REDUCETsNRangeRowKey Decisions & Assumptions
of(String). Values a newer server adds still pass through.ReadFrom.nullelement, also inside MULTI/EXEC (relies on fix(output): forward ByteBuffer error to queued command output (#3960) #3963).Testing
Notes
Known gap: on the cluster async API, invalid arguments to the label-index commands fail the returned future instead of throwing from the call (Cluster keyless commands treat invalid arguments as a routing failure (async API also reports them through the future) #3962, shared with the FT.* commands).
Credit: parts are based on Add RedisTimeSeries (TS.*) support #3852 by @HwangRock, credited with
Co-authored-byon the commits that reuse his code.Merge with "Rebase and merge" to keep the area commits and the fix commits on
main.You have read the contribution guidelines.
You have created a feature request first to discuss your contribution intent: Add support for Redis Timeseries #2716
You applied code formatting rules using the
mvn formatter:formattarget.You submit test cases (unit or integration tests) that back your changes.
Note
Medium Risk
Large new command surface with non-trivial cluster routing (multi-slot MADD) and blocking TS.READ connection semantics; changes follow existing module patterns but affect production data paths.
Overview
Adds native Redis TimeSeries in Lettuce 8.0: all 21
TS.*commands surface through newRedisTimeSeriesCommands(and async, reactive, cluster, and node-selection APIs), wired viaRedisTimeSeriesCommandBuilder, typed args/value types underio.lettuce.core.timeseries, and dedicated RESP parsers (TsSample,TsNRangeRow, error-tolerantTS.MADDlist decoding).Redis Cluster behavior is extended:
TS.MADDis split per hash slot in parallel with caller order restored (not atomic across slots); label-index commands (TS.MGET,TS.MRANGE/MREVRANGE,TS.QUERYINDEX,TS.QUERYLABELS) route like other keyless reads viarouteKeylessand honorReadFrom.Docs move TimeSeries from unsupported to Supported (8.0), add a user guide (
timeseries.md), release notes, and mkdocs nav; the custom-command example now uses RedisGraph instead of TimeSeries.Reviewed by Cursor Bugbot for commit 33b4c6b. Bugbot is set up for automated code reviews on this repo. Configure here.