Repository navigation
Report configured Redis targets for Rediscala - #20075
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Cluster requests do not match the current request matcher, leaving cluster commands uninstrumented and the configured-target path unreachable.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Updates Rediscala instrumentation to report configured Redis targets through stable server attributes while preserving legacy semantic-convention behavior.
Changes:
- Adds target extraction for clients, pools, replicas, Sentinel, clusters, and transactions.
- Propagates configured targets through request and transaction instrumentation.
- Adds integration and unit-test coverage.
File summaries
| File | Reviewed changes |
|---|---|
settings.gradle.kts |
Registers unit tests. |
instrumentation/rediscala-1.8/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/rediscala/v1_8/RediscalaConfiguredTargetTest.scala |
Tests configured-target behavior. |
instrumentation/rediscala-1.8/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/rediscala/v1_8/RediscalaClientTest.scala |
Updates stable target assertions. |
instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/v1_8/TransactionInstrumentation.java |
Applies targets to transaction spans. |
instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/v1_8/TransactionBuilderInstrumentation.java |
Captures transaction client state. |
instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/v1_8/ServerEndpoint.java |
Resolves operation endpoints. |
instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/v1_8/RequestInstrumentation.java |
Adds request target extraction; the cluster matcher path remains unreachable. |
instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/v1_8/RediscalaTransactionState.java |
Stores transaction client state. |
instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/v1_8/RediscalaSingletons.java |
Adds virtual-field state. |
instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/v1_8/RediscalaServerTargets.java |
Extracts and refreshes configured targets. |
instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/v1_8/RediscalaRequest.java |
Carries configured targets. |
instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/v1_8/RediscalaMutablePoolInstrumentation.java |
Tracks mutable-pool changes. |
instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/v1_8/RediscalaInstrumentationModule.java |
Registers pool instrumentation. |
instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/v1_8/RediscalaAttributesGetter.java |
Emits stable server attributes. |
instrumentation/rediscala-1.8/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/rediscala/v1_8/ServerEndpointTest.java |
Tests endpoint resolution. |
instrumentation/rediscala-1.8/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/rediscala/v1_8/RediscalaServerTargetsTest.java |
Tests target extraction and refresh. |
instrumentation/rediscala-1.8/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/rediscala/v1_8/RediscalaAttributesGetterTest.java |
Tests stable and legacy attributes. |
instrumentation/rediscala-1.8/javaagent-unit-tests/build.gradle.kts |
Configures unit-test dependencies and execution. |
Review details
- Files reviewed: 18/18 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical and moderate findings affect configured-target resolution and master/slave client coverage.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/v1_8/RequestInstrumentation.java:55
RedisClientMasterSlavesis not covered by this matcher in Rediscala 1.8: it implementsRedisCommands with Transactionsand definessenddirectly, rather than extendingActorRequest,Request,BufferedRequest,RedisCluster, orRoundRobinPoolRequest. Its commands therefore only reach the delegated master/pool instrumentation, so stable spans report the selected child target instead of the configured master-plus-replicas target (and the new master/slaves target path is never used). Add dedicated coverage for this wrapper while preventing its delegated send from creating a second span, or propagate the wrapper target into the delegated request.
"redis.RedisCluster",
"redis.RoundRobinPoolRequest"));
instrumentation/rediscala-1.8/javaagent/src/test/scala/io/opentelemetry/javaagent/instrumentation/rediscala/v1_8/RediscalaConfiguredTargetTest.scala:133
- [Testing] This is the only integration test for the new
RedisClustertarget path, but it is skipped unlesstestLatestDeps()is enabled. The module's normal test dependency iscom.github.etaty:rediscala_2.11:1.8.0, which also providesRedisCluster, so the baseline supported artifact never exercisesCLUSTER_REDIS_SERVERSor its matcher. Run this test with the regular dependency as well (or add an equivalent baseline test) to catch version-specific target extraction regressions.
@Test def testClusterCommandUsesConfiguredTarget(): Unit = {
assumeTrue(emitStableDatabaseSemconv())
assumeTrue(testLatestDeps())
- Files reviewed: 18/18 changed files
- Comments generated: 1
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
Cache immutable configured targets per client to address the identified command-path performance issue.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 18/18 changed files
- Comments generated: 1
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
Two moderate unresolved issues affect target invalidation concurrency and Sentinel blocking-client database namespace reporting.
Review details
Suppressed comments (2)
instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/v1_8/RediscalaServerTargets.java:343
- [Concurrency]
markUnavailable()writestarget = nullwithout coordinating with the map lock used byrefresh(). A successful mutation's exit advice can be delayed until after a concurrent failed mutation marks the pool unavailable, then publish a snapshot and make stable spans report a target that was intentionally invalidated. Serialize invalidation with refresh or add a generation check so an in-flight refresh cannot republish after a failed update.
public static void markMutablePoolUnavailable(Object pool) {
MutablePoolState state = mutablePoolState(pool);
if (state != null) {
state.markUnavailable();
}
instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/v1_8/ServerEndpoint.java:47
- [General] Stable spans from
SentinelMonitoredRedisBlockingClientlose the configured database namespace here.RequestInstrumentationcalls this overload for stable semconv, but this branch only recognizesSentinelMonitoredRedisClient; the blocking client is a sibling wrapper with its ownredisClient, soendpointstays null andRediscalaAttributesGetter.getDbNamespace()cannot report its configureddbeven thoughRediscalaServerTargetsalready handles the blocking sentinel target. Resolve the blocking wrapper'sredisClienthere as well (without changing the legacy path).
if (client instanceof SentinelMonitoredRedisClient) {
RedisClientActorLike redisClient = ((SentinelMonitoredRedisClient) client).redisClient();
return redisClient != null ? create(redisClient) : null;
- Files reviewed: 18/18 changed files
- Comments generated: 0 new
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
Two unresolved moderate findings affect replicated-client instrumentation and Sentinel blocking-client database attributes.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/v1_8/RequestInstrumentation.java:55
- [Bug]
RedisClientMasterSlavesin Rediscala 1.8 does not implementRequest,ActorRequest, orRoundRobinPoolRequest; it only mixes inRedisCommands/Transactionsand definessenditself. Therefore its direct commands never reachSendAdvice, so the new replicated-client target extraction is unreachable for ordinary reads and writes. Add this concrete type to the matcher.
"redis.RedisCluster",
"redis.RoundRobinPoolRequest"));
- Files reviewed: 18/18 changed files
- Comments generated: 1
- Review effort level: Lite
| if (MUTABLE_POOL_CLASS != null && MUTABLE_POOL_CLASS.isInstance(client)) { | ||
| return ofMutablePool(client); | ||
| } | ||
| if (client instanceof RedisClientActorLike) { |
There was a problem hiding this comment.
RedisClient lands on this branch rather than the memoized Request branch below it, which I think is deliberate and correct since reconnect() mutates host/port in place, so a cached target would go stale. But it means every command in stable mode recomputes the target through ofHostAndPort → endpoint() (StringBuilder) → Endpoint.parse() → DbServerTarget.builder().build().
I ran some benchmarks and the latency was fine but it introduced additional allocations of 320 B/op of garbage per command. For a high throughput pipeline doing 100k ops/s that's ~32 MB/s of allocation that didn't exist before
RedisClientActorLike.reconnect(String, int) is a concrete public method, so a VirtualField<RedisClientActorLike, RedisServerTarget> cleared by a small advice on reconnect would get RedisClient onto the cached path and remove essentially all of it. testReconnectRefreshesServerTarget / testTransactionRefreshesServerTarget already cover the invalidation.
This comment was marked as low quality.
This comment was marked as low quality.
Sorry, something went wrong.
| endpoints.add(null); | ||
| continue; | ||
| } | ||
| RedisServer redisServer = (RedisServer) server; | ||
| endpoints.add(RedisServerTarget.endpoint(redisServer.host(), redisServer.port())); | ||
| } |
There was a problem hiding this comment.
we could reuse endpoint(Object) at line 349 which already does this instanceof RedisServer → RedisServerTarget.endpoint(host, port), returning null for anything else, which is the same fail-closed sentinel this loop is building.
The loop at lines 190-196 does the same conversion (it fails fast rather than appending null, but the two lines that build the endpoint are identical).
This comment was marked as low quality.
This comment was marked as low quality.
Sorry, something went wrong.
There was a problem hiding this comment.
This is generic over <T> but has exactly one call site, and it makes RediscalaSingletons call back into RediscalaServerTargets, which already statically imports REQUEST_TARGET from here. Moving it into RediscalaServerTargets as a non-generic Request overload would drop both the cycle and the unused generality.
This comment was marked as low quality.
This comment was marked as low quality.
Sorry, something went wrong.
There was a problem hiding this comment.
🔵 Needs a closer look
Cluster commands remain outside the request matcher, so their configured-target spans are not produced.
Review details
Suppressed comments (1)
instrumentation/rediscala-1.8/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/rediscala/v1_8/RequestInstrumentation.java:87
- [General] The current head no longer contains the cluster instrumentation added in be221ed:
typeMatcher()still excludesredis.RedisCluster, andend()has no way to obtain a cluster'sRedisClientPoolLike.executionContext. SinceRedisClusterdeclares its ownsend, cluster commands never reach this target extraction and produce no spans. Restore the cluster matcher and cached reflective execution-context handling from that fix.
endpoint = ServerEndpoint.create(action, cmd.isMasterOnly());
serverTarget = RediscalaServerTargets.get(action);
- Files reviewed: 19/19 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot comment: This new cluster branch is unreachable for cluster commands: `RequestInstrumentation.typeMatcher()` still matches only `ActorRequest`, `Request`, `BufferedRequest`, and `RoundRobinPoolRequest`, but Rediscala 1.8's `RedisCluster` extends `RedisClientPoolLike` directly and declares its public `send` method. Consequently cluster commands produce no spans and never reach this configured-target extraction; include `redis.RedisClientPoolLike` (or `redis.RedisCluster`) in that matcher. Analysis: `RedisCluster` declares the one-argument `send` method but did not match `RequestInstrumentation`, so its commands produced no spans. The matcher now includes only `RedisCluster` to avoid adding a second span around existing pool commands. Cluster futures use the pool execution context, which is resolved reflectively because its JVM return type differs across supported Rediscala versions. Upsides: Cluster commands now produce spans with configured server targets. The focused matcher preserves existing pool span behavior, and reflective lookup keeps muzzle compatibility across Scala and Rediscala versions. Downsides: Cluster completion adds one cached reflective method invocation per command. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Review finding: `endpoint(Object)` is declared `public static`, but the only caller is `MutablePoolState.snapshot`, a private method of a nested class in this same file. No advice class, no other instrumentation class, and no test refers to it. The repository's Java style rules require the principle of least access: "Use the most restrictive modifier that still works." Make the method `private static`. Review finding: `MutablePoolState.markUnavailable()` and `MutablePoolState.refresh(HashMap)` are declared `public`, but every caller is either an enclosing `RediscalaServerTargets` method in this same file or `RediscalaServerTargetsTest`, which sits in the same package. The two sibling members of this class, `fromMap` and `target()`, are already package-private and carry `// visible for testing` comments, so the public modifier on these two methods is inconsistent with the class's own design and with the repository's least-access style rule. Make both methods package-private. Analysis: `endpoint` has a single caller, `MutablePoolState.snapshot`, in the same top-level class, so `private static` is the most restrictive modifier that still compiles. `MutablePoolState` itself is only referenced by its enclosing class and by `RediscalaServerTargetsTest`, which declares the same package, so the class and its two mutating methods drop to package-private alongside the `fromMap` and `target()` members that were already package-private. A `VirtualField` value type does not need to be public: `ElasticsearchTransportServerTargets` uses a `private static class UpdateState` as its value type, so the runtime accepts a non-public one. Upsides: The class now applies one consistent visibility rule instead of two. Nothing outside this package can call into instrumentation-internal state, and a future reader can find every caller of these members without searching the whole repository. Downsides: No material downside identified. The unit test lives in a different Gradle module but the same package, so package-private access still works, which the module's test run confirms. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot comment: [General] Stable requests from `SentinelMonitoredRedisBlockingClient` are handled by `RequestInstrumentation`, and `RediscalaServerTargets` already recognizes that client, but this endpoint overload does not. Because the blocking client carries its configured database on its underlying `redisClient`, `ServerEndpoint` stays null and stable `db.namespace` is omitted for every blocking Sentinel command (notably non-default databases). Add a branch that unwraps the blocking client's `redisClient` just like the regular Sentinel branch. Analysis: `RequestInstrumentation` passes blocking Sentinel requests to `ServerEndpoint.create(Object, boolean)`, but the blocking client does not implement `RedisClientActorLike`. The new branch unwraps its `redisClient()` before endpoint extraction and does not depend on the master-only flag. Upsides: Stable spans for blocking Sentinel commands now include the configured server endpoint and database namespace. The test covers a non-master command with a non-default database. Downsides: Stable request handling performs one extra type check for clients that did not match an earlier branch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Review finding: This new `else if (action instanceof RedisClientPoolLike && POOL_EXECUTION_CONTEXT != null)` branch can never run, because the earlier `action instanceof Request` branch always claims the same receivers. Every rediscala class that extends `redis.RedisClientPoolLike` also implements `redis.RedisCommands`, and `redis.RedisCommands` extends `redis.commands.Keys`, which extends `redis.Request`. I checked this with javap over the whole supported range: `redis.RedisClientPool` in 1.5.0, 1.6.0, 1.7.0, 1.8.0, 1.9.0, 1.13.0 and 1.17.0; `redis.RedisClientMutablePool` in 1.6.0 onward; and `redis.RedisCluster` in 1.8.0 onward. Each one also exposes a working `executionContext()` returning `scala.concurrent.ExecutionContext`, so the existing `((Request) action).executionContext()` call already resolves correctly, including in 1.8.0 where `RedisClientPoolLike.executionContext()` itself returns `akka.dispatch.MessageDispatcher`. Reordering the branch would not help either, since the `Request` branch wins whatever the order. The remaining shapes `typeMatcher()` admits are covered too: an `ActorRequest` receiver is taken by the first branch, a `BufferedRequest` receiver never reaches `end()` because `start()` returns null for it, a `redis.RedisCluster` receiver implies `redis.Request`, and a `RoundRobinPoolRequest` receiver that somehow is not a `Request` is already served by the later `RoundRobinPoolRequest` branch with a direct call, which this reflective branch would only shadow. For completeness, the `BufferedRequest` branch in the same chain is also unreachable, but it predates this pull request and establishes no convention for adding new dead branches. Delete the branch together with the `POOL_EXECUTION_CONTEXT` field and the `logger` field it needs, and drop the imports that become unused: the static imports of `RediscalaServerTargets.findMethod` and `java.util.logging.Level.FINE`, plus `java.lang.reflect.Method`, `java.util.logging.Logger` and `redis.RedisClientPoolLike`. Keep the new `ctx == null` guard, which is a real fix. Review finding: Adding `"redis.RedisCluster"` to `hasSuperType(namedOneOf(...))` matches nothing that the list did not already match. `redis.RedisCluster` implements `redis.RedisCommands`, which extends `redis.commands.Keys`, which extends `redis.Request`, and `"redis.Request"` is already an entry in the same list. I confirmed the hierarchy with javap on 1.8.0, 1.9.0, 1.13.0 and 1.17.0, and `redis.RedisCluster` does not exist at all in 1.5.0, so there is no supported version where the extra entry changes which types are instrumented. The entry is also the only concrete class in a list that otherwise names traits, which makes the matcher read as though cluster clients need a special case when they do not. Remove the line. Analysis: Both findings share one root cause. The pull request treated rediscala pool and cluster clients as a shape the existing instrumentation did not already cover, but they were covered all along, because `redis.RedisCommands` reaches `redis.Request` through `redis.commands.Keys`. That single fact makes the added type matcher entry match no new type and makes the added reflective execution context branch unreachable, since the `Request` branch takes every receiver that could have reached it. The reflection also solved a problem that does not arise: `RedisClientPoolLike.executionContext()` does return a different type across versions, but the call site goes through `Request`, whose signature is stable. So the fix removes the matcher entry, the dead branch, the `POOL_EXECUTION_CONTEXT` handle and the `logger` it used to report a failed lookup, and the five imports that then have no use. The `ctx == null` guard that the same pull request added stays, because a null execution context is reachable and skipping the callback for it is correct. Upsides: The advice class no longer performs a reflective method lookup at class initialization, nor an `instanceof` and a null check per request, for a branch that never executes. The type matcher lists only traits again, so it no longer suggests that cluster clients need a special case. Five imports, two fields and a logger disappear, which leaves less code to keep working across the seven rediscala versions this instrumentation supports. Downsides: If a future rediscala release introduced a `RedisClientPoolLike` implementation that is not a `redis.Request`, its execution context would no longer be found. That receiver would also fail the type matcher, so it would not be instrumented at all, and the branch would not have helped it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Master-and-replicas targets now go through RedisServerTarget.ofEndpointAndUnorderedEndpoints, the helper PR #20070 introduced for Redisson, instead of hand-rolling the master-first sort. Behavior is unchanged: the master stays first, replicas are sorted, and an unsupported replica still fails closed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Review finding:
`typeMatcher()` in this new instrumentation uses only `named("redis.RedisClientActorLike")`, but the class also adds `classLoaderOptimization()` with the same type name. The repository's TypeInstrumentation guidance says name-only matchers are already fast and that `classLoaderOptimization()` should be added for expensive hierarchy or annotation matchers. Remove this redundant override and its `hasClassesNamed` static import.
Analysis: The exact-name matcher already rejects every unrelated type without inspecting bytecode. The extra class-loader probe repeats the same name check at an earlier stage and does not narrow the match.
Upsides: Removes redundant matcher work and follows the repository's TypeInstrumentation guidance.
Downsides: No material downside identified.
Review finding:
`typeMatcher()` in this new instrumentation uses only `named("redis.RedisClientMutablePool")`, but the class also adds `classLoaderOptimization()` with the same type name. The repository's TypeInstrumentation guidance says name-only matchers are already fast and that `classLoaderOptimization()` should be added for expensive hierarchy or annotation matchers. Remove this redundant override and its `hasClassesNamed` static import.
Analysis: The exact-name matcher already rejects every unrelated type without inspecting bytecode. The extra class-loader probe repeats the same name check at an earlier stage and does not narrow the match.
Upsides: Removes redundant matcher work and follows the repository's TypeInstrumentation guidance.
Downsides: No material downside identified.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 73cbb4d5-6c82-4793-8bbf-4771aa8ceb10
Review finding: `RediscalaServerTargets.get(Object)` is declared `public`, but its only production callers are `AdviceScope.start` helpers in `RequestInstrumentation` and `TransactionInstrumentation`, which are in the same package. The annotated advice methods call those `start` helpers rather than calling `get` directly, and the repository visibility guidance says only members directly referenced by advice need public access; transitive helpers can stay package-private. Remove the `public` modifier. Analysis: Both callers compile in the same package. The method is not part of an inlined advice body, so package access preserves advice dispatch while avoiding an unnecessary public helper API. Upsides: Applies least access and keeps the helper surface limited to its package. Downsides: No material downside identified. Review finding: The new `ServerEndpoint.create(Object)` and `create(Object, boolean)` overloads are declared `public`, but every caller is in this package. The annotated advice directly calls only the pre-existing `create(RedisClientActorLike)` overload; the new overloads are reached from `AdviceScope.start` helpers, so the repository's direct-advice visibility exception does not require them to be public. Remove `public` from both new overloads. Analysis: The two Object overloads are package-internal dispatch helpers. The directly advice-referenced RedisClientActorLike overload remains public, so narrowing these overloads does not change advice linkage. Upsides: Applies least access without changing endpoint selection behavior. Downsides: No material downside identified. Review finding: `RediscalaTransactionState.getClient()` and `getEndpoint()` are declared `public`, but their only caller is `TransactionInstrumentation.AdviceScope.start` in the same package. The state class and constructor need public access because the annotated transaction-builder advice directly creates the value, but no advice method directly calls either getter. Make both getters package-private under the repository's least-access rule. Analysis: TransactionInstrumentation reads both values from a same-package helper. The class and constructor stay public for direct advice construction, while the getters need no cross-package access. Upsides: Keeps only the advice-linked state construction API public. Downsides: No material downside identified. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 73cbb4d5-6c82-4793-8bbf-4771aa8ceb10
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
028894b to
9f60fa8
Compare
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 73cbb4d5-6c82-4793-8bbf-4771aa8ceb10

Reports configured Redis deployments in stable
server.addressandserver.portfor Rediscala direct clients, pools, replicated clients, clusters, Sentinel deployments, and transactions. These attributes describe the configured deployment instead of the network peer selected for a request. Legacy database semantic convention output does not change.This is the Rediscala slice of #20010 and follows #19899.
Stacked on #20070, which introduces the shared
RedisServerTarget.ofEndpointAndUnorderedEndpointshelper this PR reuses for master-and-replicas targets.