Repository: redis/lettuce
Pull request #2922 (2024-07-15 09:13:43)

Bump org.apache.maven.plugins:maven-surefire-plugin from 3.2.5 to 3.3.1
Bumps [org.apache.maven.plugins:maven-surefire-plugin](https://github.com/apache/maven-surefire) from 3.2.5 to 3.3.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/apache/maven-surefire/releases">org.apache.maven.plugins:maven-surefire-plugin's releases</a>.</em></p>
<blockquote>
<h2>3.3.0</h2>
<p><a href="https://issues.apache.org/jira/secure/ReleaseNote.jspa?projectId=12317927&amp;version=12354462">Release Notes - Maven Surefire - Version 3.3.0</a></p>
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<ul>
<li>Bump org.htmlunit:htmlunit from 3.9.0 to 3.10.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/apache/maven-surefire/pull/717">apache/maven-surefire#717</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/apache/maven-surefire/commit/7e4562036f88ba082ea5a58008d799e354e4fc1e"><code>7e45620</code></a> [maven-release-plugin] prepare release surefire-3.3.1</li>
<li><a href="https://github.com/apache/maven-surefire/commit/561b4ca356e6fae53f0f16f4862fdd22305852fb"><code>561b4ca</code></a> [SUREFIRE-2250] Surefire Test Report Schema properties element is not consist...</li>
<li><a href="https://github.com/apache/maven-surefire/commit/6aaea8a6dfed1f8f5b803dc202fefa5a47f14bb7"><code>6aaea8a</code></a> [SUREFIRE-1360] Ability to disable properties for successfully passed tests</li>
<li><a href="https://github.com/apache/maven-surefire/commit/c17b92bf79d33b34bf5c6e3bab1128e8ee1c8f01"><code>c17b92b</code></a> Bump org.codehaus.mojo:animal-sniffer-maven-plugin from 1.23 to 1.24</li>
<li><a href="https://github.com/apache/maven-surefire/commit/748d9dcbf43eaf91dace4009d1aa2cc669953c91"><code>748d9dc</code></a> Fix typos</li>
<li><a href="https://github.com/apache/maven-surefire/commit/f8092e99c0d3e4439e8e74ffd1df49f256373a35"><code>f8092e9</code></a> Improve time units</li>
<li><a href="https://github.com/apache/maven-surefire/commit/c670335783cfcb43cfba7042b90748708eb299ce"><code>c670335</code></a> [SUREFIRE-1934] Ability to disable system-out/system-err for successfully pas...</li>
<li><a href="https://github.com/apache/maven-surefire/commit/bce1b39b042f8cd696c2bc9b9205303aff6409ae"><code>bce1b39</code></a> Improve docs of linkXRef</li>
<li><a href="https://github.com/apache/maven-surefire/commit/3c49ebdd6a2df6da992d8b2b6b7e7843e948fb21"><code>3c49ebd</code></a> Bump org.htmlunit:htmlunit from 4.2.0 to 4.3.0</li>
<li><a href="https://github.com/apache/maven-surefire/commit/6ff0f83ea1c9427a88d42b3a8c1b7bdb9e5e3fcb"><code>6ff0f83</code></a> [SUREFIRE-2242] Plain test report does not include names of the skipped tests</li>
<li>Additional commits viewable in <a href="https://github.com/apache/maven-surefire/compare/surefire-3.2.5...surefire-3.3.1">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.apache.maven.plugins:maven-surefire-plugin&package-manager=maven&previous-version=3.2.5&new-version=3.3.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 938cc30f4..3e796f739 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -20,7 +20,7 @@ jobs:
 
   build:
     name: Build and Test
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-24.04
     steps:
       - name: Checkout project
         uses: actions/checkout@v4
diff --git a/README.md b/README.md
index a1484030e..dc219deb2 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,11 @@
 ===============================
 
  [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.lettuce/lettuce-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.lettuce/lettuce-core)
+ [![Javadocs](https://www.javadoc.io/badge/io.lettuce/lettuce-core.svg)](https://www.javadoc.io/doc/io.lettuce/lettuce-core)
+ [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE.txt)
+ [![Integration](https://github.com/redis/lettuce/actions/workflows/integration.yml/badge.svg?branch=main)](https://github.com/redis/lettuce/actions/workflows/integration.yml)
+ [![codecov](https://codecov.io/gh/redis/lettuce/branch/main/graph/badge.svg?token=pAstxAAjYo)](https://codecov.io/gh/redis/lettuce)
+ [![Discord](https://img.shields.io/discord/697882427875393627?style=flat-square)](https://discord.gg/redis)
 
 Lettuce is a scalable thread-safe Redis client for synchronous,
 asynchronous and reactive usage. Multiple threads may share one connection if they avoid blocking and transactional
@@ -117,8 +122,8 @@ Asynchronous API
 ```java
 StatefulRedisConnection<String, String> connection = client.connect();
 RedisStringAsyncCommands<String, String> async = connection.async();
-RedisFuture<String> set = async.set("key", "value")
-RedisFuture<String> get = async.get("key")
+RedisFuture<String> set = async.set("key", "value");
+RedisFuture<String> get = async.get("key");
 
 LettuceFutures.awaitAll(set, get) == true
 
@@ -150,7 +155,7 @@ Pub/Sub
 ```java
 RedisPubSubCommands<String, String> connection = client.connectPubSub().sync();
 connection.getStatefulConnection().addListener(new RedisPubSubListener<String, String>() { ... })
-connection.subscribe("channel")
+connection.subscribe("channel");
 ```
 
 Building
@@ -177,7 +182,7 @@ $ make test
 Bugs and Feedback
 -----------
 
-For bugs, questions and discussions please use the [GitHub Issues](https://github.com/lettuce-io/lettuce-core/issues).
+For bugs, questions and discussions please use the [GitHub Issues](https://github.com/redis/lettuce/issues).
 
 License
 -------
diff --git a/pom.xml b/pom.xml
index c1c5f9215..95f1d50dc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -607,12 +607,12 @@
                 <plugin>
                     <groupId>org.apache.maven.plugins</groupId>
                     <artifactId>maven-surefire-plugin</artifactId>
-                    <version>3.2.5</version>
+                    <version>3.3.1</version>
                     <dependencies>
                         <dependency>
                             <groupId>org.apache.maven.surefire</groupId>
                             <artifactId>surefire-junit-platform</artifactId>
-                            <version>3.2.5</version>
+                            <version>3.3.1</version>
                         </dependency>
                     </dependencies>
                 </plugin>
@@ -651,7 +651,7 @@
                 <plugin>
                     <groupId>org.jacoco</groupId>
                     <artifactId>jacoco-maven-plugin</artifactId>
-                    <version>0.8.9</version>
+                    <version>0.8.12</version>
                 </plugin>
 
                 <plugin>
diff --git a/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java b/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java
index b7da8fb2f..d72accd63 100644
--- a/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java
+++ b/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java
@@ -389,6 +389,11 @@ public abstract class AbstractRedisAsyncCommands<K, V> implements RedisAclAsyncC
         return dispatch(commandBuilder.clientTracking(args));
     }
 
+    @Override
+    public RedisFuture<TrackingInfo> clientTrackinginfo() {
+        return dispatch(commandBuilder.clientTrackinginfo());
+    }
+
     @Override
     public RedisFuture<Long> clientUnblock(long id, UnblockType type) {
         return dispatch(commandBuilder.clientUnblock(id, type));
@@ -474,6 +479,11 @@ public abstract class AbstractRedisAsyncCommands<K, V> implements RedisAclAsyncC
         return dispatch(commandBuilder.clusterMyId());
     }
 
+    @Override
+    public RedisFuture<String> clusterMyShardId() {
+        return dispatch(commandBuilder.clusterMyShardId());
+    }
+
     @Override
     public RedisFuture<String> clusterNodes() {
         return dispatch(commandBuilder.clusterNodes());
diff --git a/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java b/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java
index 38404c1ea..675bc31ba 100644
--- a/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java
+++ b/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java
@@ -407,6 +407,11 @@ public abstract class AbstractRedisReactiveCommands<K, V>
         return createMono(() -> commandBuilder.clientTracking(args));
     }
 
+    @Override
+    public Mono<TrackingInfo> clientTrackinginfo() {
+        return createMono(commandBuilder::clientTrackinginfo);
+    }
+
     @Override
     public Mono<Long> clientUnblock(long id, UnblockType type) {
         return createMono(() -> commandBuilder.clientUnblock(id, type));
@@ -496,6 +501,11 @@ public abstract class AbstractRedisReactiveCommands<K, V>
         return createMono(commandBuilder::clusterMyId);
     }
 
+    @Override
+    public Mono<String> clusterMyShardId() {
+        return createMono(commandBuilder::clusterMyShardId);
+    }
+
     @Override
     public Mono<String> clusterNodes() {
         return createMono(commandBuilder::clusterNodes);
diff --git a/src/main/java/io/lettuce/core/RedisCommandBuilder.java b/src/main/java/io/lettuce/core/RedisCommandBuilder.java
index 1a5e4d164..46e5cb809 100644
--- a/src/main/java/io/lettuce/core/RedisCommandBuilder.java
+++ b/src/main/java/io/lettuce/core/RedisCommandBuilder.java
@@ -528,6 +528,12 @@ class RedisCommandBuilder<K, V> extends BaseRedisCommandBuilder<K, V> {
         return createCommand(CLIENT, new StatusOutput<>(codec), args);
     }
 
+    Command<K, V, TrackingInfo> clientTrackinginfo() {
+        CommandArgs<K, V> args = new CommandArgs<>(codec).add(TRACKINGINFO);
+
+        return new Command<>(CLIENT, new ComplexOutput<>(codec, TrackingInfoParser.INSTANCE), args);
+    }
+
     Command<K, V, Long> clientUnblock(long id, UnblockType type) {
         LettuceAssert.notNull(type, "UnblockType " + MUST_NOT_BE_NULL);
 
@@ -656,6 +662,12 @@ class RedisCommandBuilder<K, V> extends BaseRedisCommandBuilder<K, V> {
         return createCommand(CLUSTER, new StatusOutput<>(codec), args);
     }
 
+    Command<K, V, String> clusterMyShardId() {
+        CommandArgs<K, V> args = new CommandArgs<>(codec).add(MYSHARDID);
+
+        return createCommand(CLUSTER, new StatusOutput<>(codec), args);
+    }
+
     Command<K, V, String> clusterNodes() {
         CommandArgs<K, V> args = new CommandArgs<>(codec).add(NODES);
 
diff --git a/src/main/java/io/lettuce/core/TrackingInfo.java b/src/main/java/io/lettuce/core/TrackingInfo.java
new file mode 100644
index 000000000..1d69dcd06
--- /dev/null
+++ b/src/main/java/io/lettuce/core/TrackingInfo.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+
+package io.lettuce.core;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.List;
+
+/**
+ * Contains the output of a <a href="https://redis.io/docs/latest/commands/client-trackinginfo/">CLIENT TRACKINGINFO</a>
+ * command.
+ *
+ * @author Tihomir Mateev
+ * @since 6.5
+ */
+public class TrackingInfo {
+
+    private final Set<TrackingFlag> flags = new HashSet<>();
+
+    private final long redirect;
+
+    private final List<String> prefixes = new ArrayList<>();
+
+    /**
+     * Constructor
+     * 
+     * @param flags a {@link Set} of {@link TrackingFlag}s that the command returned
+     * @param redirect the client ID used for notification redirection, -1 when none
+     * @param prefixes a {@link List} of key prefixes for which notifications are sent to the client
+     *
+     * @see TrackingFlag
+     */
+    public TrackingInfo(Set<TrackingFlag> flags, long redirect, List<String> prefixes) {
+        this.flags.addAll(flags);
+        this.redirect = redirect;
+        this.prefixes.addAll(prefixes);
+    }
+
+    /**
+     * @return set of all the {@link TrackingFlag}s currently enabled on the client connection
+     */
+    public Set<TrackingFlag> getFlags() {
+        return Collections.unmodifiableSet(flags);
+    }
+
+    /**
+     * @return the client ID used for notification redirection, -1 when none
+     */
+    public long getRedirect() {
+        return redirect;
+    }
+
+    /**
+     * @return a {@link List} of key prefixes for which notifications are sent to the client
+     */
+    public List<String> getPrefixes() {
+        return Collections.unmodifiableList(prefixes);
+    }
+
+    @Override
+    public String toString() {
+        return "TrackingInfo{" + "flags=" + flags + ", redirect=" + redirect + ", prefixes=" + prefixes + '}';
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        if (o == null || getClass() != o.getClass())
+            return false;
+        TrackingInfo that = (TrackingInfo) o;
+        return redirect == that.redirect && Objects.equals(flags, that.flags) && Objects.equals(prefixes, that.prefixes);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(flags, redirect, prefixes);
+    }
+
+    /**
+     * CLIENT TRACKINGINFO flags
+     *
+     * @see <a href="https://redis.io/docs/latest/commands/client-trackinginfo/">CLIENT TRACKINGINFO</a>
+     */
+    public enum TrackingFlag {
+
+        /**
+         * The connection isn't using server assisted client side caching.
+         */
+        OFF,
+        /**
+         * Server assisted client side caching is enabled for the connection.
+         */
+        ON,
+        /**
+         * The client uses broadcasting mode.
+         */
+        BCAST,
+        /**
+         * The client does not cache keys by default.
+         */
+        OPTIN,
+        /**
+         * The client caches keys by default.
+         */
+        OPTOUT,
+        /**
+         * The next command will cache keys (exists only together with optin).
+         */
+        CACHING_YES,
+        /**
+         * The next command won't cache keys (exists only together with optout).
+         */
+        CACHING_NO,
+        /**
+         * The client isn't notified about keys modified by itself.
+         */
+        NOLOOP,
+        /**
+         * The client ID used for redirection isn't valid anymore.
+         */
+        BROKEN_REDIRECT;
+
+        /**
+         * Convert a given {@link String} flag to the corresponding {@link TrackingFlag}
+         * 
+         * @param flag a {@link String} representation of the flag
+         * @return the resulting {@link TrackingFlag} or {@link IllegalArgumentException} if unrecognized
+         */
+        public static TrackingFlag from(String flag) {
+            switch (flag.toLowerCase()) {
+                case "off":
+                    return OFF;
+                case "on":
+                    return ON;
+                case "bcast":
+                    return BCAST;
+                case "optin":
+                    return OPTIN;
+                case "optout":
+                    return OPTOUT;
+                case "caching-yes":
+                    return CACHING_YES;
+                case "caching-no":
+                    return CACHING_NO;
+                case "noloop":
+                    return NOLOOP;
+                case "broken_redirect":
+                    return BROKEN_REDIRECT;
+                default:
+                    throw new RuntimeException("Unsupported flag: " + flag);
+            }
+        }
+
+    }
+
+}
diff --git a/src/main/java/io/lettuce/core/api/async/RedisServerAsyncCommands.java b/src/main/java/io/lettuce/core/api/async/RedisServerAsyncCommands.java
index 86714aab9..a4245c2d5 100644
--- a/src/main/java/io/lettuce/core/api/async/RedisServerAsyncCommands.java
+++ b/src/main/java/io/lettuce/core/api/async/RedisServerAsyncCommands.java
@@ -30,6 +30,7 @@ import io.lettuce.core.RedisFuture;
 import io.lettuce.core.ShutdownArgs;
 import io.lettuce.core.TrackingArgs;
 import io.lettuce.core.UnblockType;
+import io.lettuce.core.TrackingInfo;
 import io.lettuce.core.protocol.CommandType;
 
 /**
@@ -177,6 +178,14 @@ public interface RedisServerAsyncCommands<K, V> {
      */
     RedisFuture<String> clientTracking(TrackingArgs args);
 
+    /**
+     * Returns information about the current client connection's use of the server assisted client side caching feature.
+     *
+     * @return {@link TrackingInfo}, for more information check the documentation
+     * @since 6.5
+     */
+    RedisFuture<TrackingInfo> clientTrackinginfo();
+
     /**
      * Unblock the specified blocked client.
      *
diff --git a/src/main/java/io/lettuce/core/api/reactive/RedisServerReactiveCommands.java b/src/main/java/io/lettuce/core/api/reactive/RedisServerReactiveCommands.java
index 3e2d0addc..e8f9c070f 100644
--- a/src/main/java/io/lettuce/core/api/reactive/RedisServerReactiveCommands.java
+++ b/src/main/java/io/lettuce/core/api/reactive/RedisServerReactiveCommands.java
@@ -28,6 +28,7 @@ import io.lettuce.core.KillArgs;
 import io.lettuce.core.ShutdownArgs;
 import io.lettuce.core.TrackingArgs;
 import io.lettuce.core.UnblockType;
+import io.lettuce.core.TrackingInfo;
 import io.lettuce.core.protocol.CommandType;
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
@@ -177,6 +178,14 @@ public interface RedisServerReactiveCommands<K, V> {
      */
     Mono<String> clientTracking(TrackingArgs args);
 
+    /**
+     * Returns information about the current client connection's use of the server assisted client side caching feature.
+     *
+     * @return {@link TrackingInfo}, for more information check the documentation
+     * @since 6.5
+     */
+    Mono<TrackingInfo> clientTrackinginfo();
+
     /**
      * Unblock the specified blocked client.
      *
diff --git a/src/main/java/io/lettuce/core/api/sync/RedisServerCommands.java b/src/main/java/io/lettuce/core/api/sync/RedisServerCommands.java
index 7454ce2e6..28b053044 100644
--- a/src/main/java/io/lettuce/core/api/sync/RedisServerCommands.java
+++ b/src/main/java/io/lettuce/core/api/sync/RedisServerCommands.java
@@ -28,6 +28,7 @@ import io.lettuce.core.FlushMode;
 import io.lettuce.core.KillArgs;
 import io.lettuce.core.ShutdownArgs;
 import io.lettuce.core.TrackingArgs;
+import io.lettuce.core.TrackingInfo;
 import io.lettuce.core.UnblockType;
 import io.lettuce.core.protocol.CommandType;
 
@@ -176,6 +177,14 @@ public interface RedisServerCommands<K, V> {
      */
     String clientTracking(TrackingArgs args);
 
+    /**
+     * Returns information about the current client connection's use of the server assisted client side caching feature.
+     *
+     * @return {@link TrackingInfo}, for more information check the documentation
+     * @since 6.5
+     */
+    TrackingInfo clientTrackinginfo();
+
     /**
      * Unblock the specified blocked client.
      *
diff --git a/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionServerAsyncCommands.java b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionServerAsyncCommands.java
index 42b308d47..f3b7b877c 100644
--- a/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionServerAsyncCommands.java
+++ b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionServerAsyncCommands.java
@@ -19,15 +19,15 @@
  */
 package io.lettuce.core.cluster.api.async;
 
-import java.util.Date;
-import java.util.List;
 import java.util.Map;
-
+import java.util.List;
+import java.util.Date;
 import io.lettuce.core.ClientListArgs;
 import io.lettuce.core.FlushMode;
 import io.lettuce.core.KillArgs;
 import io.lettuce.core.TrackingArgs;
 import io.lettuce.core.UnblockType;
+import io.lettuce.core.TrackingInfo;
 import io.lettuce.core.protocol.CommandType;
 
 /**
@@ -175,6 +175,14 @@ public interface NodeSelectionServerAsyncCommands<K, V> {
      */
     AsyncExecutions<String> clientTracking(TrackingArgs args);
 
+    /**
+     * Returns information about the current client connection's use of the server assisted client side caching feature.
+     *
+     * @return {@link TrackingInfo}, for more information check the documentation
+     * @since 6.5
+     */
+    AsyncExecutions<TrackingInfo> clientTrackinginfo();
+
     /**
      * Unblock the specified blocked client.
      *
diff --git a/src/main/java/io/lettuce/core/cluster/api/async/RedisClusterAsyncCommands.java b/src/main/java/io/lettuce/core/cluster/api/async/RedisClusterAsyncCommands.java
index 6a9c81ade..2835968fd 100644
--- a/src/main/java/io/lettuce/core/cluster/api/async/RedisClusterAsyncCommands.java
+++ b/src/main/java/io/lettuce/core/cluster/api/async/RedisClusterAsyncCommands.java
@@ -217,6 +217,16 @@ public interface RedisClusterAsyncCommands<K, V> extends BaseRedisAsyncCommands<
      */
     RedisFuture<String> clusterMyId();
 
+    /**
+     * Obtain the shard ID for the currently connected node.
+     * <p>
+     * The CLUSTER MYSHARDID command returns the unique, auto-generated identifier that is associated with the shard to which
+     * the connected cluster node belongs.
+     *
+     * @return String simple-string-reply
+     */
+    RedisFuture<String> clusterMyShardId();
+
     /**
      * Obtain details about all cluster nodes. Can be parsed using
      * {@link io.lettuce.core.cluster.models.partitions.ClusterPartitionParser#parse}
diff --git a/src/main/java/io/lettuce/core/cluster/api/reactive/RedisClusterReactiveCommands.java b/src/main/java/io/lettuce/core/cluster/api/reactive/RedisClusterReactiveCommands.java
index d7e3a9757..5f1d611f9 100644
--- a/src/main/java/io/lettuce/core/cluster/api/reactive/RedisClusterReactiveCommands.java
+++ b/src/main/java/io/lettuce/core/cluster/api/reactive/RedisClusterReactiveCommands.java
@@ -216,6 +216,16 @@ public interface RedisClusterReactiveCommands<K, V> extends BaseRedisReactiveCom
      */
     Mono<String> clusterMyId();
 
+    /**
+     * Obtain the shard ID for the currently connected node.
+     * <p>
+     * The CLUSTER MYSHARDID command returns the unique, auto-generated identifier that is associated with the shard to which
+     * the connected cluster node belongs.
+     *
+     * @return String simple-string-reply
+     */
+    Mono<String> clusterMyShardId();
+
     /**
      * Obtain details about all cluster nodes. Can be parsed using
      * {@link io.lettuce.core.cluster.models.partitions.ClusterPartitionParser#parse}
diff --git a/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionServerCommands.java b/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionServerCommands.java
index b8179a7d0..bd6b2e13d 100644
--- a/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionServerCommands.java
+++ b/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionServerCommands.java
@@ -28,6 +28,7 @@ import io.lettuce.core.FlushMode;
 import io.lettuce.core.KillArgs;
 import io.lettuce.core.TrackingArgs;
 import io.lettuce.core.UnblockType;
+import io.lettuce.core.TrackingInfo;
 import io.lettuce.core.protocol.CommandType;
 
 /**
@@ -163,7 +164,7 @@ public interface NodeSelectionServerCommands<K, V> {
      * @return simple-string-reply {@code OK} if the connection name was successfully set.
      * @since 6.3
      */
-    Executions<String> clientSetinfo(String key, V value);
+    Executions<String> clientSetinfo(String key, String value);
 
     /**
      * Enables the tracking feature of the Redis server, that is used for server assisted client side caching. Tracking messages
@@ -175,6 +176,14 @@ public interface NodeSelectionServerCommands<K, V> {
      */
     Executions<String> clientTracking(TrackingArgs args);
 
+    /**
+     * Returns information about the current client connection's use of the server assisted client side caching feature.
+     *
+     * @return {@link TrackingInfo}, for more information check the documentation
+     * @since 6.5
+     */
+    Executions<TrackingInfo> clientTrackinginfo();
+
     /**
      * Unblock the specified blocked client.
      *
diff --git a/src/main/java/io/lettuce/core/cluster/api/sync/RedisClusterCommands.java b/src/main/java/io/lettuce/core/cluster/api/sync/RedisClusterCommands.java
index 597ec53d7..2e18bc68d 100644
--- a/src/main/java/io/lettuce/core/cluster/api/sync/RedisClusterCommands.java
+++ b/src/main/java/io/lettuce/core/cluster/api/sync/RedisClusterCommands.java
@@ -214,6 +214,16 @@ public interface RedisClusterCommands<K, V> extends BaseRedisCommands<K, V>, Red
      */
     String clusterMyId();
 
+    /**
+     * Obtain the shard ID for the currently connected node.
+     * <p>
+     * The CLUSTER MYSHARDID command returns the unique, auto-generated identifier that is associated with the shard to which
+     * the connected cluster node belongs.
+     *
+     * @return String simple-string-reply
+     */
+    String clusterMyShardId();
+
     /**
      * Obtain details about all cluster nodes. Can be parsed using
      * {@link io.lettuce.core.cluster.models.partitions.ClusterPartitionParser#parse}
diff --git a/src/main/java/io/lettuce/core/output/ArrayComplexData.java b/src/main/java/io/lettuce/core/output/ArrayComplexData.java
new file mode 100644
index 000000000..d53b21a55
--- /dev/null
+++ b/src/main/java/io/lettuce/core/output/ArrayComplexData.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+
+package io.lettuce.core.output;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * An implementation of the {@link ComplexData} that handles arrays.
+ * <p>
+ * For RESP2 calling the {@link ComplexData#getDynamicMap()} would heuristically go over the list of elements assuming every odd
+ * element is a key and every even object is the value and then adding them to an {@link Map}. The logic would follow the same
+ * order that was used when the elements were added to the {@link ArrayComplexData}. Similarly calling the
+ * {@link ComplexData#getDynamicSet()} would return a set of all the elements, adding them in the same order. If - for some
+ * reason - duplicate elements exist they would be overwritten.
+ * <p>
+ * All data structures that the implementation returns are unmodifiable
+ *
+ * @see ComplexData
+ * @author Tihomir Mateev
+ * @since 6.5
+ */
+class ArrayComplexData extends ComplexData {
+
+    private final List<Object> data;
+
+    public ArrayComplexData(int count) {
+        data = new ArrayList<>(count);
+    }
+
+    @Override
+    public void storeObject(Object value) {
+        data.add(value);
+    }
+
+    @Override
+    public List<Object> getDynamicList() {
+        return Collections.unmodifiableList(data);
+    }
+
+    @Override
+    public Set<Object> getDynamicSet() {
+        // RESP2 compatibility mode - assuming the caller is aware that the array really contains a set (because in RESP2 we
+        // lack support for this data type) we make the conversion here
+        Set<Object> set = new LinkedHashSet<>(data);
+        return Collections.unmodifiableSet(set);
+    }
+
+    @Override
+    public Map<Object, Object> getDynamicMap() {
+        // RESP2 compatibility mode - assuming the caller is aware that the array really contains a map (because in RESP2 we
+        // lack support for this data type) we make the conversion here
+        Map<Object, Object> map = new LinkedHashMap<>();
+        final Boolean[] isKey = { true };
+        final Object[] key = new Object[1];
+
+        data.forEach(element -> {
+            if (isKey[0]) {
+                key[0] = element;
+                isKey[0] = false;
+            } else {
+                map.put(key[0], element);
+                isKey[0] = true;
+            }
+        });
+
+        return Collections.unmodifiableMap(map);
+    }
+
+}
diff --git a/src/main/java/io/lettuce/core/output/ComplexData.java b/src/main/java/io/lettuce/core/output/ComplexData.java
new file mode 100644
index 000000000..08ef81a20
--- /dev/null
+++ b/src/main/java/io/lettuce/core/output/ComplexData.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+
+package io.lettuce.core.output;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * The base type of all complex data, collected by a {@link ComplexOutput}
+ * <p>
+ * Commands typically result in simple types, however some of the commands could return complex nested structures. In these
+ * cases, and with the help of a {@link ComplexDataParser}, the data gathered by the {@link ComplexOutput} could be parsed to a
+ * domain object.
+ * <p>
+ * An complex data object could only be an aggregate data type as per the
+ * <a href="https://github.com/redis/redis-specifications/blob/master/protocol/RESP2.md">RESP2</a> and
+ * <a href="https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md">RESP3</a> protocol definitions. Its
+ * contents, however, could be both the simple and aggregate data types.
+ * <p>
+ * For RESP2 the only possible aggregation is an array. RESP2 commands could also return sets (obviously, by simply making sure
+ * the elements of the array are unique) or maps (by sending the keys as odd elements and their values as even elements in the
+ * right order one after another.
+ * <p>
+ * For RESP3 all the three aggregate types are supported (and indicated with special characters when the result is returned by
+ * the server).
+ * <p>
+ * Aggregate data types could also be nested by using the {@link ComplexData#storeObject(Object)} call.
+ * <p>
+ * Implementations of this class override the {@link ComplexData#getDynamicSet()}, {@link ComplexData#getDynamicList()} and
+ * {@link ComplexData#getDynamicMap()} methods to return the data received in the server in a implementation of the Collections
+ * framework. If a given implementation could not do the conversion in a meaningful way an {@link UnsupportedOperationException}
+ * would be thrown.
+ *
+ * @see ComplexOutput
+ * @see ArrayComplexData
+ * @see SetComplexData
+ * @see MapComplexData
+ * @author Tihomir Mateev
+ * @since 6.5
+ */
+public abstract class ComplexData {
+
+    /**
+     * Store a <code>long</code> value in the underlying data structure
+     *
+     * @param value the value to store
+     */
+    public void store(long value) {
+        storeObject(value);
+    }
+
+    /**
+     * Store a <code>double</code> value in the underlying data structure
+     *
+     * @param value the value to store
+     */
+    public void store(double value) {
+        storeObject(value);
+    }
+
+    /**
+     * Store a <code>boolean</code> value in the underlying data structure
+     *
+     * @param value the value to store
+     */
+    public void store(boolean value) {
+        storeObject(value);
+    }
+
+    /**
+     * Store an {@link Object} value in the underlying data structure. This method should be used when nesting one instance of
+     * {@link ComplexData} inside another
+     *
+     * @param value the value to store
+     */
+    public abstract void storeObject(Object value);
+
+    public void store(String value) {
+        storeObject(value);
+    }
+
+    /**
+     * Returns the aggregate data in a {@link List} form. If the underlying implementation could not convert the data structure
+     * to a {@link List} then an {@link UnsupportedOperationException} would be thrown.
+     *
+     * @return a {@link List} of {@link Object} values
+     */
+    public List<Object> getDynamicList() {
+        throw new UnsupportedOperationException("The type of data stored in this dynamic object is not a list");
+    }
+
+    /**
+     * Returns the aggregate data in a {@link Set} form. If the underlying implementation could not convert the data structure
+     * to a {@link Set} then an {@link UnsupportedOperationException} would be thrown.
+     *
+     * @return a {@link Set} of {@link Object} values
+     */
+    public Set<Object> getDynamicSet() {
+        throw new UnsupportedOperationException("The type of data stored in this dynamic object is not a set");
+    }
+
+    /**
+     * Returns the aggregate data in a {@link Map} form. If the underlying implementation could not convert the data structure
+     * to a {@link Map} then an {@link UnsupportedOperationException} would be thrown.
+     *
+     * @return a {@link Map} of {@link Object} values
+     */
+    public Map<Object, Object> getDynamicMap() {
+        throw new UnsupportedOperationException("The type of data stored in this dynamic object is not a map");
+    }
+
+}
diff --git a/src/main/java/io/lettuce/core/output/ComplexDataParser.java b/src/main/java/io/lettuce/core/output/ComplexDataParser.java
new file mode 100644
index 000000000..332fb61a4
--- /dev/null
+++ b/src/main/java/io/lettuce/core/output/ComplexDataParser.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+
+package io.lettuce.core.output;
+
+/**
+ * Any usage of the {@link ComplexOutput} comes hand in hand with a respective {@link ComplexDataParser} that is able to parse
+ * the data extracted from the server to a meaningful Java object.
+ * 
+ * @param <T> the type of the parsed object
+ * @author Tihomir Mateev
+ * @see ComplexData
+ * @see ComplexOutput
+ * @since 6.5
+ */
+public interface ComplexDataParser<T> {
+
+    /**
+     * Parse the data extracted from the server to a specific domain object.
+     *
+     * @param data the data to parse
+     * @return the parsed object
+     * @since 6.5
+     */
+    T parse(ComplexData data);
+
+}
diff --git a/src/main/java/io/lettuce/core/output/ComplexOutput.java b/src/main/java/io/lettuce/core/output/ComplexOutput.java
new file mode 100644
index 000000000..86ab339bd
--- /dev/null
+++ b/src/main/java/io/lettuce/core/output/ComplexOutput.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+
+package io.lettuce.core.output;
+
+import io.lettuce.core.codec.RedisCodec;
+import io.lettuce.core.codec.StringCodec;
+import io.lettuce.core.internal.LettuceFactories;
+
+import java.nio.ByteBuffer;
+import java.util.Deque;
+
+/**
+ * An implementation of the {@link CommandOutput} that is used in combination with a given {@link ComplexDataParser} to produce
+ * a domain object from the data extracted from the server. Since there already are implementations of the {@link CommandOutput}
+ * interface for most simple types, this implementation is better suited to parse complex, often nested, data structures, for
+ * example a map containing other maps, arrays or sets as values for one or more of its keys.
+ * <p>
+ * The implementation of the {@link ComplexDataParser} is responsible for mapping the data from the result to meaningful
+ * properties that the user of the LEttuce driver could then use in a statically typed manner.
+ *
+ * @see ComplexDataParser
+ * @author Tihomir Mateev
+ * @since 6.5
+ */
+public class ComplexOutput<K, V, T> extends CommandOutput<K, V, T> {
+
+    private final Deque<ComplexData> dataStack;
+
+    private final ComplexDataParser<T> parser;
+
+    private ComplexData data;
+
+    /**
+     * Constructs a new instance of the {@link ComplexOutput}
+     * 
+     * @param codec the {@link RedisCodec} to be applied
+     */
+    public ComplexOutput(RedisCodec<K, V> codec, ComplexDataParser<T> parser) {
+        super(codec, null);
+        dataStack = LettuceFactories.newSpScQueue();
+        this.parser = parser;
+    }
+
+    @Override
+    public T get() {
+        return parser.parse(data);
+    }
+
+    @Override
+    public void set(long integer) {
+        if (data == null) {
+            throw new RuntimeException("Invalid output received for dynamic aggregate output."
+                    + "Integer value should have been preceded by some sort of aggregation.");
+        }
+
+        data.store(integer);
+    }
+
+    @Override
+    public void set(double number) {
+        if (data == null) {
+            throw new RuntimeException("Invalid output received for dynamic aggregate output."
+                    + "Double value should have been preceded by some sort of aggregation.");
+        }
+
+        data.store(number);
+    }
+
+    @Override
+    public void set(boolean value) {
+        if (data == null) {
+            throw new RuntimeException("Invalid output received for dynamic aggregate output."
+                    + "Double value should have been preceded by some sort of aggregation.");
+        }
+
+        data.store(value);
+    }
+
+    @Override
+    public void set(ByteBuffer bytes) {
+        if (data == null) {
+            throw new RuntimeException("Invalid output received for dynamic aggregate output."
+                    + "ByteBuffer value should have been preceded by some sort of aggregation.");
+        }
+
+        data.storeObject(bytes == null ? null : codec.decodeValue(bytes));
+    }
+
+    @Override
+    public void setSingle(ByteBuffer bytes) {
+        if (data == null) {
+            throw new RuntimeException("Invalid output received for dynamic aggregate output."
+                    + "String value should have been preceded by some sort of aggregation.");
+        }
+
+        data.store(bytes == null ? null : StringCodec.UTF8.decodeValue(bytes));
+    }
+
+    @Override
+    public void complete(int depth) {
+        if (!dataStack.isEmpty() && depth == dataStack.size()) {
+            data = dataStack.pop();
+        }
+    }
+
+    private void multi(ComplexData newData) {
+        // if there is no data set, then we are at the root object
+        if (data == null) {
+            data = newData;
+            return;
+        }
+
+        // otherwise we need to nest the provided structure
+        data.storeObject(newData);
+        dataStack.push(data);
+        data = newData;
+    }
+
+    @Override
+    public void multiSet(int count) {
+        SetComplexData dynamicData = new SetComplexData(count);
+        multi(dynamicData);
+    }
+
+    @Override
+    public void multiArray(int count) {
+        ArrayComplexData dynamicData = new ArrayComplexData(count);
+        multi(dynamicData);
+    }
+
+    @Override
+    public void multiMap(int count) {
+        MapComplexData dynamicData = new MapComplexData(count);
+        multi(dynamicData);
+    }
+
+}
diff --git a/src/main/java/io/lettuce/core/output/MapComplexData.java b/src/main/java/io/lettuce/core/output/MapComplexData.java
new file mode 100644
index 000000000..f2f2b29a7
--- /dev/null
+++ b/src/main/java/io/lettuce/core/output/MapComplexData.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+
+package io.lettuce.core.output;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * An implementation of the {@link ComplexData} that handles maps.
+ * <p>
+ * All data structures that the implementation returns are unmodifiable
+ *
+ * @see ComplexData
+ * @author Tihomir Mateev
+ * @since 6.5
+ */
+class MapComplexData extends ComplexData {
+
+    private final Map<Object, Object> data;
+
+    private Object key;
+
+    public MapComplexData(int count) {
+        data = new HashMap<>(count);
+    }
+
+    @Override
+    public void storeObject(Object value) {
+        if (key == null) {
+            key = value;
+        } else {
+            data.put(key, value);
+            key = null;
+        }
+    }
+
+    @Override
+    public Map<Object, Object> getDynamicMap() {
+        return Collections.unmodifiableMap(data);
+    }
+
+}
diff --git a/src/main/java/io/lettuce/core/output/SetComplexData.java b/src/main/java/io/lettuce/core/output/SetComplexData.java
new file mode 100644
index 000000000..0d95afdd4
--- /dev/null
+++ b/src/main/java/io/lettuce/core/output/SetComplexData.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+
+package io.lettuce.core.output;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * An implementation of the {@link ComplexData} that handles maps.
+ * <p>
+ * All data structures that the implementation returns are unmodifiable
+ *
+ * @see ComplexData
+ * @author Tihomir Mateev
+ * @since 6.5
+ */
+public class SetComplexData extends ComplexData {
+
+    private final Set<Object> data;
+
+    public SetComplexData(int count) {
+        data = new HashSet<>(count);
+    }
+
+    @Override
+    public void storeObject(Object value) {
+        data.add(value);
+    }
+
+    @Override
+    public Set<Object> getDynamicSet() {
+        return Collections.unmodifiableSet(data);
+    }
+
+    @Override
+    public List<Object> getDynamicList() {
+        List<Object> list = new ArrayList<>(data.size());
+        list.addAll(data);
+        return Collections.unmodifiableList(list);
+    }
+
+}
diff --git a/src/main/java/io/lettuce/core/output/TrackingInfoParser.java b/src/main/java/io/lettuce/core/output/TrackingInfoParser.java
new file mode 100644
index 000000000..4239692dc
--- /dev/null
+++ b/src/main/java/io/lettuce/core/output/TrackingInfoParser.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+
+package io.lettuce.core.output;
+
+import io.lettuce.core.TrackingInfo;
+import io.lettuce.core.protocol.CommandKeyword;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Parser for Redis <a href="https://redis.io/docs/latest/commands/client-trackinginfo/">CLIENT TRACKINGINFO</a> command output.
+ *
+ * @author Tihomir Mateev
+ * @since 6.5
+ */
+public class TrackingInfoParser implements ComplexDataParser<TrackingInfo> {
+
+    public static final TrackingInfoParser INSTANCE = new TrackingInfoParser();
+
+    /**
+     * Utility constructor.
+     */
+    private TrackingInfoParser() {
+    }
+
+    /**
+     * Parse the output of the Redis CLIENT TRACKINGINFO command and convert it to a {@link TrackingInfo}
+     *
+     * @param dynamicData output of CLIENT TRACKINGINFO command
+     * @return an {@link TrackingInfo} instance
+     */
+    public TrackingInfo parse(ComplexData dynamicData) {
+        Map<Object, Object> data = verifyStructure(dynamicData);
+        Set<?> flags = ((ComplexData) data.get(CommandKeyword.FLAGS.toString().toLowerCase())).getDynamicSet();
+        Long clientId = (Long) data.get(CommandKeyword.REDIRECT.toString().toLowerCase());
+        List<?> prefixes = ((ComplexData) data.get(CommandKeyword.PREFIXES.toString().toLowerCase())).getDynamicList();
+
+        Set<TrackingInfo.TrackingFlag> parsedFlags = new HashSet<>();
+        List<String> parsedPrefixes = new ArrayList<>();
+
+        for (Object flag : flags) {
+            String toParse = (String) flag;
+            parsedFlags.add(TrackingInfo.TrackingFlag.from(toParse));
+        }
+
+        for (Object prefix : prefixes) {
+            parsedPrefixes.add((String) prefix);
+        }
+
+        return new TrackingInfo(parsedFlags, clientId, parsedPrefixes);
+    }
+
+    private Map<Object, Object> verifyStructure(ComplexData trackinginfoOutput) {
+
+        if (trackinginfoOutput == null) {
+            throw new IllegalArgumentException("Failed while parsing CLIENT TRACKINGINFO: trackinginfoOutput must not be null");
+        }
+
+        Map<Object, Object> data = trackinginfoOutput.getDynamicMap();
+        if (data == null || data.isEmpty()) {
+            throw new IllegalArgumentException("Failed while parsing CLIENT TRACKINGINFO: data must not be null or empty");
+        }
+
+        if (!data.containsKey(CommandKeyword.FLAGS.toString().toLowerCase())
+                || !data.containsKey(CommandKeyword.REDIRECT.toString().toLowerCase())
+                || !data.containsKey(CommandKeyword.PREFIXES.toString().toLowerCase())) {
+            throw new IllegalArgumentException(
+                    "Failed while parsing CLIENT TRACKINGINFO: trackinginfoOutput has missing flags");
+        }
+
+        return data;
+    }
+
+}
diff --git a/src/main/java/io/lettuce/core/protocol/CommandKeyword.java b/src/main/java/io/lettuce/core/protocol/CommandKeyword.java
index 6e1af5429..a422ad1f1 100644
--- a/src/main/java/io/lettuce/core/protocol/CommandKeyword.java
+++ b/src/main/java/io/lettuce/core/protocol/CommandKeyword.java
@@ -37,17 +37,17 @@ public enum CommandKeyword implements ProtocolKeyword {
 
     BY, BYLEX, BYSCORE, CACHING, CAT, CH, CHANNELS, COPY, COUNT, COUNTKEYSINSLOT, CONSUMERS, CREATE, DB, DELSLOTS, DELSLOTSRANGE, DELUSER, DESC, DRYRUN, SOFT, HARD, ENCODING,
 
-    FAILOVER, FORGET, FIELDS, FLUSH, FORCE, FREQ, FLUSHSLOTS, GENPASS, GETNAME, GETUSER, GETKEYSINSLOT, GETREDIR, GROUP, GROUPS, HTSTATS, ID, IDLE, INFO,
+    FAILOVER, FORGET, FIELDS, FLAGS, FLUSH, FORCE, FREQ, FLUSHSLOTS, GENPASS, GETNAME, GETUSER, GETKEYSINSLOT, GETREDIR, GROUP, GROUPS, HTSTATS, ID, IDLE, INFO,
 
     IDLETIME, JUSTID, KILL, KEYSLOT, LEFT, LEN, LIMIT, LIST, LOAD, LOG, MATCH,
 
-    MAX, MAXLEN, MEET, MIN, MINID, MOVED, NO, NOACK, NOCOMMANDS, NODE, NODES, NOMKSTREAM, NOPASS, NOSAVE, NOT, NOVALUES, NUMSUB, SHARDCHANNELS, SHARDNUMSUB, NUMPAT, NX, OFF, ON, ONE, OR, PAUSE,
+    MAX, MAXLEN, MEET, MIN, MINID, MOVED, NO, NOACK, NOCOMMANDS, NODE, NODES, NOMKSTREAM, NOPASS, NOSAVE, NOT, NOVALUES, NUMSUB, SHARDCHANNELS, SHARDNUMSUB, NUMPAT, NX, OFF, ON, ONE, OR, PAUSE, PREFIXES,
 
-    REFCOUNT, REMOVE, RELOAD, REPLACE, REPLICATE, REPLICAS, REV, RESET, RESETCHANNELS, RESETKEYS, RESETPASS,
+    REFCOUNT, REMOVE, RELOAD, REPLACE, REDIRECT, REPLICATE, REPLICAS, REV, RESET, RESETCHANNELS, RESETKEYS, RESETPASS,
 
     RESETSTAT, RESTART, RETRYCOUNT, REWRITE, RIGHT, SAVECONFIG, SDSLEN, SETINFO, SETNAME, SETSLOT, SHARDS, SLOTS, STABLE,
 
-    MIGRATING, IMPORTING, SAVE, SKIPME, SLAVES, STREAM, STORE, SUM, SEGFAULT, SETUSER, TAKEOVER, TRACKING, TYPE, UNBLOCK, USERS, USAGE, WEIGHTS, WHOAMI,
+    MIGRATING, IMPORTING, SAVE, SKIPME, SLAVES, STREAM, STORE, SUM, SEGFAULT, SETUSER, TAKEOVER, TRACKING, TRACKINGINFO, TYPE, UNBLOCK, USERS, USAGE, WEIGHTS, WHOAMI,
 
     WITHSCORE, WITHSCORES, WITHVALUES, XOR, XX, YES;
 
diff --git a/src/main/java/io/lettuce/core/protocol/CommandType.java b/src/main/java/io/lettuce/core/protocol/CommandType.java
index 427ce8018..4a44d6087 100644
--- a/src/main/java/io/lettuce/core/protocol/CommandType.java
+++ b/src/main/java/io/lettuce/core/protocol/CommandType.java
@@ -42,7 +42,7 @@ public enum CommandType implements ProtocolKeyword {
 
     // Server
 
-    BGREWRITEAOF, BGSAVE, CLIENT, COMMAND, CONFIG, DBSIZE, DEBUG, FLUSHALL, FLUSHDB, INFO, MYID, LASTSAVE, REPLICAOF, ROLE, MONITOR, SAVE, SHUTDOWN, SLAVEOF, SLOWLOG, SYNC, MEMORY,
+    BGREWRITEAOF, BGSAVE, CLIENT, COMMAND, CONFIG, DBSIZE, DEBUG, FLUSHALL, FLUSHDB, INFO, MYID, MYSHARDID, LASTSAVE, REPLICAOF, ROLE, MONITOR, SAVE, SHUTDOWN, SLAVEOF, SLOWLOG, SYNC, MEMORY,
 
     // Keys
 
diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisServerCoroutinesCommands.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisServerCoroutinesCommands.kt
index a0524be3d..b5a92e53c 100644
--- a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisServerCoroutinesCommands.kt
+++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisServerCoroutinesCommands.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-Present, Redis Ltd. and Contributors
+ * Copyright 2017-Present, Redis Ltd. and Contributors
  * All rights reserved.
  *
  * Licensed under the MIT License.
@@ -21,6 +21,7 @@
 package io.lettuce.core.api.coroutines
 
 import io.lettuce.core.*
+import io.lettuce.core.TrackingInfo
 import io.lettuce.core.protocol.CommandType
 import java.util.*
 
@@ -169,6 +170,14 @@ interface RedisServerCoroutinesCommands<K : Any, V : Any> {
      */
     suspend fun clientTracking(args: TrackingArgs): String?
 
+    /**
+     * Returns information about the current client connection's use of the server assisted client side caching feature.
+     *
+     * @return @link TrackingInfo}, for more information check the documentation
+     * @since 6.5
+     */
+    suspend fun clientTrackinginfo(): TrackingInfo?
+
     /**
      * Unblock the specified blocked client.
      *
diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisServerCoroutinesCommandsImpl.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisServerCoroutinesCommandsImpl.kt
index 3427baa78..ae0ef2548 100644
--- a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisServerCoroutinesCommandsImpl.kt
+++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisServerCoroutinesCommandsImpl.kt
@@ -21,6 +21,7 @@
 package io.lettuce.core.api.coroutines
 
 import io.lettuce.core.*
+import io.lettuce.core.TrackingInfo
 import io.lettuce.core.api.reactive.RedisServerReactiveCommands
 import io.lettuce.core.protocol.CommandType
 import kotlinx.coroutines.flow.toList
@@ -75,6 +76,8 @@ internal class RedisServerCoroutinesCommandsImpl<K : Any, V : Any>(internal val
 
     override suspend fun clientTracking(args: TrackingArgs): String? = ops.clientTracking(args).awaitFirstOrNull()
 
+    override suspend fun clientTrackinginfo(): TrackingInfo? = ops.clientTrackinginfo().awaitFirstOrNull()
+
     override suspend fun clientUnblock(id: Long, type: UnblockType): Long? = ops.clientUnblock(id, type).awaitFirstOrNull()
 
     override suspend fun command(): List<Any> = ops.command().asFlow().toList()
diff --git a/src/main/templates/io/lettuce/core/api/RedisHashCommands.java b/src/main/templates/io/lettuce/core/api/RedisHashCommands.java
index ac0559174..461563ad6 100644
--- a/src/main/templates/io/lettuce/core/api/RedisHashCommands.java
+++ b/src/main/templates/io/lettuce/core/api/RedisHashCommands.java
@@ -19,9 +19,13 @@
  */
 package io.lettuce.core.api;
 
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
 import java.util.List;
 import java.util.Map;
 
+import io.lettuce.core.ExpireArgs;
 import io.lettuce.core.KeyScanCursor;
 import io.lettuce.core.KeyValue;
 import io.lettuce.core.MapScanCursor;
diff --git a/src/main/templates/io/lettuce/core/api/RedisKeyCommands.java b/src/main/templates/io/lettuce/core/api/RedisKeyCommands.java
index 602b0442a..0357998db 100644
--- a/src/main/templates/io/lettuce/core/api/RedisKeyCommands.java
+++ b/src/main/templates/io/lettuce/core/api/RedisKeyCommands.java
@@ -24,9 +24,7 @@ import java.time.Instant;
 import java.util.Date;
 import java.util.List;
 
-import io.lettuce.core.ExpireArgs;
-import io.lettuce.core.KeyScanArgs;
-import io.lettuce.core.RestoreArgs;
+import io.lettuce.core.*;
 import io.lettuce.core.output.KeyStreamingChannel;
 import io.lettuce.core.output.ValueStreamingChannel;
 
diff --git a/src/main/templates/io/lettuce/core/api/RedisServerCommands.java b/src/main/templates/io/lettuce/core/api/RedisServerCommands.java
index 6775494a6..486443173 100644
--- a/src/main/templates/io/lettuce/core/api/RedisServerCommands.java
+++ b/src/main/templates/io/lettuce/core/api/RedisServerCommands.java
@@ -29,6 +29,7 @@ import io.lettuce.core.KillArgs;
 import io.lettuce.core.ShutdownArgs;
 import io.lettuce.core.TrackingArgs;
 import io.lettuce.core.UnblockType;
+import io.lettuce.core.TrackingInfo;
 import io.lettuce.core.protocol.CommandType;
 
 /**
@@ -175,6 +176,14 @@ public interface RedisServerCommands<K, V> {
      */
     String clientTracking(TrackingArgs args);
 
+    /**
+     * Returns information about the current client connection's use of the server assisted client side caching feature.
+     *
+     * @return {@link TrackingInfo}, for more information check the documentation
+     * @since 6.5
+     */
+    TrackingInfo clientTrackinginfo();
+
     /**
      * Unblock the specified blocked client.
      *
diff --git a/src/main/templates/io/lettuce/core/api/RedisStreamCommands.java b/src/main/templates/io/lettuce/core/api/RedisStreamCommands.java
index 232f1f618..954216a54 100644
--- a/src/main/templates/io/lettuce/core/api/RedisStreamCommands.java
+++ b/src/main/templates/io/lettuce/core/api/RedisStreamCommands.java
@@ -22,15 +22,9 @@ package io.lettuce.core.api;
 import java.util.List;
 import java.util.Map;
 
-import io.lettuce.core.Consumer;
-import io.lettuce.core.Limit;
-import io.lettuce.core.Range;
-import io.lettuce.core.StreamMessage;
-import io.lettuce.core.XAddArgs;
-import io.lettuce.core.XGroupCreateArgs;
-import io.lettuce.core.XReadArgs;
-import io.lettuce.core.XClaimArgs;
+import io.lettuce.core.*;
 import io.lettuce.core.XReadArgs.StreamOffset;
+import io.lettuce.core.models.stream.ClaimedMessages;
 import io.lettuce.core.models.stream.PendingMessage;
 import io.lettuce.core.models.stream.PendingMessages;
 


Test patch:
diff --git a/src/test/java/io/lettuce/core/RedisCommandBuilderUnitTests.java b/src/test/java/io/lettuce/core/RedisCommandBuilderUnitTests.java
index 5e6eba193..f99393cc1 100644
--- a/src/test/java/io/lettuce/core/RedisCommandBuilderUnitTests.java
+++ b/src/test/java/io/lettuce/core/RedisCommandBuilderUnitTests.java
@@ -146,4 +146,26 @@ class RedisCommandBuilderUnitTests {
                         + "3\r\n" + "$7\r\n" + "hField1\r\n" + "$7\r\n" + "hField2\r\n" + "$7\r\n" + "hField3\r\n");
     }
 
+    @Test
+    void shouldCorrectlyConstructClientTrackinginfo() {
+
+        Command<String, String, ?> command = sut.clientTrackinginfo();
+        ByteBuf buf = Unpooled.directBuffer();
+        command.encode(buf);
+
+        assertThat(buf.toString(StandardCharsets.UTF_8))
+                .isEqualTo("*2\r\n" + "$6\r\n" + "CLIENT\r\n" + "$12\r\n" + "TRACKINGINFO\r\n");
+    }
+
+    @Test
+    void shouldCorrectlyConstructClusterMyshardid() {
+
+        Command<String, String, ?> command = sut.clusterMyShardId();
+        ByteBuf buf = Unpooled.directBuffer();
+        command.encode(buf);
+
+        assertThat(buf.toString(StandardCharsets.UTF_8))
+                .isEqualTo("*2\r\n" + "$7\r\n" + "CLUSTER\r\n" + "$9\r\n" + "MYSHARDID\r\n");
+    }
+
 }
diff --git a/src/test/java/io/lettuce/core/commands/ServerCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/ServerCommandIntegrationTests.java
index a64414edf..3fed145c3 100644
--- a/src/test/java/io/lettuce/core/commands/ServerCommandIntegrationTests.java
+++ b/src/test/java/io/lettuce/core/commands/ServerCommandIntegrationTests.java
@@ -19,9 +19,6 @@
  */
 package io.lettuce.core.commands;
 
-import static org.assertj.core.api.Assertions.*;
-import static org.junit.jupiter.api.Assumptions.*;
-
 import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
@@ -57,6 +54,11 @@ import io.lettuce.test.Wait;
 import io.lettuce.test.condition.EnabledOnCommand;
 import io.lettuce.test.settings.TestSettings;
 
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assumptions.assumeFalse;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
 /**
  * Integration tests for {@link io.lettuce.core.api.sync.RedisServerCommands}.
  *
@@ -118,6 +120,30 @@ public class ServerCommandIntegrationTests extends TestSupport {
         }
     }
 
+    @Test
+    void clientTrackinginfoDefaults() {
+        TrackingInfo info = redis.clientTrackinginfo();
+
+        assertThat(info.getFlags()).contains(TrackingInfo.TrackingFlag.OFF);
+        assertThat(info.getRedirect()).isEqualTo(-1L);
+        assertThat(info.getPrefixes()).isEmpty();
+    }
+
+    @Test
+    void clientTrackinginfo() {
+        try {
+            redis.clientTracking(TrackingArgs.Builder.enabled(true).bcast().prefixes("usr:", "grp:"));
+            TrackingInfo info = redis.clientTrackinginfo();
+
+            assertThat(info.getFlags()).contains(TrackingInfo.TrackingFlag.ON);
+            assertThat(info.getFlags()).contains(TrackingInfo.TrackingFlag.BCAST);
+            assertThat(info.getRedirect()).isEqualTo(0L);
+            assertThat(info.getPrefixes()).contains("usr:", "grp:");
+        } finally {
+            redis.clientTracking(TrackingArgs.Builder.enabled(false));
+        }
+    }
+
     @Test
     void clientGetSetname() {
         assertThat(redis.clientGetname()).isNull();
@@ -180,7 +206,8 @@ public class ServerCommandIntegrationTests extends TestSupport {
     }
 
     @Test
-    @EnabledOnCommand("XAUTOCLAIM") // Redis 6.2
+    @EnabledOnCommand("XAUTOCLAIM")
+    // Redis 6.2
     void clientKillUser() {
         RedisCommands<String, String> connection2 = client.connect().sync();
         redis.aclSetuser("test_kill", AclSetuserArgs.Builder.addPassword("password1").on().addCommand(CommandType.ACL));
@@ -218,7 +245,8 @@ public class ServerCommandIntegrationTests extends TestSupport {
     }
 
     @Test
-    @EnabledOnCommand("WAITAOF") // Redis 7.2
+    @EnabledOnCommand("WAITAOF")
+    // Redis 7.2
     void clientListExtended() {
         Long clientId = redis.clientId();
 
@@ -229,7 +257,8 @@ public class ServerCommandIntegrationTests extends TestSupport {
     }
 
     @Test
-    @EnabledOnCommand("EVAL_RO") // Redis 7.0
+    @EnabledOnCommand("EVAL_RO")
+    // Redis 7.0
     void clientNoEvict() {
         assertThat(redis.clientNoEvict(true)).isEqualTo("OK");
         assertThat(redis.clientNoEvict(false)).isEqualTo("OK");
@@ -359,7 +388,8 @@ public class ServerCommandIntegrationTests extends TestSupport {
     }
 
     @Test
-    @EnabledOnCommand("EVAL_RO") // Redis 7.0
+    @EnabledOnCommand("EVAL_RO")
+    // Redis 7.0
     void configGetMultipleParameters() {
         assertThat(redis.configGet("maxmemory", "*max-*-entries*")).containsEntry("maxmemory", "0")
                 .containsEntry("hash-max-listpack-entries", "512");
@@ -382,7 +412,8 @@ public class ServerCommandIntegrationTests extends TestSupport {
     }
 
     @Test
-    @EnabledOnCommand("EVAL_RO") // Redis 7.0
+    @EnabledOnCommand("EVAL_RO")
+    // Redis 7.0
     void configSetMultipleParameters() {
         Map<String, String> original = redis.configGet("maxmemory", "hash-max-listpack-entries");
         Map<String, String> config = new HashMap<>();
@@ -416,7 +447,8 @@ public class ServerCommandIntegrationTests extends TestSupport {
     }
 
     @Test
-    @EnabledOnCommand("MEMORY") // Redis 4.0
+    @EnabledOnCommand("MEMORY")
+    // Redis 4.0
     void flushallAsync() {
         redis.set(key, value);
         assertThat(redis.flushallAsync()).isEqualTo("OK");
@@ -424,7 +456,8 @@ public class ServerCommandIntegrationTests extends TestSupport {
     }
 
     @Test
-    @EnabledOnCommand("XAUTOCLAIM") // Redis 6.2
+    @EnabledOnCommand("XAUTOCLAIM")
+    // Redis 6.2
     void flushallSync() {
         redis.set(key, value);
         assertThat(redis.flushall(FlushMode.SYNC)).isEqualTo("OK");
@@ -439,7 +472,8 @@ public class ServerCommandIntegrationTests extends TestSupport {
     }
 
     @Test
-    @EnabledOnCommand("MEMORY") // Redis 4.0
+    @EnabledOnCommand("MEMORY")
+    // Redis 4.0
     void flushdbAsync() {
         redis.set(key, value);
         redis.select(1);
@@ -451,7 +485,8 @@ public class ServerCommandIntegrationTests extends TestSupport {
     }
 
     @Test
-    @EnabledOnCommand("XAUTOCLAIM") // Redis 6.2
+    @EnabledOnCommand("XAUTOCLAIM")
+    // Redis 6.2
     void flushdbSync() {
         redis.set(key, value);
         assertThat(redis.flushdb(FlushMode.SYNC)).isEqualTo("OK");
@@ -575,19 +610,22 @@ public class ServerCommandIntegrationTests extends TestSupport {
     }
 
     @Test
-    @Disabled("Run me manually") // Redis 7.0
+    @Disabled("Run me manually")
+    // Redis 7.0
     void shutdown() {
         redis.shutdown(new ShutdownArgs().save(true).now());
     }
 
     @Test
-    @EnabledOnCommand("WAITAOF") // Redis 7.2
+    @EnabledOnCommand("WAITAOF")
+    // Redis 7.2
     void clientInfo() {
         assertThat(redis.clientInfo().contains("addr=")).isTrue();
     }
 
     @Test
-    @EnabledOnCommand("WAITAOF") // Redis 7.2
+    @EnabledOnCommand("WAITAOF")
+    // Redis 7.2
     void clientSetinfo() {
         redis.clientSetinfo("lib-name", "lettuce");
 
diff --git a/src/test/java/io/lettuce/core/output/TrackingInfoParserTest.java b/src/test/java/io/lettuce/core/output/TrackingInfoParserTest.java
new file mode 100644
index 000000000..9ac594538
--- /dev/null
+++ b/src/test/java/io/lettuce/core/output/TrackingInfoParserTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+
+package io.lettuce.core.output;
+
+import io.lettuce.core.TrackingInfo;
+import io.lettuce.core.protocol.CommandKeyword;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class TrackingInfoParserTest {
+
+    @Test
+    void parseResp3() {
+        ComplexData flags = new SetComplexData(2);
+        flags.store(TrackingInfo.TrackingFlag.ON.toString());
+        flags.store(TrackingInfo.TrackingFlag.OPTIN.toString());
+
+        ComplexData prefixes = new ArrayComplexData(0);
+
+        ComplexData input = new MapComplexData(3);
+        input.store(CommandKeyword.FLAGS.toString().toLowerCase());
+        input.storeObject(flags);
+        input.store(CommandKeyword.REDIRECT.toString().toLowerCase());
+        input.store(0L);
+        input.store(CommandKeyword.PREFIXES.toString().toLowerCase());
+        input.storeObject(prefixes);
+
+        TrackingInfo info = TrackingInfoParser.INSTANCE.parse(input);
+
+        assertThat(info.getFlags()).contains(TrackingInfo.TrackingFlag.ON, TrackingInfo.TrackingFlag.OPTIN);
+        assertThat(info.getRedirect()).isEqualTo(0L);
+        assertThat(info.getPrefixes()).isEmpty();
+    }
+
+    @Test
+    void parseFailEmpty() {
+        ComplexData input = new MapComplexData(0);
+
+        Exception exception = assertThrows(IllegalArgumentException.class, () -> {
+            TrackingInfo info = TrackingInfoParser.INSTANCE.parse(input);
+        });
+    }
+
+    @Test
+    void parseFailNumberOfElements() {
+        ComplexData flags = new SetComplexData(2);
+        flags.store(TrackingInfo.TrackingFlag.ON.toString());
+        flags.store(TrackingInfo.TrackingFlag.OPTIN.toString());
+
+        ComplexData prefixes = new ArrayComplexData(0);
+
+        ComplexData input = new MapComplexData(3);
+        input.store(CommandKeyword.FLAGS.toString().toLowerCase());
+        input.storeObject(flags);
+        input.store(CommandKeyword.REDIRECT.toString().toLowerCase());
+        input.store(-1L);
+
+        Exception exception = assertThrows(IllegalArgumentException.class, () -> {
+            TrackingInfo info = TrackingInfoParser.INSTANCE.parse(input);
+        });
+    }
+
+    @Test
+    void parseResp2Compatibility() {
+        ComplexData flags = new ArrayComplexData(2);
+        flags.store(TrackingInfo.TrackingFlag.ON.toString());
+        flags.store(TrackingInfo.TrackingFlag.OPTIN.toString());
+
+        ComplexData prefixes = new ArrayComplexData(0);
+
+        ComplexData input = new ArrayComplexData(3);
+        input.store(CommandKeyword.FLAGS.toString().toLowerCase());
+        input.storeObject(flags);
+        input.store(CommandKeyword.REDIRECT.toString().toLowerCase());
+        input.store(0L);
+        input.store(CommandKeyword.PREFIXES.toString().toLowerCase());
+        input.storeObject(prefixes);
+
+        TrackingInfo info = TrackingInfoParser.INSTANCE.parse(input);
+
+        assertThat(info.getFlags()).contains(TrackingInfo.TrackingFlag.ON, TrackingInfo.TrackingFlag.OPTIN);
+        assertThat(info.getRedirect()).isEqualTo(0L);
+        assertThat(info.getPrefixes()).isEmpty();
+    }
+
+}