Add RedisTimeSeries (TS.*) support - #3852
Open
HwangRock wants to merge 3 commits into
Open
Conversation
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.
Collaborator
|
Hey @HwangRock |
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
self-requested a review
July 23, 2026 12:12
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the CRUD portion of the RedisTimeSeries umbrella #2716.
Motivation
Lettuce has no RedisTimeSeries support;
docs/compatibility-roadmap.mdlists 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:
Structure mirrors the existing Bloom/Cuckoo Filter modules:
RedisTimeSeriesCommandBuilder(package-private) for RESP encoding.src/main/templates/.../RedisTimeSeriesCommands.java) that the apigenerator turns into the sync/async/reactive and node-selection variants.io.lettuce.core.timeseries.arguments.io.lettuce.core.timeseries(TsSample,TsInfoValue,TsMGetValue).Two things worth calling out for review:
TS.INFOandTS.MGETreturn 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.DECRBYshare one args class (TsIncrByArgs) because the server registers both to the same handler (TSDB_incrby), differing only by anargv[0]compare. They intentionally exposeduplicatePolicy()but notonDuplicate(), since the server hardcodesDP_LASTon that path.Read-only commands (
TS.GET/TS.MGET/TS.INFO/TS.QUERYINDEX) are registered inReadOnlyCommandsfor replica routing.Range queries (
TS.RANGE/TS.REVRANGE/TS.MRANGE/TS.MREVRANGE, #2708–#2711) are left for a follow-up PR. TheirGROUPBY/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:IGNOREfilters,NaN/Infinityvalues, compaction timing, and label charset limits — each assertion pinned to observed server behaviour.Two known gaps:
@Disabledpending Signal onError instead of hanging when a reactive complex-output command errors #3851. That PR fixes a pre-existing hang inRedisPublisher(unrelated to this change) where reactive commands don't terminate on a server error. sync/async are unaffected and tested here.client-libs-testimage 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.MGETrather 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, andio.lettuce.core.timeseriestypes/argument builders.AbstractRedisAsyncCommands/AbstractRedisReactiveCommandsnow implementRedisTimeSeries*Commandsand delegatets*methods to the new builder. Cluster node-selection interfaces and Kotlin coroutine facades are extended the same way. Protocol layer gainsTS_*command types, TimeSeriesCommandKeywords, and read-only registration for replica routing.Response parsing for
TS.INFOandTS.MGETbranches on actual reply shape (ComplexData.isMap()), not negotiated RESP version, so nested RESP2 label arrays and RESP3 maps both decode correctly.TsIncrByArgsis shared byTS.INCRBY/TS.DECRBY(noonDuplicate). Range commands (TS.RANGE/MRANGEetc.) 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.