Repository: redis/lettuce
Pull request #2887 (2024-06-10 09:49:50)

Bump org.apache.maven.plugins:maven-assembly-plugin from 3.6.0 to 3.7.1
Bumps [org.apache.maven.plugins:maven-assembly-plugin](https://github.com/apache/maven-assembly-plugin) from 3.6.0 to 3.7.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/apache/maven-assembly-plugin/releases">org.apache.maven.plugins:maven-assembly-plugin's releases</a>.</em></p>
<blockquote>
<h2>3.7.1</h2>
<h2><a href="https://issues.apache.org/jira/secure/ReleaseNote.jspa?projectId=12317220&amp;version=12354406">Release Notes - Maven Assembly Plugin - Version 3.7.1</a></h2>
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<ul>
<li>[MASSEMBLY-1023] Bump org.apache.maven.shared:maven-filtering from 3.3.1 to 3.3.2 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/apache/maven-assembly-plugin/pull/192">apache/maven-assembly-plugin#192</a></li>
<li>[MASSEMBLY-1024] Bump org.apache.commons:commons-compress from 1.25.0 to 1.26.1 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/apache/maven-assembly-plugin/pull/191">apache/maven-assembly-plugin#191</a></li>
<li>[MASSEMBLY-1025] Bump org.codehaus.plexus:plexus-archiver from 4.9.1 to 4.9.2 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/apache/maven-assembly-plugin/pull/195">apache/maven-assembly-plugin#195</a></li>
<li>[MASSEMBLY-1022] Unresolved artifacts should be not processed by <a href="https://github.com/slawekjaranowski"><code>@​slawekjaranowski</code></a> in <a href="https://redirect.github.com/apache/maven-assembly-plugin/pull/194">apache/maven-assembly-plugin#194</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/apache/maven-assembly-plugin/compare/maven-assembly-plugin-3.7.0...maven-assembly-plugin-3.7.1">https://github.com/apache/maven-assembly-plugin/compare/maven-assembly-plugin-3.7.0...maven-assembly-plugin-3.7.1</a></p>
<h2>3.7.0</h2>
<h1><a href="https://issues.apache.org/jira/secure/ReleaseNote.jspa?projectId=12317220&amp;version=12353243">Release Notes - Maven Assembly Plugin - Version 3.7.0</a></h1>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/apache/maven-assembly-plugin/commit/0afbb3e1252e0cdfb5e4abe3c4b2a108567059a7"><code>0afbb3e</code></a> [maven-release-plugin] prepare release maven-assembly-plugin-3.7.1</li>
<li><a href="https://github.com/apache/maven-assembly-plugin/commit/74e858ad40849cc3f8bef462e310336a8c6a0cd5"><code>74e858a</code></a> [MASSEMBLY-1022] Unresolved artifacts should be not processed</li>
<li><a href="https://github.com/apache/maven-assembly-plugin/commit/cb5638223ebaf93ff06290a6ebb64c6dac12ebd4"><code>cb56382</code></a> [MASSEMBLY-1025] Bump org.codehaus.plexus:plexus-archiver from 4.9.1 to 4.9.2...</li>
<li><a href="https://github.com/apache/maven-assembly-plugin/commit/86bbed043dbe78f6c32e99c1e433e052b2d78f69"><code>86bbed0</code></a> [MASSEMBLY-1024] Bump org.apache.commons:commons-compress from 1.25.0 to 1.26...</li>
<li><a href="https://github.com/apache/maven-assembly-plugin/commit/bdcc4d0d093de4484820b2f9c4f84b5d0f7eaaa0"><code>bdcc4d0</code></a> [MASSEMBLY-1023] Bump org.apache.maven.shared:maven-filtering from 3.3.1 to 3...</li>
<li><a href="https://github.com/apache/maven-assembly-plugin/commit/74fe92e979db0479ee58db580d0e7295e6113e78"><code>74fe92e</code></a> [maven-release-plugin] prepare for next development iteration</li>
<li><a href="https://github.com/apache/maven-assembly-plugin/commit/9be6e8733f187b15c0e6c0fb856765e12fb2c055"><code>9be6e87</code></a> [maven-release-plugin] prepare release maven-assembly-plugin-3.7.0</li>
<li><a href="https://github.com/apache/maven-assembly-plugin/commit/e8630dc91637860d1ca690b2c64c2c18e240eae7"><code>e8630dc</code></a> Bump apache/maven-gh-actions-shared from 3 to 4</li>
<li><a href="https://github.com/apache/maven-assembly-plugin/commit/98f97a5dca6eac582ea5e52d7d62e186d9b8e25f"><code>98f97a5</code></a> Bump org.postgresql:postgresql in /src/it/projects/bugs/massembly-730</li>
<li><a href="https://github.com/apache/maven-assembly-plugin/commit/c84e11089a785d56441fac2314224c0bf86b200d"><code>c84e110</code></a> [MASSEMBLY-1019] Maven 3.6.3 as minimum requirements</li>
<li>Additional commits viewable in <a href="https://github.com/apache/maven-assembly-plugin/compare/maven-assembly-plugin-3.6.0...maven-assembly-plugin-3.7.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-assembly-plugin&package-manager=maven&previous-version=3.6.0&new-version=3.7.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

You can trigger a rebase of this PR 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>

> **Note**
> Automatic rebases have been disabled on this pull request as it has been open for over 30 days.



Patch:
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index e33dcd630..6509c5c87 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -5,7 +5,7 @@ Make sure that:
 
 - [ ] You have read the [contribution guidelines](https://github.com/lettuce-io/lettuce-core/blob/main/.github/CONTRIBUTING.md).
 - [ ] You have created a feature request first to discuss your contribution intent. Please reference the feature request ticket number in the pull request.
-- [ ] You use the code formatters provided [here](https://github.com/lettuce-io/lettuce-core/blob/main/formatting.xml) and have them applied to your changes. Don’t submit any formatting related changes.
+- [ ] You applied code formatting rules using the `mvn formatter:format` target. Don’t submit any formatting related changes.
 - [ ] You submit test cases (unit or integration tests) that back your changes.
 
  <!--
diff --git a/.github/wordlist.txt b/.github/wordlist.txt
index 7c8f1adf4..2714717cb 100644
--- a/.github/wordlist.txt
+++ b/.github/wordlist.txt
@@ -73,3 +73,10 @@ github
 runtime
 transactional
 Github
+formatter
+RedisConnectionCommands
+BaseRedisCommands
+DnsResolver
+dnsResolver
+evalReadOnly
+gg
diff --git a/.github/workflows/version-and-release.yaml b/.github/workflows/version-and-release.yaml
index 93ee19348..cc6b766e7 100644
--- a/.github/workflows/version-and-release.yaml
+++ b/.github/workflows/version-and-release.yaml
@@ -10,17 +10,11 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - name: 1. Checkout sources
+      - name: Checkout sources
         uses: actions/checkout@v4
 
-      - name: 2. Extract version from tag
-        id: get_version
-        run: |
-          realversion="${GITHUB_REF/refs\/tags\//}"
-          realversion="${realversion//v/}"
-          echo "VERSION=$realversion" >> $GITHUB_OUTPUT
+      - name: Set up Java with Maven cache
 
-      - name: 3. Set up Java with Maven cache
         uses: actions/setup-java@v4
         with:
           java-version: '8'
@@ -30,20 +24,27 @@ jobs:
           server-username: MAVEN_USERNAME
           server-password: MAVEN_PASSWORD
 
-      - name: 4. Update version in Maven configuration
-        run: mvn versions:set -DnewVersion=${{ steps.get_version.outputs.VERSION }}
+      - name: Update version in Maven configuration
+        run: |
+          mvn --no-transfer-progress \
+            --batch-mode \
+            versions:set -DnewVersion=${{ github.event.release.tag_name  }}
+        if: ${{ github.event.release.tag_name  }}
+
+      - name: Install GPG key
 
-      - name: 5. Install GPG key
         run: |
           cat <(echo -e "${{ secrets.OSSH_GPG_SECRET_KEY }}") | gpg --batch --import
           gpg --list-secret-keys --keyid-format LONG
 
-      - name: 6. Publish to Maven
+      - name: Publish to Maven
+
         run: |
           mvn --no-transfer-progress \
             --batch-mode \
             -Dgpg.passphrase='${{ secrets.OSSH_GPG_SECRET_KEY_PASSWORD }}' \
-            release:perform
+            deploy -P sonatype-oss-release
+
         env:
           MAVEN_USERNAME: ${{secrets.OSSH_USERNAME}}
           MAVEN_PASSWORD: ${{secrets.OSSH_TOKEN}}
diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 227a07354..481c2b4cf 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -1,124 +1,60 @@
-Lettuce 6.3.0 RELEASE NOTES
+Lettuce 6.4.0 RELEASE NOTES
 ==============================
 
-The Lettuce team is delighted to announce general availability of Lettuce 6.3.
+The Redis team is delighted to announce general availability of Lettuce 6.4.
 
-This release ships with support for Observability through Micrometer Tracing that has been
-ported from Spring Data into Lettuce directly. Another infrastructure change is to report
-the driver version and driver name to Redis through `CLIENT SETINFO` if the Redis version
-is 7.2 or later using RESP3.
-
-This release ships also with refinements around Cluster Topology refresh to suspend
-periodic refresh. This is useful for JVM Checkpoint-Restore arrangements (Project CRaC)
-to ensure that no background activity opens connections.
-
-This release also upgrades to Kotlin 1.7 as baseline.
+This Lettuce driver is now going to be shipped under the MIT licensing scheme. The `CLIENT SETINFO`
+is now working in a fire-and-forget mode to allow better compatibility with Redis servers that do
+not support this command.
 
 Lettuce 6 supports Redis 2.6+ up to Redis 7.x. In terms of Java runtime, Lettuce requires
 at least Java 8 and works with Java 21.
 
-Thanks to all contributors who made Lettuce 6.3.0 possible.
+Thanks to all contributors who made Lettuce 6.4.0 possible.
 
 If you need any support, meet Lettuce at
 
 * GitHub Discussions: https://github.com/lettuce-io/lettuce-core/discussions
 * Stack Overflow (Questions): https://stackoverflow.com/questions/tagged/lettuce
-* Join the chat at https://gitter.im/lettuce-io/Lobby for general discussion
+* Join the chat at https://discord.gg/redis for general discussion
 * GitHub Issues (Bug reports, feature
   requests): https://github.com/lettuce-io/lettuce-core/issues
-* Documentation: https://lettuce.io/core/6.3.0.RELEASE/reference/
-* Javadoc: https://lettuce.io/core/6.3.0.RELEASE/api/
+* Documentation: https://lettuce.io/core/6.4.0.RELEASE/reference/
+* Javadoc: https://lettuce.io/core/6.4.0.RELEASE/api/
 
 Commands
 --------
 
-* Add `WITHSCORE` option to `ZRANK` and `ZREVRANK` commands #2410
-* Add support for `CLIENT SETINFO`, `CLIENT INFO`, and enhanced `CLIENT LIST` #2439
-* ZMPOP and BZMPOP commands #2435
-* Support 'FCALL' commands to Call Lua-scripts that are loaded as Function in redis #2185
+* Add `PUBSUB` shard channel commands `SHARDCHANNELS` #2756, `SHARDNUMSUB` #2776
+* Add `PUBSUB` shard channel commands `SPUBLISH` #2757, `SSUBSCRIBE` #2758 and `SUNSUBSCRIBE` #2758
+* Add support for `CLIENT KILL [MAXAGE]` #2782
+* Hash field expiration commands `HEXPIRE`, `HEXPIREAT`, `HEXPIRETIME` and `HPERSIST` #2836
+* Hash field expiration commands `HPEXPIRE`, `HPEXPIREAT`, `HPEXPIRETIME`, `HTTL` and `HPTTL` #2857
 
 Enhancements
 ------------
 
-* Add support for disconnect on timeout to recover early from no `RST` packet failures
-  #2082
-* Avoid buffer copies in `RedisStateMachine` #2173
-* Make SlotHash utility methods public #2199
-* Improve `AdaptiveRefreshTriggeredEvent` to provide the cause and contextual details
-  #2338
-* Refine `RedisException` instantiation to avoid exception instances if they are not used
-  #2353
-* Add capability of FailOver with takeOver option #2358
-* Add option to disable tracing for individual commands #2373
-* ReplicaTopologyProvider can't parse replicas from INFO #2375
-* Add support for Micrometer Tracing #2391
-* Add Command filter to `MicrometerCommandLatencyRecorder` #2406
-* Expose methods to suspend periodic topology refresh and to check whether a topology
-  refresh is running #2428
-* Accept Double and Boolean in `MapOutput` #2429
-* Array lists with set capacities in SimpleBatcher #2445
-* Add fallback to RESP2 upon `NOPROTO` response #2455
-* Introduce generic Object output #2467
-* Propagate initialization failures from `ChannelInitializer` #2475
-* Register library name and library version on Redis 7.2 or greater #2483
-* Add support for cluster-announced hostname #2487
+* Add support for `HSCAN NOVALUES` #2763
+* Send the `CLIENT SETINFO` command in a fire-and-forget way #2082
+* Change the license to more permissive MIT #2173
+* Add a evalReadOnly overload that accepts the script as a String #2868
+* `XREAD` support for reading last message from stream #2863
+* Mark dnsResolver(DnsResolver) as deprecated  #2855
+* Remove connection-related methods from commands API #2027
+* Move connection-related commands from BaseRedisCommands to RedisConnectionCommands #2031
 
 Fixes
 -----
 
-* Proper creation of `AttributeKey` #2111
-* INFO response parsing throws on encountering '\' on NodeTopologyView #2161
-* `PartitionSelectorException` during refresh of `Partitions` #2178
-* RedisURI.Builder#withSsl(RedisURI) not working with SslVerifyMode#CA #2182
-* SMISMEMBER is not marked a readonly command #2197
-* Eval lua script expects return integer but null #2200
-* `ZRANGESTORE` does not support by Rank comparison #2202
-* zrevrangestorebylex/zrevrangestorebyscore range arguments flipped #2203
-* Own `RedisCredentialsProvider` causes issue with protocol handshake on Redis 5 #2234
-* NullPointerException if INFO command on redis cluster fails #2243
-* XTrimArgs Should Allow Limit = 0 #2250
-* The hostname and password cannot parse even if escaping with RedisURI redis-sentinel the
-  password include '@' and '#' #2254
-* Fix password parsing error when redis-sentinel URI contains @ #2255
-* Handle unknown endpoints in MOVED response #2290
-* Fallback to RESP2 hides potential authentication configuration problems #2313
-* Accept slots as String using `CLUSTER SHARDS` #2325
-* `RedisURI.applySsl(…)` does not retain `SslVerifyMode` #2328
-* Apply `SslVerifyMode` in `RedisURI.applySsl(…)` #2329
-* Fix long overflow in `RedisSubscription#potentiallyReadMore` #2383
-* Consistently implement CompositeArgument in arg types #2387
-* Reactive Cluster `MGET` is not running in parallel #2395
-* Polish RedisObservation name & javadoc #2404
-* `memory usage` command passes key as `String` instead of using the codec #2424
-* Fix NPE when manually flushing a batch #2444
-* `flushCommands` leads to random inbound command order when using large argument values
-  with SSL #2456
-* `CommandListener` notified twice on error #2457
-* `RoleParser` does not define `none`, `handshake`, and `unknown` replica states #2482
-* StatefulRedisClusterPubSubConnectionImpl's activated() method will report exception
-  after resubscribe() was call. #2534
+* None
 
 Other
 -----
 
-* Improve Document on pingBeforeActivateConnection #2138
-* Improve Document on dynamicRefreshSources #2139
-* Fixes typo in ReadFrom #2213
-* Fix duplicate word occurrences #2307
-* Update netty.version to 4.1.89.Final #2311
-* Avoid using port 7443 in Lettuce tests #2326
-* Upgrade to Reactor 3.4.27 #2330
-* Fix Set unit test sscanMultiple fail in redis7 #2349
-* README.md demo has a error #2377
-* Upgrade to Kotlin 1.7 #2392
-* Upgrade to Netty 4.1.94.Final #2431
-* Update SetArgs.java builder method param comment #2441
-* Use enum for no-op `PauseDetectorWrapper` #2474
-* Upgrade build to Redis 7.2 #2481
-* Refine command outputs to capture whether a segment has been received instead of relying
-  on the deserialized value state #2498
-* Upgrade to Reactor 3.6.0 #2517
-* Docs on metrics (wiki) are misleading #2538
-* Upgrade to Micrometer 1.12.0 #2549
-* Upgrade to netty 4.1.101.Final #2550
-
+* Bump `org.apache.commons:commons-pool2` from 2.11.1 to 2.12.0 #2877 
+* Bump `org.openjdk.jmh:jmh-generator-annprocess` from 1.21 to 1.37 #2876
+* Bump `org.apache.maven.plugins:maven-jar-plugin` from 3.3.0 to 3.4.1 #2875 
+* Bump `org.codehaus.mojo:flatten-maven-plugin from` 1.5.0 to 1.6.0 #2874
+* Bump `org.apache.maven.plugins:maven-javadoc-plugin` from 3.6.3 to 3.7.0 #2873
+* Applying code formatter each time we run a Maven build #2841 
+* Bump `setup-java` to v4 #2807 
diff --git a/pom.xml b/pom.xml
index 9be9d982f..c1c5f9215 100644
--- a/pom.xml
+++ b/pom.xml
@@ -64,7 +64,7 @@
         <mockito.version>4.9.0</mockito.version>
         <netty.version>4.1.107.Final</netty.version>
         <openwebbeans.version>2.0.27</openwebbeans.version>
-        <reactor.version>3.6.4</reactor.version>
+        <reactor.version>3.6.6</reactor.version>
         <rxjava.version>1.3.8</rxjava.version>
         <rxjava-reactive-streams.version>1.2.1</rxjava-reactive-streams.version>
         <rxjava2.version>2.2.21</rxjava2.version>
@@ -559,7 +559,7 @@
                 <plugin>
                     <groupId>org.apache.maven.plugins</groupId>
                     <artifactId>maven-assembly-plugin</artifactId>
-                    <version>3.6.0</version>
+                    <version>3.7.1</version>
                 </plugin>
 
                 <plugin>
@@ -900,6 +900,18 @@
                 </configuration>
             </plugin>
 
+            <plugin>
+                <groupId>org.sonatype.plugins</groupId>
+                <artifactId>nexus-staging-maven-plugin</artifactId>
+                <version>1.6.13</version>
+                <extensions>true</extensions>
+                <configuration>
+                    <serverId>ossrh</serverId>
+                    <nexusUrl>https://oss.sonatype.org/</nexusUrl>
+                    <autoReleaseAfterClose>true</autoReleaseAfterClose>
+                </configuration>
+            </plugin>
+
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-source-plugin</artifactId>
@@ -1019,6 +1031,12 @@
                     <plugin>
                         <groupId>org.apache.maven.plugins</groupId>
                         <artifactId>maven-gpg-plugin</artifactId>
+                        <configuration>
+                            <gpgArguments>
+                                <arg>--pinentry-mode</arg>
+                                <arg>loopback</arg>
+                            </gpgArguments>
+                        </configuration>
                         <executions>
                             <execution>
                                 <id>sign-artifacts</id>
diff --git a/src/main/java/io/lettuce/core/codec/ByteArrayCodec.java b/src/main/java/io/lettuce/core/codec/ByteArrayCodec.java
index c8a9cffd0..bcecd3d28 100644
--- a/src/main/java/io/lettuce/core/codec/ByteArrayCodec.java
+++ b/src/main/java/io/lettuce/core/codec/ByteArrayCodec.java
@@ -90,9 +90,11 @@ public class ByteArrayCodec implements RedisCodec<byte[], byte[]>, ToByteBufEnco
     }
 
     private static byte[] getBytes(ByteBuffer buffer) {
+        if (buffer == null) {
+            return EMPTY;
+        }
 
         int remaining = buffer.remaining();
-
         if (remaining == 0) {
             return EMPTY;
         }
diff --git a/src/main/java/io/lettuce/core/masterreplica/ReadOnlyCommands.java b/src/main/java/io/lettuce/core/masterreplica/ReadOnlyCommands.java
deleted file mode 100644
index 67616a7ef..000000000
--- a/src/main/java/io/lettuce/core/masterreplica/ReadOnlyCommands.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright 2020-Present, Redis Ltd. and Contributors
- * All rights reserved.
- *
- * Licensed under the MIT License.
- *
- * This file contains contributions from third-party contributors
- * licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package io.lettuce.core.masterreplica;
-
-import java.util.Collections;
-import java.util.EnumSet;
-import java.util.Set;
-
-import io.lettuce.core.protocol.CommandType;
-import io.lettuce.core.protocol.ProtocolKeyword;
-
-/**
- * Contains all command names that are read-only commands.
- *
- * @author Mark Paluch
- */
-class ReadOnlyCommands {
-
-    private static final Set<CommandType> READ_ONLY_COMMANDS = EnumSet.noneOf(CommandType.class);
-
-    static {
-        for (CommandName commandNames : CommandName.values()) {
-            READ_ONLY_COMMANDS.add(CommandType.valueOf(commandNames.name()));
-        }
-    }
-
-    /**
-     * @param protocolKeyword must not be {@code null}.
-     * @return {@code true} if {@link ProtocolKeyword} is a read-only command.
-     */
-    public static boolean isReadOnlyCommand(ProtocolKeyword protocolKeyword) {
-        return READ_ONLY_COMMANDS.contains(protocolKeyword);
-    }
-
-    /**
-     * @return an unmodifiable {@link Set} of {@link CommandType read-only} commands.
-     */
-    public static Set<CommandType> getReadOnlyCommands() {
-        return Collections.unmodifiableSet(READ_ONLY_COMMANDS);
-    }
-
-    enum CommandName {
-        ASKING, BITCOUNT, BITPOS, CLIENT, COMMAND, DUMP, ECHO, EVAL_RO, EVALSHA_RO, EXISTS, FCALL_RO, //
-        GEODIST, GEOPOS, GEORADIUS, GEORADIUS_RO, GEORADIUSBYMEMBER, GEORADIUSBYMEMBER_RO, GEOSEARCH, GEOHASH, GET, GETBIT, //
-        GETRANGE, HEXISTS, HGET, HGETALL, HKEYS, HLEN, HMGET, HRANDFIELD, HSCAN, HSTRLEN, //
-        HVALS, INFO, KEYS, LINDEX, LLEN, LPOS, LRANGE, SORT_RO, MGET, PFCOUNT, PTTL, //
-        RANDOMKEY, READWRITE, SCAN, SCARD, SCRIPT, //
-        SDIFF, SINTER, SISMEMBER, SMISMEMBER, SMEMBERS, SRANDMEMBER, SSCAN, STRLEN, //
-        SUNION, TIME, TTL, TYPE, //
-        XINFO, XLEN, XPENDING, XRANGE, XREVRANGE, XREAD, //
-        ZCARD, ZCOUNT, ZLEXCOUNT, ZRANGE, //
-        ZRANDMEMBER, ZRANGEBYLEX, ZRANGEBYSCORE, ZRANK, ZREVRANGE, ZREVRANGEBYLEX, ZREVRANGEBYSCORE, ZREVRANK, ZSCAN, ZSCORE,
-    }
-
-}
diff --git a/src/main/java/io/lettuce/core/protocol/ReadOnlyCommands.java b/src/main/java/io/lettuce/core/protocol/ReadOnlyCommands.java
index a6bfc6333..81671bf76 100644
--- a/src/main/java/io/lettuce/core/protocol/ReadOnlyCommands.java
+++ b/src/main/java/io/lettuce/core/protocol/ReadOnlyCommands.java
@@ -71,7 +71,7 @@ public class ReadOnlyCommands {
 
     enum CommandName {
         ASKING, BITCOUNT, BITPOS, CLIENT, COMMAND, DUMP, ECHO, EVAL_RO, EVALSHA_RO, EXISTS, FCALL_RO, //
-        GEODIST, GEOPOS, GEORADIUS, GEORADIUS_RO, GEORADIUSBYMEMBER, GEORADIUSBYMEMBER_RO, GEOSEARCH, GEOHASH, GET, GETBIT, //
+        GEODIST, GEOPOS, GEORADIUS_RO, GEORADIUSBYMEMBER_RO, GEOSEARCH, GEOHASH, GET, GETBIT, //
         GETRANGE, HEXISTS, HGET, HGETALL, HKEYS, HLEN, HMGET, HRANDFIELD, HSCAN, HSTRLEN, //
         HVALS, INFO, KEYS, LINDEX, LLEN, LPOS, LRANGE, SORT_RO, MGET, PFCOUNT, PTTL, //
         RANDOMKEY, READWRITE, SCAN, SCARD, SCRIPT, //


Test patch:
diff --git a/src/test/java/io/lettuce/core/cluster/ClusterReadOnlyCommandsUnitTests.java b/src/test/java/io/lettuce/core/cluster/ClusterReadOnlyCommandsUnitTests.java
index b1f6cad9f..502198b7a 100644
--- a/src/test/java/io/lettuce/core/cluster/ClusterReadOnlyCommandsUnitTests.java
+++ b/src/test/java/io/lettuce/core/cluster/ClusterReadOnlyCommandsUnitTests.java
@@ -16,7 +16,7 @@ class ClusterReadOnlyCommandsUnitTests {
 
     @Test
     void testCount() {
-        assertThat(ClusterReadOnlyCommands.getReadOnlyCommands()).hasSize(86);
+        assertThat(ClusterReadOnlyCommands.getReadOnlyCommands()).hasSize(84);
     }
 
     @Test
diff --git a/src/test/java/io/lettuce/core/codec/ByteArrayCodecUnitTests.java b/src/test/java/io/lettuce/core/codec/ByteArrayCodecUnitTests.java
new file mode 100644
index 000000000..c8f2394da
--- /dev/null
+++ b/src/test/java/io/lettuce/core/codec/ByteArrayCodecUnitTests.java
@@ -0,0 +1,35 @@
+package io.lettuce.core.codec;
+
+import org.junit.jupiter.api.Test;
+
+import java.nio.ByteBuffer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class ByteArrayCodecUnitTests {
+
+    @Test
+    void testDecodeValue_withNonEmptyByteBuffer() {
+        final ByteArrayCodec byteArrayCodec = ByteArrayCodec.INSTANCE;
+        final byte[] expectedBytes = { 1, 2, 3, 4, 5 };
+        final ByteBuffer buffer = ByteBuffer.wrap(expectedBytes);
+        final byte[] result = byteArrayCodec.decodeValue(buffer);
+        assertThat(result).isEqualTo(expectedBytes);
+    }
+
+    @Test
+    void testDecodeValue_withEmptyByteBuffer() {
+        final ByteArrayCodec byteArrayCodec = ByteArrayCodec.INSTANCE;
+        final ByteBuffer buffer = ByteBuffer.allocate(0);
+        final byte[] result = byteArrayCodec.decodeValue(buffer);
+        assertThat(result).isEmpty();
+    }
+
+    @Test
+    void testDecodeValue_withNullByteBuffer() {
+        final ByteArrayCodec byteArrayCodec = ByteArrayCodec.INSTANCE;
+        final byte[] result = byteArrayCodec.decodeValue(null);
+        assertThat(result).isEmpty();
+    }
+
+}
diff --git a/src/test/java/io/lettuce/core/commands/HashCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/HashCommandIntegrationTests.java
index f6148209c..2cd1608e4 100644
--- a/src/test/java/io/lettuce/core/commands/HashCommandIntegrationTests.java
+++ b/src/test/java/io/lettuce/core/commands/HashCommandIntegrationTests.java
@@ -560,8 +560,7 @@ public class HashCommandIntegrationTests extends TestSupport {
         assertThat(redis.hexpire(MY_KEY, 0, MY_FIELD)).containsExactly(2L);
         assertThat(redis.hset(MY_KEY, MY_FIELD, MY_VALUE)).isTrue();
         assertThat(redis.hexpire(MY_KEY, 1, MY_FIELD, MY_SECOND_FIELD)).containsExactly(1L, -2L);
-        assertThat(redis.hexpire("invalidKey", 1, MY_FIELD)).isEmpty();
-
+        assertThat(redis.hexpire("invalidKey", 1, MY_FIELD)).containsExactly(-2L);
         await().until(() -> redis.hget(MY_KEY, MY_FIELD) == null);
     }
 
@@ -573,7 +572,6 @@ public class HashCommandIntegrationTests extends TestSupport {
         assertThat(redis.hexpire(MY_KEY, Duration.ofSeconds(1), ExpireArgs.Builder.xx(), MY_FIELD)).containsExactly(1L);
         assertThat(redis.hexpire(MY_KEY, Duration.ofSeconds(10), ExpireArgs.Builder.gt(), MY_FIELD)).containsExactly(1L);
         assertThat(redis.hexpire(MY_KEY, Duration.ofSeconds(1), ExpireArgs.Builder.lt(), MY_FIELD)).containsExactly(1L);
-
         await().until(() -> redis.hget(MY_KEY, MY_FIELD) == null);
     }
 
@@ -584,8 +582,7 @@ public class HashCommandIntegrationTests extends TestSupport {
         assertThat(redis.hexpireat(MY_KEY, Instant.now().minusSeconds(1), MY_FIELD)).containsExactly(2L);
         assertThat(redis.hset(MY_KEY, MY_FIELD, MY_VALUE)).isTrue();
         assertThat(redis.hexpireat(MY_KEY, Instant.now().plusSeconds(1), MY_FIELD)).containsExactly(1L);
-        assertThat(redis.hexpireat("invalidKey", Instant.now().plusSeconds(1), MY_FIELD)).isEmpty();
-
+        assertThat(redis.hexpireat("invalidKey", Instant.now().plusSeconds(1), MY_FIELD)).containsExactly(-2L);
         await().until(() -> redis.hget(MY_KEY, MY_FIELD) == null);
     }
 
@@ -609,8 +606,7 @@ public class HashCommandIntegrationTests extends TestSupport {
     @Test
     @EnabledOnCommand("HPERSIST")
     void hpersist() {
-        assertThat(redis.hpersist(MY_KEY, MY_FIELD)).isEmpty();
-
+        assertThat(redis.hpersist(MY_KEY, MY_FIELD)).containsExactly(-2L);
         assertThat(redis.hset(MY_KEY, MY_FIELD, MY_VALUE)).isTrue();
         assertThat(redis.hpersist(MY_KEY, MY_FIELD)).containsExactly(-1L);
 
diff --git a/src/test/java/io/lettuce/core/commands/ServerCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/ServerCommandIntegrationTests.java
index b9be862e7..a64414edf 100644
--- a/src/test/java/io/lettuce/core/commands/ServerCommandIntegrationTests.java
+++ b/src/test/java/io/lettuce/core/commands/ServerCommandIntegrationTests.java
@@ -33,6 +33,8 @@ import java.util.regex.Pattern;
 
 import javax.inject.Inject;
 
+import io.lettuce.core.cluster.ClusterReadOnlyCommands;
+import io.lettuce.core.protocol.ProtocolKeyword;
 import io.lettuce.test.condition.RedisConditions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Disabled;
@@ -592,6 +594,13 @@ public class ServerCommandIntegrationTests extends TestSupport {
         assertThat(redis.clientInfo().contains("lib-name=lettuce")).isTrue();
     }
 
+    @Test
+    void testReadOnlyCommands() {
+        for (ProtocolKeyword readOnlyCommand : ClusterReadOnlyCommands.getReadOnlyCommands()) {
+            assertThat(isCommandReadOnly(readOnlyCommand.name())).isTrue();
+        }
+    }
+
     private boolean noSaveInProgress() {
 
         String info = redis.info();
@@ -599,4 +608,19 @@ public class ServerCommandIntegrationTests extends TestSupport {
         return !info.contains("aof_rewrite_in_progress:1") && !info.contains("rdb_bgsave_in_progress:1");
     }
 
+    private boolean isCommandReadOnly(String commandName) {
+        List<Object> commandInfo = redis.commandInfo(commandName);
+        if (commandInfo == null || commandInfo.isEmpty()) {
+            throw new IllegalArgumentException("Command not found: " + commandName);
+        }
+
+        List<CommandDetail> details = CommandDetailParser.parse(commandInfo);
+        if (details.isEmpty()) {
+            throw new IllegalArgumentException("Command details could not be parsed: " + commandName);
+        }
+
+        CommandDetail detail = details.get(0);
+        return !detail.getFlags().contains(CommandDetail.Flag.WRITE);
+    }
+
 }