Extend the Jedis Commands API
Implement a new Redis command (or extend an existing one) in Jedis, following the conventions Jedis maintainers enforce in review.
Phase 0 — Gather evidence BEFORE planning
Do all of the following before writing any plan or code:
Ask the user for the HLD. Interactively ask the user for the path to a markdown file containing the High-Level Design for the command(s) (or confirm that no HLD exists). If a path is given, read it fully — it is the primary source for syntax, semantics, reply shape, and edge cases.
Find the server-side PR in the
redis/redisGitHub repo. First verify thatghworks in the current (sandboxed) environment:gh auth statusSandboxes often block access to credential files (
~/.netrc, gh config), soghmay report unauthenticated here even though it works on the user's machine. Ifgh auth statusfails or reports no authentication, ask the user for permission to run theghcommands outside the sandbox (e.g. with the sandbox override for these specific read-only commands), explaining thatghcannot reach its credentials from inside the sandbox. Only if the user declines (orghis genuinely not logged in anywhere) fall back to the unauthenticated GitHub REST API or fetchinghttps://github.com/redis/redis/pulls?q=<command>.Then search for the PR that adds/extends the command on the server:
gh search prs --repo redis/redis "<COMMAND NAME>" --limit 10 gh pr view <num> --repo redis/redis gh pr diff <num> --repo redis/redis # look at src/commands/*.json for exact syntaxFrom the server PR, extract: exact wire syntax (argument order and optionality), reply type per RESP2/RESP3, error conditions, time complexity, and — critically — the first server version carrying the feature (RC builds like
8.7.225= Redis 8.8 RC are used for test gating). Note whether the server marks the feature experimental / in preview.Verify the command exists on the "next" Redis OSS version. Start the integration environment using the latest
.env.vX.XXfile undersrc/test/resources/env/(pick the highest version numerically, not lexicographically — e.g.8.10>8.8):ls src/test/resources/env/.env.v* | sort -V | tail -1 # → e.g. .env.v8.10 make start version=8.10Then probe the
standalone0endpoint. Read its connection details fromsrc/test/resources/endpoints.json(currentlyredis://localhost:6379, passwordfoobared) and check the target command via redis-cli:redis-cli -u redis://localhost:6379 -a foobared INFO server | grep redis_version redis-cli -u redis://localhost:6379 -a foobared COMMAND INFO <COMMAND> # non-empty → command exists redis-cli -u redis://localhost:6379 -a foobared COMMAND DOCS <COMMAND> # arity/args — compare with the server PRFor a NEW command,
COMMAND INFOmust return a non-empty reply. For an EXTENDED command, additionally invoke it with the new syntax against a scratch key and confirm the server accepts it (noERR syntax error/ERR unknown argument).If the command/option is missing on the latest env version, the image tag pinned in the
.env.vX.XXfile is too old for the target change. Interactively ask the user for aredislabs/client-libs-testimage tag that contains it (e.g. an RC/edge/milestone build — tags are listed at https://hub.docker.com/r/redislabs/client-libs-test/tags; you may check them yourself and suggest candidates). Then restart the environment with that tag and re-run the probes:make stop make start CLIENT_LIBS_TEST_IMAGE_TAG=<tag>If the user has no tag to offer (or the probe still fails), report it and proceed anyway: the implementation can continue, but integration tests will be gated/skipped until a
redislabs/client-libs-testimage ships the change. Keep the environment running for later test runs, ormake stopif you won't need it soon.Create redis-cli showcase test cases. Once the command is available on the running environment, derive a small set of redis-cli scenarios from the HLD and the server PR and run them against
standalone0. These serve two purposes at once:- Smoke test: prove the documented behavior actually holds on the target
server — happy path, each new option/keyword, reply shape (run with
redis-cli -3too if RESP3 replies differ), edge cases and error conditions called out in the HLD (empty/missing key, out-of-range args, conflicting options), so the Java implementation is built against observed replies, not assumptions. - Showcase: each scenario should read as a mini use-case that explains
WHY the command/option exists — what a user could not do (or could only do
awkwardly) before, and how the new API covers it. Prefer realistic data
over
foo/bar(e.g. rate-limit counters for a bounded INCR, sensor readings for a time-series aggregator).
Save the scenarios as a commented script in a scratch file (one block per use-case: a one-line "what this demonstrates" comment, the redis-cli commands, and the observed reply pasted back as comments). Carry this material forward: it feeds the Phase 1 plan (as the explanation of the feature and the source of expected values for tests), the Java test assertions, and the PR description. If the command could not be made available on any image (see step 3), still write the scenarios from the HLD/PR as expected transcripts and mark them unverified.
- Smoke test: prove the documented behavior actually holds on the target
server — happy path, each new option/keyword, reply shape (run with
Read
docs/integration-testing.mdin the Jedis repo — it defines the test environment, endpoint discovery,*Testvs*ITnaming, and how to run tests.Trace one analogous existing command end-to-end in the codebase (same command group, similar reply shape) so the plan mirrors real code, not guesswork.
Determine the
@sinceversion:mvn help:evaluate -Dexpression=project.version -q -DforceStdoutStrip
-SNAPSHOT(e.g.8.0.0-SNAPSHOT→@since 8.0).
Phase 1 — Plan mode, then explicit approval
Enter plan mode. Using the evidence, classify the change with the decision tree below, enumerate the exact file-by-file touch list, the test matrix, and the gating annotations. Open the plan with a short "what this feature enables" section built from the redis-cli showcase scenarios (Phase 0 step 4), including one or two representative command/reply transcripts, so the user sees the use-case before the file list. Present the plan to the user and explicitly ask permission to execute in auto-accept mode before implementing. Do not start editing files until the user approves.
Decision tree — what kind of change is this?
A. Extension of an existing command that fits an existing params class (e.g. new enum value / new option token):
- Touch ONLY the params/model classes + tests. Do NOT touch command interfaces,
CommandObjects, UnifiedJedis, PipeliningBase, or Jedis — the existing
IParams.addParams(CommandArguments)delegation carries the new option through automatically, for both String and binary surfaces. - If the option is a bare token: add an enum constant implementing
Rawable(bytes come fromSafeEncoder.encode(name())— self-wiring). - If it's a new field: add a fluent setter returning
this, extendaddParams(), and updateequals/hashCodein sync. - If the reply shape grows (e.g. more array elements): extend the response model
class backward-compatibly — keep old constructors/getters working, add new
accessors, update the builder in the relevant
*BuilderFactoryto branch on reply size. Jedis does not defensively copy response collections (stated maintainer convention).
B. New core command(s) — the FULL matrix, every layer:
Protocol.java— newCommandenum constant(s); newKeywordconstants for sub-tokens, grouped under a// <FEATURE> keywordscomment, alphabetical. Never add a Keyword that duplicates a token already carried by a dedicatedRawableenum (dead keywords get flagged in review).- Command interfaces in
commands/:<Group>Commands,<Group>BinaryCommands,<Group>PipelineCommands,<Group>PipelineBinaryCommands. For a whole new command family, create four new<Family>*Commandsinterfaces and add oneextendsentry toJedisCommands,JedisBinaryCommands,PipelineCommands,PipelineBinaryCommands. CommandObjects— String and byte[] factory methods side by side under a// <Feature> commandssection.ClusterCommandObjectsneeds overrides ONLY for multi-key commands requiring slot checks; single-key commands need nothing.UnifiedJedis— one-line@Overridedelegating toexecuteCommand(commandObjects.xxx(...)).PipeliningBase—appendCommand(commandObjects.xxx(...))returningResponse<T>.Jedis(legacy) —checkIsInMultiOrPipeline(); connection.executeCommand(commandObjects.xxx(...)).pom.xml— add every new file to the formatter-maven-plugin includes list (this repo format-enforces an allowlist; new files must be registered).
C. Extension needing new overloads / a new params class (hybrid): new methods go through the full matrix of B; the option plumbing follows the params conventions below.
D. Module command (search / timeseries / json / bloom):
- Module interfaces are String-only — there are NO
*BinaryCommandsvariants for modules; never create them. - Module commands/keywords live in the module protocol class
(
TimeSeriesProtocol.TimeSeriesCommand/TimeSeriesKeyword,SearchProtocol.SearchCommand/SearchKeyword, …), each an enum implementingProtocolCommand/RawablewithSafeEncoder.encode()-cached bytes. - Strongly prefer extending the module's params/builder classes over adding interface methods — PRs #4504 and #4534 shipped whole features with ZERO changes to interfaces, CommandObjects, UnifiedJedis, or PipeliningBase.
- Search aggregation builders (
Reducersubclasses) build aList<Object>viagetOwnArgs(); the parent computesnargautomatically. Canonicalize clause order at serialization time regardless of builder call order. - Module response parsing lives in the module's builder factory
(
TimeSeriesBuilderFactory,SearchBuilderFactory); search aggregation results are loosely typed and often absorb new reply content with no parsing changes.
Binary (byte[]) variant policy
- Core commands: full String/byte[] parity is mandatory — all four interfaces, paired CommandObjects methods, and all three clients. Reviewers explicitly request binary integration tests too.
- Only keys and textual values get
byte[]overloads. Numeric/structural args (long index, ranges, booleans, enums) stay identical. - Params classes are shared, never duplicated: one params class serves both
surfaces; if a param carries text, give the params class both
foo(String)andfoo(byte[])setters (seeArgrepParams). - Return types mirror the key type (
String→byte[],List<String>→List<byte[]>); count/index/model results stay shared. - Modules: no binary variants at all.
Params class conventions (params/)
- Implement
IParamswithaddParams(CommandArguments args). Fluent setters returnthis; provide a static factory named after the class (increxParams(),rangeParams()). - Do NOT extend another command's params hierarchy. If two overloads share
options but differ in a typed field (long vs double), create a self-typed
abstract base:
BaseFooParams<T extends BaseFooParams<T>>with a privateself()cast, plus two concrete subclasses — compile-time type safety over a polymorphic single class. - Implement
equals/hashCode(needed for Mockito matching in mocked tests) and keep them in sync with every new field.toStringis not required on params classes. - Validate eagerly in setters with descriptive
IllegalArgumentException/IllegalStateExceptionmessages, consistent wording across sibling classes (e.g. "Aggregators must be non-null and non-empty"). Required-config checks may run at serialization time. No client-side server-version checks — an old server returning an error is acceptable and should be noted in the PR description. - Expiration-style option groups are single-slot, last-wins (mirror
SetParams). - Document the wire-emission order in javadoc and lock it down in unit tests.
Response mapping (BuilderFactory / resps/)
- Reuse existing generic builders whenever the reply maps to a standard shape
(
LONG_LIST,DOUBLE_LIST,STRING,ENCODED_OBJECT_MAP, …). Maintainers actively reject bespoke response classes for 2-element arrays and the like. - Reuse
redis.clients.jedis.util.KeyValue<K,V>for pair replies. - When a model class is genuinely needed (map-shaped INFO replies), put it in
resps/: publicStringconstants for reply field names, aMap<String,Object>-taking constructor, typed getters, plus a raw-map accessor for forward compatibility; subclass for FULL/extended variants. - Nullable numeric replies are boxed
Long— neverOptional/OptionalLongin the command API; consistency with existing signatures (e.g.zrank) wins. - Multi-mode commands (one wire command, several reply types by mode) are split
into distinctly-named typed Java methods (
aropAggregate/aropBitwise/aropCount) rather than one polymorphic method. But mode selected by argument type uses overloads of one name (increx(key, long, …)/increx(key, double, …)), with the mode keyword (BYINT/BYFLOAT) implied by the overload.
Encoding & enum rules
- String ↔ bytes:
SafeEncoder.encode()— neverString.getBytes(). - Numeric → bytes:
Protocol.toByteArray(). - Token-valued argument enums live in
args/and implementRawablewithraw = SafeEncoder.encode(name())cached in the constructor. - Compose multi-token byte values (e.g. comma-joined lists) at the byte level.
Javadoc, @since, @Experimental
- Interface methods only carry javadoc; implementations (
UnifiedJedis,Jedis,PipeliningBase,CommandObjects) carry none. - String-interface javadoc:
<b><a href="https://redis.io/commands/xxx">XXX Command</a></b>, prose semantics,Time complexity: O(...),@param,@return,@since <version>. Binary variants get short javadoc with@seeto the String variant. Pipeline variants: one line — "Pipeline variant of {@link ...}" +@since. @sinceon every new public method and class, computed frompom.xml.- If the server feature is preview/experimental, annotate ALL new public API
(interfaces, methods, params, args, resps) with
redis.clients.jedis.annots.Experimentaland label the PRexperimental. Do NOT use@Experimentalfor stable GA features.
Test matrix — what to write
Follow docs/integration-testing.md for environment, layout, and naming. The
established per-command test layers (write all that apply):
- Params unit test (
params/FooParamsTest, plain*Test, no Redis):@Nestedgroups (ValidationTests,AddParamsTests, overload-equivalence), asserting exact wire args and order withredis.clients.jedis.util.CommandArgumentsMatchers(hasArgumentCount,hasArguments) andRawableFactory.from(...); also test theequals/hashCodecontract. - Mocked delegation tests (unit, Mockito): add
@Mock CommandObject<T>fields toMockedCommandObjectsTestBase(name them by TYPE, e.g.listLongCommandObject— reuse existing ones when the type matches), thenwhen/verifytests inmocked/unified/UnifiedJedis<Group>CommandsTestandmocked/pipeline/PipeliningBase<Group>CommandsTest. - Unified integration tests: add methods to the existing abstract
commands/unified/<Group>CommandsTestBasewhen one exists; for a new family, create<Family>CommandsTestBase extends UnifiedJedisCommandsTestBaseplus thin per-topology runners — standalone (RedisClientCommandsTestHelper.getClient(protocol)) and cluster (ClusterCommandsTestHelper.getCleanCluster(protocol)). New concrete integration classes MUST be named*IT— never*IntegrationTest, never@Tag("integration")on new classes. - Cluster handling: multi-key commands need cluster test overrides using
hash-tagged keys (
s1{:}) so keys share a slot; tests semantically incompatible with cluster get@Test @Override+@Disabled("<reason>")empty bodies. - Legacy
Jediscoverage: extendcommands/jedis/<Group>*CommandsTest(which extendsJedisCommandsTestBase) — can be minimal (smoke/missing-key) when the unified base covers behavior fully. Binary variants get their own integration tests (reviewers ask for these explicitly).
Cross-cutting test conventions:
- RESP2/RESP3 comes free: test bases are
@ParameterizedClass+@MethodSource("redis.clients.jedis.commands.CommandsTestsParameters#respVersions")(or#jedisRespVersionsfor legacy) — no per-test work; just make sure new classes inherit the right base. - Gating: command already released →
@SinceRedisVersion("<RC build>")(e.g."8.7.225"for 8.8) at the shared base-class level ONCE — do not repeat it on subclasses (maintainers remove redundant ones). Command not yet in any GA server →@EnabledOnCommand("<COMMAND>")(capability probe via COMMAND INFO). Module presence as precondition →assumeTrue(hasCommand(...))probes. Environment exclusions →@ConditionalOnEnv(value = TestEnvUtil.ENV_..., enabled = false)(e.g. skip Redis Enterprise for brand-new features). - Cover the whole family the option touches (store + non-store variants,
with/without optional args, standalone + cluster), asserting real semantics,
not just "no error". Avoid brittle assertions (no hard-coded
hashCode()literals).
Running the tests
Jedis pins the CI JDK (Java 8) for the test build — check
.github/workflows/ for the exact version and use a matching local JDK. Verify
with java -version before running anything; a newer JDK may fail the build or
silently produce the wrong bytecode target.
- Unit tests (Surefire, no Redis):
mvn -B test, ormvn -Dtest=FooParamsTest test. - Integration tests (Failsafe) need the Docker env and the
verifylifecycle:
Never run integration coverage viamake start version=8.8 # pick the version that carries the new command mvn -B verify # or: mvn -Dtest=FooIT verify make stopmvn test/surefire:test— Failsafe owns*ITclasses. If a brand-new command isn't in any publishedredislabs/client-libs-testtag yet, say so: the integration tests will be skipped/gated (that is expected and acceptable —@EnabledOnCommand/@SinceRedisVersionhandle it), but they must still be written and compile.
PR hygiene checklist (verify before finishing)
- Every layer of the chosen matrix updated consistently (a return-type change must propagate across all 4 interfaces × 3 implementations × all test layers).
- New files added to the
pom.xmlformatter-plugin includes. -
@since(and@Experimentalif preview) on all new public API. - No unused imports, no wildcard imports, no dead
Protocol.Keywordconstants. -
equals/hashCodeon params updated and unit-tested. -
docs/updated if user-facing behavior/configuration changed; migration guide entry if anything breaks. - PR description states: server PR link, version gate choice and why, which layers intentionally did NOT change and why, and behavior against older servers.