Skip to content

Add RedisTimeSeries (TS.*) support - #3852

Open
HwangRock wants to merge 3 commits into
redis:mainfrom
HwangRock:feature/ts-commands
Open

Add RedisTimeSeries (TS.*) support#3852
HwangRock wants to merge 3 commits into
redis:mainfrom
HwangRock:feature/ts-commands

Conversation

@HwangRock

@HwangRock HwangRock commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Implements the CRUD portion of the RedisTimeSeries umbrella #2716.

Motivation

Lettuce has no RedisTimeSeries support; docs/compatibility-roadmap.md lists it as "Not Supported". Jedis, redis-py, node-redis, and go-redis all ship it. This adds the create/read/update/delete surface so the module is usable from Lettuce.

Modification

13 commands across the sync, async, reactive, cluster, and coroutine APIs:

TS.CREATE  TS.ALTER  TS.CREATERULE  TS.DELETERULE  TS.DEL
TS.ADD     TS.MADD   TS.INCRBY      TS.DECRBY
TS.GET     TS.MGET   TS.INFO        TS.QUERYINDEX

Structure mirrors the existing Bloom/Cuckoo Filter modules:

  • RedisTimeSeriesCommandBuilder (package-private) for RESP encoding.
  • A template interface (src/main/templates/.../RedisTimeSeriesCommands.java) that the apigenerator turns into the sync/async/reactive and node-selection variants.
  • Argument builders under io.lettuce.core.timeseries.arguments.
  • Value/parser types under io.lettuce.core.timeseries (TsSample, TsInfoValue, TsMGetValue).

Two things worth calling out for review:

  • RESP2/RESP3 parsing. TS.INFO and TS.MGET return different container shapes across protocols, and the labels field is a nested pair-array under RESP2 but a map under RESP3. The parsers branch on the actual reply type (ComplexData.isMap()), not the negotiated protocol version — the server falls back to arrays on older module APIs even under RESP3, so keying off the protocol version would misparse.
  • TS.INCRBY/TS.DECRBY share one args class (TsIncrByArgs) because the server registers both to the same handler (TSDB_incrby), differing only by an argv[0] compare. They intentionally expose duplicatePolicy() but not onDuplicate(), since the server hardcodes DP_LAST on that path.

Read-only commands (TS.GET/TS.MGET/TS.INFO/TS.QUERYINDEX) are registered in ReadOnlyCommands for replica routing.

Range queries (TS.RANGE/TS.REVRANGE/TS.MRANGE/TS.MREVRANGE, #2708#2711) are left for a follow-up PR. Their GROUPBY/aggregation responses have RESP2/RESP3 shapes that are not documented and need separate handling, so I kept them out to keep this reviewable.

Result

Verified against redis:8. Tests included:

  • Unit tests for the builder, each args class, and the parsers (including RESP2/RESP3 branch coverage).
  • Integration tests for CRUD round-trips.
  • Edge-case integration tests: duplicate-policy behaviour (all six policies), IGNORE filters, NaN/Infinity values, compaction timing, and label charset limits — each assertion pinned to observed server behaviour.

Two known gaps:

  • One reactive integration test is @Disabled pending Signal onError instead of hanging when a reactive complex-output command errors #3851. That PR fixes a pre-existing hang in RedisPublisher (unrelated to this change) where reactive commands don't terminate on a server error. sync/async are unaffected and tested here.
  • The cluster integration test compiles but couldn't run locally (the client-libs-test image has no arm64 manifest); it needs CI or an x86 host.

Issues

Resolves #2699
Resolves #2700
Resolves #2701
Resolves #2702
Resolves #2703
Resolves #2704
Resolves #2705
Resolves #2706
Resolves #2707
Resolves #2712
Resolves #2713
Resolves #2714
Resolves #2715

Part of #2716. #2708, #2709, #2710, #2711 remain for the range-query follow-up.

I can split this differently or adjust the API if the direction looks off.


Note

Medium Risk
Large additive API surface with non-trivial RESP2/RESP3 parsing; mistakes would misdecode TS.INFO/TS.MGET rather than affecting core Redis commands, but cluster/replica routing depends on correct read-only classification.

Overview
Adds first-class RedisTimeSeries support to Lettuce (create/alter/rules, add/madd/incr/decr, get/mget/info/queryindex), wired through the same module pattern as Bloom/Cuckoo/TopK: RedisTimeSeriesCommandBuilder, apigenerator-driven APIs, and io.lettuce.core.timeseries types/argument builders.

AbstractRedisAsyncCommands / AbstractRedisReactiveCommands now implement RedisTimeSeries*Commands and delegate ts* methods to the new builder. Cluster node-selection interfaces and Kotlin coroutine facades are extended the same way. Protocol layer gains TS_* command types, TimeSeries CommandKeywords, and read-only registration for replica routing.

Response parsing for TS.INFO and TS.MGET branches on actual reply shape (ComplexData.isMap()), not negotiated RESP version, so nested RESP2 label arrays and RESP3 maps both decode correctly. TsIncrByArgs is shared by TS.INCRBY/TS.DECRBY (no onDuplicate). Range commands (TS.RANGE / MRANGE etc.) are explicitly out of scope for this PR.

Reviewed by Cursor Bugbot for commit e745b62. Bugbot is set up for automated code reviews on this repo. Configure here.

Implements 13 RedisTimeSeries commands across the sync, async, reactive,
cluster, and coroutine APIs:

  TS.CREATE, TS.ALTER, TS.CREATERULE, TS.DELETERULE, TS.DEL,
  TS.ADD, TS.MADD, TS.INCRBY, TS.DECRBY,
  TS.GET, TS.MGET, TS.INFO, TS.QUERYINDEX

This covers the create/read/update/delete surface. Range queries
(TS.RANGE/REVRANGE/MRANGE/MREVRANGE) are left for a follow-up, since their
GROUPBY/aggregation response shapes need separate handling.

The command set mirrors the existing Bloom/Cuckoo Filter structure: a
package-private RedisTimeSeriesCommandBuilder, a template-driven interface
generating the sync/async/reactive/node-selection variants, argument builders
under io.lettuce.core.timeseries.arguments, and value/parser types under
io.lettuce.core.timeseries. TS.INFO/TS.MGET parse RESP2 and RESP3 into the same
domain objects; the parsers branch on the actual reply type (isMap()) rather
than the negotiated protocol version, because the server falls back to arrays
on older module APIs even under RESP3.

Verified against redis:8. Read-only commands (TS.GET/MGET/INFO/QUERYINDEX) are
registered for replica routing. Includes unit tests for the builder, args, and
parsers, plus integration tests covering CRUD round-trips and edge cases
(duplicate policies, IGNORE filters, NaN/Infinity values, compaction timing,
label charset limits).

One reactive integration test is @disabled pending redis#3851, which
fixes an unrelated pre-existing hang in RedisPublisher on server errors.
The cluster suite inherits the standalone tests over a cluster connection.
Six of them cannot pass unchanged on a cluster:

- tsMAddAndGetRoundTrip sends multiple keys in one TS.MADD; without a hash
  tag they land on different slots and the server returns CROSSSLOT. Tagged
  the keys ({ts-madd}:k1/k2) so they share a slot; the braces are inert on
  standalone.
- TS.MGET/TS.QUERYINDEX with a label FILTER match series across the whole
  keyspace, which a single-node connection can't cover on a cluster. Disabled
  those five via @OverRide, matching how Geo/Stream cluster suites handle
  keyless commands.

Verified against a real 7-node cluster (test-cluster) plus standalone: 124
tests, 0 failures, 5 skipped.
RedisTimeSeries only accepts NaN values from Redis 8.6 onward; 8.2 and 8.4
reject TS.ADD ... nan with "ERR TSDB: invalid value". The NaN tests assumed
8.8 behaviour and failed on the 8.2 CI matrix job.

Guard the six NaN tests with assumeTrue(RedisConditions ... "8.6"), matching
the assumeTrue/RedisConditions pattern already used for version-gated Vector
Set tests. The Infinity and async tests in the same class stay unguarded since
they pass on every version.

Verified locally: redis:8.4 skips the six (10 run, 6 skipped), redis:8.8 runs
all ten, the rest of the TS suite is unaffected.
@a-TODO-rov

Copy link
Copy Markdown
Collaborator

Hey @HwangRock
Thanks for this big contribution 😅
I guess it will be some long review ahead.

@HwangRock

Copy link
Copy Markdown
Contributor Author

@a-TODO-rov Thanks! 😄 Yeah, it ended up being a pretty big PR. I tried to keep it as one coherent slice covering the CRUD commands, while leaving the range queries (TS.RANGE/MRANGE, etc.) for a separate PR to keep this one focused.

If you'd still prefer smaller chunks, I'd be happy to split the CRUD commands further (for example, lifecycle / writes / reads). Just let me know what would make the review easier.

@Dgramada
Dgramada self-requested a review July 23, 2026 12:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants