Repository: redis/lettuce
Pull request #3167 (2025-02-07 13:43:06)

Include command type in the timeout message
It is very frustrating to get a stacktrace which does not indicate which command has failed when using async/reactive clients.

I did not create a feature request first, as this is a straightforward change.


Patch:
diff --git a/.github/wordlist.txt b/.github/wordlist.txt
index ad57186af..b4dbd628a 100644
--- a/.github/wordlist.txt
+++ b/.github/wordlist.txt
@@ -272,4 +272,9 @@ bom
 ubuntu
 behaviour
 databind
-jackson
\ No newline at end of file
+jackson
+ACR
+AMR
+Entra
+authx
+entraid
\ No newline at end of file
diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml
index 8010acf33..f71826201 100644
--- a/.github/workflows/benchmarks.yml
+++ b/.github/workflows/benchmarks.yml
@@ -41,25 +41,24 @@ jobs:
       - name: Install missing dependencies to container
         run: |
           sudo apt update
-          sudo apt install -y stunnel make git gcc
-      - name: Maven offline
-        run: |
-          mvn -q dependency:go-offline
-      - name: Clean environment
+      - name: Set up Docker Compose environment
         run: |
-          make cleanup
+          mkdir -m 777 $REDIS_ENV_WORK_DIR
+          make start
         env:
-          JVM_OPTS: -Xmx3200m
-          TERM: dumb
-      - name: Start servers
+          REDIS_ENV_WORK_DIR: ${{ github.workspace }}/work
+      - name: Maven offline
         run: |
-          make start
+          mvn -q dependency:go-offline
       - name: Run benchmarks
         run: |
           mvn -Pjmh clean test
         env:
           JVM_OPTS: -Xmx3200m
           TERM: dumb
+      - name: Tear down Docker Compose environment
+        run: |
+          docker compose $COMPOSE_ENV_FILES -f src/test/resources/docker-env/docker-compose.yml down
       # Download previous benchmark result from cache (if exists)
       - name: Download previous benchmark data
         uses: actions/cache@v4
diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index a065c2321..289a2b568 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -24,26 +24,13 @@ jobs:
       fail-fast: false
       matrix:
         redis_version:
-          - "unstable"
           - "8.0"
           - "7.4"
           - "7.2"
+    env:
+      REDIS_ENV_WORK_DIR: ${{ github.workspace }}/work
 
     steps:
-      - name: Test Redis Server Version
-        id: map-tags
-        run: |
-          # Map requested version to github or  tag
-          case "${{ matrix.redis_version }}" in
-            "unstable") redis_branch="unstable" stack_version="8.0-M04-pre" ;;
-            "8.0") redis_branch="8.0" stack_version="8.0-M04-pre" ;;
-            "7.4") redis_branch="7.4" stack_version="rs-7.4.0-v2" ;;
-            "7.2") redis_branch="7.2" stack_version="rs-7.2.0-v14" ;;
-            *) echo "Unsupported version: ${{ matrix.redis_version }}" && exit 1 ;;
-          esac
-          # Save them as outputs for later use
-          echo "redis_branch=$redis_branch" >> $GITHUB_OUTPUT
-          echo "redis_stack_version=$stack_version" >> $GITHUB_OUTPUT
       - name: Checkout project
         uses: actions/checkout@v4
       - name: Set Java up in the runner
@@ -59,24 +46,26 @@ jobs:
       - name: Install missing dependencies to container
         run: |
           sudo apt update
-          sudo apt install -y stunnel make git gcc
+      - name: Set up Docker Compose environment
+        run: |
+          mkdir -m 777 $REDIS_ENV_WORK_DIR
+          make start version=${{ matrix.redis_version }}
       - name: Maven offline
         run: |
           mvn -q dependency:go-offline
-      - name: Clean environment
-        run: |
-          make cleanup
-        env:
-          JVM_OPTS: -Xmx3200m
-          TERM: dumb
+        continue-on-error: true
       - name: Run tests
         run: |
+          export TEST_WORK_FOLDER=$REDIS_ENV_WORK_DIR
+          echo $TEST_WORK_FOLDER
+          ls -la $TEST_WORK_FOLDER
           make test-coverage
         env:
-          REDIS: ${{ steps.map-tags.outputs.redis_branch }}
-          REDIS_STACK_VERSION: ${{ steps.map-tags.outputs.redis_stack_version }}
           JVM_OPTS: -Xmx3200m
           TERM: dumb
+      - name: Tear down Docker Compose environment
+        run: |
+          docker compose $COMPOSE_ENV_FILES -f src/test/resources/docker-env/docker-compose.yml down
       - name: Upload coverage reports to Codecov
         uses: codecov/codecov-action@v4
         with:
diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml
index c7f091def..16605d6b0 100644
--- a/.github/workflows/stale-issues.yml
+++ b/.github/workflows/stale-issues.yml
@@ -12,15 +12,15 @@ jobs:
 
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/stale@v3
+      - uses: actions/stale@v9
         with:
           repo-token: ${{ secrets.GITHUB_TOKEN }}
-          stale-issue-message: 'If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 30 days this issue will be closed.'
+          stale-issue-message: 'If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 2 weeks this issue will be closed.'
           stale-pr-message: 'Closing due to lack of requested feedback. If you would like us to look at this issue, please provide the requested information and we will re-open the issue.'
-          days-before-stale: 365
-          days-before-close: 30
+          days-before-stale: 30
+          days-before-close: 14
           stale-issue-label: "status: feedback-reminder"
           stale-pr-label: "status: feedback-reminder"
-          operations-per-run: 10
-          remove-stale-when-updated: false
+          operations-per-run: 30
+          remove-stale-when-updated: true
           only-labels: "status: waiting-for-feedback"
diff --git a/.gitignore b/.gitignore
index 76cfba0e0..106e03552 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,5 @@ work/
 dependency-reduced-pom.xml
 .idea
 .flattened-pom.xml
+*.java-version
+*.DS_Store
\ No newline at end of file
diff --git a/Makefile b/Makefile
index f288f218d..6df7a01fe 100644
--- a/Makefile
+++ b/Makefile
@@ -1,459 +1,47 @@
 SHELL := /bin/bash
 PATH := ./work/redis-git/src:${PATH}
 ROOT_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
-STUNNEL_BIN := $(shell which stunnel)
-BREW_BIN := $(shell which brew)
-YUM_BIN := $(shell which yum)
-APT_BIN := $(shell which apt-get)
 PROFILE ?= ci
-REDIS ?= unstable
-
-define REDIS_CLUSTER_CONFIG1
-c2043458aa5646cee429fdd5e3c18220dddf2ce5 127.0.0.1:7380 master - 1434887920102 1434887920002 0 connected 12000-16383
-27f88788f03a86296b7d860152f4ae24ee59c8c9 127.0.0.1:7379 myself,master - 0 0 1 connected 0-11999
-2c07344ffa94ede5ea57a2367f190af6144c1adb 127.0.0.1:7382 slave c2043458aa5646cee429fdd5e3c18220dddf2ce5 1434887920102 1434887920002 2 connected
-1c541b6daf98719769e6aacf338a7d81f108a180 127.0.0.1:7381 slave 27f88788f03a86296b7d860152f4ae24ee59c8c9 1434887920102 1434887920002 3 connected
-vars currentEpoch 3 lastVoteEpoch 0
-endef
-
-define REDIS_CLUSTER_CONFIG2
-2c07344ffa94ede5ea57a2367f190af6144c1adb 127.0.0.1:7382 slave c2043458aa5646cee429fdd5e3c18220dddf2ce5 1434887920102 1434887920002 2 connected
-27f88788f03a86296b7d860152f4ae24ee59c8c9 127.0.0.1:7379 master - 1434887920102 1434887920002 1 connected 0-11999
-1c541b6daf98719769e6aacf338a7d81f108a180 127.0.0.1:7381 slave 27f88788f03a86296b7d860152f4ae24ee59c8c9 1434887920102 1434887920002 3 connected
-c2043458aa5646cee429fdd5e3c18220dddf2ce5 127.0.0.1:7380 myself,master - 0 0 0 connected 12000-16383
-vars currentEpoch 3 lastVoteEpoch 0
-endef
-
-define REDIS_CLUSTER_CONFIG3
-1c541b6daf98719769e6aacf338a7d81f108a180 127.0.0.1:7381 myself,slave 27f88788f03a86296b7d860152f4ae24ee59c8c9 0 0 3 connected
-2c07344ffa94ede5ea57a2367f190af6144c1adb 127.0.0.1:7382 slave c2043458aa5646cee429fdd5e3c18220dddf2ce5 1434887920102 1434887920002 2 connected
-c2043458aa5646cee429fdd5e3c18220dddf2ce5 127.0.0.1:7380 master - 1434887920102 1434887920002 0 connected 12000-16383
-27f88788f03a86296b7d860152f4ae24ee59c8c9 127.0.0.1:7379 master - 1434887920102 1434887920002 1 connected 0-11999
-vars currentEpoch 3 lastVoteEpoch 0
-endef
-
-define REDIS_CLUSTER_CONFIG4
-c2043458aa5646cee429fdd5e3c18220dddf2ce5 127.0.0.1:7380 master - 0 1434887920102 0 connected 12000-16383
-1c541b6daf98719769e6aacf338a7d81f108a180 127.0.0.1:7381 slave 27f88788f03a86296b7d860152f4ae24ee59c8c9 0 1434887920102 3 connected
-2c07344ffa94ede5ea57a2367f190af6144c1adb 127.0.0.1:7382 myself,slave c2043458aa5646cee429fdd5e3c18220dddf2ce5 0 0 2 connected
-27f88788f03a86296b7d860152f4ae24ee59c8c9 127.0.0.1:7379 master - 0 1434887920102 1 connected 0-11999
-vars currentEpoch 3 lastVoteEpoch 0
-endef
-
-define REDIS_CLUSTER_CONFIG8
-c2043458aa5646cee429fdd5e3c18220dddf2ce5 127.0.0.1:7580 master - 1434887920102 1434887920002 0 connected 10001-16383
-27f88788f03a86296b7d860152f4ae24ee59c8c9 127.0.0.1:7579 myself,master - 0 0 1 connected 0-10000
-2c07344ffa94ede5ea57a2367f190af6144c1adb 127.0.0.1:7582 slave c2043458aa5646cee429fdd5e3c18220dddf2ce5 1434887920102 1434887920002 2 connected
-1c541b6daf98719769e6aacf338a7d81f108a180 127.0.0.1:7581 slave 27f88788f03a86296b7d860152f4ae24ee59c8c9 1434887920102 1434887920002 3 connected
-vars currentEpoch 3 lastVoteEpoch 0
-endef
-
-define REDIS_CLUSTER_CONFIG_SSL_1
-cf2354ef19ee813a962350b51438314aebce1fe2 127.0.0.1:7479@17479 myself,master - 0 1578163609000 0 connected 0-10000
-cac8e053dd6f85fab470be57d29dcbac2a4b85c4 127.0.0.1:7480@17480 slave cf2354ef19ee813a962350b51438314aebce1fe2 0 1578163609301 1 connected
-6554e5b1b158dccd4b1d9ca294a3e46a2d3e556d 127.0.0.1:7481@17481 master - 0 1578163609301 2 connected 10001-16383
-vars currentEpoch 2 lastVoteEpoch 0
-endef
-
-define REDIS_CLUSTER_CONFIG_SSL_2
-cf2354ef19ee813a962350b51438314aebce1fe2 127.0.0.1:7479@17479 master - 0 1578163609245 0 connected  0-10000
-cac8e053dd6f85fab470be57d29dcbac2a4b85c4 127.0.0.1:7480@17480 myself,slave cf2354ef19ee813a962350b51438314aebce1fe2 0 1578163609000 1 connected
-6554e5b1b158dccd4b1d9ca294a3e46a2d3e556d 127.0.0.1:7481@17481 master - 0 1578163609245 2 connected 10001-16383
-vars currentEpoch 2 lastVoteEpoch 0
-endef
-
-define REDIS_CLUSTER_CONFIG_SSL_3
-cac8e053dd6f85fab470be57d29dcbac2a4b85c4 127.0.0.1:7480@17480 slave cf2354ef19ee813a962350b51438314aebce1fe2 0 1578163609279 1 connected
-cf2354ef19ee813a962350b51438314aebce1fe2 127.0.0.1:7479@17479 master - 0 1578163609279 0 connected 0-10000
-6554e5b1b158dccd4b1d9ca294a3e46a2d3e556d 127.0.0.1:7481@17481 myself,master - 0 1578163609000 2 connected 10001-16383
-vars currentEpoch 2 lastVoteEpoch 0
-endef
-
-
-#######
-# Redis
-#######
-.PRECIOUS: work/redis-%.conf
-
-# Sentinel monitored slave
-work/redis-6483.conf:
-	@mkdir -p $(@D)
-
-	@echo port 6483 >> $@
-	@echo daemonize yes >> $@
-	@echo pidfile $(shell pwd)/work/redis-6483.pid >> $@
-	@echo logfile $(shell pwd)/work/redis-6483.log >> $@
-	@echo save \"\" >> $@
-	@echo appendonly no >> $@
-	@echo client-output-buffer-limit pubsub 256k 128k 5 >> $@
-	@echo unixsocket $(ROOT_DIR)/work/socket-6483 >> $@
-	@echo unixsocketperm 777 >> $@
-	@echo enable-debug-command yes >> $@
-ifeq ($(REDIS),unstable)
-	@echo slaveof localhost 6482 >> $@
-	@echo replica-announce-ip localhost >> $@
-else
-	@echo slaveof 127.0.0.1 6482 >> $@
-endif
-
-
-work/redis-%.conf:
-	@mkdir -p $(@D)
-
-	@echo port $* >> $@
-	@echo daemonize yes >> $@
-	@echo pidfile $(shell pwd)/work/redis-$*.pid >> $@
-	@echo logfile $(shell pwd)/work/redis-$*.log >> $@
-	@echo save \"\" >> $@
-	@echo appendonly no >> $@
-	@echo client-output-buffer-limit pubsub 256k 128k 5 >> $@
-	@echo unixsocket $(ROOT_DIR)/work/socket-$* >> $@
-	@echo unixsocketperm 777 >> $@
-	@echo enable-debug-command yes >> $@
-ifeq ($(REDIS),unstable)
-	@echo replica-announce-ip localhost >> $@
-endif
-
-work/redis-%.pid: work/redis-%.conf work/redis-git/src/redis-server
-	work/redis-git/src/redis-server $<
-
-redis-start: work/redis-6479.pid work/redis-6480.pid work/redis-6481.pid work/redis-6482.pid work/redis-6483.pid work/redis-6484.pid
-
-##########
-# Sentinel
-##########
-.PRECIOUS: work/sentinel-%.conf
-
-work/sentinel-%.conf:
-	@mkdir -p $(@D)
-
-	@echo port $* >> $@
-	@echo daemonize yes >> $@
-	@echo pidfile $(shell pwd)/work/redis-sentinel-$*.pid >> $@
-	@echo logfile $(shell pwd)/work/redis-sentinel-$*.log >> $@
-
-ifeq ($(REDIS),unstable)
-	@echo sentinel monitor mymaster localhost 6482 1 >> $@
-	@echo sentinel announce-hostnames yes >> $@
-	@echo sentinel resolve-hostnames yes >> $@
-	@echo sentinel announce-ip localhost >> $@
-else
-	@echo sentinel monitor mymaster 127.0.0.1 6482 1 >> $@
-endif
-	@echo sentinel down-after-milliseconds mymaster 200 >> $@
-	@echo sentinel failover-timeout mymaster 200 >> $@
-	@echo sentinel parallel-syncs mymaster 1 >> $@
-	@echo unixsocket $(ROOT_DIR)/work/socket-$* >> $@
-	@echo unixsocketperm 777 >> $@
-
-work/sentinel-26381.conf:
-	@mkdir -p $(@D)
-
-	@echo port 26381 >> $@
-	@echo daemonize yes >> $@
-	@echo pidfile $(shell pwd)/work/redis-sentinel-26381.pid >> $@
-	@echo logfile $(shell pwd)/work/redis-sentinel-26381.log >> $@
-
-ifeq ($(REDIS),unstable)
-	@echo sentinel monitor mymaster localhost 6484 1 >> $@
-	@echo sentinel announce-hostnames yes >> $@
-	@echo sentinel resolve-hostnames yes >> $@
-else
-	@echo sentinel monitor mymaster 127.0.0.1 6484 1 >> $@
-endif
-	@echo sentinel down-after-milliseconds mymaster 200 >> $@
-	@echo sentinel failover-timeout mymaster 200 >> $@
-	@echo sentinel parallel-syncs mymaster 1 >> $@
-	@echo unixsocket $(ROOT_DIR)/work/socket-$* >> $@
-	@echo unixsocketperm 777 >> $@
-	@echo requirepass foobared >> $@
-
-work/sentinel-%.pid: work/sentinel-%.conf work/redis-git/src/redis-server
-	work/redis-git/src/redis-server $< --sentinel
-	sleep 0.5
-
-sentinel-start: work/sentinel-26379.pid work/sentinel-26380.pid work/sentinel-26381.pid
-
-##########
-# Cluster
-##########
-.PRECIOUS: work/cluster-node-%.conf
-
-work/cluster-node-7385.conf:
-	@mkdir -p $(@D)
-
-	@echo port 7385 >> $@
-	@echo daemonize yes >> $@
-	@echo pidfile $(shell pwd)/work/cluster-node-7385.pid >> $@
-	@echo logfile $(shell pwd)/work/cluster-node-7385.log >> $@
-	@echo save \"\" >> $@
-	@echo appendonly no >> $@
-	@echo unixsocket $(ROOT_DIR)/work/socket-7385 >> $@
-	@echo cluster-enabled yes >> $@
-	@echo cluster-node-timeout 150 >> $@
-	@echo cluster-config-file $(shell pwd)/work/cluster-node-config-7385.conf >> $@
-	@echo requirepass foobared >> $@
-
-
-work/cluster-node-7479.conf:
-	@mkdir -p $(@D)
-
-	@echo port 7479 >> $@
-	@echo daemonize yes >> $@
-	@echo pidfile $(shell pwd)/work/cluster-node-7479.pid >> $@
-	@echo logfile $(shell pwd)/work/cluster-node-7479.log >> $@
-	@echo save \"\" >> $@
-	@echo appendonly no >> $@
-	@echo cluster-enabled yes >> $@
-	@echo cluster-node-timeout 150 >> $@
-	@echo cluster-config-file $(shell pwd)/work/cluster-node-config-7479.conf >> $@
-	@echo cluster-announce-port 7442 >> $@
-	@echo requirepass foobared >> $@
-
-
-work/cluster-node-7480.conf:
-	@mkdir -p $(@D)
-
-	@echo port 7480 >> $@
-	@echo daemonize yes >> $@
-	@echo pidfile $(shell pwd)/work/cluster-node-7480.pid >> $@
-	@echo logfile $(shell pwd)/work/cluster-node-7480.log >> $@
-	@echo save \"\" >> $@
-	@echo appendonly no >> $@
-	@echo cluster-enabled yes >> $@
-	@echo cluster-node-timeout 150 >> $@
-	@echo cluster-config-file $(shell pwd)/work/cluster-node-config-7480.conf >> $@
-	@echo cluster-announce-port 7444 >> $@
-	@echo requirepass foobared >> $@
-
-
-work/cluster-node-7481.conf:
-	@mkdir -p $(@D)
-
-	@echo port 7481 >> $@
-	@echo daemonize yes >> $@
-	@echo pidfile $(shell pwd)/work/cluster-node-7481.pid >> $@
-	@echo logfile $(shell pwd)/work/cluster-node-7481.log >> $@
-	@echo save \"\" >> $@
-	@echo appendonly no >> $@
-	@echo cluster-enabled yes >> $@
-	@echo cluster-node-timeout 150 >> $@
-	@echo cluster-config-file $(shell pwd)/work/cluster-node-config-7481.conf >> $@
-	@echo cluster-announce-port 7445 >> $@
-	@echo requirepass foobared >> $@
-
-
-work/cluster-node-%.conf:
-	@mkdir -p $(@D)
-
-	@echo port $* >> $@
-	@echo daemonize yes >> $@
-	@echo pidfile $(shell pwd)/work/cluster-node-$*.pid >> $@
-	@echo logfile $(shell pwd)/work/cluster-node-$*.log >> $@
-	@echo save \"\" >> $@
-	@echo appendonly no >> $@
-	@echo client-output-buffer-limit pubsub 256k 128k 5 >> $@
-	@echo unixsocket $(ROOT_DIR)/work/socket-$* >> $@
-	@echo cluster-enabled yes >> $@
-	@echo cluster-node-timeout 150 >> $@
-	@echo cluster-config-file $(shell pwd)/work/cluster-node-config-$*.conf >> $@
-
-work/cluster-node-%.pid: work/cluster-node-%.conf work/redis-git/src/redis-server
-	work/redis-git/src/redis-server $< || true
-
-cluster-start: work/cluster-node-7379.pid work/cluster-node-7380.pid work/cluster-node-7381.pid work/cluster-node-7382.pid work/cluster-node-7383.pid work/cluster-node-7384.pid work/cluster-node-7385.pid work/cluster-node-7479.pid work/cluster-node-7480.pid work/cluster-node-7481.pid work/cluster-node-7582.pid
-
-##########
-# stunnel
-##########
-
-work/stunnel.conf:
-	@mkdir -p $(@D)
-
-	@echo cert=$(ROOT_DIR)/work/ca/certs/localhost.cert.pem >> $@
-	@echo key=$(ROOT_DIR)/work/ca/private/localhost.decrypted.key.pem >> $@
-	@echo capath=$(ROOT_DIR)/work/ca/certs/ca.cert.pem >> $@
-	@echo cafile=$(ROOT_DIR)/work/ca/certs/ca.cert.pem >> $@
-	@echo delay=yes >> $@
-	@echo pid=$(ROOT_DIR)/work/stunnel.pid >> $@
-	@echo foreground = no >> $@
-
-	@echo [stunnel] >> $@
-	@echo accept = 127.0.0.1:6443 >> $@
-	@echo connect = 127.0.0.1:6479 >> $@
-
-	@echo [redis-sni-vritual] >> $@
-	@echo accept = 127.0.0.1:36443 >> $@
-	@echo cert=$(ROOT_DIR)/work/ca/certs/foo-host.cert.pem >> $@
-	@echo key=$(ROOT_DIR)/work/ca/private/foo-host.decrypted.key.pem >> $@
-	@echo connect = unavailable.internal.mydomain.com:6666 >> $@
-
-	@echo [redis-sni1] >> $@
-	@echo sni = redis-sni-vritual:redis-sni1.local >> $@
-	@echo key=$(ROOT_DIR)/work/ca/private/localhost.decrypted.key.pem >> $@
-	@echo cert=$(ROOT_DIR)/work/ca/certs/localhost.cert.pem >> $@
-	@echo connect = localhost:6480 >> $@
-
-	@echo [redis-sni2] >> $@
-	@echo sni = redis-sni-vritual:redis-sni2.local >> $@
-	@echo connect = localhost:6479 >> $@
-	@echo cert=$(ROOT_DIR)/work/ca/certs/foo-host.cert.pem >> $@
-	@echo key=$(ROOT_DIR)/work/ca/private/foo-host.decrypted.key.pem >> $@
-
-	@echo [foo-host] >> $@
-	@echo accept = 127.0.0.1:6444 >> $@
-	@echo connect = 127.0.0.1:6479 >> $@
-	@echo cert=$(ROOT_DIR)/work/ca/certs/foo-host.cert.pem >> $@
-	@echo key=$(ROOT_DIR)/work/ca/private/foo-host.decrypted.key.pem >> $@
-
-	@echo [ssl-cluster-node-1] >> $@
-	@echo accept = 127.0.0.1:7442 >> $@
-	@echo connect = 127.0.0.1:7479 >> $@
-
-	@echo [ssl-cluster-node-2] >> $@
-	@echo accept = 127.0.0.1:7444 >> $@
-	@echo connect = 127.0.0.1:7480 >> $@
-
-	@echo [ssl-cluster-node-3] >> $@
-	@echo accept = 127.0.0.1:7445 >> $@
-	@echo connect = 127.0.0.1:7481 >> $@
-
-	@echo [ssl-sentinel-1] >> $@
-	@echo accept = 127.0.0.1:26822 >> $@
-	@echo connect = 127.0.0.1:26379 >> $@
-
-	@echo [ssl-sentinel-2] >> $@
-	@echo accept = 127.0.0.1:26823 >> $@
-	@echo connect = 127.0.0.1:26380 >> $@
-
-	@echo [ssl-sentinel-3] >> $@
-	@echo accept = 127.0.0.1:26824 >> $@
-	@echo connect = 127.0.0.1:26381 >> $@
-
-	@echo [ssl-sentinel-master] >> $@
-	@echo accept = 127.0.0.1:6925 >> $@
-	@echo connect = 127.0.0.1:6482 >> $@
-
-	@echo [ssl-sentinel-slave] >> $@
-	@echo accept = 127.0.0.1:6926 >> $@
-	@echo connect = 127.0.0.1:6482 >> $@
-
-	@echo [stunnel-client-cert] >> $@
-	@echo accept = 127.0.0.1:6445 >> $@
-	@echo connect = 127.0.0.1:6479 >> $@
-	@echo verify=2 >> $@
-
-	@echo [stunnel-master-slave-node-1] >> $@
-	@echo accept = 127.0.0.1:8443 >> $@
-	@echo connect = 127.0.0.1:6482 >> $@
-
-	@echo [stunnel-master-slave-node-2] >> $@
-	@echo accept = 127.0.0.1:8444 >> $@
-	@echo connect = 127.0.0.1:6483 >> $@
-
-work/stunnel.pid: work/stunnel.conf ssl-keys
-	which stunnel4 >/dev/null 2>&1 && stunnel4 $(ROOT_DIR)/work/stunnel.conf || stunnel $(ROOT_DIR)/work/stunnel.conf
-
-stunnel-start: work/stunnel.pid
-
-export REDIS_CLUSTER_CONFIG1
-export REDIS_CLUSTER_CONFIG2
-export REDIS_CLUSTER_CONFIG3
-export REDIS_CLUSTER_CONFIG4
-export REDIS_CLUSTER_CONFIG8
-export REDIS_CLUSTER_CONFIG_SSL_1
-export REDIS_CLUSTER_CONFIG_SSL_2
-export REDIS_CLUSTER_CONFIG_SSL_3
-
-start: cleanup
-	@echo "$$REDIS_CLUSTER_CONFIG1" > work/cluster-node-config-7379.conf
-	@echo "$$REDIS_CLUSTER_CONFIG2" > work/cluster-node-config-7380.conf
-	@echo "$$REDIS_CLUSTER_CONFIG3" > work/cluster-node-config-7381.conf
-	@echo "$$REDIS_CLUSTER_CONFIG4" > work/cluster-node-config-7382.conf
-	@echo "$$REDIS_CLUSTER_CONFIG8" > work/cluster-node-config-7582.conf
-	@echo "$$REDIS_CLUSTER_CONFIG_SSL_1" > work/cluster-node-config-7479.conf
-	@echo "$$REDIS_CLUSTER_CONFIG_SSL_2" > work/cluster-node-config-7480.conf
-	@echo "$$REDIS_CLUSTER_CONFIG_SSL_3" > work/cluster-node-config-7481.conf
-	$(MAKE) redis-start
-	$(MAKE) sentinel-start
-	$(MAKE) cluster-start
-	$(MAKE) stunnel-start
-
-
-cleanup: stop
-	@mkdir -p work
-	rm -f work/cluster-node*.conf 2>/dev/null
-	rm -f work/*.rdb work/*.aof work/*.conf work/*.log 2>/dev/null
-	rm -f *.aof
-	rm -f *.rdb
-	rm -f work/socket-*
-
-##########
-# SSL Keys
-#  - remove Java keystore as becomes stale
-##########
-work/keystore.jks:
-	@mkdir -p $(@D)
-	- rm -f work/*.jks
-	- rm -Rf work/ca
-	src/test/bash/create_certificates.sh
-
-ssl-keys: work/keystore.jks
-
-stop:
-	pkill stunnel || true
-	pkill redis-server && sleep 1 || true
-	pkill redis-sentinel && sleep 1 || true
-
-test-coverage: start
-	mvn -DskipITs=false clean compile verify jacoco:report -P$(PROFILE)
-	$(MAKE) stop
-
-test: start
+SUPPORTED_TEST_ENV_VERSIONS := 8.0 7.4 7.2
+DEFAULT_TEST_ENV_VERSION := 8.0
+REDIS_ENV_WORK_DIR := $(or ${REDIS_ENV_WORK_DIR},$(ROOT_DIR)/work)
+
+start:
+	@if [ -z "$(version)" ]; then \
+		version=$(arg); \
+		if [ -z "$$version" ]; then \
+			version="$(DEFAULT_TEST_ENV_VERSION)"; \
+		fi; \
+	fi; \
+	if ! echo "$(SUPPORTED_TEST_ENV_VERSIONS)" | grep -qw "$$version"; then \
+		echo "Error: Invalid version '$$version'. Supported versions are: $(SUPPORTED_TEST_ENV_VERSIONS)."; \
+		exit 1; \
+	fi; \
+	echo "Version: $(version)"; \
+    default_env_file="src/test/resources/docker-env/.env"; \
+	custom_env_file="src/test/resources/docker-env/.env.v$$version"; \
+	env_files="--env-file $$default_env_file"; \
+	if [ -f "$$custom_env_file" ]; then \
+		env_files="$$env_files --env-file $$custom_env_file"; \
+	fi; \
+	echo "Environment work directory: $(REDIS_ENV_WORK_DIR)"; \
+	rm -rf "$(REDIS_ENV_WORK_DIR)"; \
+	mkdir -p "$(REDIS_ENV_WORK_DIR)"; \
+	docker compose $$env_files -f src/test/resources/docker-env/docker-compose.yml up -d; \
+	echo "Started test environment with Redis version $$version."
+
+
+test:
 	mvn -DskipITs=false clean compile verify -P$(PROFILE)
-	$(MAKE) stop
-
-prepare: stop
 
-ifndef STUNNEL_BIN
-ifeq ($(shell uname -s),Linux)
-ifdef APT_BIN
-	sudo apt-get install -y stunnel
-else
-
-ifdef YUM_BIN
-	sudo yum install stunnel
-else
-	@@echo "Cannot install stunnel using yum/apt-get"
-	@exit 1
-endif
-
-endif
-
-endif
-
-ifeq ($(shell uname -s),Darwin)
-
-ifndef BREW_BIN
-	@@echo "Cannot install stunnel because missing brew.sh"
-	@exit 1
-endif
-
-	brew install stunnel
-
-endif
-
-endif
+test-coverage:
+	mvn -DskipITs=false clean compile verify jacoco:report -P$(PROFILE)
 
-work/redis-git/src/redis-cli work/redis-git/src/redis-server:
-	[ -d "work/redis-git" ] && cd work/redis-git && git reset --hard || \
-	git clone https://github.com/antirez/redis.git work/redis-git
-	cd work/redis-git && git checkout -q $(REDIS) && git pull origin $(REDIS)
-	$(MAKE) -C work/redis-git clean
-	$(MAKE) -C work/redis-git -j4
+stop:
+	docker compose --env-file src/test/resources/docker-env/.env -f src/test/resources/docker-env/docker-compose.yml down; \
+	rm -rf "$(REDIS_ENV_WORK_DIR)"
 
 clean:
-	rm -Rf work/
 	rm -Rf target/
 
 release:
diff --git a/README.md b/README.md
index 21860f878..374cc9893 100644
--- a/README.md
+++ b/README.md
@@ -155,22 +155,20 @@ Building
 -----------
 
 Lettuce is built with Apache Maven. The tests require multiple running Redis instances for different test cases which
-are configured using a ```Makefile```. Tests run by default against Redis `unstable`.
+are configured using a ```Makefile```. Tests run by default against Redis `latest`.
 
 To build:
 
 ```
 $ git clone https://github.com/redis/lettuce.git
 $ cd lettuce/
-$ make prepare ssl-keys
-$ make test
+$ make start
 ```
 
-* Initial environment setup (clone and build `redis`): ```make prepare```
-* Setup SSL Keys: ```make ssl-keys```
 * Run the build: ```make test```
 * Start Redis (manually): ```make start```
 * Stop Redis (manually): ```make stop```
+* Clean up: ```make clean```
 
 Bugs and Feedback
 -----------
diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md
index d37abd238..74f154dee 100644
--- a/docs/advanced-usage.md
+++ b/docs/advanced-usage.md
@@ -1133,7 +1133,7 @@ events is:
     `ConnectionDeactivatedEvent`
 
 5.  Since 5.3: Reconnect failed: A reconnect attempt failed. Contains
-    the reconnect failure and and the retry counter. Event type:
+    the reconnect failure and the retry counter. Event type:
     `ReconnectFailedEvent`
 
 #### Metrics events
@@ -2515,7 +2515,7 @@ Commands that are issued as long as the failure persists are buffered.
 
 #### Exceptions to *at-least-once*
 
-Lettuce does not loose commands while sending them. A command execution
+Lettuce does not lose commands while sending them. A command execution
 can, however, fail for the same reasons as a normal method call can on
 the JVM:
 
@@ -2558,7 +2558,7 @@ from the response queue, and the connection stays usable.
 
 In general, when `Errors` occur while operating on a connection, you
 should close the connection and use a new one. Connections, that
-experienced such severe failures get into a unrecoverable state, and no
+experienced such severe failures get into an unrecoverable state, and no
 further response processing is possible.
 
 Executing commands more than once
diff --git a/docs/user-guide/connecting-redis.md b/docs/user-guide/connecting-redis.md
index dac3102b8..9e2d2eed5 100644
--- a/docs/user-guide/connecting-redis.md
+++ b/docs/user-guide/connecting-redis.md
@@ -237,3 +237,162 @@ RedisClient client = RedisClient.create(redisUri);
 client.shutdown();
 ```
 
+## Streaming Credentials Provider
+[Lettuce 6.6.0](https://github.com/redis/lettuce/releases/tag/6.6.0.RELEASE)  extends `RedisCredentialsProvider` to support streaming credentials. 
+It is useful when you need to refresh credentials periodically. Example use cases include: token expiration, rotating credentials, etc.
+Connection configured with `RedisCredentialsProvider` supporting streaming will be re-authenticated automatically when new credentials are emitted and `ReauthenticateBehavior` is set to `ON_NEW_CREDENTIALS`.
+
+### Step 1 - Create a Streaming Credentials Provider
+A simple example of a streaming credentials provider that emits new credentials.
+```java
+public class MyStreamingRedisCredentialsProvider implements RedisCredentialsProvider {
+
+    private final Sinks.Many<RedisCredentials> credentialsSink = Sinks.many().replay().latest();
+
+    @Override
+    public boolean supportsStreaming() {
+        return true;
+    }
+
+    @Override
+    public Mono<RedisCredentials> resolveCredentials() {
+        return credentialsSink.asFlux().next();
+    }
+    
+    @Override
+    // Provide a continuous stream of credentials
+    public Flux<RedisCredentials> credentials() {
+        return credentialsSink.asFlux().onBackpressureLatest(); 
+    }
+
+    public void close() {
+        credentialsSink.tryEmitComplete();
+    }
+    // Emit new credentials when needed
+    public void emitCredentials(String username, char[] password) {
+        credentialsSink.tryEmitNext(new StaticRedisCredentials(username, password));
+    }
+
+}
+```
+###  Step 2 - Create a RedisURI with streaming credentials provider
+
+```java
+    // Create a streaming credentials provider
+    MyStreamingRedisCredentialsProvider streamingCredentialsProvider = new MyStreamingRedisCredentialsProvider();
+    
+    // Emit initial credentials
+    streamingCredentialsProvider.emitCredentials("testuser", "testpass".toCharArray());
+   
+    // Enable automatic re-authentication
+    ClientOptions clientOptions = ClientOptions.builder()
+            // enable automatic re-authentication
+            .reauthenticateBehavior(ClientOptions.ReauthenticateBehavior.ON_NEW_CREDENTIALS)
+            .build();
+    
+    // Create a RedisURI with streaming credentials provider
+    RedisURI redisURI = RedisURI.builder().withHost(HOST).withPort(PORT)
+            .withAuthentication(streamingCredentialsProvider)
+            .build();
+    
+    // RedisClient
+    RedisClient redisClient = RedisClient.create(redisURI);
+    rediscClient.connect().sync().ping();
+    
+    // ...
+    // Emit new credentials when needed
+    streamingCredentialsProvider.emitCredentials("testuser", "password-rotated".toCharArray());
+    
+```
+
+## Microsoft Entra ID Authentication
+
+[Lettuce 6.6.0](https://github.com/redis/lettuce/releases/tag/6.6.0.RELEASE) introduces built-in support for authentication with [Azure Managed Redis](https://azure.microsoft.com/en-us/products/managed-redis) and Azure Cache for Redis using Microsoft Entra ID (formerly Azure Active Directory). It enables seamless integration with Azure's Redis services by fetching authentication tokens and managing the token renewal in the background. 
+Integration is built on top of [redis-authx](https://github.com/redis/jvm-redis-authx-entraid) library, and provides support for:
+
+ - System-assigned managed identity
+ - User-assigned managed identity
+ - Service principal
+
+You can learn more about managed identities in the [Microsoft Entra ID documentation](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview).
+
+### Basic Usage
+
+#### Pre-requisites
+* [register an application and create a service principal](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser) in Azure.
+* Create a Redis cache in Azure and grant your service principal access: [AMR](https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/managed-redis/managed-redis-entra-for-authentication) or [ACR](https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/cache-azure-active-directory-for-authentication) documentation.
+ 
+#### Step 1 - Add the dependencies
+Lettuce requires [redis-authx-entraid](https://github.com/redis/jvm-redis-authx-entraid/) dependency to provide Microsoft Entra ID authentication support. Make sure to include that dependency on your classpath.
+
+If using Maven, add the following dependency to your `pom.xml`:
+
+``` xml
+        <dependency>
+            <groupId>redis.clients.authentication</groupId>
+            <artifactId>redis-authx-entraid</artifactId>
+            <version>0.1.1-beta1</version>
+        </dependency>
+```
+
+
+### Step 2 - Create Entra ID enabled credentials provider
+The lifecycle of the credentials provider is not managed by the Lettuce client. You can create it once and reuse it across multiple clients\connections. When no longer needed, you should close the provider to release resources `TokenBasedRedisCredentialsProvider#close`.
+
+#### Create Microsoft Entra ID enabled credentials provider
+```java
+  // Entra ID enabled credentials provider for Service Principle Identity with Client Secret
+  TokenBasedRedisCredentialsProvider credentialsSP;
+  try ( EntraIDTokenAuthConfigBuilder builder = EntraIDTokenAuthConfigBuilder.builder()) {
+       builder.clientId(CLIENT_ID)
+              .secret(CLIENT_SECRET)
+              .authority(AUTHORITY) // "https://login.microsoftonline.com/{YOUR_TENANT_ID}";
+              .scopes(SCOPES);      // "https://redis.azure.com/.default";
+      credentialsSP = TokenBasedRedisCredentialsProvider.create(builder.build());
+  }
+```
+
+You can test the credentials provider by obtaining a token.
+
+```java
+  // Test Entra ID credentials provider can resolve credentials
+  credentialsSP.resolveCredentials()
+      .doOnNext(c-> System.out.println(c.getUsername()))
+      .block();
+```
+
+### Step 3 - Enable automatic re-authentication
+Microsoft Entra ID tokens have a limited lifetime. Lettuce provides a mechanism to automatically re-authenticate when new credentials are emitted by a `RedisCredentialsProvider`.
+```java
+  // Enable automatic re-authentication
+  ClientOptions clientOptions = ClientOptions.builder()
+        .reauthenticateBehavior(ClientOptions.ReauthenticateBehavior.ON_NEW_CREDENTIALS)
+        .build();
+```
+
+### Step 4 - Connect with Entra ID enabled credentials provider
+
+```java
+  // Use Entra ID credentials provider
+  RedisURI redisURI = RedisURI.builder()
+      .withHost(HOST)
+      .withPort(PORT)
+      .withSsl(true)
+      .withAuthentication(credentialsSP).build();
+
+  // RedisClient
+  RedisClient redisClient = RedisClient.create(redisURI);
+  redisClient.setOptions(clientOptions);
+  
+  try {
+      redisClient.setOptions(clientOptions);
+      // Connect with Entra ID credentials provider
+      try (StatefulRedisConnection<String, String> user1 = redisClient.connect(StringCodec.UTF8)) {
+          System.out.println("Connected to redis as :" + user1.sync().aclWhoami());
+          System.out.println("Db size :" + user1.sync().dbsize());
+      }
+  } finally {
+      redisClient.shutdown();  // Shutdown Redis client and close connections
+      credentialsSP.close(); // Shutdown Entra ID Credentials provider
+  }
+```
diff --git a/pom.xml b/pom.xml
index 068f30e9f..5c796ebab 100644
--- a/pom.xml
+++ b/pom.xml
@@ -64,7 +64,7 @@
         <micrometer.version>1.14.2</micrometer.version>
         <micrometer-tracing.version>1.2.4</micrometer-tracing.version>
         <mockito.version>4.9.0</mockito.version>
-        <netty.version>4.1.115.Final</netty.version>
+        <netty.version>4.1.118.Final</netty.version>
         <openwebbeans.version>2.0.27</openwebbeans.version>
         <reactor.version>3.6.6</reactor.version>
         <rxjava.version>1.3.8</rxjava.version>
diff --git a/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java b/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java
index de095893f..fd1ad55eb 100644
--- a/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java
+++ b/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java
@@ -48,7 +48,6 @@ import io.lettuce.core.protocol.CommandArgs;
 import io.lettuce.core.protocol.CommandType;
 import io.lettuce.core.protocol.ProtocolKeyword;
 import io.lettuce.core.protocol.RedisCommand;
-import reactor.core.publisher.Mono;
 
 import java.time.Duration;
 import java.time.Instant;
@@ -56,6 +55,7 @@ import java.util.Date;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Supplier;
 
 import static io.lettuce.core.ClientOptions.DEFAULT_JSON_PARSER;
 import static io.lettuce.core.protocol.CommandType.EXEC;
@@ -87,7 +87,7 @@ public abstract class AbstractRedisAsyncCommands<K, V> implements RedisAclAsyncC
 
     private final RedisJsonCommandBuilder<K, V> jsonCommandBuilder;
 
-    private final Mono<JsonParser> parser;
+    private final Supplier<JsonParser> parser;
 
     /**
      * Initialize a new instance.
@@ -96,7 +96,8 @@ public abstract class AbstractRedisAsyncCommands<K, V> implements RedisAclAsyncC
      * @param codec the codec for command encoding
      * @param parser the implementation of the {@link JsonParser} to use
      */
-    public AbstractRedisAsyncCommands(StatefulConnection<K, V> connection, RedisCodec<K, V> codec, Mono<JsonParser> parser) {
+    public AbstractRedisAsyncCommands(StatefulConnection<K, V> connection, RedisCodec<K, V> codec,
+            Supplier<JsonParser> parser) {
         this.parser = parser;
         this.connection = connection;
         this.commandBuilder = new RedisCommandBuilder<>(codec);
@@ -3396,7 +3397,7 @@ public abstract class AbstractRedisAsyncCommands<K, V> implements RedisAclAsyncC
 
     @Override
     public JsonParser getJsonParser() {
-        return this.parser.block();
+        return this.parser.get();
     }
 
     private byte[] encodeFunction(String functionCode) {
diff --git a/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java b/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java
index 1e9365821..e1651e562 100644
--- a/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java
+++ b/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java
@@ -97,7 +97,7 @@ public abstract class AbstractRedisReactiveCommands<K, V>
 
     private final RedisJsonCommandBuilder<K, V> jsonCommandBuilder;
 
-    private final Mono<JsonParser> parser;
+    private final Supplier<JsonParser> parser;
 
     private final ClientResources clientResources;
 
@@ -112,7 +112,8 @@ public abstract class AbstractRedisReactiveCommands<K, V>
      * @param codec the codec for command encoding.
      * @param parser the implementation of the {@link JsonParser} to use
      */
-    public AbstractRedisReactiveCommands(StatefulConnection<K, V> connection, RedisCodec<K, V> codec, Mono<JsonParser> parser) {
+    public AbstractRedisReactiveCommands(StatefulConnection<K, V> connection, RedisCodec<K, V> codec,
+            Supplier<JsonParser> parser) {
         this.connection = connection;
         this.parser = parser;
         this.commandBuilder = new RedisCommandBuilder<>(codec);
@@ -149,7 +150,7 @@ public abstract class AbstractRedisReactiveCommands<K, V>
 
     @Override
     public JsonParser getJsonParser() {
-        return parser.block();
+        return parser.get();
     }
 
     @Override
diff --git a/src/main/java/io/lettuce/core/ClientOptions.java b/src/main/java/io/lettuce/core/ClientOptions.java
index 3fd635e4e..5c02c3b18 100644
--- a/src/main/java/io/lettuce/core/ClientOptions.java
+++ b/src/main/java/io/lettuce/core/ClientOptions.java
@@ -25,6 +25,7 @@ import java.nio.charset.StandardCharsets;
 import java.util.Iterator;
 import java.util.ServiceConfigurationError;
 import java.util.ServiceLoader;
+import java.util.function.Supplier;
 
 import io.lettuce.core.api.StatefulConnection;
 import io.lettuce.core.internal.LettuceAssert;
@@ -71,7 +72,7 @@ public class ClientOptions implements Serializable {
 
     public static final SocketOptions DEFAULT_SOCKET_OPTIONS = SocketOptions.create();
 
-    public static final Mono<JsonParser> DEFAULT_JSON_PARSER = Mono.defer(() -> Mono.fromCallable(() -> {
+    public static final Supplier<JsonParser> DEFAULT_JSON_PARSER = () -> {
         try {
             Iterator<JsonParser> services = ServiceLoader.load(JsonParser.class).iterator();
             return services.hasNext() ? services.next() : null;
@@ -79,7 +80,7 @@ public class ClientOptions implements Serializable {
             throw new RedisJsonException("Could not load JsonParser, please consult the guide"
                     + "at https://redis.github.io/lettuce/user-guide/redis-json/", e);
         }
-    }));
+    };
 
     public static final SslOptions DEFAULT_SSL_OPTIONS = SslOptions.create();
 
@@ -111,7 +112,7 @@ public class ClientOptions implements Serializable {
 
     private final Charset scriptCharset;
 
-    private final Mono<JsonParser> jsonParser;
+    private final Supplier<JsonParser> jsonParser;
 
     private final SocketOptions socketOptions;
 
@@ -216,7 +217,7 @@ public class ClientOptions implements Serializable {
 
         private Charset scriptCharset = DEFAULT_SCRIPT_CHARSET;
 
-        private Mono<JsonParser> jsonParser = DEFAULT_JSON_PARSER;
+        private Supplier<JsonParser> jsonParser = DEFAULT_JSON_PARSER;
 
         private SocketOptions socketOptions = DEFAULT_SOCKET_OPTIONS;
 
@@ -429,7 +430,7 @@ public class ClientOptions implements Serializable {
          * @see JsonParser
          * @since 6.5
          */
-        public Builder jsonParser(Mono<JsonParser> parser) {
+        public Builder jsonParser(Supplier<JsonParser> parser) {
 
             LettuceAssert.notNull(parser, "JsonParser must not be null");
             this.jsonParser = parser;
@@ -705,7 +706,7 @@ public class ClientOptions implements Serializable {
      * @return the implementation of the {@link JsonParser} to use.
      * @since 6.5
      */
-    public Mono<JsonParser> getJsonParser() {
+    public Supplier<JsonParser> getJsonParser() {
         return jsonParser;
     }
 
diff --git a/src/main/java/io/lettuce/core/RedisAsyncCommandsImpl.java b/src/main/java/io/lettuce/core/RedisAsyncCommandsImpl.java
index 87bae13e0..4e2075519 100644
--- a/src/main/java/io/lettuce/core/RedisAsyncCommandsImpl.java
+++ b/src/main/java/io/lettuce/core/RedisAsyncCommandsImpl.java
@@ -5,7 +5,7 @@ import io.lettuce.core.api.async.RedisAsyncCommands;
 import io.lettuce.core.cluster.api.async.RedisClusterAsyncCommands;
 import io.lettuce.core.codec.RedisCodec;
 import io.lettuce.core.json.JsonParser;
-import reactor.core.publisher.Mono;
+import java.util.function.Supplier;
 
 /**
  * An asynchronous and thread-safe API for a Redis connection.
@@ -24,7 +24,8 @@ public class RedisAsyncCommandsImpl<K, V> extends AbstractRedisAsyncCommands<K,
      * @param codec the codec for command encoding
      * @param parser the implementation of the {@link JsonParser} to use
      */
-    public RedisAsyncCommandsImpl(StatefulRedisConnection<K, V> connection, RedisCodec<K, V> codec, Mono<JsonParser> parser) {
+    public RedisAsyncCommandsImpl(StatefulRedisConnection<K, V> connection, RedisCodec<K, V> codec,
+            Supplier<JsonParser> parser) {
         super(connection, codec, parser);
     }
 
diff --git a/src/main/java/io/lettuce/core/RedisJsonCommandBuilder.java b/src/main/java/io/lettuce/core/RedisJsonCommandBuilder.java
index ee7e8cf97..42e8e40cf 100644
--- a/src/main/java/io/lettuce/core/RedisJsonCommandBuilder.java
+++ b/src/main/java/io/lettuce/core/RedisJsonCommandBuilder.java
@@ -20,9 +20,9 @@ import io.lettuce.core.output.*;
 import io.lettuce.core.protocol.BaseRedisCommandBuilder;
 import io.lettuce.core.protocol.Command;
 import io.lettuce.core.protocol.CommandArgs;
-import reactor.core.publisher.Mono;
 
 import java.util.List;
+import java.util.function.Supplier;
 
 import static io.lettuce.core.protocol.CommandType.*;
 
@@ -34,9 +34,9 @@ import static io.lettuce.core.protocol.CommandType.*;
  */
 class RedisJsonCommandBuilder<K, V> extends BaseRedisCommandBuilder<K, V> {
 
-    private final Mono<JsonParser> parser;
+    private final Supplier<JsonParser> parser;
 
-    RedisJsonCommandBuilder(RedisCodec<K, V> codec, Mono<JsonParser> theParser) {
+    RedisJsonCommandBuilder(RedisCodec<K, V> codec, Supplier<JsonParser> theParser) {
         super(codec);
         parser = theParser;
     }
@@ -47,6 +47,7 @@ class RedisJsonCommandBuilder<K, V> extends BaseRedisCommandBuilder<K, V> {
         CommandArgs<K, V> args = new CommandArgs<>(codec).addKey(key);
 
         if (jsonPath != null && !jsonPath.isRootPath()) {
+            // OPTIONAL as per API
             args.add(jsonPath.toString());
         }
 
@@ -62,10 +63,7 @@ class RedisJsonCommandBuilder<K, V> extends BaseRedisCommandBuilder<K, V> {
 
         CommandArgs<K, V> args = new CommandArgs<>(codec).addKey(key);
 
-        if (jsonPath != null && !jsonPath.isRootPath()) {
-            args.add(jsonPath.toString());
-        }
-
+        args.add(jsonPath.toString());
         args.add(value.asByteBuffer().array());
 
         if (range != null) {
@@ -81,10 +79,7 @@ class RedisJsonCommandBuilder<K, V> extends BaseRedisCommandBuilder<K, V> {
 
         CommandArgs<K, V> args = new CommandArgs<>(codec).addKey(key);
 
-        if (jsonPath != null && !jsonPath.isRootPath()) {
-            args.add(jsonPath.toString());
-        }
-
+        args.add(jsonPath.toString());
         args.add(index);
 
         for (JsonValue value : values) {
@@ -100,6 +95,7 @@ class RedisJsonCommandBuilder<K, V> extends BaseRedisCommandBuilder<K, V> {
         CommandArgs<K, V> args = new CommandArgs<>(codec).addKey(key);
 
         if (jsonPath != null && !jsonPath.isRootPath()) {
+            // OPTIONAL as per API
             args.add(jsonPath.toString());
         }
         return createCommand(JSON_ARRLEN, (CommandOutput) new ArrayOutput<>(codec), args);
@@ -111,14 +107,17 @@ class RedisJsonCommandBuilder<K, V> extends BaseRedisCommandBuilder<K, V> {
         CommandArgs<K, V> args = new CommandArgs<>(codec).addKey(key);
 
         if (jsonPath != null) {
-            args.add(jsonPath.toString());
-
             if (index != -1) {
+                // OPTIONAL as per API
+                args.add(jsonPath.toString());
                 args.add(index);
+            } else if (!jsonPath.isRootPath()) {
+                // OPTIONAL as per API
+                args.add(jsonPath.toString());
             }
         }
 
-        return createCommand(JSON_ARRPOP, new JsonValueListOutput<>(codec, parser.block()), args);
+        return createCommand(JSON_ARRPOP, new JsonValueListOutput<>(codec, parser.get()), args);
     }
 
     Command<K, V, List<Long>> jsonArrtrim(K key, JsonPath jsonPath, JsonRangeArgs range) {
@@ -126,10 +125,7 @@ class RedisJsonCommandBuilder<K, V> extends BaseRedisCommandBuilder<K, V> {
         notNullKey(key);
 
         CommandArgs<K, V> args = new CommandArgs<>(codec).addKey(key);
-
-        if (jsonPath != null && !jsonPath.isRootPath()) {
-            args.add(jsonPath.toString());
-        }
+        args.add(jsonPath.toString());
 
         if (range != null) {
             range.build(args);
@@ -144,6 +140,7 @@ class RedisJsonCommandBuilder<K, V> extends BaseRedisCommandBuilder<K, V> {
         CommandArgs<K, V> args = new CommandArgs<>(codec).addKey(key);
 
         if (jsonPath != null && !jsonPath.isRootPath()) {
+            // OPTIONAL as per API
             args.add(jsonPath.toString());
         }
 
@@ -156,10 +153,12 @@ class RedisJsonCommandBuilder<K, V> extends BaseRedisCommandBuilder<K, V> {
         CommandArgs<K, V> args = new CommandArgs<>(codec).addKey(key);
 
         if (options != null) {
+            // OPTIONAL as per API
             options.build(args);
         }
 
         if (jsonPaths != null) {
+            // OPTIONAL as per API
             for (JsonPath jsonPath : jsonPaths) {
                 if (jsonPath != null) {
                     args.add(jsonPath.toString());
@@ -167,7 +166,7 @@ class RedisJsonCommandBuilder<K, V> extends BaseRedisCommandBuilder<K, V> {
             }
         }
 
-        return createCommand(JSON_GET, new JsonValueListOutput<>(codec, parser.block()), args);
+        return createCommand(JSON_GET, new JsonValueListOutput<>(codec, parser.get()), args);
     }
 
     Command<K, V, String> jsonMerge(K key, JsonPath jsonPath, JsonValue value) {
@@ -175,11 +174,7 @@ class RedisJsonCommandBuilder<K, V> extends BaseRedisCommandBuilder<K, V> {
         notNullKey(key);
 
         CommandArgs<K, V> args = new CommandArgs<>(codec).addKey(key);
-
-        if (jsonPath != null && !jsonPath.isRootPath()) {
-            args.add(jsonPath.toString());
-        }
-
+        args.add(jsonPath.toString());
         args.add(value.asByteBuffer().array());
 
         return createCommand(JSON_MERGE, new StatusOutput<>(codec), args);
@@ -189,12 +184,9 @@ class RedisJsonCommandBuilder<K, V> extends BaseRedisCommandBuilder<K, V> {
         notEmpty(keys);
 
         CommandArgs<K, V> args = new CommandArgs<>(codec).addKeys(keys);
+        args.add(jsonPath.toString());
 
-        if (jsonPath != null) {
-            args.add(jsonPath.toString());
-        }
-
-        return createCommand(JSON_MGET, new JsonValueListOutput<>(codec, parser.block()), args);
+        return createCommand(JSON_MGET, new JsonValueListOutput<>(codec, parser.get()), args);
     }
 
     Command<K, V, String> jsonMSet(List<JsonMsetArgs<K, V>> arguments) {
@@ -214,11 +206,7 @@ class RedisJsonCommandBuilder<K, V> extends BaseRedisCommandBuilder<K, V> {
         notNullKey(key);
 
         CommandArgs<K, V> args = new CommandArgs<>(codec).addKey(key);
-
-        if (jsonPath != null && !jsonPath.isRootPath()) {
-            args.add(jsonPath.toString());
-        }
-
+        args.add(jsonPath.toString());
         args.add(number.toString());
 
         return createCommand(JSON_NUMINCRBY, new NumberListOutput<>(codec), args);
@@ -230,6 +218,7 @@ class RedisJsonCommandBuilder<K, V> extends BaseRedisCommandBuilder<K, V> {
         CommandArgs<K, V> args = new CommandArgs<>(codec).addKey(key);
 
         if (jsonPath != null && !jsonPath.isRootPath()) {
+            // OPTIONAL as per API
             args.add(jsonPath.toString());
         }
 
@@ -243,6 +232,7 @@ class RedisJsonCommandBuilder<K, V> extends BaseRedisCommandBuilder<K, V> {
         CommandArgs<K, V> args = new CommandArgs<>(codec).addKey(key);
 
         if (jsonPath != null && !jsonPath.isRootPath()) {
+            // OPTIONAL as per API
             args.add(jsonPath.toString());
         }
 
@@ -259,6 +249,7 @@ class RedisJsonCommandBuilder<K, V> extends BaseRedisCommandBuilder<K, V> {
         args.add(value.asByteBuffer().array());
 
         if (options != null) {
+            // OPTIONAL as per API
             options.build(args);
         }
 
@@ -271,6 +262,7 @@ class RedisJsonCommandBuilder<K, V> extends BaseRedisCommandBuilder<K, V> {
         CommandArgs<K, V> args = new CommandArgs<>(codec).addKey(key);
 
         if (jsonPath != null && !jsonPath.isRootPath()) {
+            // OPTIONAL as per API
             args.add(jsonPath.toString());
         }
 
@@ -286,6 +278,7 @@ class RedisJsonCommandBuilder<K, V> extends BaseRedisCommandBuilder<K, V> {
         CommandArgs<K, V> args = new CommandArgs<>(codec).addKey(key);
 
         if (jsonPath != null && !jsonPath.isRootPath()) {
+            // OPTIONAL as per API
             args.add(jsonPath.toString());
         }
 
@@ -296,10 +289,7 @@ class RedisJsonCommandBuilder<K, V> extends BaseRedisCommandBuilder<K, V> {
         notNullKey(key);
 
         CommandArgs<K, V> args = new CommandArgs<>(codec).addKey(key);
-
-        if (jsonPath != null && !jsonPath.isRootPath()) {
-            args.add(jsonPath.toString());
-        }
+        args.add(jsonPath.toString());
 
         return createCommand(JSON_TOGGLE, (CommandOutput) new ArrayOutput<>(codec), args);
     }
@@ -310,6 +300,7 @@ class RedisJsonCommandBuilder<K, V> extends BaseRedisCommandBuilder<K, V> {
         CommandArgs<K, V> args = new CommandArgs<>(codec).addKey(key);
 
         if (jsonPath != null && !jsonPath.isRootPath()) {
+            // OPTIONAL as per API
             args.add(jsonPath.toString());
         }
         return createCommand(JSON_DEL, new IntegerOutput<>(codec), args);
@@ -321,6 +312,7 @@ class RedisJsonCommandBuilder<K, V> extends BaseRedisCommandBuilder<K, V> {
         CommandArgs<K, V> args = new CommandArgs<>(codec).addKey(key);
 
         if (jsonPath != null && !jsonPath.isRootPath()) {
+            // OPTIONAL as per API
             args.add(jsonPath.toString());
         }
 
diff --git a/src/main/java/io/lettuce/core/RedisReactiveCommandsImpl.java b/src/main/java/io/lettuce/core/RedisReactiveCommandsImpl.java
index de0195739..620b5510a 100644
--- a/src/main/java/io/lettuce/core/RedisReactiveCommandsImpl.java
+++ b/src/main/java/io/lettuce/core/RedisReactiveCommandsImpl.java
@@ -7,6 +7,8 @@ import io.lettuce.core.codec.RedisCodec;
 import io.lettuce.core.json.JsonParser;
 import reactor.core.publisher.Mono;
 
+import java.util.function.Supplier;
+
 /**
  * A reactive and thread-safe API for a Redis Sentinel connection.
  *
@@ -25,7 +27,7 @@ public class RedisReactiveCommandsImpl<K, V> extends AbstractRedisReactiveComman
      * @param parser the implementation of the {@link JsonParser} to use
      */
     public RedisReactiveCommandsImpl(StatefulRedisConnection<K, V> connection, RedisCodec<K, V> codec,
-            Mono<JsonParser> parser) {
+            Supplier<JsonParser> parser) {
         super(connection, codec, parser);
     }
 
diff --git a/src/main/java/io/lettuce/core/StatefulRedisConnectionImpl.java b/src/main/java/io/lettuce/core/StatefulRedisConnectionImpl.java
index b51ee8ffa..8052b9561 100644
--- a/src/main/java/io/lettuce/core/StatefulRedisConnectionImpl.java
+++ b/src/main/java/io/lettuce/core/StatefulRedisConnectionImpl.java
@@ -27,6 +27,7 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.function.Consumer;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
 import io.lettuce.core.api.StatefulRedisConnection;
@@ -67,7 +68,7 @@ public class StatefulRedisConnectionImpl<K, V> extends RedisChannelHandler<K, V>
 
     private final PushHandler pushHandler;
 
-    private final Mono<JsonParser> parser;
+    private final Supplier<JsonParser> parser;
 
     protected MultiOutput<K, V> multi;
 
@@ -96,7 +97,7 @@ public class StatefulRedisConnectionImpl<K, V> extends RedisChannelHandler<K, V>
      * @param parser the parser to use for JSON commands.
      */
     public StatefulRedisConnectionImpl(RedisChannelWriter writer, PushHandler pushHandler, RedisCodec<K, V> codec,
-            Duration timeout, Mono<JsonParser> parser) {
+            Duration timeout, Supplier<JsonParser> parser) {
 
         super(writer, timeout);
 
diff --git a/src/main/java/io/lettuce/core/cluster/ClusterTopologyRefreshOptions.java b/src/main/java/io/lettuce/core/cluster/ClusterTopologyRefreshOptions.java
index ddf33481a..882e4718f 100644
--- a/src/main/java/io/lettuce/core/cluster/ClusterTopologyRefreshOptions.java
+++ b/src/main/java/io/lettuce/core/cluster/ClusterTopologyRefreshOptions.java
@@ -169,7 +169,7 @@ public class ClusterTopologyRefreshOptions {
 
             LettuceAssert.notNull(refreshTrigger, "RefreshTriggers must not be null");
             LettuceAssert.noNullElements(refreshTrigger, "RefreshTriggers must not contain null elements");
-            LettuceAssert.notEmpty(refreshTrigger, "RefreshTriggers must not contain at least one element");
+            LettuceAssert.notEmpty(refreshTrigger, "RefreshTriggers must contain at least one element");
 
             adaptiveRefreshTriggers.addAll(Arrays.asList(refreshTrigger));
             return this;
diff --git a/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterAsyncCommandsImpl.java b/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterAsyncCommandsImpl.java
index 840a6ef1f..d8f8e4092 100644
--- a/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterAsyncCommandsImpl.java
+++ b/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterAsyncCommandsImpl.java
@@ -29,13 +29,13 @@ import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.function.BiFunction;
 import java.util.function.Function;
 import java.util.function.Predicate;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
 import io.lettuce.core.*;
@@ -64,7 +64,6 @@ import io.lettuce.core.protocol.AsyncCommand;
 import io.lettuce.core.protocol.Command;
 import io.lettuce.core.protocol.CommandType;
 import io.lettuce.core.protocol.ConnectionIntent;
-import reactor.core.publisher.Mono;
 
 /**
  * An advanced asynchronous and thread-safe API for a Redis Cluster connection.
@@ -89,11 +88,11 @@ public class RedisAdvancedClusterAsyncCommandsImpl<K, V> extends AbstractRedisAs
      * @param codec Codec used to encode/decode keys and values.
      * @param parser the implementation of the {@link JsonParser} to use
      * @deprecated since 5.1, use
-     *             {@link #RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnection, RedisCodec, Mono)}.
+     *             {@link #RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnection, RedisCodec, Supplier)}.
      */
     @Deprecated
     public RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnectionImpl<K, V> connection, RedisCodec<K, V> codec,
-            Mono<JsonParser> parser) {
+            Supplier<JsonParser> parser) {
         super(connection, codec, parser);
         this.codec = codec;
     }
@@ -104,7 +103,7 @@ public class RedisAdvancedClusterAsyncCommandsImpl<K, V> extends AbstractRedisAs
      * @param connection the stateful connection
      * @param codec Codec used to encode/decode keys and values.
      * @deprecated since 5.1, use
-     *             {@link #RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnection, RedisCodec, Mono)}.
+     *             {@link #RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnection, RedisCodec, Supplier)}.
      */
     @Deprecated
     public RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnectionImpl<K, V> connection, RedisCodec<K, V> codec) {
@@ -120,7 +119,7 @@ public class RedisAdvancedClusterAsyncCommandsImpl<K, V> extends AbstractRedisAs
      * @param parser the implementation of the {@link JsonParser} to use
      */
     public RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnection<K, V> connection, RedisCodec<K, V> codec,
-            Mono<JsonParser> parser) {
+            Supplier<JsonParser> parser) {
         super(connection, codec, parser);
         this.codec = codec;
     }
diff --git a/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterReactiveCommandsImpl.java b/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterReactiveCommandsImpl.java
index e12a352c4..448c4073b 100644
--- a/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterReactiveCommandsImpl.java
+++ b/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterReactiveCommandsImpl.java
@@ -32,6 +32,7 @@ import java.util.concurrent.ThreadLocalRandom;
 import java.util.function.BiFunction;
 import java.util.function.Function;
 import java.util.function.Predicate;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
 import io.lettuce.core.json.JsonParser;
@@ -78,11 +79,11 @@ public class RedisAdvancedClusterReactiveCommandsImpl<K, V> extends AbstractRedi
      * @param codec Codec used to encode/decode keys and values.
      * @param parser the implementation of the {@link JsonParser} to use
      * @deprecated since 5.2, use
-     *             {@link #RedisAdvancedClusterReactiveCommandsImpl(StatefulRedisClusterConnection, RedisCodec, Mono)}.
+     *             {@link #RedisAdvancedClusterReactiveCommandsImpl(StatefulRedisClusterConnection, RedisCodec, Supplier)}.
      */
     @Deprecated
     public RedisAdvancedClusterReactiveCommandsImpl(StatefulRedisClusterConnectionImpl<K, V> connection, RedisCodec<K, V> codec,
-            Mono<JsonParser> parser) {
+            Supplier<JsonParser> parser) {
         super(connection, codec, parser);
         this.codec = codec;
     }
@@ -93,7 +94,7 @@ public class RedisAdvancedClusterReactiveCommandsImpl<K, V> extends AbstractRedi
      * @param connection the stateful connection.
      * @param codec Codec used to encode/decode keys and values.
      * @deprecated since 5.2, use
-     *             {@link #RedisAdvancedClusterReactiveCommandsImpl(StatefulRedisClusterConnection, RedisCodec, Mono)}.
+     *             {@link #RedisAdvancedClusterReactiveCommandsImpl(StatefulRedisClusterConnection, RedisCodec, Supplier)}.
      */
     @Deprecated
     public RedisAdvancedClusterReactiveCommandsImpl(StatefulRedisClusterConnectionImpl<K, V> connection,
@@ -110,7 +111,7 @@ public class RedisAdvancedClusterReactiveCommandsImpl<K, V> extends AbstractRedi
      * @param parser the implementation of the {@link JsonParser} to use
      */
     public RedisAdvancedClusterReactiveCommandsImpl(StatefulRedisClusterConnection<K, V> connection, RedisCodec<K, V> codec,
-            Mono<JsonParser> parser) {
+            Supplier<JsonParser> parser) {
         super(connection, codec, parser);
         this.codec = codec;
     }
diff --git a/src/main/java/io/lettuce/core/cluster/RedisClusterClient.java b/src/main/java/io/lettuce/core/cluster/RedisClusterClient.java
index 31125d93c..b5fa5cf19 100644
--- a/src/main/java/io/lettuce/core/cluster/RedisClusterClient.java
+++ b/src/main/java/io/lettuce/core/cluster/RedisClusterClient.java
@@ -587,7 +587,7 @@ public class RedisClusterClient extends AbstractRedisClient {
      * @return new instance of StatefulRedisConnectionImpl
      */
     protected <K, V> StatefulRedisConnectionImpl<K, V> newStatefulRedisConnection(RedisChannelWriter channelWriter,
-            PushHandler pushHandler, RedisCodec<K, V> codec, Duration timeout, Mono<JsonParser> parser) {
+            PushHandler pushHandler, RedisCodec<K, V> codec, Duration timeout, Supplier<JsonParser> parser) {
         return new StatefulRedisConnectionImpl<>(channelWriter, pushHandler, codec, timeout, parser);
     }
 
@@ -734,7 +734,7 @@ public class RedisClusterClient extends AbstractRedisClient {
      */
     protected <V, K> StatefulRedisClusterConnectionImpl<K, V> newStatefulRedisClusterConnection(
             RedisChannelWriter channelWriter, ClusterPushHandler pushHandler, RedisCodec<K, V> codec, Duration timeout,
-            Mono<JsonParser> parser) {
+            Supplier<JsonParser> parser) {
         return new StatefulRedisClusterConnectionImpl(channelWriter, pushHandler, codec, timeout, parser);
     }
 
diff --git a/src/main/java/io/lettuce/core/cluster/StatefulRedisClusterConnectionImpl.java b/src/main/java/io/lettuce/core/cluster/StatefulRedisClusterConnectionImpl.java
index c84193491..d967ada1e 100644
--- a/src/main/java/io/lettuce/core/cluster/StatefulRedisClusterConnectionImpl.java
+++ b/src/main/java/io/lettuce/core/cluster/StatefulRedisClusterConnectionImpl.java
@@ -30,6 +30,7 @@ import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import java.util.function.Consumer;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
 import io.lettuce.core.AbstractRedisClient;
@@ -77,7 +78,7 @@ public class StatefulRedisClusterConnectionImpl<K, V> extends RedisChannelHandle
 
     protected final RedisCodec<K, V> codec;
 
-    protected final Mono<JsonParser> parser;
+    protected final Supplier<JsonParser> parser;
 
     protected final RedisAdvancedClusterCommands<K, V> sync;
 
@@ -113,7 +114,7 @@ public class StatefulRedisClusterConnectionImpl<K, V> extends RedisChannelHandle
      * @param parser the JSON parser
      */
     public StatefulRedisClusterConnectionImpl(RedisChannelWriter writer, ClusterPushHandler pushHandler, RedisCodec<K, V> codec,
-            Duration timeout, Mono<JsonParser> parser) {
+            Duration timeout, Supplier<JsonParser> parser) {
 
         super(writer, timeout);
         this.pushHandler = pushHandler;
diff --git a/src/main/java/io/lettuce/core/dynamic/SimpleBatcher.java b/src/main/java/io/lettuce/core/dynamic/SimpleBatcher.java
index a2ecf4883..53e23fad6 100644
--- a/src/main/java/io/lettuce/core/dynamic/SimpleBatcher.java
+++ b/src/main/java/io/lettuce/core/dynamic/SimpleBatcher.java
@@ -40,6 +40,7 @@ import io.lettuce.core.protocol.RedisCommand;
  *
  * @author Mark Paluch
  * @author Lucio Paiva
+ * @author Ivo Gaydajiev
  */
 class SimpleBatcher implements Batcher {
 
@@ -51,6 +52,11 @@ class SimpleBatcher implements Batcher {
 
     private final AtomicBoolean flushing = new AtomicBoolean();
 
+    // forceFlushRequested indicates that a flush was requested while there is already a flush in progress
+    // This flag is used to ensure we will flush again after the current flush is done
+    // to ensure that any commands added while dispatching the current flush are also dispatched
+    private final AtomicBoolean forceFlushRequested = new AtomicBoolean();
+
     public SimpleBatcher(StatefulConnection<Object, Object> connection, int batchSize) {
 
         LettuceAssert.isTrue(batchSize == -1 || batchSize > 1, "Batch size must be greater zero or -1");
@@ -95,37 +101,56 @@ class SimpleBatcher implements Batcher {
 
         List<RedisCommand<?, ?, ?>> commands = newDrainTarget();
 
-        while (flushing.compareAndSet(false, true)) {
+        while (true) {
+            if (flushing.compareAndSet(false, true)) {
+                try {
 
-            try {
+                    int consume = -1;
 
-                int consume = -1;
+                    if (!forcedFlush) {
+                        long queuedItems = queue.size();
+                        if (queuedItems >= batchSize) {
+                            consume = batchSize;
+                            defaultFlush = true;
+                        }
+                    }
 
-                if (!forcedFlush) {
-                    long queuedItems = queue.size();
-                    if (queuedItems >= batchSize) {
-                        consume = batchSize;
-                        defaultFlush = true;
+                    List<? extends RedisCommand<?, ?, ?>> batch = doFlush(forcedFlush, defaultFlush, consume);
+                    if (batch != null) {
+                        commands.addAll(batch);
                     }
-                }
 
-                List<? extends RedisCommand<?, ?, ?>> batch = doFlush(forcedFlush, defaultFlush, consume);
-                if (batch != null) {
-                    commands.addAll(batch);
-                }
+                    if (defaultFlush && !queue.isEmpty() && queue.size() > batchSize) {
+                        continue;
+                    }
+
+                    if (forceFlushRequested.compareAndSet(true, false)) {
+                        continue;
+                    }
 
-                if (defaultFlush && !queue.isEmpty() && queue.size() > batchSize) {
-                    continue;
+                    return new BatchTasks(commands);
+
+                } finally {
+                    flushing.set(false);
                 }
 
-                return new BatchTasks(commands);
+            } else {
+                // Another thread is already flushing
+                if (forcedFlush) {
+                    forceFlushRequested.set(true);
+                }
 
-            } finally {
-                flushing.set(false);
+                if (commands.isEmpty()) {
+                    return BatchTasks.EMPTY;
+                } else {
+                    // Scenario: A default flush is started in Thread T1.
+                    // If multiple default batches need processing, T1 will release `flushing` and try to reacquire it.
+                    // However, in the brief moment when T1 releases `flushing`, another thread (T2) might acquire it.
+                    // This lead to a state where T2 has taken over processing from T1 and T1 should return commands processed
+                    return new BatchTasks(commands);
+                }
             }
         }
-
-        return BatchTasks.EMPTY;
     }
 
     private List<? extends RedisCommand<?, ?, ?>> doFlush(boolean forcedFlush, boolean defaultFlush, int consume) {
diff --git a/src/main/java/io/lettuce/core/json/DefaultJsonParser.java b/src/main/java/io/lettuce/core/json/DefaultJsonParser.java
index afab7afa7..dd20fa41c 100644
--- a/src/main/java/io/lettuce/core/json/DefaultJsonParser.java
+++ b/src/main/java/io/lettuce/core/json/DefaultJsonParser.java
@@ -10,6 +10,7 @@ package io.lettuce.core.json;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.NullNode;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
@@ -61,6 +62,10 @@ public class DefaultJsonParser implements JsonParser {
     }
 
     private JsonValue parse(String value) {
+        if (value == null) {
+            return DelegateJsonValue.wrap(NullNode.getInstance());
+        }
+
         ObjectMapper mapper = new ObjectMapper();
         try {
             JsonNode root = mapper.readTree(value);
@@ -72,6 +77,10 @@ public class DefaultJsonParser implements JsonParser {
     }
 
     private JsonValue parse(ByteBuffer byteBuffer) {
+        if (byteBuffer == null) {
+            return DelegateJsonValue.wrap(NullNode.getInstance());
+        }
+
         ObjectMapper mapper = new ObjectMapper();
         try {
             byte[] bytes = new byte[byteBuffer.remaining()];
diff --git a/src/main/java/io/lettuce/core/json/UnproccessedJsonValue.java b/src/main/java/io/lettuce/core/json/UnproccessedJsonValue.java
index 0a0016750..92e98418a 100644
--- a/src/main/java/io/lettuce/core/json/UnproccessedJsonValue.java
+++ b/src/main/java/io/lettuce/core/json/UnproccessedJsonValue.java
@@ -45,6 +45,11 @@ class UnproccessedJsonValue implements JsonValue {
 
     @Override
     public String toString() {
+
+        if (unprocessedData == null) {
+            return null;
+        }
+
         if (isDeserialized()) {
             return jsonValue.toString();
         }
@@ -161,7 +166,9 @@ class UnproccessedJsonValue implements JsonValue {
             try {
                 if (!isDeserialized()) {
                     jsonValue = parser.createJsonValue(unprocessedData);
-                    unprocessedData.clear();
+                    if (unprocessedData != null) {
+                        unprocessedData.clear();
+                    }
                 }
             } finally {
                 lock.unlock();
diff --git a/src/main/java/io/lettuce/core/masterreplica/StatefulRedisMasterReplicaConnectionImpl.java b/src/main/java/io/lettuce/core/masterreplica/StatefulRedisMasterReplicaConnectionImpl.java
index ad1b5a53f..babafd51e 100644
--- a/src/main/java/io/lettuce/core/masterreplica/StatefulRedisMasterReplicaConnectionImpl.java
+++ b/src/main/java/io/lettuce/core/masterreplica/StatefulRedisMasterReplicaConnectionImpl.java
@@ -1,12 +1,12 @@
 package io.lettuce.core.masterreplica;
 
 import java.time.Duration;
+import java.util.function.Supplier;
 
 import io.lettuce.core.ReadFrom;
 import io.lettuce.core.StatefulRedisConnectionImpl;
 import io.lettuce.core.codec.RedisCodec;
 import io.lettuce.core.json.JsonParser;
-import reactor.core.publisher.Mono;
 
 import static io.lettuce.core.ClientOptions.DEFAULT_JSON_PARSER;
 
@@ -36,7 +36,7 @@ class StatefulRedisMasterReplicaConnectionImpl<K, V> extends StatefulRedisConnec
      * @param parser the JSON parser to use
      */
     StatefulRedisMasterReplicaConnectionImpl(MasterReplicaChannelWriter writer, RedisCodec<K, V> codec, Duration timeout,
-            Mono<JsonParser> parser) {
+            Supplier<JsonParser> parser) {
         super(writer, NoOpPushHandler.INSTANCE, codec, timeout, parser);
     }
 
diff --git a/src/main/java/io/lettuce/core/output/ClaimedMessagesOutput.java b/src/main/java/io/lettuce/core/output/ClaimedMessagesOutput.java
index 49d36c3f5..3b08480d4 100644
--- a/src/main/java/io/lettuce/core/output/ClaimedMessagesOutput.java
+++ b/src/main/java/io/lettuce/core/output/ClaimedMessagesOutput.java
@@ -70,12 +70,12 @@ public class ClaimedMessagesOutput<K, V> extends CommandOutput<K, V, ClaimedMess
     @Override
     public void set(ByteBuffer bytes) {
         if (startId == null) {
-            startId = decodeAscii(bytes);
+            startId = decodeString(bytes);
             return;
         }
 
         if (id == null) {
-            id = decodeAscii(bytes);
+            id = decodeString(bytes);
             return;
         }
 
diff --git a/src/main/java/io/lettuce/core/output/CommandOutput.java b/src/main/java/io/lettuce/core/output/CommandOutput.java
index 2b4a85126..5775f8967 100644
--- a/src/main/java/io/lettuce/core/output/CommandOutput.java
+++ b/src/main/java/io/lettuce/core/output/CommandOutput.java
@@ -20,6 +20,7 @@
 package io.lettuce.core.output;
 
 import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
 
 import io.lettuce.core.codec.RedisCodec;
 import io.lettuce.core.internal.LettuceAssert;
@@ -139,7 +140,7 @@ public abstract class CommandOutput<K, V, T> {
      * @param error Error message.
      */
     public void setError(ByteBuffer error) {
-        this.error = decodeAscii(error);
+        this.error = decodeString(error);
     }
 
     /**
@@ -179,16 +180,8 @@ public abstract class CommandOutput<K, V, T> {
         // nothing to do by default
     }
 
-    protected String decodeAscii(ByteBuffer bytes) {
-        if (bytes == null) {
-            return null;
-        }
-
-        char[] chars = new char[bytes.remaining()];
-        for (int i = 0; i < chars.length; i++) {
-            chars[i] = (char) bytes.get();
-        }
-        return new String(chars);
+    protected String decodeString(ByteBuffer bytes) {
+        return bytes == null ? null : StandardCharsets.UTF_8.decode(bytes).toString();
     }
 
     @Override
diff --git a/src/main/java/io/lettuce/core/output/DoubleListOutput.java b/src/main/java/io/lettuce/core/output/DoubleListOutput.java
index de1c5dfd6..72a2e5a42 100644
--- a/src/main/java/io/lettuce/core/output/DoubleListOutput.java
+++ b/src/main/java/io/lettuce/core/output/DoubleListOutput.java
@@ -46,7 +46,7 @@ public class DoubleListOutput<K, V> extends CommandOutput<K, V, List<Double>> {
 
     @Override
     public void set(ByteBuffer bytes) {
-        output.add(bytes != null ? parseDouble(decodeAscii(bytes)) : null);
+        output.add(bytes != null ? parseDouble(decodeString(bytes)) : null);
     }
 
     @Override
diff --git a/src/main/java/io/lettuce/core/output/DoubleOutput.java b/src/main/java/io/lettuce/core/output/DoubleOutput.java
index 415fe3b99..8f8aac9a6 100644
--- a/src/main/java/io/lettuce/core/output/DoubleOutput.java
+++ b/src/main/java/io/lettuce/core/output/DoubleOutput.java
@@ -40,7 +40,7 @@ public class DoubleOutput<K, V> extends CommandOutput<K, V, Double> {
 
     @Override
     public void set(ByteBuffer bytes) {
-        output = (bytes == null) ? null : parseDouble(decodeAscii(bytes));
+        output = (bytes == null) ? null : parseDouble(decodeString(bytes));
     }
 
     @Override
diff --git a/src/main/java/io/lettuce/core/output/EnumSetOutput.java b/src/main/java/io/lettuce/core/output/EnumSetOutput.java
index dd2c1c40f..dfd627e70 100644
--- a/src/main/java/io/lettuce/core/output/EnumSetOutput.java
+++ b/src/main/java/io/lettuce/core/output/EnumSetOutput.java
@@ -51,7 +51,7 @@ public class EnumSetOutput<K, V, E extends Enum<E>> extends CommandOutput<K, V,
             return;
         }
 
-        E enumConstant = resolve(enumValuePreprocessor.apply(decodeAscii(bytes)));
+        E enumConstant = resolve(enumValuePreprocessor.apply(decodeString(bytes)));
 
         if (enumConstant == null) {
             return;
diff --git a/src/main/java/io/lettuce/core/output/GeoCoordinatesListOutput.java b/src/main/java/io/lettuce/core/output/GeoCoordinatesListOutput.java
index bb51449cf..ce3cef58e 100644
--- a/src/main/java/io/lettuce/core/output/GeoCoordinatesListOutput.java
+++ b/src/main/java/io/lettuce/core/output/GeoCoordinatesListOutput.java
@@ -39,7 +39,7 @@ public class GeoCoordinatesListOutput<K, V> extends CommandOutput<K, V, List<Geo
             return;
         }
 
-        double value = (bytes == null) ? 0 : parseDouble(decodeAscii(bytes));
+        double value = (bytes == null) ? 0 : parseDouble(decodeString(bytes));
         set(value);
     }
 
diff --git a/src/main/java/io/lettuce/core/output/GeoCoordinatesValueListOutput.java b/src/main/java/io/lettuce/core/output/GeoCoordinatesValueListOutput.java
index 4bd8927c2..7fbb3f93c 100644
--- a/src/main/java/io/lettuce/core/output/GeoCoordinatesValueListOutput.java
+++ b/src/main/java/io/lettuce/core/output/GeoCoordinatesValueListOutput.java
@@ -40,7 +40,7 @@ public class GeoCoordinatesValueListOutput<K, V> extends CommandOutput<K, V, Lis
             return;
         }
 
-        double value = parseDouble(decodeAscii(bytes));
+        double value = parseDouble(decodeString(bytes));
         set(value);
     }
 
diff --git a/src/main/java/io/lettuce/core/output/GeoWithinListOutput.java b/src/main/java/io/lettuce/core/output/GeoWithinListOutput.java
index 01d494191..5a13bbd04 100644
--- a/src/main/java/io/lettuce/core/output/GeoWithinListOutput.java
+++ b/src/main/java/io/lettuce/core/output/GeoWithinListOutput.java
@@ -73,7 +73,7 @@ public class GeoWithinListOutput<K, V> extends CommandOutput<K, V, List<GeoWithi
             return;
         }
 
-        double value = (bytes == null) ? 0 : parseDouble(decodeAscii(bytes));
+        double value = (bytes == null) ? 0 : parseDouble(decodeString(bytes));
         set(value);
     }
 
diff --git a/src/main/java/io/lettuce/core/output/JsonTypeListOutput.java b/src/main/java/io/lettuce/core/output/JsonTypeListOutput.java
index 96f942f28..942d80cd3 100644
--- a/src/main/java/io/lettuce/core/output/JsonTypeListOutput.java
+++ b/src/main/java/io/lettuce/core/output/JsonTypeListOutput.java
@@ -35,7 +35,7 @@ public class JsonTypeListOutput<K, V> extends CommandOutput<K, V, List<JsonType>
             multi(1);
         }
 
-        output.add(JsonType.fromString(decodeAscii(bytes)));
+        output.add(JsonType.fromString(decodeString(bytes)));
     }
 
     @Override
diff --git a/src/main/java/io/lettuce/core/output/JsonValueListOutput.java b/src/main/java/io/lettuce/core/output/JsonValueListOutput.java
index 389b69662..e415f46a7 100644
--- a/src/main/java/io/lettuce/core/output/JsonValueListOutput.java
+++ b/src/main/java/io/lettuce/core/output/JsonValueListOutput.java
@@ -39,9 +39,13 @@ public class JsonValueListOutput<K, V> extends CommandOutput<K, V, List<JsonValu
             multi(1);
         }
 
-        ByteBuffer fetched = ByteBuffer.allocate(bytes.remaining());
-        fetched.put(bytes);
-        fetched.flip();
+        ByteBuffer fetched = null;
+        if (bytes != null) {
+            fetched = ByteBuffer.allocate(bytes.remaining());
+            fetched.put(bytes);
+            fetched.flip();
+        }
+
         output.add(parser.loadJsonValue(fetched));
     }
 
diff --git a/src/main/java/io/lettuce/core/output/KeyValueListScoredValueOutput.java b/src/main/java/io/lettuce/core/output/KeyValueListScoredValueOutput.java
index d4b79034f..e3b1b62ed 100644
--- a/src/main/java/io/lettuce/core/output/KeyValueListScoredValueOutput.java
+++ b/src/main/java/io/lettuce/core/output/KeyValueListScoredValueOutput.java
@@ -45,7 +45,7 @@ public class KeyValueListScoredValueOutput<K, V> extends CommandOutput<K, V, Key
                 value = codec.decodeValue(bytes);
                 return;
             }
-            score = parseDouble(decodeAscii(bytes));
+            score = parseDouble(decodeString(bytes));
         }
     }
 
diff --git a/src/main/java/io/lettuce/core/output/KeyValueOfScoredValueOutput.java b/src/main/java/io/lettuce/core/output/KeyValueOfScoredValueOutput.java
index 6bb09df87..fc730b794 100644
--- a/src/main/java/io/lettuce/core/output/KeyValueOfScoredValueOutput.java
+++ b/src/main/java/io/lettuce/core/output/KeyValueOfScoredValueOutput.java
@@ -40,7 +40,7 @@ public class KeyValueOfScoredValueOutput<K, V> extends CommandOutput<K, V, KeyVa
                 return;
             }
 
-            output = KeyValue.just(key, ScoredValue.just(parseDouble(decodeAscii(bytes)), value));
+            output = KeyValue.just(key, ScoredValue.just(parseDouble(decodeString(bytes)), value));
         }
     }
 
diff --git a/src/main/java/io/lettuce/core/output/KeyValueScoredValueOutput.java b/src/main/java/io/lettuce/core/output/KeyValueScoredValueOutput.java
index 994735d61..bbeb0484f 100644
--- a/src/main/java/io/lettuce/core/output/KeyValueScoredValueOutput.java
+++ b/src/main/java/io/lettuce/core/output/KeyValueScoredValueOutput.java
@@ -48,7 +48,7 @@ public class KeyValueScoredValueOutput<K, V> extends CommandOutput<K, V, KeyValu
             return;
         }
 
-        double score = LettuceStrings.toDouble(decodeAscii(bytes));
+        double score = LettuceStrings.toDouble(decodeString(bytes));
 
         set(score);
     }
diff --git a/src/main/java/io/lettuce/core/output/MultiOutput.java b/src/main/java/io/lettuce/core/output/MultiOutput.java
index 949a64cbf..c8cc27781 100644
--- a/src/main/java/io/lettuce/core/output/MultiOutput.java
+++ b/src/main/java/io/lettuce/core/output/MultiOutput.java
@@ -133,7 +133,7 @@ public class MultiOutput<K, V> extends CommandOutput<K, V, TransactionResult> {
         }
 
         CommandOutput<K, V, ?> output = queue.isEmpty() ? this : queue.peek().getOutput();
-        output.setError(decodeAscii(error));
+        output.setError(decodeString(error));
     }
 
     @Override
diff --git a/src/main/java/io/lettuce/core/output/NumberListOutput.java b/src/main/java/io/lettuce/core/output/NumberListOutput.java
index cbbb9bd9b..70041984d 100644
--- a/src/main/java/io/lettuce/core/output/NumberListOutput.java
+++ b/src/main/java/io/lettuce/core/output/NumberListOutput.java
@@ -65,7 +65,7 @@ public class NumberListOutput<K, V> extends CommandOutput<K, V, List<Number>> {
     private Number parseNumber(ByteBuffer bytes) {
         Number result = 0;
         try {
-            result = NumberFormat.getNumberInstance().parse(decodeAscii(bytes));
+            result = NumberFormat.getNumberInstance().parse(decodeString(bytes));
         } catch (ParseException e) {
             LOG.warn("Failed to parse " + bytes, e);
         }
diff --git a/src/main/java/io/lettuce/core/output/PendingMessageListOutput.java b/src/main/java/io/lettuce/core/output/PendingMessageListOutput.java
index ed1c8957d..306ec4ce2 100644
--- a/src/main/java/io/lettuce/core/output/PendingMessageListOutput.java
+++ b/src/main/java/io/lettuce/core/output/PendingMessageListOutput.java
@@ -41,7 +41,7 @@ public class PendingMessageListOutput<K, V> extends CommandOutput<K, V, List<Pen
     public void set(ByteBuffer bytes) {
 
         if (messageId == null) {
-            messageId = decodeAscii(bytes);
+            messageId = decodeString(bytes);
             return;
         }
 
@@ -51,7 +51,7 @@ public class PendingMessageListOutput<K, V> extends CommandOutput<K, V, List<Pen
             return;
         }
 
-        set(Long.parseLong(decodeAscii(bytes)));
+        set(Long.parseLong(decodeString(bytes)));
     }
 
     @Override
diff --git a/src/main/java/io/lettuce/core/output/PendingMessagesOutput.java b/src/main/java/io/lettuce/core/output/PendingMessagesOutput.java
index 3c66f6dc6..b8c942a7f 100644
--- a/src/main/java/io/lettuce/core/output/PendingMessagesOutput.java
+++ b/src/main/java/io/lettuce/core/output/PendingMessagesOutput.java
@@ -39,12 +39,12 @@ public class PendingMessagesOutput<K, V> extends CommandOutput<K, V, PendingMess
     public void set(ByteBuffer bytes) {
 
         if (messageIdsFrom == null) {
-            messageIdsFrom = decodeAscii(bytes);
+            messageIdsFrom = decodeString(bytes);
             return;
         }
 
         if (messageIdsTo == null) {
-            messageIdsTo = decodeAscii(bytes);
+            messageIdsTo = decodeString(bytes);
             return;
         }
 
@@ -54,7 +54,7 @@ public class PendingMessagesOutput<K, V> extends CommandOutput<K, V, PendingMess
             return;
         }
 
-        set(Long.parseLong(decodeAscii(bytes)));
+        set(Long.parseLong(decodeString(bytes)));
     }
 
     @Override
diff --git a/src/main/java/io/lettuce/core/output/ReplayOutput.java b/src/main/java/io/lettuce/core/output/ReplayOutput.java
index 4dafced0f..f8407723d 100644
--- a/src/main/java/io/lettuce/core/output/ReplayOutput.java
+++ b/src/main/java/io/lettuce/core/output/ReplayOutput.java
@@ -33,6 +33,11 @@ public class ReplayOutput<K, V> extends CommandOutput<K, V, List<ReplayOutput.Si
         output.add(new Integer(integer));
     }
 
+    @Override
+    public void set(double number) {
+        output.add(new Double(number));
+    }
+
     @Override
     public void setError(ByteBuffer error) {
         error.mark();
@@ -130,6 +135,21 @@ public class ReplayOutput<K, V> extends CommandOutput<K, V, List<ReplayOutput.Si
 
     }
 
+    static class Double extends Signal {
+
+        final double message;
+
+        Double(double message) {
+            this.message = message;
+        }
+
+        @Override
+        protected void replay(CommandOutput<?, ?, ?> target) {
+            target.set(message);
+        }
+
+    }
+
     public static class ErrorBytes extends BulkStringSupport {
 
         ErrorBytes(ByteBuffer message) {
diff --git a/src/main/java/io/lettuce/core/output/ScanOutput.java b/src/main/java/io/lettuce/core/output/ScanOutput.java
index 92a32aa21..c6a916877 100644
--- a/src/main/java/io/lettuce/core/output/ScanOutput.java
+++ b/src/main/java/io/lettuce/core/output/ScanOutput.java
@@ -24,7 +24,7 @@ public abstract class ScanOutput<K, V, T extends ScanCursor> extends CommandOutp
     public void set(ByteBuffer bytes) {
 
         if (output.getCursor() == null) {
-            output.setCursor(decodeAscii(bytes));
+            output.setCursor(decodeString(bytes));
             if (LettuceStrings.isNotEmpty(output.getCursor()) && "0".equals(output.getCursor())) {
                 output.setFinished(true);
             }
diff --git a/src/main/java/io/lettuce/core/output/ScoredValueListOutput.java b/src/main/java/io/lettuce/core/output/ScoredValueListOutput.java
index 2c2179683..28c6ec9b2 100644
--- a/src/main/java/io/lettuce/core/output/ScoredValueListOutput.java
+++ b/src/main/java/io/lettuce/core/output/ScoredValueListOutput.java
@@ -60,7 +60,7 @@ public class ScoredValueListOutput<K, V> extends CommandOutput<K, V, List<Scored
             return;
         }
 
-        double score = LettuceStrings.toDouble(decodeAscii(bytes));
+        double score = LettuceStrings.toDouble(decodeString(bytes));
         set(score);
     }
 
diff --git a/src/main/java/io/lettuce/core/output/ScoredValueOutput.java b/src/main/java/io/lettuce/core/output/ScoredValueOutput.java
index 08d57798d..c0db5d1b9 100644
--- a/src/main/java/io/lettuce/core/output/ScoredValueOutput.java
+++ b/src/main/java/io/lettuce/core/output/ScoredValueOutput.java
@@ -37,7 +37,7 @@ public class ScoredValueOutput<K, V> extends CommandOutput<K, V, ScoredValue<V>>
             return;
         }
 
-        double score = LettuceStrings.toDouble(decodeAscii(bytes));
+        double score = LettuceStrings.toDouble(decodeString(bytes));
         set(score);
     }
 
diff --git a/src/main/java/io/lettuce/core/output/ScoredValueScanOutput.java b/src/main/java/io/lettuce/core/output/ScoredValueScanOutput.java
index c7f8dc57e..3e777df46 100644
--- a/src/main/java/io/lettuce/core/output/ScoredValueScanOutput.java
+++ b/src/main/java/io/lettuce/core/output/ScoredValueScanOutput.java
@@ -33,7 +33,7 @@ public class ScoredValueScanOutput<K, V> extends ScanOutput<K, V, ScoredValueSca
             return;
         }
 
-        double score = LettuceStrings.toDouble(decodeAscii(bytes));
+        double score = LettuceStrings.toDouble(decodeString(bytes));
         set(score);
     }
 
diff --git a/src/main/java/io/lettuce/core/output/ScoredValueScanStreamingOutput.java b/src/main/java/io/lettuce/core/output/ScoredValueScanStreamingOutput.java
index 352febdaf..3a15f9215 100644
--- a/src/main/java/io/lettuce/core/output/ScoredValueScanStreamingOutput.java
+++ b/src/main/java/io/lettuce/core/output/ScoredValueScanStreamingOutput.java
@@ -35,7 +35,7 @@ public class ScoredValueScanStreamingOutput<K, V> extends ScanOutput<K, V, Strea
             return;
         }
 
-        double score = LettuceStrings.toDouble(decodeAscii(bytes));
+        double score = LettuceStrings.toDouble(decodeString(bytes));
         set(score);
     }
 
diff --git a/src/main/java/io/lettuce/core/output/ScoredValueStreamingOutput.java b/src/main/java/io/lettuce/core/output/ScoredValueStreamingOutput.java
index aecb443c7..919d522fa 100644
--- a/src/main/java/io/lettuce/core/output/ScoredValueStreamingOutput.java
+++ b/src/main/java/io/lettuce/core/output/ScoredValueStreamingOutput.java
@@ -35,7 +35,7 @@ public class ScoredValueStreamingOutput<K, V> extends CommandOutput<K, V, Long>
             return;
         }
 
-        double score = LettuceStrings.toDouble(decodeAscii(bytes));
+        double score = LettuceStrings.toDouble(decodeString(bytes));
         set(score);
     }
 
diff --git a/src/main/java/io/lettuce/core/output/SocketAddressOutput.java b/src/main/java/io/lettuce/core/output/SocketAddressOutput.java
index b8cf65264..2a19ed04b 100644
--- a/src/main/java/io/lettuce/core/output/SocketAddressOutput.java
+++ b/src/main/java/io/lettuce/core/output/SocketAddressOutput.java
@@ -26,12 +26,12 @@ public class SocketAddressOutput<K, V> extends CommandOutput<K, V, SocketAddress
     public void set(ByteBuffer bytes) {
 
         if (!hasHostname) {
-            hostname = decodeAscii(bytes);
+            hostname = decodeString(bytes);
             hasHostname = true;
             return;
         }
 
-        int port = Integer.parseInt(decodeAscii(bytes));
+        int port = Integer.parseInt(decodeString(bytes));
         output = InetSocketAddress.createUnresolved(hostname, port);
     }
 
diff --git a/src/main/java/io/lettuce/core/output/StatusOutput.java b/src/main/java/io/lettuce/core/output/StatusOutput.java
index 9024b01de..f62d30488 100644
--- a/src/main/java/io/lettuce/core/output/StatusOutput.java
+++ b/src/main/java/io/lettuce/core/output/StatusOutput.java
@@ -41,7 +41,7 @@ public class StatusOutput<K, V> extends CommandOutput<K, V, String> {
 
     @Override
     public void set(ByteBuffer bytes) {
-        output = OK.equals(bytes) ? "OK" : decodeAscii(bytes);
+        output = OK.equals(bytes) ? "OK" : decodeString(bytes);
     }
 
 }
diff --git a/src/main/java/io/lettuce/core/output/StreamMessageListOutput.java b/src/main/java/io/lettuce/core/output/StreamMessageListOutput.java
index c2d0b603b..84493f80b 100644
--- a/src/main/java/io/lettuce/core/output/StreamMessageListOutput.java
+++ b/src/main/java/io/lettuce/core/output/StreamMessageListOutput.java
@@ -41,7 +41,7 @@ public class StreamMessageListOutput<K, V> extends CommandOutput<K, V, List<Stre
     public void set(ByteBuffer bytes) {
 
         if (id == null) {
-            id = decodeAscii(bytes);
+            id = decodeString(bytes);
             return;
         }
 
diff --git a/src/main/java/io/lettuce/core/output/StreamReadOutput.java b/src/main/java/io/lettuce/core/output/StreamReadOutput.java
index 67186df93..977b7bf86 100644
--- a/src/main/java/io/lettuce/core/output/StreamReadOutput.java
+++ b/src/main/java/io/lettuce/core/output/StreamReadOutput.java
@@ -52,7 +52,7 @@ public class StreamReadOutput<K, V> extends CommandOutput<K, V, List<StreamMessa
         }
 
         if (id == null) {
-            id = decodeAscii(bytes);
+            id = decodeString(bytes);
             return;
         }
 
diff --git a/src/main/java/io/lettuce/core/output/StringListOutput.java b/src/main/java/io/lettuce/core/output/StringListOutput.java
index df60ecbfb..eecdb773e 100644
--- a/src/main/java/io/lettuce/core/output/StringListOutput.java
+++ b/src/main/java/io/lettuce/core/output/StringListOutput.java
@@ -32,7 +32,7 @@ public class StringListOutput<K, V> extends CommandOutput<K, V, List<String>> im
             multi(1);
         }
 
-        subscriber.onNext(output, bytes == null ? null : decodeAscii(bytes));
+        subscriber.onNext(output, bytes == null ? null : decodeString(bytes));
     }
 
     @Override
diff --git a/src/main/java/io/lettuce/core/output/StringValueListOutput.java b/src/main/java/io/lettuce/core/output/StringValueListOutput.java
index 12b0e4631..d69dbeace 100644
--- a/src/main/java/io/lettuce/core/output/StringValueListOutput.java
+++ b/src/main/java/io/lettuce/core/output/StringValueListOutput.java
@@ -30,7 +30,7 @@ public class StringValueListOutput<K, V> extends CommandOutput<K, V, List<Value<
 
     @Override
     public void set(ByteBuffer bytes) {
-        subscriber.onNext(output, bytes == null ? Value.empty() : Value.fromNullable(decodeAscii(bytes)));
+        subscriber.onNext(output, bytes == null ? Value.empty() : Value.fromNullable(decodeString(bytes)));
     }
 
     @Override
diff --git a/src/main/java/io/lettuce/core/protocol/CommandExpiryWriter.java b/src/main/java/io/lettuce/core/protocol/CommandExpiryWriter.java
index dcd71e6d4..160deed18 100644
--- a/src/main/java/io/lettuce/core/protocol/CommandExpiryWriter.java
+++ b/src/main/java/io/lettuce/core/protocol/CommandExpiryWriter.java
@@ -179,8 +179,8 @@ public class CommandExpiryWriter implements RedisChannelWriter {
 
         Timeout commandTimeout = timer.newTimeout(t -> {
             if (!command.isDone()) {
-                executors.submit(() -> command.completeExceptionally(
-                        ExceptionFactory.createTimeoutException(Duration.ofNanos(timeUnit.toNanos(timeout)))));
+                executors.submit(() -> command.completeExceptionally(ExceptionFactory
+                        .createTimeoutException(command.getType().toString(), Duration.ofNanos(timeUnit.toNanos(timeout)))));
 
             }
         }, timeout, timeUnit);
diff --git a/src/main/java/io/lettuce/core/pubsub/PubSubOutput.java b/src/main/java/io/lettuce/core/pubsub/PubSubOutput.java
index 0ec41ff64..012e7e4fc 100644
--- a/src/main/java/io/lettuce/core/pubsub/PubSubOutput.java
+++ b/src/main/java/io/lettuce/core/pubsub/PubSubOutput.java
@@ -93,7 +93,7 @@ public class PubSubOutput<K, V> extends CommandOutput<K, V, V> implements PubSub
         }
 
         if (type == null) {
-            type = Type.valueOf(decodeAscii(bytes));
+            type = Type.valueOf(decodeString(bytes));
             return;
         }
 
diff --git a/src/main/java/io/lettuce/core/sentinel/RedisSentinelReactiveCommandsImpl.java b/src/main/java/io/lettuce/core/sentinel/RedisSentinelReactiveCommandsImpl.java
index e02cf4d96..af5937d4e 100644
--- a/src/main/java/io/lettuce/core/sentinel/RedisSentinelReactiveCommandsImpl.java
+++ b/src/main/java/io/lettuce/core/sentinel/RedisSentinelReactiveCommandsImpl.java
@@ -21,6 +21,7 @@ package io.lettuce.core.sentinel;
 
 import java.net.SocketAddress;
 import java.util.Map;
+import java.util.function.Supplier;
 
 import io.lettuce.core.AbstractRedisReactiveCommands;
 import io.lettuce.core.ClientListArgs;
@@ -52,7 +53,7 @@ public class RedisSentinelReactiveCommandsImpl<K, V> extends AbstractRedisReacti
     private final SentinelCommandBuilder<K, V> commandBuilder;
 
     public RedisSentinelReactiveCommandsImpl(StatefulConnection<K, V> connection, RedisCodec<K, V> codec,
-            Mono<JsonParser> parser) {
+            Supplier<JsonParser> parser) {
         super(connection, codec, parser);
         commandBuilder = new SentinelCommandBuilder<K, V>(codec);
     }
diff --git a/src/main/java/io/lettuce/core/sentinel/StatefulRedisSentinelConnectionImpl.java b/src/main/java/io/lettuce/core/sentinel/StatefulRedisSentinelConnectionImpl.java
index f5a9a9c62..2eb209046 100644
--- a/src/main/java/io/lettuce/core/sentinel/StatefulRedisSentinelConnectionImpl.java
+++ b/src/main/java/io/lettuce/core/sentinel/StatefulRedisSentinelConnectionImpl.java
@@ -21,6 +21,7 @@ package io.lettuce.core.sentinel;
 
 import java.time.Duration;
 import java.util.Collection;
+import java.util.function.Supplier;
 
 import io.lettuce.core.ConnectionState;
 import io.lettuce.core.RedisChannelHandler;
@@ -34,7 +35,6 @@ import io.lettuce.core.sentinel.api.StatefulRedisSentinelConnection;
 import io.lettuce.core.sentinel.api.async.RedisSentinelAsyncCommands;
 import io.lettuce.core.sentinel.api.reactive.RedisSentinelReactiveCommands;
 import io.lettuce.core.sentinel.api.sync.RedisSentinelCommands;
-import reactor.core.publisher.Mono;
 
 import static io.lettuce.core.ClientOptions.DEFAULT_JSON_PARSER;
 
@@ -74,7 +74,7 @@ public class StatefulRedisSentinelConnectionImpl<K, V> extends RedisChannelHandl
      * @param parser the parser used to parse JSON responses
      */
     public StatefulRedisSentinelConnectionImpl(RedisChannelWriter writer, RedisCodec<K, V> codec, Duration timeout,
-            Mono<JsonParser> parser) {
+            Supplier<JsonParser> parser) {
 
         super(writer, timeout);
 


Test patch:
diff --git a/src/test/bash/create_certificates.sh b/src/test/bash/create_certificates.sh
deleted file mode 100755
index 28db1eb51..000000000
--- a/src/test/bash/create_certificates.sh
+++ /dev/null
@@ -1,131 +0,0 @@
-#!/bin/bash
-
-DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
-CA_DIR=work/ca
-TRUSTSTORE_FILE=work/truststore.jks
-KEYSTORE_FILE=work/keystore.jks
-
-if [[ -d work/ca ]] ; then
-    rm -Rf ${CA_DIR}
-fi
-
-if [[ -f ${TRUSTSTORE_FILE} ]] ; then
-    rm -Rf ${TRUSTSTORE_FILE}
-fi
-
-if [[ -f ${KEYSTORE_FILE} ]] ; then
-    rm -Rf ${KEYSTORE_FILE}
-fi
-
-if [ ! -x "$(which openssl)" ] ; then
-   echo "[ERROR] No openssl in PATH"
-   exit 1
-fi
-
-KEYTOOL=keytool
-
-if [  ! -x "${KEYTOOL}" ] ; then
-   KEYTOOL=${JAVA_HOME}/bin/keytool
-fi
-
-if [  ! -x "${KEYTOOL}" ] ; then
-   echo "[ERROR] No keytool in PATH/JAVA_HOME"
-   exit 1
-fi
-
-mkdir -p ${CA_DIR}/private ${CA_DIR}/certs ${CA_DIR}/crl ${CA_DIR}/csr ${CA_DIR}/newcerts ${CA_DIR}/intermediate
-
-echo "[INFO] Generating CA private key"
-# Less bits = less secure = faster to generate
-openssl genrsa -passout pass:changeit -aes256 -out ${CA_DIR}/private/ca.key.pem 2048
-
-chmod 400 ${CA_DIR}/private/ca.key.pem
-
-echo "[INFO] Generating CA certificate"
-openssl req -config ${DIR}/openssl.cnf \
-      -key ${CA_DIR}/private/ca.key.pem \
-      -new -x509 -days 7300 -sha256 -extensions v3_ca \
-      -out ${CA_DIR}/certs/ca.cert.pem \
-      -passin pass:changeit \
-      -subj "/C=NN/ST=Unknown/L=Unknown/O=lettuce/CN=CA Certificate"
-
-echo "[INFO] Prepare CA database"
-echo 1000 > ${CA_DIR}/serial
-touch ${CA_DIR}/index.txt
-
-function generateKey {
-
-    host=$1
-    ip=$2
-
-    echo "[INFO] Generating server private key"
-    openssl genrsa -aes256 \
-          -passout pass:changeit \
-          -out ${CA_DIR}/private/${host}.key.pem 2048
-
-    openssl rsa -in ${CA_DIR}/private/${host}.key.pem \
-          -out ${CA_DIR}/private/${host}.decrypted.key.pem \
-          -passin pass:changeit
-
-    chmod 400 ${CA_DIR}/private/${host}.key.pem
-    chmod 400 ${CA_DIR}/private/${host}.decrypted.key.pem
-
-    echo "[INFO] Generating server certificate request"
-    openssl req -config <(cat ${DIR}/openssl.cnf \
-        <(printf "\n[SAN]\nsubjectAltName=DNS:${host},IP:${ip}")) \
-          -reqexts SAN \
-          -key ${CA_DIR}/private/${host}.key.pem \
-          -passin pass:changeit \
-          -new -sha256 -out ${CA_DIR}/csr/${host}.csr.pem \
-          -subj "/C=NN/ST=Unknown/L=Unknown/O=lettuce/CN=${host}"
-
-    echo "[INFO] Signing certificate request"
-    openssl ca -config ${DIR}/openssl.cnf \
-          -extensions server_cert -days 375 -notext -md sha256 \
-          -passin pass:changeit \
-          -batch \
-          -in ${CA_DIR}/csr/${host}.csr.pem \
-          -out ${CA_DIR}/certs/${host}.cert.pem
-}
-
-generateKey "localhost" "127.0.0.1"
-generateKey "foo-host" "1.2.3.4"
-
-echo "[INFO] Generating client auth private key"
-openssl genrsa -aes256 \
-      -passout pass:changeit \
-      -out ${CA_DIR}/private/client.key.pem 2048
-
-openssl rsa -in ${CA_DIR}/private/client.key.pem \
-      -out ${CA_DIR}/private/client.decrypted.key.pem \
-      -passin pass:changeit
-
-chmod 400 ${CA_DIR}/private/client.key.pem
-
-echo "[INFO] Generating client certificate request"
-openssl req -config ${DIR}/openssl.cnf \
-      -key ${CA_DIR}/private/client.key.pem \
-      -passin pass:changeit \
-      -new -sha256 -out ${CA_DIR}/csr/client.csr.pem \
-      -subj "/C=NN/ST=Unknown/L=Unknown/O=lettuce/CN=client"
-
-echo "[INFO] Signing certificate request"
-openssl ca -config ${DIR}/openssl.cnf \
-      -extensions usr_cert -days 375 -notext -md sha256 \
-      -passin pass:changeit \
-      -batch \
-      -in ${CA_DIR}/csr/client.csr.pem \
-      -out ${CA_DIR}/certs/client.cert.pem
-
-echo "[INFO] Creating PKCS12 file with client certificate"
-openssl pkcs12 -export -clcerts \
-      -in ${CA_DIR}/certs/client.cert.pem \
-      -inkey ${CA_DIR}/private/client.decrypted.key.pem \
-      -passout pass:changeit \
-      -out ${CA_DIR}/client.p12
-
-${KEYTOOL} -importcert -keystore ${TRUSTSTORE_FILE} -file ${CA_DIR}/certs/ca.cert.pem -noprompt -storepass changeit
-${KEYTOOL} -importkeystore \
-                              -srckeystore ${CA_DIR}/client.p12 -srcstoretype PKCS12 -srcstorepass changeit\
-                              -destkeystore ${KEYSTORE_FILE} -deststoretype PKCS12 \
-                              -noprompt -storepass changeit
diff --git a/src/test/bash/openssl.cnf b/src/test/bash/openssl.cnf
deleted file mode 100644
index 721bd488d..000000000
--- a/src/test/bash/openssl.cnf
+++ /dev/null
@@ -1,107 +0,0 @@
-[ ca ]
-# `man ca`
-default_ca = CA_default
-
-[ CA_default ]
-# Directory and file locations.
-dir               = work/ca
-certs             = $dir/certs
-crl_dir           = $dir/crl
-new_certs_dir     = $dir/newcerts
-database          = $dir/index.txt
-serial            = $dir/serial
-RANDFILE          = $dir/private/.rand
-
-# The root key and root certificate.
-private_key       = $dir/private/ca.key.pem
-certificate       = $dir/certs/ca.cert.pem
-
-# For certificate revocation lists.
-crlnumber         = $dir/crlnumber
-crl               = $dir/crl/ca.crl.pem
-crl_extensions    = crl_ext
-default_crl_days  = 30
-
-# SHA-1 is deprecated, so use SHA-2 instead.
-default_md        = sha256
-
-name_opt          = ca_default
-cert_opt          = ca_default
-default_days      = 375
-preserve          = no
-policy            = policy_strict
-copy_extensions   = copy
-
-[ policy_strict ]
-# The root CA should only sign intermediate certificates that match.
-# See the POLICY FORMAT section of `man ca`.
-countryName             = match
-stateOrProvinceName     = match
-organizationName        = match
-organizationalUnitName  = optional
-commonName              = supplied
-emailAddress            = optional
-
-[ req ]
-# Options for the `req` tool (`man req`).
-default_bits        = 2048
-distinguished_name  = req_distinguished_name
-string_mask         = utf8only
-
-# SHA-1 is deprecated, so use SHA-2 instead.
-default_md          = sha256
-
-# Extension to add when the -x509 option is used.
-x509_extensions     = v3_ca
-
-[ req_distinguished_name ]
-# See <https://en.wikipedia.org/wiki/Certificate_signing_request>.
-countryName                     = Country Name (2 letter code)
-stateOrProvinceName             = State or Province Name
-localityName                    = Locality Name
-0.organizationName              = Organization Name
-organizationalUnitName          = Organizational Unit Name
-commonName                      = Common Name
-emailAddress                    = Email Address
-
-# Optionally, specify some defaults.
-countryName_default             = NN
-stateOrProvinceName_default     = Vault Test
-localityName_default            =
-0.organizationName_default      = spring-cloud-vault-config
-#organizationalUnitName_default =
-#emailAddress_default           = info@spring-cloud-vault-config.dummy
-
-[ v3_ca ]
-# Extensions for a typical CA (`man x509v3_config`).
-subjectKeyIdentifier = hash
-authorityKeyIdentifier = keyid:always,issuer
-basicConstraints = critical, CA:true
-keyUsage = critical, digitalSignature, cRLSign, keyCertSign
-
-[ v3_intermediate_ca ]
-# Extensions for a typical intermediate CA (`man x509v3_config`).
-subjectKeyIdentifier = hash
-authorityKeyIdentifier = keyid:always,issuer
-basicConstraints = critical, CA:true, pathlen:0
-keyUsage = critical, digitalSignature, cRLSign, keyCertSign
-
-[ usr_cert ]
-# Extensions for client certificates (`man x509v3_config`).
-basicConstraints = CA:FALSE
-nsCertType = client, email
-nsComment = "OpenSSL Generated Client Certificate"
-subjectKeyIdentifier = hash
-authorityKeyIdentifier = keyid,issuer
-keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
-extendedKeyUsage = clientAuth, emailProtection
-
-[ server_cert ]
-# Extensions for server certificates (`man x509v3_config`).
-basicConstraints = CA:FALSE
-nsCertType = server
-nsComment = "OpenSSL Generated Server Certificate"
-subjectKeyIdentifier = hash
-authorityKeyIdentifier = keyid,issuer:always
-keyUsage = critical, digitalSignature, keyEncipherment
-extendedKeyUsage = serverAuth
diff --git a/src/test/java/biz/paluch/redis/extensibility/MyExtendedRedisClusterClient.java b/src/test/java/biz/paluch/redis/extensibility/MyExtendedRedisClusterClient.java
index 1ac284807..f0d738336 100644
--- a/src/test/java/biz/paluch/redis/extensibility/MyExtendedRedisClusterClient.java
+++ b/src/test/java/biz/paluch/redis/extensibility/MyExtendedRedisClusterClient.java
@@ -20,6 +20,7 @@
 package biz.paluch.redis.extensibility;
 
 import java.time.Duration;
+import java.util.function.Supplier;
 
 import javax.enterprise.inject.Alternative;
 
@@ -51,7 +52,7 @@ public class MyExtendedRedisClusterClient extends RedisClusterClient {
     @Override
     protected <V, K> StatefulRedisClusterConnectionImpl<K, V> newStatefulRedisClusterConnection(
             RedisChannelWriter channelWriter, ClusterPushHandler pushHandler, RedisCodec<K, V> codec, Duration timeout,
-            Mono<JsonParser> parser) {
+            Supplier<JsonParser> parser) {
         return new MyRedisClusterConnection<>(channelWriter, pushHandler, codec, timeout, parser);
     }
 
diff --git a/src/test/java/biz/paluch/redis/extensibility/MyRedisClusterConnection.java b/src/test/java/biz/paluch/redis/extensibility/MyRedisClusterConnection.java
index 04632e463..a1152b976 100644
--- a/src/test/java/biz/paluch/redis/extensibility/MyRedisClusterConnection.java
+++ b/src/test/java/biz/paluch/redis/extensibility/MyRedisClusterConnection.java
@@ -20,6 +20,7 @@
 package biz.paluch.redis.extensibility;
 
 import java.time.Duration;
+import java.util.function.Supplier;
 
 import io.lettuce.core.RedisChannelWriter;
 import io.lettuce.core.cluster.ClusterPushHandler;
@@ -37,7 +38,7 @@ import reactor.core.publisher.Mono;
 class MyRedisClusterConnection<K, V> extends StatefulRedisClusterConnectionImpl<K, V> {
 
     public MyRedisClusterConnection(RedisChannelWriter writer, ClusterPushHandler pushHandler, RedisCodec<K, V> codec,
-            Duration timeout, Mono<JsonParser> parser) {
+            Duration timeout, Supplier<JsonParser> parser) {
         super(writer, pushHandler, codec, timeout, parser);
     }
 
diff --git a/src/test/java/io/lettuce/core/ClientOptionsUnitTests.java b/src/test/java/io/lettuce/core/ClientOptionsUnitTests.java
index bb2d0f91a..49811ea5c 100644
--- a/src/test/java/io/lettuce/core/ClientOptionsUnitTests.java
+++ b/src/test/java/io/lettuce/core/ClientOptionsUnitTests.java
@@ -40,7 +40,7 @@ class ClientOptionsUnitTests {
         assertThat(options.getReadOnlyCommands().isReadOnly(new Command<>(CommandType.SET, null))).isFalse();
         assertThat(options.getReadOnlyCommands().isReadOnly(new Command<>(CommandType.PUBLISH, null))).isFalse();
         assertThat(options.getReadOnlyCommands().isReadOnly(new Command<>(CommandType.GET, null))).isTrue();
-        assertThat(options.getJsonParser().block()).isInstanceOf(DefaultJsonParser.class);
+        assertThat(options.getJsonParser().get()).isInstanceOf(DefaultJsonParser.class);
     }
 
     @Test
@@ -66,8 +66,8 @@ class ClientOptionsUnitTests {
     @Test
     void jsonParser() {
         JsonParser parser = new CustomJsonParser();
-        ClientOptions options = ClientOptions.builder().jsonParser(Mono.justOrEmpty(parser)).build();
-        assertThat(options.getJsonParser().block()).isInstanceOf(CustomJsonParser.class);
+        ClientOptions options = ClientOptions.builder().jsonParser(() -> parser).build();
+        assertThat(options.getJsonParser().get()).isInstanceOf(CustomJsonParser.class);
     }
 
     static class CustomJsonParser implements JsonParser {
diff --git a/src/test/java/io/lettuce/core/RedisContainerIntegrationTests.java b/src/test/java/io/lettuce/core/RedisContainerIntegrationTests.java
deleted file mode 100644
index caa258f74..000000000
--- a/src/test/java/io/lettuce/core/RedisContainerIntegrationTests.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright 2024, Redis Ltd. and Contributors
- * All rights reserved.
- *
- * Licensed under the MIT License.
- */
-
-package io.lettuce.core;
-
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.junit.jupiter.api.BeforeAll;
-import org.testcontainers.containers.ComposeContainer;
-import org.testcontainers.containers.output.OutputFrame;
-import org.testcontainers.containers.wait.strategy.Wait;
-import org.testcontainers.junit.jupiter.Testcontainers;
-
-import java.io.File;
-
-@Testcontainers
-public class RedisContainerIntegrationTests {
-
-    private static final Logger LOGGER = LogManager.getLogger(RedisContainerIntegrationTests.class);
-
-    private static final String REDIS_STACK_STANDALONE = "standalone-stack";
-
-    private static final String REDIS_STACK_CLUSTER = "clustered-stack";
-
-    private static final String REDIS_STACK_VERSION = System.getProperty("REDIS_STACK_VERSION");
-
-    private static Exception initializationException;
-
-    public static ComposeContainer CLUSTERED_STACK = new ComposeContainer(
-            new File("src/test/resources/docker/docker-compose.yml")).withExposedService(REDIS_STACK_CLUSTER, 36379)
-                    .withExposedService(REDIS_STACK_CLUSTER, 36380).withExposedService(REDIS_STACK_CLUSTER, 36381)
-                    .withExposedService(REDIS_STACK_STANDALONE, 6379).withPull(false).withLocalCompose(true);
-
-    // Singleton container pattern - start the containers only once
-    // See https://java.testcontainers.org/test_framework_integration/manual_lifecycle_control/#singleton-containers
-    static {
-        int attempts = 0;
-
-        if (REDIS_STACK_VERSION != null && !REDIS_STACK_VERSION.isEmpty()) {
-            CLUSTERED_STACK.withEnv("REDIS_VERSION", REDIS_STACK_VERSION);
-        }
-        // In case you need to debug the container uncomment these lines to redirect the output
-        CLUSTERED_STACK.withLogConsumer(REDIS_STACK_CLUSTER, (OutputFrame frame) -> LOGGER.debug(frame.getUtf8String()));
-        CLUSTERED_STACK.withLogConsumer(REDIS_STACK_STANDALONE, (OutputFrame frame) -> LOGGER.debug(frame.getUtf8String()));
-
-        CLUSTERED_STACK.waitingFor(REDIS_STACK_CLUSTER, Wait.forLogMessage("Cluster created with nodes:.*", 1));
-        do {
-            try {
-                CLUSTERED_STACK.start();
-            } catch (Exception e) {
-                initializationException = e;
-            }
-            // Attempt to stabilize the pipeline - sometime the `docker compose up` fails randomly
-        } while (initializationException != null && attempts++ < 3);
-    }
-
-    @BeforeAll
-    public static void checkContainerInitialization() {
-        if (initializationException != null) {
-            throw new IllegalStateException("Failed to initialize containers", initializationException);
-        }
-    }
-
-}
diff --git a/src/test/java/io/lettuce/core/RedisJsonCommandBuilderUnitTests.java b/src/test/java/io/lettuce/core/RedisJsonCommandBuilderUnitTests.java
index 4106a711c..28df5f01a 100644
--- a/src/test/java/io/lettuce/core/RedisJsonCommandBuilderUnitTests.java
+++ b/src/test/java/io/lettuce/core/RedisJsonCommandBuilderUnitTests.java
@@ -49,7 +49,7 @@ class RedisJsonCommandBuilderUnitTests {
 
     public static final JsonPath MY_PATH = JsonPath.of("$..commuter_bikes");
 
-    RedisJsonCommandBuilder<String, String> builder = new RedisJsonCommandBuilder<>(StringCodec.UTF8, Mono.just(PARSER));
+    RedisJsonCommandBuilder<String, String> builder = new RedisJsonCommandBuilder<>(StringCodec.UTF8, () -> PARSER);
 
     @Test
     void shouldCorrectlyConstructJsonArrappend() {
@@ -57,8 +57,18 @@ class RedisJsonCommandBuilderUnitTests {
         ByteBuf buf = Unpooled.directBuffer();
         command.encode(buf);
 
-        assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*4\r\n" + "$14\r\n" + "JSON.ARRAPPEND\r\n" + "$15\r\n"
-                + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n" + "$14\r\n" + ID_BIKE_6 + "\r\n");
+        assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*4\r\n" + "$14\r\nJSON.ARRAPPEND\r\n"
+                + "$15\r\nbikes:inventory\r\n" + "$17\r\n$..commuter_bikes\r\n" + "$14\r\n" + ID_BIKE_6 + "\r\n");
+    }
+
+    @Test
+    void shouldCorrectlyConstructJsonArrappendRootPath() {
+        Command<String, String, List<Long>> command = builder.jsonArrappend(MY_KEY, JsonPath.ROOT_PATH, ELEMENT);
+        ByteBuf buf = Unpooled.directBuffer();
+        command.encode(buf);
+
+        assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo(
+                "*3\r\n" + "$14\r\nJSON.ARRAPPEND\r\n" + "$15\r\nbikes:inventory\r\n" + "$14\r\n" + ID_BIKE_6 + "\r\n");
     }
 
     @Test
@@ -68,9 +78,21 @@ class RedisJsonCommandBuilderUnitTests {
         ByteBuf buf = Unpooled.directBuffer();
         command.encode(buf);
 
-        assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*6\r\n" + "$13\r\n" + "JSON.ARRINDEX\r\n" + "$15\r\n"
-                + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n" + "$14\r\n" + ID_BIKE_6 + "\r\n" + "$1" + "\r\n"
-                + "0" + "\r\n" + "$1" + "\r\n" + "1" + "\r\n");
+        assertThat(buf.toString(StandardCharsets.UTF_8))
+                .isEqualTo("*6\r\n" + "$13\r\nJSON.ARRINDEX\r\n" + "$15\r\nbikes:inventory\r\n" + "$17\r\n$..commuter_bikes\r\n"
+                        + "$14\r\n" + ID_BIKE_6 + "\r\n" + "$1\r\n0\r\n" + "$1\r\n1\r\n");
+    }
+
+    @Test
+    void shouldCorrectlyConstructJsonArrindexRootPath() {
+        JsonRangeArgs range = JsonRangeArgs.Builder.start(0).stop(1);
+        Command<String, String, List<Long>> command = builder.jsonArrindex(MY_KEY, JsonPath.ROOT_PATH, ELEMENT, range);
+        ByteBuf buf = Unpooled.directBuffer();
+        command.encode(buf);
+
+        assertThat(buf.toString(StandardCharsets.UTF_8))
+                .isEqualTo("*6\r\n" + "$13\r\nJSON.ARRINDEX\r\n" + "$15\r\nbikes:inventory\r\n" + "$1\r\n$\r\n" + "$14\r\n"
+                        + ID_BIKE_6 + "\r\n" + "$1\r\n0\r\n" + "$1\r\n1\r\n");
     }
 
     @Test
@@ -80,8 +102,18 @@ class RedisJsonCommandBuilderUnitTests {
         command.encode(buf);
 
         assertThat(buf.toString(StandardCharsets.UTF_8))
-                .isEqualTo("*5\r\n" + "$14\r\n" + "JSON.ARRINSERT\r\n" + "$15\r\n" + "bikes:inventory\r\n" + "$17\r\n"
-                        + "$..commuter_bikes\r\n" + "$1" + "\r\n" + "1" + "\r\n" + "$14\r\n" + ID_BIKE_6 + "\r\n");
+                .isEqualTo("*5\r\n" + "$14\r\nJSON.ARRINSERT\r\n" + "$15\r\nbikes:inventory\r\n"
+                        + "$17\r\n$..commuter_bikes\r\n" + "$1\r\n1\r\n" + "$14\r\n" + ID_BIKE_6 + "\r\n");
+    }
+
+    @Test
+    void shouldCorrectlyConstructJsonArrinsertRootPath() {
+        Command<String, String, List<Long>> command = builder.jsonArrinsert(MY_KEY, JsonPath.ROOT_PATH, 1, ELEMENT);
+        ByteBuf buf = Unpooled.directBuffer();
+        command.encode(buf);
+
+        assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*5\r\n" + "$14\r\n" + "JSON.ARRINSERT\r\n" + "$15\r\n"
+                + "bikes:inventory\r\n" + "$1\r\n" + "$\r\n" + "$1" + "\r\n" + "1" + "\r\n" + "$14\r\n" + ID_BIKE_6 + "\r\n");
     }
 
     @Test
@@ -94,6 +126,16 @@ class RedisJsonCommandBuilderUnitTests {
                 + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n");
     }
 
+    @Test
+    void shouldCorrectlyConstructJsonArrlenRootPath() {
+        Command<String, String, List<Long>> command = builder.jsonArrlen(MY_KEY, JsonPath.ROOT_PATH);
+        ByteBuf buf = Unpooled.directBuffer();
+        command.encode(buf);
+
+        assertThat(buf.toString(StandardCharsets.UTF_8))
+                .isEqualTo("*2\r\n" + "$11\r\nJSON.ARRLEN\r\n" + "$15\r\nbikes:inventory\r\n");
+    }
+
     @Test
     void shouldCorrectlyConstructJsonArrpop() {
         Command<String, String, List<JsonValue>> command = builder.jsonArrpop(MY_KEY, MY_PATH, 3);
@@ -104,6 +146,36 @@ class RedisJsonCommandBuilderUnitTests {
                 + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n" + "$1" + "\r\n" + "3" + "\r\n");
     }
 
+    @Test
+    void shouldCorrectlyConstructJsonArrpopNoIndex() {
+        Command<String, String, List<JsonValue>> command = builder.jsonArrpop(MY_KEY, MY_PATH, -1);
+        ByteBuf buf = Unpooled.directBuffer();
+        command.encode(buf);
+
+        assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*3\r\n" + "$11\r\n" + "JSON.ARRPOP\r\n" + "$15\r\n"
+                + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n");
+    }
+
+    @Test
+    void shouldCorrectlyConstructJsonArrpopRootPathNoIndex() {
+        Command<String, String, List<JsonValue>> command = builder.jsonArrpop(MY_KEY, JsonPath.ROOT_PATH, -1);
+        ByteBuf buf = Unpooled.directBuffer();
+        command.encode(buf);
+
+        assertThat(buf.toString(StandardCharsets.UTF_8))
+                .isEqualTo("*2\r\n" + "$11\r\n" + "JSON.ARRPOP\r\n" + "$15\r\n" + "bikes:inventory\r\n");
+    }
+
+    @Test
+    void shouldCorrectlyConstructJsonArrpopRootPath() {
+        Command<String, String, List<JsonValue>> command = builder.jsonArrpop(MY_KEY, JsonPath.ROOT_PATH, 1);
+        ByteBuf buf = Unpooled.directBuffer();
+        command.encode(buf);
+
+        assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*4\r\n" + "$11\r\n" + "JSON.ARRPOP\r\n" + "$15\r\n"
+                + "bikes:inventory\r\n" + "$1\r\n" + "$\r\n" + "$1\r\n" + "1\r\n");
+    }
+
     @Test
     void shouldCorrectlyConstructJsonArrtrim() {
         JsonRangeArgs range = JsonRangeArgs.Builder.start(0).stop(1);
@@ -116,6 +188,17 @@ class RedisJsonCommandBuilderUnitTests {
                         + "$..commuter_bikes\r\n" + "$1" + "\r\n" + "0" + "\r\n" + "$1" + "\r\n" + "1" + "\r\n");
     }
 
+    @Test
+    void shouldCorrectlyConstructJsonArrtrimRootPath() {
+        JsonRangeArgs range = JsonRangeArgs.Builder.start(0).stop(1);
+        Command<String, String, List<Long>> command = builder.jsonArrtrim(MY_KEY, JsonPath.ROOT_PATH, range);
+        ByteBuf buf = Unpooled.directBuffer();
+        command.encode(buf);
+
+        assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*5\r\n" + "$12\r\n" + "JSON.ARRTRIM\r\n" + "$15\r\n"
+                + "bikes:inventory\r\n" + "$1\r\n" + "$\r\n" + "$1" + "\r\n" + "0" + "\r\n" + "$1" + "\r\n" + "1" + "\r\n");
+    }
+
     @Test
     void shouldCorrectlyConstructJsonClear() {
         Command<String, String, Long> command = builder.jsonClear(MY_KEY, MY_PATH);
@@ -126,6 +209,16 @@ class RedisJsonCommandBuilderUnitTests {
                 + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n");
     }
 
+    @Test
+    void shouldCorrectlyConstructJsonClearRootPath() {
+        Command<String, String, Long> command = builder.jsonClear(MY_KEY, JsonPath.ROOT_PATH);
+        ByteBuf buf = Unpooled.directBuffer();
+        command.encode(buf);
+
+        assertThat(buf.toString(StandardCharsets.UTF_8))
+                .isEqualTo("*2\r\n" + "$10\r\n" + "JSON.CLEAR\r\n" + "$15\r\n" + "bikes:inventory\r\n");
+    }
+
     @Test
     void shouldCorrectlyConstructJsonGet() {
         JsonGetArgs args = JsonGetArgs.Builder.indent("   ").newline("\n").space("/");
@@ -138,6 +231,18 @@ class RedisJsonCommandBuilderUnitTests {
                 + "\n\r\n" + "$5\r\n" + "SPACE\r\n" + "$1\r\n" + "/\r\n" + "$17\r\n" + "$..commuter_bikes\r\n");
     }
 
+    @Test
+    void shouldCorrectlyConstructJsonGetRootPath() {
+        JsonGetArgs args = JsonGetArgs.Builder.indent("   ").newline("\n").space("/");
+        Command<String, String, List<JsonValue>> command = builder.jsonGet(MY_KEY, args, JsonPath.ROOT_PATH);
+        ByteBuf buf = Unpooled.directBuffer();
+        command.encode(buf);
+
+        assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*9\r\n" + "$8\r\n" + "JSON.GET\r\n" + "$15\r\n"
+                + "bikes:inventory\r\n" + "$6\r\n" + "INDENT\r\n" + "$3\r\n" + "   \r\n" + "$7\r\n" + "NEWLINE\r\n" + "$1\r\n"
+                + "\n\r\n" + "$5\r\n" + "SPACE\r\n" + "$1\r\n" + "/\r\n" + "$1\r\n" + "$\r\n");
+    }
+
     @Test
     void shouldCorrectlyConstructJsonMerge() {
         Command<String, String, String> command = builder.jsonMerge(MY_KEY, MY_PATH, ELEMENT);
@@ -148,6 +253,16 @@ class RedisJsonCommandBuilderUnitTests {
                 + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n" + "$14\r\n" + ID_BIKE_6 + "\r\n");
     }
 
+    @Test
+    void shouldCorrectlyConstructJsonMergeRootPath() {
+        Command<String, String, String> command = builder.jsonMerge(MY_KEY, JsonPath.ROOT_PATH, ELEMENT);
+        ByteBuf buf = Unpooled.directBuffer();
+        command.encode(buf);
+
+        assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*4\r\n" + "$10\r\n" + "JSON.MERGE\r\n" + "$15\r\n"
+                + "bikes:inventory\r\n" + "$1\r\n" + "$\r\n" + "$14\r\n" + ID_BIKE_6 + "\r\n");
+    }
+
     @Test
     void shouldCorrectlyConstructJsonMget() {
         Command<String, String, List<JsonValue>> command = builder.jsonMGet(MY_PATH, MY_KEY, MY_KEY2);
@@ -158,6 +273,16 @@ class RedisJsonCommandBuilderUnitTests {
                 + "bikes:inventory\r\n" + "$15\r\n" + "bikes:repairLog\r\n" + "$17\r\n" + "$..commuter_bikes\r\n");
     }
 
+    @Test
+    void shouldCorrectlyConstructJsonMgetRootPath() {
+        Command<String, String, List<JsonValue>> command = builder.jsonMGet(JsonPath.ROOT_PATH, MY_KEY, MY_KEY2);
+        ByteBuf buf = Unpooled.directBuffer();
+        command.encode(buf);
+
+        assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*4\r\n" + "$9\r\n" + "JSON.MGET\r\n" + "$15\r\n"
+                + "bikes:inventory\r\n" + "$15\r\n" + "bikes:repairLog\r\n" + "$1\r\n" + "$\r\n");
+    }
+
     @Test
     void shouldCorrectlyConstructJsonMset() {
         JsonMsetArgs<String, String> args1 = new JsonMsetArgs<>(MY_KEY, MY_PATH, ELEMENT);
@@ -169,6 +294,17 @@ class RedisJsonCommandBuilderUnitTests {
                 + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n" + "$14\r\n" + ID_BIKE_6 + "\r\n");
     }
 
+    @Test
+    void shouldCorrectlyConstructJsonMsetRootPath() {
+        JsonMsetArgs<String, String> args1 = new JsonMsetArgs<>(MY_KEY, JsonPath.ROOT_PATH, ELEMENT);
+        Command<String, String, String> command = builder.jsonMSet(Collections.singletonList(args1));
+        ByteBuf buf = Unpooled.directBuffer();
+        command.encode(buf);
+
+        assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*4\r\n" + "$9\r\n" + "JSON.MSET\r\n" + "$15\r\n"
+                + "bikes:inventory\r\n" + "$1\r\n" + "$\r\n" + "$14\r\n" + ID_BIKE_6 + "\r\n");
+    }
+
     @Test
     void shouldCorrectlyConstructJsonNumincrby() {
         Command<String, String, List<Number>> command = builder.jsonNumincrby(MY_KEY, MY_PATH, 3);
@@ -179,6 +315,16 @@ class RedisJsonCommandBuilderUnitTests {
                 + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n" + "$1" + "\r\n" + "3" + "\r\n");
     }
 
+    @Test
+    void shouldCorrectlyConstructJsonNumincrbyRootPath() {
+        Command<String, String, List<Number>> command = builder.jsonNumincrby(MY_KEY, JsonPath.ROOT_PATH, 3);
+        ByteBuf buf = Unpooled.directBuffer();
+        command.encode(buf);
+
+        assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*4\r\n" + "$14\r\n" + "JSON.NUMINCRBY\r\n" + "$15\r\n"
+                + "bikes:inventory\r\n" + "$1\r\n" + "$\r\n" + "$1" + "\r\n" + "3" + "\r\n");
+    }
+
     @Test
     void shouldCorrectlyConstructJsonObjkeys() {
         Command<String, String, List<String>> command = builder.jsonObjkeys(MY_KEY, MY_PATH);
@@ -189,6 +335,16 @@ class RedisJsonCommandBuilderUnitTests {
                 + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n");
     }
 
+    @Test
+    void shouldCorrectlyConstructJsonObjkeysRootPath() {
+        Command<String, String, List<String>> command = builder.jsonObjkeys(MY_KEY, JsonPath.ROOT_PATH);
+        ByteBuf buf = Unpooled.directBuffer();
+        command.encode(buf);
+
+        assertThat(buf.toString(StandardCharsets.UTF_8))
+                .isEqualTo("*2\r\n" + "$12\r\n" + "JSON.OBJKEYS\r\n" + "$15\r\n" + "bikes:inventory\r\n");
+    }
+
     @Test
     void shouldCorrectlyConstructJsonObjlen() {
         Command<String, String, List<Long>> command = builder.jsonObjlen(MY_KEY, MY_PATH);
@@ -199,6 +355,16 @@ class RedisJsonCommandBuilderUnitTests {
                 + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n");
     }
 
+    @Test
+    void shouldCorrectlyConstructJsonObjlenRootPath() {
+        Command<String, String, List<Long>> command = builder.jsonObjlen(MY_KEY, JsonPath.ROOT_PATH);
+        ByteBuf buf = Unpooled.directBuffer();
+        command.encode(buf);
+
+        assertThat(buf.toString(StandardCharsets.UTF_8))
+                .isEqualTo("*2\r\n" + "$11\r\n" + "JSON.OBJLEN\r\n" + "$15\r\n" + "bikes:inventory\r\n");
+    }
+
     @Test
     void shouldCorrectlyConstructJsonSet() {
         JsonSetArgs args = JsonSetArgs.Builder.nx();
@@ -211,6 +377,17 @@ class RedisJsonCommandBuilderUnitTests {
                         + "$..commuter_bikes\r\n" + "$14\r\n" + ID_BIKE_6 + "\r\n" + "$2\r\n" + "NX\r\n");
     }
 
+    @Test
+    void shouldCorrectlyConstructJsonSetRootPath() {
+        JsonSetArgs args = JsonSetArgs.Builder.nx();
+        Command<String, String, String> command = builder.jsonSet(MY_KEY, JsonPath.ROOT_PATH, ELEMENT, args);
+        ByteBuf buf = Unpooled.directBuffer();
+        command.encode(buf);
+
+        assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*5\r\n" + "$8\r\n" + "JSON.SET\r\n" + "$15\r\n"
+                + "bikes:inventory\r\n" + "$1\r\n" + "$\r\n" + "$14\r\n" + ID_BIKE_6 + "\r\n" + "$2\r\n" + "NX\r\n");
+    }
+
     @Test
     void shouldCorrectlyConstructJsonStrappend() {
         Command<String, String, List<Long>> command = builder.jsonStrappend(MY_KEY, MY_PATH, ELEMENT);
@@ -221,6 +398,16 @@ class RedisJsonCommandBuilderUnitTests {
                 + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n" + "$14\r\n" + ID_BIKE_6 + "\r\n");
     }
 
+    @Test
+    void shouldCorrectlyConstructJsonStrappendRootPath() {
+        Command<String, String, List<Long>> command = builder.jsonStrappend(MY_KEY, JsonPath.ROOT_PATH, ELEMENT);
+        ByteBuf buf = Unpooled.directBuffer();
+        command.encode(buf);
+
+        assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*3\r\n" + "$14\r\n" + "JSON.STRAPPEND\r\n" + "$15\r\n"
+                + "bikes:inventory\r\n" + "$14\r\n" + ID_BIKE_6 + "\r\n");
+    }
+
     @Test
     void shouldCorrectlyConstructJsonStrlen() {
         Command<String, String, List<Long>> command = builder.jsonStrlen(MY_KEY, MY_PATH);
@@ -231,6 +418,16 @@ class RedisJsonCommandBuilderUnitTests {
                 + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n");
     }
 
+    @Test
+    void shouldCorrectlyConstructJsonStrlenRootPath() {
+        Command<String, String, List<Long>> command = builder.jsonStrlen(MY_KEY, JsonPath.ROOT_PATH);
+        ByteBuf buf = Unpooled.directBuffer();
+        command.encode(buf);
+
+        assertThat(buf.toString(StandardCharsets.UTF_8))
+                .isEqualTo("*2\r\n" + "$11\r\n" + "JSON.STRLEN\r\n" + "$15\r\n" + "bikes:inventory\r\n");
+    }
+
     @Test
     void shouldCorrectlyConstructJsonToggle() {
         Command<String, String, List<Long>> command = builder.jsonToggle(MY_KEY, MY_PATH);
@@ -241,6 +438,16 @@ class RedisJsonCommandBuilderUnitTests {
                 + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n");
     }
 
+    @Test
+    void shouldCorrectlyConstructJsonToggleRootPath() {
+        Command<String, String, List<Long>> command = builder.jsonToggle(MY_KEY, JsonPath.ROOT_PATH);
+        ByteBuf buf = Unpooled.directBuffer();
+        command.encode(buf);
+
+        assertThat(buf.toString(StandardCharsets.UTF_8))
+                .isEqualTo("*3\r\n" + "$11\r\n" + "JSON.TOGGLE\r\n" + "$15\r\n" + "bikes:inventory\r\n" + "$1\r\n" + "$\r\n");
+    }
+
     @Test
     void shouldCorrectlyConstructJsonDel() {
         Command<String, String, Long> command = builder.jsonDel(MY_KEY, MY_PATH);
@@ -251,6 +458,16 @@ class RedisJsonCommandBuilderUnitTests {
                 "*3\r\n" + "$8\r\n" + "JSON.DEL\r\n" + "$15\r\n" + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n");
     }
 
+    @Test
+    void shouldCorrectlyConstructJsonDelRootPath() {
+        Command<String, String, Long> command = builder.jsonDel(MY_KEY, JsonPath.ROOT_PATH);
+        ByteBuf buf = Unpooled.directBuffer();
+        command.encode(buf);
+
+        assertThat(buf.toString(StandardCharsets.UTF_8))
+                .isEqualTo("*2\r\n" + "$8\r\n" + "JSON.DEL\r\n" + "$15\r\n" + "bikes:inventory\r\n");
+    }
+
     @Test
     void shouldCorrectlyConstructJsonType() {
         Command<String, String, ?> command = builder.jsonType(MY_KEY, MY_PATH);
diff --git a/src/test/java/io/lettuce/core/SslIntegrationTests.java b/src/test/java/io/lettuce/core/SslIntegrationTests.java
index bbad87472..dbea9f90b 100644
--- a/src/test/java/io/lettuce/core/SslIntegrationTests.java
+++ b/src/test/java/io/lettuce/core/SslIntegrationTests.java
@@ -42,6 +42,8 @@ import javax.inject.Inject;
 import java.io.File;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.time.Duration;
 import java.util.List;
 import java.util.function.Function;
@@ -50,6 +52,7 @@ import java.util.stream.IntStream;
 
 import static io.lettuce.TestTags.INTEGRATION_TEST;
 import static io.lettuce.test.settings.TestSettings.sslPort;
+import static io.lettuce.test.settings.TlsSettings.*;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.junit.jupiter.api.Assumptions.assumeTrue;
@@ -66,11 +69,13 @@ class SslIntegrationTests extends TestSupport {
 
     private static final String KEYSTORE = "work/keystore.jks";
 
-    private static final String TRUSTSTORE = "work/truststore.jks";
+    private static File truststoreFile0;
 
-    private static final File TRUSTSTORE_FILE = new File(TRUSTSTORE);
+    private static File truststoreFile1;
 
-    private static final File CA_CERT_FILE = new File("work/ca/certs/ca.cert.pem");
+    private static File truststoreFile2;
+
+    private static File truststoreFile3;
 
     private static final int MASTER_SLAVE_BASE_PORT_OFFSET = 2000;
 
@@ -111,9 +116,25 @@ class SslIntegrationTests extends TestSupport {
 
     @BeforeAll
     static void beforeClass() {
+        Path path0 = createAndSaveTestTruststore("redis-standalone-0", Paths.get("redis-standalone-0/work/tls"), "changeit");
+        truststoreFile0 = path0.toFile();
+
+        Path path = createAndSaveTestTruststore("redis-standalone-1", Paths.get("redis-standalone-1/work/tls"), "changeit");
+        truststoreFile1 = path.toFile();
+
+        Path path2 = createAndSaveTestTruststore("redis-standalone-sentinel-controlled",
+                Paths.get("redis-standalone-sentinel-controlled/work/tls"), "changeit");
+        truststoreFile2 = path2.toFile();
+
+        truststoreFile3 = createAndSaveTestTruststore("redis-standalone-5-client-cert",
+                Paths.get("redis-standalone-5-client-cert/work/tls"), "changeit").toFile();
 
         assumeTrue(CanConnect.to(TestSettings.host(), sslPort()), "Assume that stunnel runs on port 6443");
-        assertThat(TRUSTSTORE_FILE).exists();
+        // Maybe we should do a list.
+        assertThat(truststoreFile0).exists();
+        assertThat(truststoreFile1).exists();
+        assertThat(truststoreFile2).exists();
+        assertThat(truststoreFile3).exists();
     }
 
     @Test
@@ -130,7 +151,7 @@ class SslIntegrationTests extends TestSupport {
 
         SslOptions sslOptions = SslOptions.builder() //
                 .jdkSslProvider() //
-                .truststore(TRUSTSTORE_FILE) //
+                .truststore(truststoreFile1, "changeit") //
                 .build();
         setOptions(sslOptions);
 
@@ -142,7 +163,7 @@ class SslIntegrationTests extends TestSupport {
 
         SslOptions sslOptions = SslOptions.builder() //
                 .jdkSslProvider() //
-                .truststore(TRUSTSTORE_FILE) //
+                .truststore(truststoreFile0, "changeit") //
                 .build();
         setOptions(sslOptions);
 
@@ -153,7 +174,7 @@ class SslIntegrationTests extends TestSupport {
     void standaloneWithPemCert() {
 
         SslOptions sslOptions = SslOptions.builder() //
-                .trustManager(CA_CERT_FILE) //
+                .trustManager(envCa(Paths.get("redis-standalone-1/work/tls")).toFile()) //
                 .build();
         setOptions(sslOptions);
         verifyConnection(URI_VERIFY);
@@ -164,7 +185,7 @@ class SslIntegrationTests extends TestSupport {
 
         Assertions.setMaxStackTraceElementsDisplayed(30);
         SslOptions sslOptions = SslOptions.builder() //
-                .trustManager(CA_CERT_FILE) //
+                .trustManager(envCa(Paths.get("redis-standalone-1/work/tls")).toFile()) //
                 .build();
         setOptions(sslOptions);
         redisClient.setOptions(ClientOptions.builder().protocolVersion(ProtocolVersion.RESP3).sslOptions(sslOptions).build());
@@ -183,7 +204,7 @@ class SslIntegrationTests extends TestSupport {
 
         SslOptions sslOptions = SslOptions.builder() //
                 .jdkSslProvider() //
-                .truststore(truststoreURL()) //
+                .truststore(truststoreURL(truststoreFile1), "changeit") //
                 .build();
         setOptions(sslOptions);
 
@@ -192,11 +213,12 @@ class SslIntegrationTests extends TestSupport {
 
     @Test
     void standaloneWithClientCertificates() {
-
+        // 6445
+        File keystore = envClientP12(Paths.get("redis-standalone-5-client-cert/work/tls")).toFile();
         SslOptions sslOptions = SslOptions.builder() //
                 .jdkSslProvider() //
-                .keystore(new File(KEYSTORE), "changeit".toCharArray()) //
-                .truststore(TRUSTSTORE_FILE) //
+                .keystore(keystore, "changeit".toCharArray()) //
+                .truststore(truststoreFile3, "changeit") //
                 .build();
         setOptions(sslOptions);
 
@@ -208,7 +230,7 @@ class SslIntegrationTests extends TestSupport {
 
         SslOptions sslOptions = SslOptions.builder() //
                 .jdkSslProvider() //
-                .truststore(TRUSTSTORE_FILE) //
+                .truststore(truststoreFile1, "changeit") //
                 .build();
         setOptions(sslOptions);
 
@@ -220,7 +242,7 @@ class SslIntegrationTests extends TestSupport {
 
         SslOptions sslOptions = SslOptions.builder() //
                 .jdkSslProvider() //
-                .truststore(truststoreURL(), "knödel") //
+                .truststore(truststoreURL(truststoreFile0), "knödel") //
                 .build();
         setOptions(sslOptions);
 
@@ -245,7 +267,7 @@ class SslIntegrationTests extends TestSupport {
 
         SslOptions sslOptions = SslOptions.builder() //
                 .openSslProvider() //
-                .truststore(TRUSTSTORE_FILE) //
+                .truststore(truststoreFile0, "changeit") //
                 .build();
         setOptions(sslOptions);
 
@@ -298,7 +320,7 @@ class SslIntegrationTests extends TestSupport {
 
         SslOptions sslOptions = SslOptions.builder() //
                 .jdkSslProvider() //
-                .truststore(TRUSTSTORE_FILE) //
+                .truststore(truststoreFile2, "changeit") //
                 .build();
         setOptions(sslOptions);
 
@@ -310,7 +332,7 @@ class SslIntegrationTests extends TestSupport {
 
         SslOptions sslOptions = SslOptions.builder() //
                 .jdkSslProvider() //
-                .truststore(truststoreURL()) //
+                .truststore(truststoreURL(truststoreFile2), "changeit") //
                 .build();
         setOptions(sslOptions);
 
@@ -322,7 +344,7 @@ class SslIntegrationTests extends TestSupport {
 
         SslOptions sslOptions = SslOptions.builder() //
                 .jdkSslProvider() //
-                .truststore(truststoreURL(), "knödel") //
+                .truststore(truststoreURL(truststoreFile0), "knödel") //
                 .build();
         setOptions(sslOptions);
 
@@ -363,7 +385,7 @@ class SslIntegrationTests extends TestSupport {
 
         SslOptions sslOptions = SslOptions.builder() //
                 .jdkSslProvider() //
-                .truststore(TRUSTSTORE_FILE) //
+                .truststore(truststoreFile2, "changeit") //
                 .build();
         setOptions(sslOptions);
 
@@ -375,7 +397,7 @@ class SslIntegrationTests extends TestSupport {
 
         SslOptions sslOptions = SslOptions.builder() //
                 .jdkSslProvider() //
-                .truststore(TRUSTSTORE_FILE) //
+                .truststore(truststoreFile0, "changeit") //
                 .build();
         setOptions(sslOptions);
 
@@ -415,8 +437,8 @@ class SslIntegrationTests extends TestSupport {
                 .map(builderCustomizer).map(RedisURI.Builder::build).collect(Collectors.toList());
     }
 
-    private URL truststoreURL() throws MalformedURLException {
-        return TRUSTSTORE_FILE.toURI().toURL();
+    private URL truststoreURL(File truststoreFile) throws MalformedURLException {
+        return truststoreFile.toURI().toURL();
     }
 
     private void setOptions(SslOptions sslOptions) {
diff --git a/src/test/java/io/lettuce/core/commands/AclCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/AclCommandIntegrationTests.java
index 4d154dc0f..7fbeb7cf9 100644
--- a/src/test/java/io/lettuce/core/commands/AclCommandIntegrationTests.java
+++ b/src/test/java/io/lettuce/core/commands/AclCommandIntegrationTests.java
@@ -131,7 +131,7 @@ public class AclCommandIntegrationTests extends TestSupport {
 
     @Test
     void aclList() {
-        assertThat(redis.aclList()).hasSize(2).first().asString().contains("user default");
+        assertThat(redis.aclList()).hasSize(1).first().asString().contains("user default");
     }
 
     @Test
@@ -161,7 +161,7 @@ public class AclCommandIntegrationTests extends TestSupport {
 
     @Test
     void aclUsers() {
-        assertThat(redis.aclUsers()).hasSize(2).first().isEqualTo("default");
+        assertThat(redis.aclUsers()).hasSize(1).first().isEqualTo("default");
     }
 
     @Test
diff --git a/src/test/java/io/lettuce/core/commands/CommandInterfacesIntegrationTests.java b/src/test/java/io/lettuce/core/commands/CommandInterfacesIntegrationTests.java
index 88325440a..87843459c 100644
--- a/src/test/java/io/lettuce/core/commands/CommandInterfacesIntegrationTests.java
+++ b/src/test/java/io/lettuce/core/commands/CommandInterfacesIntegrationTests.java
@@ -20,9 +20,7 @@
 package io.lettuce.core.commands;
 
 import io.lettuce.core.RedisClient;
-import io.lettuce.core.RedisContainerIntegrationTests;
 import io.lettuce.core.RedisURI;
-import io.lettuce.core.TestSupport;
 import io.lettuce.core.api.StatefulConnection;
 import io.lettuce.core.api.StatefulRedisConnection;
 import io.lettuce.core.api.sync.RedisCommands;
@@ -30,28 +28,17 @@ import io.lettuce.core.dynamic.Commands;
 import io.lettuce.core.dynamic.RedisCommandFactory;
 import io.lettuce.core.dynamic.annotation.Command;
 import io.lettuce.core.dynamic.annotation.Param;
-import io.lettuce.test.LettuceExtension;
-import io.lettuce.test.condition.EnabledOnCommand;
 import org.junit.jupiter.api.Assumptions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.TestInstance;
-import org.junit.jupiter.api.extension.ExtendWith;
 import reactor.core.publisher.Flux;
 
-import javax.inject.Inject;
 import java.lang.reflect.Proxy;
 import java.util.List;
 
 import static io.lettuce.TestTags.INTEGRATION_TEST;
-import static io.lettuce.core.SetArgs.Builder.ex;
-import static io.lettuce.core.SetArgs.Builder.exAt;
-import static io.lettuce.core.SetArgs.Builder.px;
-import static io.lettuce.core.SetArgs.Builder.pxAt;
-import static io.lettuce.core.StringMatchResult.Position;
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 /**
  * Integration tests for {@link io.lettuce.core.dynamic.annotation.Command}.
@@ -59,7 +46,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
  * @author Tihomir Mateev
  */
 @Tag(INTEGRATION_TEST)
-public class CommandInterfacesIntegrationTests extends RedisContainerIntegrationTests {
+public class CommandInterfacesIntegrationTests {
 
     protected static RedisClient client;
 
diff --git a/src/test/java/io/lettuce/core/commands/ConsolidatedAclCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/ConsolidatedAclCommandIntegrationTests.java
index f0519af54..08264b4b3 100644
--- a/src/test/java/io/lettuce/core/commands/ConsolidatedAclCommandIntegrationTests.java
+++ b/src/test/java/io/lettuce/core/commands/ConsolidatedAclCommandIntegrationTests.java
@@ -37,7 +37,7 @@ import static org.junit.jupiter.api.Assumptions.assumeTrue;
  * @author M Sazzadul Hoque
  */
 @Tag(INTEGRATION_TEST)
-public class ConsolidatedAclCommandIntegrationTests extends RedisContainerIntegrationTests {
+public class ConsolidatedAclCommandIntegrationTests {
 
     private static RedisClient client;
 
diff --git a/src/test/java/io/lettuce/core/commands/ConsolidatedConfigurationCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/ConsolidatedConfigurationCommandIntegrationTests.java
index 3a009fc47..f25adff7a 100644
--- a/src/test/java/io/lettuce/core/commands/ConsolidatedConfigurationCommandIntegrationTests.java
+++ b/src/test/java/io/lettuce/core/commands/ConsolidatedConfigurationCommandIntegrationTests.java
@@ -31,7 +31,6 @@ import io.lettuce.test.condition.RedisConditions;
 import org.junit.jupiter.api.*;
 
 import java.util.Collections;
-import java.util.Map;
 
 /**
  * Integration tests for {@link io.lettuce.core.api.sync.RedisServerCommands} with Redis modules since Redis 8.0.
@@ -39,7 +38,7 @@ import java.util.Map;
  * @author M Sazzadul Hoque
  */
 @Tag(INTEGRATION_TEST)
-public class ConsolidatedConfigurationCommandIntegrationTests extends RedisContainerIntegrationTests {
+public class ConsolidatedConfigurationCommandIntegrationTests {
 
     private static RedisClient client;
 
diff --git a/src/test/java/io/lettuce/core/commands/KeyCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/KeyCommandIntegrationTests.java
index c24e231f1..2e1ba647c 100644
--- a/src/test/java/io/lettuce/core/commands/KeyCommandIntegrationTests.java
+++ b/src/test/java/io/lettuce/core/commands/KeyCommandIntegrationTests.java
@@ -33,10 +33,8 @@ import java.util.Set;
 
 import javax.inject.Inject;
 
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Tag;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.TestInstance;
+import org.junit.Ignore;
+import org.junit.jupiter.api.*;
 import org.junit.jupiter.api.extension.ExtendWith;
 
 import io.lettuce.core.CopyArgs;
@@ -410,6 +408,7 @@ public class KeyCommandIntegrationTests extends TestSupport {
     }
 
     @Test
+    @Disabled("https://github.com/redis/lettuce/issues/3181")
     void restoreIdleTime() {
 
         redis.set(key, value);
diff --git a/src/test/java/io/lettuce/core/commands/RunOnlyOnceServerCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/RunOnlyOnceServerCommandIntegrationTests.java
index 2e94eb035..ca8233e02 100644
--- a/src/test/java/io/lettuce/core/commands/RunOnlyOnceServerCommandIntegrationTests.java
+++ b/src/test/java/io/lettuce/core/commands/RunOnlyOnceServerCommandIntegrationTests.java
@@ -9,9 +9,7 @@ import java.util.Arrays;
 
 import javax.inject.Inject;
 
-import org.junit.jupiter.api.Disabled;
-import org.junit.jupiter.api.Tag;
-import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.*;
 import org.junit.jupiter.api.extension.ExtendWith;
 
 import io.lettuce.core.MigrateArgs;
@@ -32,6 +30,7 @@ import io.lettuce.test.settings.TestSettings;
  */
 @Tag(INTEGRATION_TEST)
 @ExtendWith(LettuceExtension.class)
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
 class RunOnlyOnceServerCommandIntegrationTests extends TestSupport {
 
     private final RedisClient client;
@@ -54,6 +53,7 @@ class RunOnlyOnceServerCommandIntegrationTests extends TestSupport {
      */
     @Test
     @Disabled
+    @Order(1)
     void debugSegfault() {
 
         assumeTrue(CanConnect.to(host(), port(1)));
@@ -74,13 +74,14 @@ class RunOnlyOnceServerCommandIntegrationTests extends TestSupport {
      * Executed in order: 2
      */
     @Test
+    @Order(2)
     void migrate() {
 
-        assumeTrue(CanConnect.to(host(), port(2)));
+        assumeTrue(CanConnect.to(host(), port(7)));
 
         redis.set(key, value);
 
-        String result = redis.migrate("localhost", TestSettings.port(2), key, 0, 10);
+        String result = redis.migrate("localhost", TestSettings.port(7), key, 0, 10);
         assertThat(result).isEqualTo("OK");
     }
 
@@ -88,18 +89,19 @@ class RunOnlyOnceServerCommandIntegrationTests extends TestSupport {
      * Executed in order: 3
      */
     @Test
+    @Order(3)
     void migrateCopyReplace() {
 
-        assumeTrue(CanConnect.to(host(), port(2)));
+        assumeTrue(CanConnect.to(host(), port(7)));
 
         redis.set(key, value);
-        redis.set("key1", value);
         redis.set("key2", value);
+        redis.set("key3", value);
 
-        String result = redis.migrate("localhost", TestSettings.port(2), 0, 10, MigrateArgs.Builder.keys(key).copy().replace());
+        String result = redis.migrate("localhost", TestSettings.port(7), 0, 10, MigrateArgs.Builder.keys(key).copy().replace());
         assertThat(result).isEqualTo("OK");
 
-        result = redis.migrate("localhost", TestSettings.port(2), 0, 10,
+        result = redis.migrate("localhost", TestSettings.port(7), 0, 10,
                 MigrateArgs.Builder.keys(Arrays.asList("key1", "key2")).replace());
         assertThat(result).isEqualTo("OK");
     }
@@ -109,9 +111,10 @@ class RunOnlyOnceServerCommandIntegrationTests extends TestSupport {
      * redis.
      */
     @Test
+    @Order(4)
     void shutdown() {
 
-        assumeTrue(CanConnect.to(host(), port(2)));
+        assumeTrue(CanConnect.to(host(), port(7)));
 
         final RedisAsyncCommands<String, String> commands = client.connect(RedisURI.Builder.redis(host(), port(2)).build())
                 .async();
diff --git a/src/test/java/io/lettuce/core/dynamic/SimpleBatcherUnitTests.java b/src/test/java/io/lettuce/core/dynamic/SimpleBatcherUnitTests.java
index 592bfe29b..4b8861dfc 100644
--- a/src/test/java/io/lettuce/core/dynamic/SimpleBatcherUnitTests.java
+++ b/src/test/java/io/lettuce/core/dynamic/SimpleBatcherUnitTests.java
@@ -5,6 +5,7 @@ import static org.assertj.core.api.Assertions.*;
 import static org.mockito.Mockito.*;
 
 import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
 
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
@@ -21,6 +22,7 @@ import io.lettuce.core.protocol.RedisCommand;
 
 /**
  * @author Mark Paluch
+ * @author Ivo Gaydajiev
  */
 @Tag(UNIT_TEST)
 @ExtendWith(MockitoExtension.class)
@@ -127,6 +129,44 @@ class SimpleBatcherUnitTests {
         verify(connection).dispatch(Arrays.asList(c1, c2));
     }
 
+    @Test
+    void shouldDispatchCommandsQueuedDuringOngoingFlush() throws InterruptedException {
+        RedisCommand<Object, Object, Object> c1 = createCommand();
+        RedisCommand<Object, Object, Object> c2 = createCommand();
+
+        CountDownLatch batchFlushLatch1 = new CountDownLatch(1);
+        CountDownLatch batchFlushLatch2 = new CountDownLatch(1);
+
+        when(connection.dispatch((RedisCommand<Object, Object, Object>) any())).thenAnswer(invocation -> {
+            batchFlushLatch1.countDown();
+            batchFlushLatch2.await();
+
+            return null;
+        });
+
+        SimpleBatcher batcher = new SimpleBatcher(connection, 4);
+
+        Thread batchThread1 = new Thread(() -> {
+            batcher.batch(c1, CommandBatching.flush());
+        });
+        batchThread1.start();
+
+        Thread batchThread2 = new Thread(() -> {
+            try {
+                batchFlushLatch1.await();
+            } catch (InterruptedException ignored) {
+            }
+            batcher.batch(c2, CommandBatching.flush());
+            batchFlushLatch2.countDown();
+        });
+        batchThread2.start();
+
+        batchThread1.join();
+        batchThread2.join();
+        verify(connection, times(1)).dispatch(c1);
+        verify(connection, times(1)).dispatch(c2);
+    }
+
     private static RedisCommand<Object, Object, Object> createCommand() {
         return new AsyncCommand<>(new Command<>(CommandType.COMMAND, null, null));
     }
diff --git a/src/test/java/io/lettuce/core/json/RedisJsonClusterIntegrationTests.java b/src/test/java/io/lettuce/core/json/RedisJsonClusterIntegrationTests.java
index 5a3eefeeb..ee8d7c40c 100644
--- a/src/test/java/io/lettuce/core/json/RedisJsonClusterIntegrationTests.java
+++ b/src/test/java/io/lettuce/core/json/RedisJsonClusterIntegrationTests.java
@@ -7,7 +7,6 @@
 
 package io.lettuce.core.json;
 
-import io.lettuce.core.RedisContainerIntegrationTests;
 import io.lettuce.core.RedisURI;
 import io.lettuce.core.cluster.RedisClusterClient;
 import io.lettuce.core.cluster.api.async.RedisClusterAsyncCommands;
@@ -39,7 +38,7 @@ import static io.lettuce.TestTags.INTEGRATION_TEST;
 import static org.assertj.core.api.Assertions.assertThat;
 
 @Tag(INTEGRATION_TEST)
-public class RedisJsonClusterIntegrationTests extends RedisContainerIntegrationTests {
+public class RedisJsonClusterIntegrationTests {
 
     protected static RedisClusterClient client;
 
diff --git a/src/test/java/io/lettuce/core/json/RedisJsonIntegrationTests.java b/src/test/java/io/lettuce/core/json/RedisJsonIntegrationTests.java
index 76fdd0661..225dd749f 100644
--- a/src/test/java/io/lettuce/core/json/RedisJsonIntegrationTests.java
+++ b/src/test/java/io/lettuce/core/json/RedisJsonIntegrationTests.java
@@ -9,7 +9,6 @@ package io.lettuce.core.json;
 
 import io.lettuce.core.ClientOptions;
 import io.lettuce.core.RedisClient;
-import io.lettuce.core.RedisContainerIntegrationTests;
 import io.lettuce.core.RedisFuture;
 import io.lettuce.core.RedisURI;
 import io.lettuce.core.api.StatefulRedisConnection;
@@ -26,7 +25,6 @@ import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
-import reactor.core.publisher.Mono;
 import reactor.test.StepVerifier;
 
 import java.io.IOException;
@@ -43,7 +41,7 @@ import static io.lettuce.TestTags.INTEGRATION_TEST;
 import static org.assertj.core.api.Assertions.assertThat;
 
 @Tag(INTEGRATION_TEST)
-public class RedisJsonIntegrationTests extends RedisContainerIntegrationTests {
+public class RedisJsonIntegrationTests {
 
     private static final String BIKES_INVENTORY = "bikes:inventory";
 
@@ -159,6 +157,24 @@ public class RedisJsonIntegrationTests extends RedisContainerIntegrationTests {
                 "{\"id\":\"bike:3\",\"model\":\"Weywot\",\"description\":\"This bike gives kids aged six years and old");
     }
 
+    @Test
+    public void jsonArrpopEmptyArray() {
+        JsonValue value = redis.getJsonParser().createJsonValue("[\"one\"]");
+        redis.jsonSet("myKey", JsonPath.ROOT_PATH, value);
+        List<JsonValue> result = redis.jsonArrpop("myKey");
+        assertThat(result.toString()).isEqualTo("[\"one\"]");
+        assertThat(redis.jsonGet("myKey").get(0).toString()).isEqualTo("[]");
+    }
+
+    @Test
+    public void jsonArrpopWithRootPathAndIndex() {
+        JsonValue value = redis.getJsonParser().createJsonValue("[\"one\",\"two\",\"three\"]");
+        redis.jsonSet("myKey", JsonPath.ROOT_PATH, value);
+        List<JsonValue> result = redis.jsonArrpop("myKey", JsonPath.ROOT_PATH, 1);
+        assertThat(result.toString()).isEqualTo("[\"two\"]");
+        assertThat(redis.jsonGet("myKey").get(0).toString()).isEqualTo("[\"one\",\"three\"]");
+    }
+
     @ParameterizedTest(name = "With {0} as path")
     @ValueSource(strings = { BIKE_COLORS_V1, BIKE_COLORS_V2 })
     void jsonArrtrim(String path) {
@@ -601,7 +617,7 @@ public class RedisJsonIntegrationTests extends RedisContainerIntegrationTests {
         RedisURI redisURI = RedisURI.Builder.redis("127.0.0.1").withPort(16379).build();
 
         try (RedisClient client = RedisClient.create(redisURI)) {
-            client.setOptions(ClientOptions.builder().jsonParser(Mono.just(new CustomParser())).build());
+            client.setOptions(ClientOptions.builder().jsonParser(CustomParser::new).build());
             StatefulRedisConnection<String, String> connection = client.connect(StringCodec.UTF8);
             RedisCommands<String, String> redis = connection.sync();
             assertThat(redis.getJsonParser()).isInstanceOf(CustomParser.class);
diff --git a/src/test/java/io/lettuce/core/masterreplica/MasterReplicaSentinelSslIntegrationTests.java b/src/test/java/io/lettuce/core/masterreplica/MasterReplicaSentinelSslIntegrationTests.java
index 169783539..e6e0e00b3 100644
--- a/src/test/java/io/lettuce/core/masterreplica/MasterReplicaSentinelSslIntegrationTests.java
+++ b/src/test/java/io/lettuce/core/masterreplica/MasterReplicaSentinelSslIntegrationTests.java
@@ -2,14 +2,11 @@ package io.lettuce.core.masterreplica;
 
 import javax.inject.Inject;
 
+import io.lettuce.core.*;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 
-import io.lettuce.core.ReadFrom;
-import io.lettuce.core.RedisClient;
-import io.lettuce.core.RedisURI;
-import io.lettuce.core.TestSupport;
 import io.lettuce.core.codec.StringCodec;
 import io.lettuce.core.internal.HostAndPort;
 import io.lettuce.core.resource.ClientResources;
@@ -19,7 +16,15 @@ import io.lettuce.test.LettuceExtension;
 import io.lettuce.test.resource.FastShutdown;
 import io.lettuce.test.settings.TestSettings;
 
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
 import static io.lettuce.TestTags.INTEGRATION_TEST;
+import static io.lettuce.test.settings.TlsSettings.createAndSaveTestTruststore;
 
 /**
  * Integration test for Master/Replica using Redis Sentinel over SSL.
@@ -32,19 +37,33 @@ class MasterReplicaSentinelSslIntegrationTests extends TestSupport {
 
     private final ClientResources clientResources;
 
+    private static File truststoreFile;
+
+    private static Map<Integer, Integer> portMap = new HashMap<>();
+    static {
+        portMap.put(26379, 26822);
+        portMap.put(6482, 8443);
+        portMap.put(6483, 8444);
+    }
+
     @Inject
     MasterReplicaSentinelSslIntegrationTests(ClientResources clientResources) {
+
         this.clientResources = clientResources.mutate()
                 .socketAddressResolver(MappingSocketAddressResolver.create(DnsResolver.jvmDefault(), hostAndPort -> {
+                    int port = hostAndPort.getPort();
+                    if (portMap.containsKey(port)) {
+                        return HostAndPort.of(hostAndPort.getHostText(), portMap.get(port));
+                    }
 
-                    return HostAndPort.of(hostAndPort.getHostText(), hostAndPort.getPort() + 443);
+                    return hostAndPort;
                 })).build();
     }
 
     @Test
     void testMasterReplicaSentinelBasic() {
-
         RedisClient client = RedisClient.create(clientResources);
+
         RedisURI redisURI = RedisURI.create("rediss-sentinel://" + TestSettings.host() + ":26379?sentinelMasterId=mymaster");
         redisURI.setVerifyPeer(false);
         StatefulRedisMasterReplicaConnection<String, String> connection = MasterReplica.connect(client, StringCodec.UTF8,
diff --git a/src/test/java/io/lettuce/core/pubsub/PubSubCommandIntegrationTests.java b/src/test/java/io/lettuce/core/pubsub/PubSubCommandIntegrationTests.java
index 9936ba428..347266028 100644
--- a/src/test/java/io/lettuce/core/pubsub/PubSubCommandIntegrationTests.java
+++ b/src/test/java/io/lettuce/core/pubsub/PubSubCommandIntegrationTests.java
@@ -28,6 +28,7 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.regex.Matcher;
@@ -543,6 +544,15 @@ class PubSubCommandIntegrationTests extends AbstractRedisClientTest {
         assertThat(messages.take()).isEqualTo(shardMessage);
     }
 
+    @Test
+    void interleaveCommands() throws ExecutionException, InterruptedException {
+
+        // relay Double and Long
+        assertThat(pubsub.zadd("myzset", 1.0, "one").get()).isEqualTo(1L);
+        assertThat(pubsub.zadd("myzset", 2.0, "two").get()).isEqualTo(1L);
+        assertThat(pubsub.zpopmin("myzset", 1).get().get(0).getValue()).isEqualTo("one");
+    }
+
     @Test
     void adapter() throws Exception {
         final BlockingQueue<Long> localCounts = LettuceFactories.newBlockingQueue();
diff --git a/src/test/java/io/lettuce/core/sentinel/SentinelSslIntegrationTests.java b/src/test/java/io/lettuce/core/sentinel/SentinelSslIntegrationTests.java
index b8fdc0313..9fd831328 100644
--- a/src/test/java/io/lettuce/core/sentinel/SentinelSslIntegrationTests.java
+++ b/src/test/java/io/lettuce/core/sentinel/SentinelSslIntegrationTests.java
@@ -2,10 +2,15 @@ package io.lettuce.core.sentinel;
 
 import static io.lettuce.TestTags.INTEGRATION_TEST;
 import static io.lettuce.test.settings.TestSettings.sslPort;
+import static io.lettuce.test.settings.TlsSettings.createAndSaveTestTruststore;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assumptions.assumeTrue;
 
 import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
 
 import javax.inject.Inject;
 
@@ -36,23 +41,38 @@ import io.lettuce.test.settings.TestSettings;
 @ExtendWith(LettuceExtension.class)
 class SentinelSslIntegrationTests extends TestSupport {
 
-    private static final File TRUSTSTORE_FILE = new File("work/truststore.jks");
+    private static File truststoreFile;
 
     private final ClientResources clientResources;
 
+    private static Map<Integer, Integer> portMap = new HashMap<>();
+    static {
+        portMap.put(26379, 26822);
+        portMap.put(6482, 8443);
+        portMap.put(6483, 8444);
+    }
+
     @Inject
     SentinelSslIntegrationTests(ClientResources clientResources) {
+
         this.clientResources = clientResources.mutate()
                 .socketAddressResolver(MappingSocketAddressResolver.create(DnsResolver.jvmDefault(), hostAndPort -> {
+                    int port = hostAndPort.getPort();
+                    if (portMap.containsKey(port)) {
+                        return HostAndPort.of(hostAndPort.getHostText(), portMap.get(port));
+                    }
 
-                    return HostAndPort.of(hostAndPort.getHostText(), hostAndPort.getPort() + 443);
+                    return hostAndPort;
                 })).build();
     }
 
     @BeforeAll
     static void beforeAll() {
         assumeTrue(CanConnect.to(TestSettings.host(), sslPort()), "Assume that stunnel runs on port 6443");
-        assertThat(TRUSTSTORE_FILE).exists();
+        Path path2 = createAndSaveTestTruststore("redis-standalone-sentinel-controlled",
+                Paths.get("redis-standalone-sentinel-controlled/work/tls"), "changeit");
+        truststoreFile = path2.toFile();
+        assertThat(truststoreFile).exists();
     }
 
     @Test
@@ -75,7 +95,7 @@ class SentinelSslIntegrationTests extends TestSupport {
 
         RedisURI redisURI = RedisURI.create("rediss-sentinel://" + TestSettings.host() + ":" + RedisURI.DEFAULT_SENTINEL_PORT
                 + "?sentinelMasterId=mymaster");
-        SslOptions options = SslOptions.builder().truststore(TRUSTSTORE_FILE).build();
+        SslOptions options = SslOptions.builder().truststore(truststoreFile, "changeit").build();
 
         RedisClient client = RedisClient.create(clientResources);
         client.setOptions(ClientOptions.builder().sslOptions(options).build());
diff --git a/src/test/java/io/lettuce/core/support/CommonsPool2ConfigConverterUnitTests.java b/src/test/java/io/lettuce/core/support/CommonsPool2ConfigConverterUnitTests.java
index 82772e35c..7e6fea08f 100644
--- a/src/test/java/io/lettuce/core/support/CommonsPool2ConfigConverterUnitTests.java
+++ b/src/test/java/io/lettuce/core/support/CommonsPool2ConfigConverterUnitTests.java
@@ -19,22 +19,27 @@ import org.junit.jupiter.api.Test;
 @Tag(UNIT_TEST)
 class CommonsPool2ConfigConverterUnitTests {
 
+    private static final int MIN_IDLE_EXPECTED = 2;
+
+    private static final int MAX_IDLE_EXPECTED = 12;
+
+    private static final int MAX_TOTAL_EXPECTED = 13;
+
     @Test
     void shouldAdaptConfiguration() {
-
         GenericObjectPoolConfig<String> config = new GenericObjectPoolConfig<>();
-        config.setMinIdle(2);
-        config.setMaxIdle(12);
-        config.setMaxTotal(13);
+        config.setMinIdle(MIN_IDLE_EXPECTED);
+        config.setMaxIdle(MAX_IDLE_EXPECTED);
+        config.setMaxTotal(MAX_TOTAL_EXPECTED);
         config.setTestOnBorrow(true);
         config.setTestOnReturn(true);
         config.setTestOnCreate(true);
 
         BoundedPoolConfig result = CommonsPool2ConfigConverter.bounded(config);
 
-        assertThat(result.getMinIdle()).isEqualTo(2);
-        assertThat(result.getMaxIdle()).isEqualTo(12);
-        assertThat(result.getMaxTotal()).isEqualTo(13);
+        assertThat(result.getMinIdle()).isEqualTo(MIN_IDLE_EXPECTED);
+        assertThat(result.getMaxIdle()).isEqualTo(MAX_IDLE_EXPECTED);
+        assertThat(result.getMaxTotal()).isEqualTo(MAX_TOTAL_EXPECTED);
         assertThat(result.isTestOnAcquire()).isTrue();
         assertThat(result.isTestOnCreate()).isTrue();
         assertThat(result.isTestOnRelease()).isTrue();
diff --git a/src/test/java/io/lettuce/test/settings/TestSettings.java b/src/test/java/io/lettuce/test/settings/TestSettings.java
index cf5411b1e..3c56dfb5c 100644
--- a/src/test/java/io/lettuce/test/settings/TestSettings.java
+++ b/src/test/java/io/lettuce/test/settings/TestSettings.java
@@ -49,7 +49,7 @@ public class TestSettings {
      *         {@code -Ddomainsocket=YourSocket}
      */
     public static String socket() {
-        return System.getProperty("domainsocket", "work/socket-6479");
+        return System.getProperty("domainsocket", "work/socket-6482");
     }
 
     /**
diff --git a/src/test/java/io/lettuce/test/settings/TlsSettings.java b/src/test/java/io/lettuce/test/settings/TlsSettings.java
new file mode 100644
index 000000000..0187e84f1
--- /dev/null
+++ b/src/test/java/io/lettuce/test/settings/TlsSettings.java
@@ -0,0 +1,133 @@
+package io.lettuce.test.settings;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.*;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+public class TlsSettings {
+
+    private static final String TRUST_STORE_TYPE = "PKCS12";
+
+    private static final String TEST_WORK_FOLDER = System.getenv().getOrDefault("TEST_WORK_FOLDER", "work/docker");
+
+    private static final String TEST_SERVER_CERT = "redis.crt";
+
+    private static final String TEST_CLIENT_P12 = "client.p12";
+
+    private static final String TEST_CLIENT_CERT = "client.crt";
+
+    private static final String TEST_CLIENT_KEY = "client.key";
+
+    private static final String TEST_CA_CERT = "ca.crt";
+
+    private static final String TEST_TRUSTSTORE = "truststore.jks";
+
+    public static Path envClientP12(Path certLocation) {
+        return Paths.get(TEST_WORK_FOLDER, certLocation.toString(), TEST_CLIENT_P12);
+    }
+
+    public static Path envServerCert(Path certLocation) {
+        return Paths.get(TEST_WORK_FOLDER, certLocation.toString(), TEST_SERVER_CERT);
+    }
+
+    public static Path envCa(Path certLocation) {
+        return Paths.get(TEST_WORK_FOLDER, certLocation.toString(), TEST_CA_CERT);
+    }
+
+    public static Path testTruststorePath(String name) {
+        return Paths.get(TEST_WORK_FOLDER, name + '-' + TEST_TRUSTSTORE);
+    }
+
+    /**
+     * Creates an empty truststore.
+     *
+     * @return An empty KeyStore object.
+     * @throws KeyStoreException If there's an error initializing the truststore.
+     * @throws IOException If there's an error loading the truststore.
+     * @throws NoSuchAlgorithmException If the algorithm used to check the integrity of the truststore cannot be found.
+     * @throws CertificateException If any of the certificates in the truststore could not be loaded.
+     */
+    private static KeyStore createTruststore()
+            throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
+        KeyStore trustStore = KeyStore.getInstance(TRUST_STORE_TYPE);
+        trustStore.load(null, null);
+        return trustStore;
+    }
+
+    /**
+     * Loads an X.509 certificate from the given file path.
+     *
+     * @param certPath Path to the certificate file.
+     * @return An X509Certificate object.
+     * @throws Exception If there's an error loading the certificate.
+     */
+    private static X509Certificate loadCertificate(Path certPath) throws Exception {
+        try (FileInputStream fis = new FileInputStream(certPath.toFile())) {
+            CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+            return (X509Certificate) certFactory.generateCertificate(fis);
+        }
+    }
+
+    /**
+     * Adds a trusted certificate to the given truststore.
+     *
+     * @param trustStore The KeyStore object.
+     * @param alias Alias for the certificate.
+     * @param certPath Path to the certificate file.
+     * @throws Exception If there's an error adding the certificate.
+     */
+    private static void addTrustedCertificate(KeyStore trustStore, String alias, Path certPath) throws Exception {
+        X509Certificate cert = loadCertificate(certPath);
+        trustStore.setCertificateEntry(alias, cert);
+    }
+
+    /**
+     * Creates a truststore, adds multiple trusted certificates, and saves it to the specified path.
+     *
+     * @param trustedCertPaths List of certificate file paths to add to the truststore.
+     * @param truststorePath Path to save the generated truststore.
+     * @param truststorePassword Password for the truststore.
+     * @return Path to the saved truststore file.
+     */
+    public static Path createAndSaveTruststore(List<Path> trustedCertPaths, Path truststorePath, String truststorePassword) {
+        try {
+            KeyStore trustStore = createTruststore();
+
+            for (Path certPath : trustedCertPaths) {
+                addTrustedCertificate(trustStore, "trusted-cert-" + UUID.randomUUID(), certPath);
+            }
+
+            try (FileOutputStream fos = new FileOutputStream(truststorePath.toFile())) {
+                trustStore.store(fos, truststorePassword.toCharArray());
+            } catch (IOException e) {
+                throw new RuntimeException("Failed to save truststore to " + truststorePath + ": " + e.getMessage(), e);
+            }
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to create and save truststore: " + e.getMessage(), e);
+        }
+
+        return truststorePath;
+    }
+
+    public static Path createAndSaveTestTruststore(String trustStoreName, Path certificateLocations,
+            String truststorePassword) {
+        List<Path> trustedCertPaths = new ArrayList<>();
+        trustedCertPaths.add(envCa(certificateLocations).toAbsolutePath());
+        trustedCertPaths.add(envServerCert(certificateLocations).toAbsolutePath());
+
+        Path trustStorePath = testTruststorePath(trustStoreName).toAbsolutePath();
+
+        return createAndSaveTruststore(trustedCertPaths, trustStorePath, truststorePassword);
+    }
+
+}
diff --git a/src/test/java/io/redis/examples/async/StringExample.java b/src/test/java/io/redis/examples/async/StringExample.java
index 9be29395b..af856743e 100644
--- a/src/test/java/io/redis/examples/async/StringExample.java
+++ b/src/test/java/io/redis/examples/async/StringExample.java
@@ -7,15 +7,12 @@ import io.lettuce.core.api.StatefulRedisConnection;
 
 // REMOVE_START
 import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
 // REMOVE_END
 
 import java.util.*;
 import java.util.concurrent.CompletableFuture;
 
-// REMOVE_START
-import static org.assertj.core.api.Assertions.assertThat;
-// REMOVE_END
-
 public class StringExample {
 
     // REMOVE_START
@@ -29,7 +26,7 @@ public class StringExample {
 
             // STEP_START set_get
             CompletableFuture<Void> setAndGet = asyncCommands.set("bike:1", "Deimos").thenCompose(v -> {
-                System.out.println(v); // OK
+                System.out.println(v); // >>> OK
                 // REMOVE_START
                 assertThat(v).isEqualTo("OK");
                 // REMOVE_END
@@ -41,13 +38,16 @@ public class StringExample {
                         return res;
                     })
                     // REMOVE_END
-                    .thenAccept(System.out::println) // Deimos
+                    .thenAccept(System.out::println) // >>> Deimos
                     .toCompletableFuture();
             // STEP_END
+            // HIDE_START
+            setAndGet.join();
+            // HIDE_END
 
             // STEP_START setnx_xx
             CompletableFuture<Void> setnx = asyncCommands.setnx("bike:1", "bike").thenCompose(v -> {
-                System.out.println(v); // false (because key already exists)
+                System.out.println(v); // >>> false (because key already exists)
                 // REMOVE_START
                 assertThat(v).isFalse();
                 // REMOVE_END
@@ -59,8 +59,11 @@ public class StringExample {
                         return res;
                     })
                     // REMOVE_END
-                    .thenAccept(System.out::println) // Deimos (value is unchanged)
+                    .thenAccept(System.out::println) // >>> Deimos (value is unchanged)
                     .toCompletableFuture();
+            // HIDE_START
+            setnx.join();
+            // HIDE_END
 
             // set the value to "bike" if it already exists
             CompletableFuture<Void> setxx = asyncCommands.set("bike:1", "bike", SetArgs.Builder.xx())
@@ -70,8 +73,11 @@ public class StringExample {
                         return res;
                     })
                     // REMOVE_END
-                    .thenAccept(System.out::println) // OK
+                    .thenAccept(System.out::println) // >>> OK
                     .toCompletableFuture();
+            // HIDE_START
+            setxx.join();
+            // HIDE_END
             // STEP_END
 
             // STEP_START mset
@@ -81,7 +87,7 @@ public class StringExample {
             bikeMap.put("bike:3", "Vanth");
 
             CompletableFuture<Void> mset = asyncCommands.mset(bikeMap).thenCompose(v -> {
-                System.out.println(v); // OK
+                System.out.println(v); // >>> OK
                 return asyncCommands.mget("bike:1", "bike:2", "bike:3");
             })
                     // REMOVE_START
@@ -93,15 +99,19 @@ public class StringExample {
                         return res;
                     })
                     // REMOVE_END
-                    .thenAccept(System.out::println) // [KeyValue[bike:1, Deimos], KeyValue[bike:2, Ares], KeyValue[bike:3,
-                                                     // Vanth]]
+                    .thenAccept(System.out::println)
+                    // >>> [KeyValue[bike:1, Deimos], KeyValue[bike:2, Ares], KeyValue[bike:3,
+                    // Vanth]]
                     .toCompletableFuture();
             // STEP_END
+            // HIDE_START
+            mset.join();
+            // HIDE_END
 
             // STEP_START incr
             CompletableFuture<Void> incrby = asyncCommands.set("total_crashes", "0")
                     .thenCompose(v -> asyncCommands.incr("total_crashes")).thenCompose(v -> {
-                        System.out.println(v); // 1
+                        System.out.println(v); // >>> 1
                         // REMOVE_START
                         assertThat(v).isEqualTo(1L);
                         // REMOVE_END
@@ -113,12 +123,12 @@ public class StringExample {
                         return res;
                     })
                     // REMOVE_END
-                    .thenAccept(System.out::println) // 11
+                    .thenAccept(System.out::println) // >>> 11
                     .toCompletableFuture();
             // STEP_END
-
-            CompletableFuture.allOf(setAndGet, setnx, setxx, mset, incrby).join();
-
+            // HIDE_START
+            incrby.join();
+            // HIDE_END
         } finally {
             redisClient.shutdown();
         }
diff --git a/src/test/jmh/io/lettuce/core/dynamic/RedisCommandFactoryBenchmark.java b/src/test/jmh/io/lettuce/core/dynamic/RedisCommandFactoryBenchmark.java
index 83ce32599..631e18409 100644
--- a/src/test/jmh/io/lettuce/core/dynamic/RedisCommandFactoryBenchmark.java
+++ b/src/test/jmh/io/lettuce/core/dynamic/RedisCommandFactoryBenchmark.java
@@ -32,7 +32,7 @@ public class RedisCommandFactoryBenchmark {
         redisCommandFactory = new RedisCommandFactory(new MockStatefulConnection(EmptyRedisChannelWriter.INSTANCE));
         regularCommands = redisCommandFactory.getCommands(RegularCommands.class);
 
-        asyncCommands = new RedisAsyncCommandsImpl<>(EmptyStatefulRedisConnection.INSTANCE, StringCodec.UTF8, Mono.just(new DefaultJsonParser()));
+        asyncCommands = new RedisAsyncCommandsImpl<>(EmptyStatefulRedisConnection.INSTANCE, StringCodec.UTF8);
     }
 
     @Benchmark
diff --git a/src/test/resources/docker-env/.env b/src/test/resources/docker-env/.env
new file mode 100644
index 000000000..8949c9f58
--- /dev/null
+++ b/src/test/resources/docker-env/.env
@@ -0,0 +1,3 @@
+REDIS_ENV_WORK_DIR=../../../../work/docker
+REDIS_VERSION=8.0-M05-pre
+REDIS_STACK_VERSION=8.0-M05-pre
\ No newline at end of file
diff --git a/src/test/resources/docker-env/.env.v7.2 b/src/test/resources/docker-env/.env.v7.2
new file mode 100644
index 000000000..35a627f2d
--- /dev/null
+++ b/src/test/resources/docker-env/.env.v7.2
@@ -0,0 +1,3 @@
+REDIS_ENV_WORK_DIR=../../../../work/docker
+REDIS_VERSION=7.2.7
+REDIS_STACK_VERSION=rs-7.2.0-v14
\ No newline at end of file
diff --git a/src/test/resources/docker-env/.env.v7.4 b/src/test/resources/docker-env/.env.v7.4
new file mode 100644
index 000000000..68aba20a3
--- /dev/null
+++ b/src/test/resources/docker-env/.env.v7.4
@@ -0,0 +1,3 @@
+REDIS_ENV_WORK_DIR=../../../../work/docker
+REDIS_VERSION=7.4.2
+REDIS_STACK_VERSION=rs-7.4.0-v2
\ No newline at end of file
diff --git a/src/test/resources/docker-env/docker-compose.yml b/src/test/resources/docker-env/docker-compose.yml
new file mode 100644
index 000000000..cc4b6bb28
--- /dev/null
+++ b/src/test/resources/docker-env/docker-compose.yml
@@ -0,0 +1,158 @@
+x-client-libs-image: &client-libs-image
+  image: "redislabs/client-libs-test:${REDIS_VERSION:-8.0-M05-pre}"
+x-client-libs-stack-image: &client-libs-stack-image
+  image: "redislabs/client-libs-test:${REDIS_STACK_VERSION:-8.0-M05-pre}"
+
+services:
+  # Standalone Redis Servers
+  redis-standalone-0:
+    <<: *client-libs-image
+    container_name: redis-standalone-0
+    environment:
+      - TLS_ENABLED=yes
+    volumes:
+      - ./redis-standalone-0/config:/redis/config:r
+      - ${REDIS_ENV_WORK_DIR}/redis-standalone-0/work:/redis/work:rw
+    ports:
+      - "6478:6478"
+      - "6444:6444" # TLS Port
+    networks:
+      - redis-network
+
+  redis-standalone-1:
+    <<: *client-libs-image
+    container_name: redis-standalone-1
+    environment:
+      - TLS_ENABLED=yes
+    volumes:
+      - ./redis-standalone-1/config:/redis/config:r
+      - ${REDIS_ENV_WORK_DIR}/redis-standalone-1/work:/redis/work:rw
+    ports:
+      - "6479:6479"
+      - "6486:6486"
+      - "6443:6443" # TLS Port
+    networks:
+      - redis-network
+
+  redis-standalone-2:
+    <<: *client-libs-image
+    container_name: redis-standalone-2
+    volumes:
+      - ./redis-standalone-2/config:/redis/config:r
+    ports:
+      - "6480:6480"
+    networks:
+      - redis-network
+
+  redis-standalone-3:
+    <<: *client-libs-image
+    container_name: redis-standalone-3
+    volumes:
+      - ./redis-standalone-3/config:/redis/config:r
+    ports:
+      - "6481:6481"
+    networks:
+      - redis-network
+
+  redis-standalone-4:
+    <<: *client-libs-image
+    container_name: redis-standalone-4
+    volumes:
+      - ./redis-standalone-4/config:/redis/config:r
+
+    environment:
+      - REDIS_CLUSTER=no
+    ports:
+      - "6484:6484"
+      - "26381:26381"
+    networks:
+      - redis-network
+
+  redis-standalone-5-client-cert:
+    <<: *client-libs-image
+    container_name: redis-standalone-5-client-cert
+    environment:
+      - TLS_ENABLED=yes
+    volumes:
+      - ./redis-standalone-5-client-cert/config:/redis/config:r
+      - ${REDIS_ENV_WORK_DIR}/redis-standalone-5-client-cert/work:/redis/work:rw
+    ports:
+      - "6485:6485"
+      - "6445:6445" # TLS Port
+    networks:
+      - redis-network
+
+  redis-standalone-sentinel-controlled:
+    <<: *client-libs-image
+    container_name: redis-standalone-sentinel-controlled
+    environment:
+      - REDIS_CLUSTER=no
+      - TLS_ENABLED=yes
+    volumes:
+      - ./redis-standalone-sentinel-controlled/config:/redis/config:r
+      - ${REDIS_ENV_WORK_DIR}/redis-standalone-sentinel-controlled/work:/redis/work:rw
+      - ${REDIS_ENV_WORK_DIR}:/work
+    ports:
+      - "26380:26380"
+      - "26822:26822" # sentinel tls port
+      - "26379:26379"
+      - "6482:6482"
+      - "6483:6483"
+      - "8443:8443" # TLS Port
+      - "8444:8444" # TLS Port
+    networks:
+      - redis-network
+
+  standalone-stack:
+    <<: *client-libs-stack-image
+    environment:
+      - REDIS_CLUSTER=no
+      - PORT=6379
+    ports:
+      - "16379:6379"
+
+  clustered-stack:
+    <<: *client-libs-stack-image
+    environment:
+      - REDIS_CLUSTER=yes
+      - PORT=36379
+    ports:
+      - "36379-36381:36379-36381"
+
+  ssl-test-cluster:
+    <<: *client-libs-image
+    container_name: ssl-test-cluster
+    environment:
+      - REDIS_CLUSTER=yes
+    volumes:
+      - ./ssl-test-cluster/config:/redis/config:r
+    ports:
+      - "7479:7479"
+      - "7480:7480"
+      - "7481:7481"
+    networks:
+      - redis-network
+
+  # Non-SSL Cluster
+  test-cluster:
+    <<: *client-libs-image
+    container_name: test-cluster
+    environment:
+      - REDIS_CLUSTER=yes
+    volumes:
+      - ./test-cluster/config:/redis/config:r
+    ports:
+      - "7379:7379"
+      - "7380:7380"
+      - "7381:7381"
+      - "7382:7382"
+      - "7383:7383"
+      - "7384:7384"
+      - "7385:7385"
+      - "7582:7582"
+    networks:
+      - redis-network
+
+networks:
+  redis-network:
+    driver: bridge
diff --git a/src/test/resources/docker-env/redis-standalone-0/config/node-6478/redis.conf b/src/test/resources/docker-env/redis-standalone-0/config/node-6478/redis.conf
new file mode 100644
index 000000000..6fa6ea217
--- /dev/null
+++ b/src/test/resources/docker-env/redis-standalone-0/config/node-6478/redis.conf
@@ -0,0 +1,10 @@
+port 6478
+tls-port 6444
+tls-auth-clients no
+save ""
+appendonly no
+client-output-buffer-limit pubsub 256k 128k 5
+enable-debug-command yes
+unixsocket /work/socket-6478
+unixsocketperm 777
+replica-announce-ip localhost
\ No newline at end of file
diff --git a/src/test/resources/docker-env/redis-standalone-1/config/node-6479/redis.conf b/src/test/resources/docker-env/redis-standalone-1/config/node-6479/redis.conf
new file mode 100644
index 000000000..544300d1e
--- /dev/null
+++ b/src/test/resources/docker-env/redis-standalone-1/config/node-6479/redis.conf
@@ -0,0 +1,10 @@
+port 6479
+tls-port 6443
+tls-auth-clients no
+save ""
+appendonly no
+client-output-buffer-limit pubsub 256k 128k 5
+enable-debug-command yes
+unixsocket /work/socket-6479
+unixsocketperm 777
+replica-announce-ip localhost
\ No newline at end of file
diff --git a/src/test/resources/docker-env/redis-standalone-1/config/node-6486/redis.conf b/src/test/resources/docker-env/redis-standalone-1/config/node-6486/redis.conf
new file mode 100644
index 000000000..4fa8e59d3
--- /dev/null
+++ b/src/test/resources/docker-env/redis-standalone-1/config/node-6486/redis.conf
@@ -0,0 +1,8 @@
+port 6486
+save ""
+appendonly no
+client-output-buffer-limit pubsub 256k 128k 5
+unixsocket /work/socket-6486
+unixsocketperm 777
+enable-debug-command yes
+replica-announce-ip localhost
\ No newline at end of file
diff --git a/src/test/resources/docker-env/redis-standalone-2/config/node-6480/redis.conf b/src/test/resources/docker-env/redis-standalone-2/config/node-6480/redis.conf
new file mode 100644
index 000000000..ebaf1511a
--- /dev/null
+++ b/src/test/resources/docker-env/redis-standalone-2/config/node-6480/redis.conf
@@ -0,0 +1,8 @@
+port 6480
+save ""
+appendonly no
+client-output-buffer-limit pubsub 256k 128k 5
+unixsocket /work/socket-6480
+unixsocketperm 777
+enable-debug-command yes
+replica-announce-ip localhost
\ No newline at end of file
diff --git a/src/test/resources/docker-env/redis-standalone-3/config/node-6481/redis.conf b/src/test/resources/docker-env/redis-standalone-3/config/node-6481/redis.conf
new file mode 100644
index 000000000..25e45088c
--- /dev/null
+++ b/src/test/resources/docker-env/redis-standalone-3/config/node-6481/redis.conf
@@ -0,0 +1,8 @@
+port 6481
+save ""
+appendonly no
+client-output-buffer-limit pubsub 256k 128k 5
+unixsocket /work/socket-6481
+unixsocketperm 777
+enable-debug-command yes
+replica-announce-ip localhost
\ No newline at end of file
diff --git a/src/test/resources/docker-env/redis-standalone-4/config/node-6484/redis.conf b/src/test/resources/docker-env/redis-standalone-4/config/node-6484/redis.conf
new file mode 100644
index 000000000..1afd6beb3
--- /dev/null
+++ b/src/test/resources/docker-env/redis-standalone-4/config/node-6484/redis.conf
@@ -0,0 +1,8 @@
+port 6484
+save ""
+appendonly no
+client-output-buffer-limit pubsub 256k 128k 5
+unixsocket /work/socket-6484
+unixsocketperm 777
+enable-debug-command yes
+replica-announce-ip localhost
\ No newline at end of file
diff --git a/src/test/resources/docker-env/redis-standalone-4/config/node-sentinel-26381/redis.conf b/src/test/resources/docker-env/redis-standalone-4/config/node-sentinel-26381/redis.conf
new file mode 100644
index 000000000..5208b41f5
--- /dev/null
+++ b/src/test/resources/docker-env/redis-standalone-4/config/node-sentinel-26381/redis.conf
@@ -0,0 +1,11 @@
+port 26381
+sentinel monitor mymaster localhost 6484 1
+sentinel announce-hostnames yes
+sentinel resolve-hostnames yes
+sentinel announce-ip localhost
+sentinel down-after-milliseconds mymaster 200
+sentinel failover-timeout mymaster 200
+sentinel parallel-syncs mymaster 1
+sentinel auth-pass mymaster foobared
+unixsocket /work/socket-26381
+unixsocketperm 777
\ No newline at end of file
diff --git a/src/test/resources/docker-env/redis-standalone-5-client-cert/config/node-6478/redis.conf b/src/test/resources/docker-env/redis-standalone-5-client-cert/config/node-6478/redis.conf
new file mode 100644
index 000000000..e5e275ca4
--- /dev/null
+++ b/src/test/resources/docker-env/redis-standalone-5-client-cert/config/node-6478/redis.conf
@@ -0,0 +1,8 @@
+port 6485
+tls-port 6445
+#tls-auth-clients no
+save ""
+appendonly no
+client-output-buffer-limit pubsub 256k 128k 5
+enable-debug-command yes
+replica-announce-ip localhost
\ No newline at end of file
diff --git a/src/test/resources/docker-env/redis-standalone-sentinel-controlled/config/node-6482/redis.conf b/src/test/resources/docker-env/redis-standalone-sentinel-controlled/config/node-6482/redis.conf
new file mode 100644
index 000000000..1b15dfab2
--- /dev/null
+++ b/src/test/resources/docker-env/redis-standalone-sentinel-controlled/config/node-6482/redis.conf
@@ -0,0 +1,10 @@
+port 6482
+tls-port 8443
+tls-auth-clients no
+save ""
+appendonly no
+client-output-buffer-limit pubsub 256k 128k 5
+unixsocket /work/socket-6482
+unixsocketperm 777
+enable-debug-command yes
+replica-announce-ip localhost
\ No newline at end of file
diff --git a/src/test/resources/docker-env/redis-standalone-sentinel-controlled/config/node-6483/redis.conf b/src/test/resources/docker-env/redis-standalone-sentinel-controlled/config/node-6483/redis.conf
new file mode 100644
index 000000000..2d40968e1
--- /dev/null
+++ b/src/test/resources/docker-env/redis-standalone-sentinel-controlled/config/node-6483/redis.conf
@@ -0,0 +1,11 @@
+port 6483
+tls-port 8444
+tls-auth-clients no
+save ""
+appendonly no
+client-output-buffer-limit pubsub 256k 128k 5
+unixsocket /work/socket-6483
+unixsocketperm 777
+enable-debug-command yes
+slaveof 127.0.0.1 6482
+replica-announce-ip localhost
\ No newline at end of file
diff --git a/src/test/resources/docker-env/redis-standalone-sentinel-controlled/config/node-sentinel-26379/redis.conf b/src/test/resources/docker-env/redis-standalone-sentinel-controlled/config/node-sentinel-26379/redis.conf
new file mode 100644
index 000000000..96b2c5beb
--- /dev/null
+++ b/src/test/resources/docker-env/redis-standalone-sentinel-controlled/config/node-sentinel-26379/redis.conf
@@ -0,0 +1,12 @@
+port 26379
+tls-port 26822
+tls-auth-clients no
+sentinel monitor mymaster localhost 6482 1
+sentinel announce-hostnames yes
+sentinel resolve-hostnames yes
+sentinel announce-ip localhost
+sentinel down-after-milliseconds mymaster 200
+sentinel failover-timeout mymaster 200
+sentinel parallel-syncs mymaster 1
+unixsocket /work/socket-26379
+unixsocketperm 777
diff --git a/src/test/resources/docker-env/redis-standalone-sentinel-controlled/config/node-sentinel-26380/redis.conf b/src/test/resources/docker-env/redis-standalone-sentinel-controlled/config/node-sentinel-26380/redis.conf
new file mode 100644
index 000000000..5eba90ec3
--- /dev/null
+++ b/src/test/resources/docker-env/redis-standalone-sentinel-controlled/config/node-sentinel-26380/redis.conf
@@ -0,0 +1,10 @@
+port 26380
+sentinel monitor mymaster localhost 6482 1
+sentinel announce-hostnames yes
+sentinel resolve-hostnames yes
+sentinel announce-ip localhost
+sentinel down-after-milliseconds mymaster 200
+sentinel failover-timeout mymaster 200
+sentinel parallel-syncs mymaster 1
+unixsocket /work/socket-26380
+unixsocketperm 777
\ No newline at end of file
diff --git a/src/test/resources/docker-env/ssl-test-cluster/config/node-7479/nodes.conf b/src/test/resources/docker-env/ssl-test-cluster/config/node-7479/nodes.conf
new file mode 100644
index 000000000..d578e24d7
--- /dev/null
+++ b/src/test/resources/docker-env/ssl-test-cluster/config/node-7479/nodes.conf
@@ -0,0 +1,4 @@
+cf2354ef19ee813a962350b51438314aebce1fe2 127.0.0.1:7479@17479 myself,master - 0 1578163609000 0 connected 0-10000
+cac8e053dd6f85fab470be57d29dcbac2a4b85c4 127.0.0.1:7480@17480 slave cf2354ef19ee813a962350b51438314aebce1fe2 0 1578163609301 1 connected
+6554e5b1b158dccd4b1d9ca294a3e46a2d3e556d 127.0.0.1:7481@17481 master - 0 1578163609301 2 connected 10001-16383
+vars currentEpoch 2 lastVoteEpoch 0
\ No newline at end of file
diff --git a/src/test/resources/docker-env/ssl-test-cluster/config/node-7479/redis.conf b/src/test/resources/docker-env/ssl-test-cluster/config/node-7479/redis.conf
new file mode 100644
index 000000000..8a51dd683
--- /dev/null
+++ b/src/test/resources/docker-env/ssl-test-cluster/config/node-7479/redis.conf
@@ -0,0 +1,7 @@
+port 7479
+save ""
+appendonly no
+cluster-enabled yes
+cluster-node-timeout 150
+cluster-announce-port 7445
+requirepass foobared
\ No newline at end of file
diff --git a/src/test/resources/docker-env/ssl-test-cluster/config/node-7480/nodes.conf b/src/test/resources/docker-env/ssl-test-cluster/config/node-7480/nodes.conf
new file mode 100644
index 000000000..8da078b92
--- /dev/null
+++ b/src/test/resources/docker-env/ssl-test-cluster/config/node-7480/nodes.conf
@@ -0,0 +1,4 @@
+cf2354ef19ee813a962350b51438314aebce1fe2 127.0.0.1:7479@17479 master - 0 1578163609245 0 connected  0-10000
+cac8e053dd6f85fab470be57d29dcbac2a4b85c4 127.0.0.1:7480@17480 myself,slave cf2354ef19ee813a962350b51438314aebce1fe2 0 1578163609000 1 connected
+6554e5b1b158dccd4b1d9ca294a3e46a2d3e556d 127.0.0.1:7481@17481 master - 0 1578163609245 2 connected 10001-16383
+vars currentEpoch 2 lastVoteEpoch 0
\ No newline at end of file
diff --git a/src/test/resources/docker-env/ssl-test-cluster/config/node-7480/redis.conf b/src/test/resources/docker-env/ssl-test-cluster/config/node-7480/redis.conf
new file mode 100644
index 000000000..190236043
--- /dev/null
+++ b/src/test/resources/docker-env/ssl-test-cluster/config/node-7480/redis.conf
@@ -0,0 +1,7 @@
+port 7480
+save ""
+appendonly no
+cluster-enabled yes
+cluster-node-timeout 150
+cluster-announce-port 7444
+requirepass foobared
\ No newline at end of file
diff --git a/src/test/resources/docker-env/ssl-test-cluster/config/node-7481/nodes.conf b/src/test/resources/docker-env/ssl-test-cluster/config/node-7481/nodes.conf
new file mode 100644
index 000000000..fdca668d2
--- /dev/null
+++ b/src/test/resources/docker-env/ssl-test-cluster/config/node-7481/nodes.conf
@@ -0,0 +1,4 @@
+cac8e053dd6f85fab470be57d29dcbac2a4b85c4 127.0.0.1:7480@17480 slave cf2354ef19ee813a962350b51438314aebce1fe2 0 1578163609279 1 connected
+cf2354ef19ee813a962350b51438314aebce1fe2 127.0.0.1:7479@17479 master - 0 1578163609279 0 connected 0-10000
+6554e5b1b158dccd4b1d9ca294a3e46a2d3e556d 127.0.0.1:7481@17481 myself,master - 0 1578163609000 2 connected 10001-16383
+vars currentEpoch 2 lastVoteEpoch 0
\ No newline at end of file
diff --git a/src/test/resources/docker-env/ssl-test-cluster/config/node-7481/redis.conf b/src/test/resources/docker-env/ssl-test-cluster/config/node-7481/redis.conf
new file mode 100644
index 000000000..3892728a3
--- /dev/null
+++ b/src/test/resources/docker-env/ssl-test-cluster/config/node-7481/redis.conf
@@ -0,0 +1,7 @@
+port 7481
+save ""
+appendonly no
+cluster-enabled yes
+cluster-node-timeout 150
+cluster-announce-port 7445
+requirepass foobared
\ No newline at end of file
diff --git a/src/test/resources/docker-env/test-cluster/config/node-7379/nodes.conf b/src/test/resources/docker-env/test-cluster/config/node-7379/nodes.conf
new file mode 100644
index 000000000..8a59dbabd
--- /dev/null
+++ b/src/test/resources/docker-env/test-cluster/config/node-7379/nodes.conf
@@ -0,0 +1,5 @@
+c2043458aa5646cee429fdd5e3c18220dddf2ce5 127.0.0.1:7380 master - 1434887920102 1434887920002 0 connected 12000-16383
+27f88788f03a86296b7d860152f4ae24ee59c8c9 127.0.0.1:7379 myself,master - 0 0 1 connected 0-11999
+2c07344ffa94ede5ea57a2367f190af6144c1adb 127.0.0.1:7382 slave c2043458aa5646cee429fdd5e3c18220dddf2ce5 1434887920102 1434887920002 2 connected
+1c541b6daf98719769e6aacf338a7d81f108a180 127.0.0.1:7381 slave 27f88788f03a86296b7d860152f4ae24ee59c8c9 1434887920102 1434887920002 3 connected
+vars currentEpoch 3 lastVoteEpoch 0
\ No newline at end of file
diff --git a/src/test/resources/docker-env/test-cluster/config/node-7379/redis.conf b/src/test/resources/docker-env/test-cluster/config/node-7379/redis.conf
new file mode 100644
index 000000000..61ab74a1f
--- /dev/null
+++ b/src/test/resources/docker-env/test-cluster/config/node-7379/redis.conf
@@ -0,0 +1,7 @@
+port 7379
+save ""
+appendonly no
+client-output-buffer-limit pubsub 256k 128k 5
+unixsocket /work/socket-7379
+cluster-enabled yes
+cluster-node-timeout 150
\ No newline at end of file
diff --git a/src/test/resources/docker-env/test-cluster/config/node-7380/nodes.conf b/src/test/resources/docker-env/test-cluster/config/node-7380/nodes.conf
new file mode 100644
index 000000000..f71c380f9
--- /dev/null
+++ b/src/test/resources/docker-env/test-cluster/config/node-7380/nodes.conf
@@ -0,0 +1,5 @@
+2c07344ffa94ede5ea57a2367f190af6144c1adb 127.0.0.1:7382 slave c2043458aa5646cee429fdd5e3c18220dddf2ce5 1434887920102 1434887920002 2 connected
+27f88788f03a86296b7d860152f4ae24ee59c8c9 127.0.0.1:7379 master - 1434887920102 1434887920002 1 connected 0-11999
+1c541b6daf98719769e6aacf338a7d81f108a180 127.0.0.1:7381 slave 27f88788f03a86296b7d860152f4ae24ee59c8c9 1434887920102 1434887920002 3 connected
+c2043458aa5646cee429fdd5e3c18220dddf2ce5 127.0.0.1:7380 myself,master - 0 0 0 connected 12000-16383
+vars currentEpoch 3 lastVoteEpoch 0
\ No newline at end of file
diff --git a/src/test/resources/docker-env/test-cluster/config/node-7380/redis.conf b/src/test/resources/docker-env/test-cluster/config/node-7380/redis.conf
new file mode 100644
index 000000000..2f2ed61e2
--- /dev/null
+++ b/src/test/resources/docker-env/test-cluster/config/node-7380/redis.conf
@@ -0,0 +1,7 @@
+port 7380
+save ""
+appendonly no
+client-output-buffer-limit pubsub 256k 128k 5
+unixsocket /work/socket-7380
+cluster-enabled yes
+cluster-node-timeout 150
\ No newline at end of file
diff --git a/src/test/resources/docker-env/test-cluster/config/node-7381/nodes.conf b/src/test/resources/docker-env/test-cluster/config/node-7381/nodes.conf
new file mode 100644
index 000000000..aed925c86
--- /dev/null
+++ b/src/test/resources/docker-env/test-cluster/config/node-7381/nodes.conf
@@ -0,0 +1,5 @@
+1c541b6daf98719769e6aacf338a7d81f108a180 127.0.0.1:7381 myself,slave 27f88788f03a86296b7d860152f4ae24ee59c8c9 0 0 3 connected
+2c07344ffa94ede5ea57a2367f190af6144c1adb 127.0.0.1:7382 slave c2043458aa5646cee429fdd5e3c18220dddf2ce5 1434887920102 1434887920002 2 connected
+c2043458aa5646cee429fdd5e3c18220dddf2ce5 127.0.0.1:7380 master - 1434887920102 1434887920002 0 connected 12000-16383
+27f88788f03a86296b7d860152f4ae24ee59c8c9 127.0.0.1:7379 master - 1434887920102 1434887920002 1 connected 0-11999
+vars currentEpoch 3 lastVoteEpoch 0
\ No newline at end of file
diff --git a/src/test/resources/docker-env/test-cluster/config/node-7381/redis.conf b/src/test/resources/docker-env/test-cluster/config/node-7381/redis.conf
new file mode 100644
index 000000000..0bfd25910
--- /dev/null
+++ b/src/test/resources/docker-env/test-cluster/config/node-7381/redis.conf
@@ -0,0 +1,7 @@
+port 7381
+save ""
+appendonly no
+client-output-buffer-limit pubsub 256k 128k 5
+unixsocket /work/socket-7381
+cluster-enabled yes
+cluster-node-timeout 150
\ No newline at end of file
diff --git a/src/test/resources/docker-env/test-cluster/config/node-7382/nodes.conf b/src/test/resources/docker-env/test-cluster/config/node-7382/nodes.conf
new file mode 100644
index 000000000..bdcf179a5
--- /dev/null
+++ b/src/test/resources/docker-env/test-cluster/config/node-7382/nodes.conf
@@ -0,0 +1,5 @@
+c2043458aa5646cee429fdd5e3c18220dddf2ce5 127.0.0.1:7380 master - 0 1434887920102 0 connected 12000-16383
+1c541b6daf98719769e6aacf338a7d81f108a180 127.0.0.1:7381 slave 27f88788f03a86296b7d860152f4ae24ee59c8c9 0 1434887920102 3 connected
+2c07344ffa94ede5ea57a2367f190af6144c1adb 127.0.0.1:7382 myself,slave c2043458aa5646cee429fdd5e3c18220dddf2ce5 0 0 2 connected
+27f88788f03a86296b7d860152f4ae24ee59c8c9 127.0.0.1:7379 master - 0 1434887920102 1 connected 0-11999
+vars currentEpoch 3 lastVoteEpoch 0
\ No newline at end of file
diff --git a/src/test/resources/docker-env/test-cluster/config/node-7382/redis.conf b/src/test/resources/docker-env/test-cluster/config/node-7382/redis.conf
new file mode 100644
index 000000000..a658fe114
--- /dev/null
+++ b/src/test/resources/docker-env/test-cluster/config/node-7382/redis.conf
@@ -0,0 +1,7 @@
+port 7382
+save ""
+appendonly no
+client-output-buffer-limit pubsub 256k 128k 5
+unixsocket /work/socket-7382
+cluster-enabled yes
+cluster-node-timeout 150
\ No newline at end of file
diff --git a/src/test/resources/docker-env/test-cluster/config/node-7383/redis.conf b/src/test/resources/docker-env/test-cluster/config/node-7383/redis.conf
new file mode 100644
index 000000000..262122f52
--- /dev/null
+++ b/src/test/resources/docker-env/test-cluster/config/node-7383/redis.conf
@@ -0,0 +1,7 @@
+port 7383
+save ""
+appendonly no
+client-output-buffer-limit pubsub 256k 128k 5
+unixsocket /work/socket-7383
+cluster-enabled yes
+cluster-node-timeout 150
\ No newline at end of file
diff --git a/src/test/resources/docker-env/test-cluster/config/node-7384/redis.conf b/src/test/resources/docker-env/test-cluster/config/node-7384/redis.conf
new file mode 100644
index 000000000..1594941ae
--- /dev/null
+++ b/src/test/resources/docker-env/test-cluster/config/node-7384/redis.conf
@@ -0,0 +1,7 @@
+port 7384
+save ""
+appendonly no
+client-output-buffer-limit pubsub 256k 128k 5
+unixsocket /work/socket-7384
+cluster-enabled yes
+cluster-node-timeout 150
\ No newline at end of file
diff --git a/src/test/resources/docker-env/test-cluster/config/node-7385/redis.conf b/src/test/resources/docker-env/test-cluster/config/node-7385/redis.conf
new file mode 100644
index 000000000..3aee9a7da
--- /dev/null
+++ b/src/test/resources/docker-env/test-cluster/config/node-7385/redis.conf
@@ -0,0 +1,8 @@
+port 7385
+save ""
+appendonly no
+client-output-buffer-limit pubsub 256k 128k 5
+unixsocket /work/socket-7385
+cluster-enabled yes
+cluster-node-timeout 150
+requirepass foobared
\ No newline at end of file
diff --git a/src/test/resources/docker-env/test-cluster/config/node-7582/nodes.conf b/src/test/resources/docker-env/test-cluster/config/node-7582/nodes.conf
new file mode 100644
index 000000000..1c71f1fd5
--- /dev/null
+++ b/src/test/resources/docker-env/test-cluster/config/node-7582/nodes.conf
@@ -0,0 +1,5 @@
+c2043458aa5646cee429fdd5e3c18220dddf2ce5 127.0.0.1:7580 master - 1434887920102 1434887920002 0 connected 10001-16383
+27f88788f03a86296b7d860152f4ae24ee59c8c9 127.0.0.1:7579 myself,master - 0 0 1 connected 0-10000
+2c07344ffa94ede5ea57a2367f190af6144c1adb 127.0.0.1:7582 slave c2043458aa5646cee429fdd5e3c18220dddf2ce5 1434887920102 1434887920002 2 connected
+1c541b6daf98719769e6aacf338a7d81f108a180 127.0.0.1:7581 slave 27f88788f03a86296b7d860152f4ae24ee59c8c9 1434887920102 1434887920002 3 connected
+vars currentEpoch 3 lastVoteEpoch 0
\ No newline at end of file
diff --git a/src/test/resources/docker-env/test-cluster/config/node-7582/redis.conf b/src/test/resources/docker-env/test-cluster/config/node-7582/redis.conf
new file mode 100644
index 000000000..e6fb7424a
--- /dev/null
+++ b/src/test/resources/docker-env/test-cluster/config/node-7582/redis.conf
@@ -0,0 +1,7 @@
+port 7582
+save ""
+appendonly no
+client-output-buffer-limit pubsub 256k 128k 5
+unixsocket /work/socket-7582
+cluster-enabled yes
+cluster-node-timeout 150
\ No newline at end of file
diff --git a/src/test/resources/docker/docker-compose.yml b/src/test/resources/docker/docker-compose.yml
deleted file mode 100644
index 2cc05fa3d..000000000
--- a/src/test/resources/docker/docker-compose.yml
+++ /dev/null
@@ -1,21 +0,0 @@
----
-x-client-libs-stack-image: &client-libs-stack-image
-    image: "redislabs/client-libs-test:${REDIS_STACK_VERSION:-8.0-M02}"
-
-services:
-
-  standalone-stack:
-    <<: *client-libs-stack-image
-    environment:
-        - REDIS_CLUSTER=no
-        - PORT=6379
-    ports:
-      - "16379:6379"
-
-  clustered-stack:
-    <<: *client-libs-stack-image
-    environment:
-      - REDIS_CLUSTER=yes
-      - PORT=36379
-    ports:
-      - "36379-36381:36379-36381"
\ No newline at end of file
diff --git a/src/test/resources/log4j2-test.xml b/src/test/resources/log4j2-test.xml
index efb537153..981918b55 100644
--- a/src/test/resources/log4j2-test.xml
+++ b/src/test/resources/log4j2-test.xml
@@ -12,8 +12,10 @@
 
     <Loggers>
         <Logger name="io.lettuce.core.protocol" level="FATAL"/>
-        <!-- Remove the line below after we stabilize the test-containers deployment -->
+        <!-- Uncomment the line below to enable test-containers debug logging -->
+        <!--
         <Logger name="org.testcontainers" level="DEBUG"/>
+        -->
         <Root level="FATAL">
             <AppenderRef ref="Console"/>
             <AppenderRef ref="File"/>