diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 728531486ed90a50099ea87e32ebf74d06bbad2e..4f8d5d27592a293e266bf93500a0bccc168171ef 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -10,40 +10,33 @@ on:
 jobs:
   lint:
     name: Rubocop
-    timeout-minutes: 30
+    timeout-minutes: 15
     runs-on: ubuntu-latest
     steps:
       - name: Check out code
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Set up Ruby
         uses: ruby/setup-ruby@v1
         with:
-          ruby-version: "2.4"
-      - name: Set up Gems
-        run: |
-          gem update --system --no-document
-          gem install bundler --no-document
-          bundle install --jobs 4 --retry 3 --path=.bundle
+          ruby-version: "3.2"
+          bundler-cache: true
       - name: Lint
         run: bundle exec rubocop
 
   rubies:
     name: Ruby
-    timeout-minutes: 30
+    timeout-minutes: 15
     strategy:
       fail-fast: false
       matrix:
-        ruby: ["3.1", "3.0", "2.7", "2.6", "2.5", "2.4", "jruby-9.3.6.0"]
+        ruby: ["3.3", "3.2", "3.1", "3.0", "2.7", "2.6", "jruby-9.3.6.0"]
     runs-on: ubuntu-latest
     env:
-      VERBOSE: "true"
-      TIMEOUT: "30"
       LOW_TIMEOUT: "0.01"
-      DRIVER: ruby
-      REDIS_BRANCH: "7.0"
+      REDIS_BRANCH: "7.2"
     steps:
       - name: Check out code
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Print environment variables
         run: |
           echo "TIMEOUT=${TIMEOUT}"
@@ -56,7 +49,7 @@ jobs:
           ruby-version: ${{ matrix.ruby }}
           bundler-cache: true
       - name: Cache local temporary directory
-        uses: actions/cache@v3
+        uses: actions/cache@v4
         with:
           path: tmp
           key: "local-tmp-redis-7.0-on-ubuntu-latest"
@@ -67,23 +60,59 @@ jobs:
       - name: Shutting down Redis
         run: make stop
 
+  truffle:
+    name: TruffleRuby
+    timeout-minutes: 15
+    strategy:
+      fail-fast: false
+      matrix:
+        suite: ["redis", "distributed"]
+    runs-on: ubuntu-latest
+    env:
+      LOW_TIMEOUT: "0.01"
+      REDIS_BRANCH: "7.2"
+      TRUFFLERUBYOPT: "--engine.Mode=latency"
+    steps:
+      - name: Check out code
+        uses: actions/checkout@v4
+      - name: Print environment variables
+        run: |
+          echo "TIMEOUT=${TIMEOUT}"
+          echo "LOW_TIMEOUT=${LOW_TIMEOUT}"
+          echo "DRIVER=${DRIVER}"
+          echo "REDIS_BRANCH=${REDIS_BRANCH}"
+      - name: Set up Ruby
+        uses: ruby/setup-ruby@v1
+        with:
+          ruby-version: truffleruby
+          bundler-cache: true
+      - name: Cache local temporary directory
+        uses: actions/cache@v4
+        with:
+          path: tmp
+          key: "local-tmp-redis-7.0-on-ubuntu-latest"
+      - name: Booting up Redis
+        run: make start
+      - name: Test
+        run: bundle exec rake test:${{ matrix.suite }}
+      - name: Shutting down Redis
+        run: make stop
+
   drivers:
     name: Driver
-    timeout-minutes: 30
+    timeout-minutes: 15
     strategy:
       fail-fast: false
       matrix:
-        driver: ["hiredis", "synchrony"]
+        driver: ["hiredis"]
     runs-on: ubuntu-latest
     env:
-      VERBOSE: "true"
-      TIMEOUT: "30"
       LOW_TIMEOUT: "0.01"
       DRIVER: ${{ matrix.driver }}
-      REDIS_BRANCH: "7.0"
+      REDIS_BRANCH: "7.2"
     steps:
       - name: Check out code
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Print environment variables
         run: |
           echo "TIMEOUT=${TIMEOUT}"
@@ -93,10 +122,10 @@ jobs:
       - name: Set up Ruby
         uses: ruby/setup-ruby@v1
         with:
-          ruby-version: "2.4"
+          ruby-version: "2.6"
           bundler-cache: true
       - name: Cache local temporary directory
-        uses: actions/cache@v3
+        uses: actions/cache@v4
         with:
           path: tmp
           key: "local-tmp-redis-7.0-on-ubuntu-latest"
@@ -109,21 +138,18 @@ jobs:
 
   redises:
     name: Redis
-    timeout-minutes: 30
+    timeout-minutes: 15
     strategy:
       fail-fast: false
       matrix:
-        redis: ["6.2", "6.0", "5.0"]
+        redis: ["7.0", "6.2", "6.0", "5.0"]
     runs-on: ubuntu-latest
     env:
-      VERBOSE: "true"
-      TIMEOUT: "30"
       LOW_TIMEOUT: "0.14"
-      DRIVER: ruby
       REDIS_BRANCH: ${{ matrix.redis }}
     steps:
       - name: Check out code
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Print environment variables
         run: |
           echo "TIMEOUT=${TIMEOUT}"
@@ -133,10 +159,10 @@ jobs:
       - name: Set up Ruby
         uses: ruby/setup-ruby@v1
         with:
-          ruby-version: "2.4"
+          ruby-version: "2.6"
           bundler-cache: true
       - name: Cache local temporary directory
-        uses: actions/cache@v3
+        uses: actions/cache@v4
         with:
           path: tmp
           key: "local-tmp-redis-${{ matrix.redis }}-on-ubuntu-latest"
@@ -149,19 +175,16 @@ jobs:
 
   sentinel:
     name: Sentinel
-    timeout-minutes: 30
+    timeout-minutes: 15
     strategy:
       fail-fast: false
     runs-on: ubuntu-latest
     env:
-      VERBOSE: "true"
-      TIMEOUT: "30"
       LOW_TIMEOUT: "0.14"
-      DRIVER: ruby
       REDIS_BRANCH: "7.0"
     steps:
       - name: Check out code
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Print environment variables
         run: |
           echo "TIMEOUT=${TIMEOUT}"
@@ -171,15 +194,15 @@ jobs:
       - name: Set up Ruby
         uses: ruby/setup-ruby@v1
         with:
-          ruby-version: "2.4"
+          ruby-version: "2.6"
           bundler-cache: true
       - name: Cache local temporary directory
-        uses: actions/cache@v3
+        uses: actions/cache@v4
         with:
           path: tmp
           key: "local-tmp-redis-7.0-on-ubuntu-latest"
       - name: Booting up Redis
-        run: make start_all
+        run: make start_sentinel wait_for_sentinel
       - name: Test
         run: bundle exec rake test:sentinel
       - name: Shutting down Redis
@@ -187,19 +210,18 @@ jobs:
 
   cluster:
     name: Cluster
-    timeout-minutes: 30
+    timeout-minutes: 15
     strategy:
       fail-fast: false
     runs-on: ubuntu-latest
     env:
-      VERBOSE: "true"
-      TIMEOUT: "30"
+      TIMEOUT: "15"
       LOW_TIMEOUT: "0.14"
-      DRIVER: ruby
-      REDIS_BRANCH: "7.0"
+      REDIS_BRANCH: "7.2"
+      BUNDLE_GEMFILE: cluster/Gemfile
     steps:
       - name: Check out code
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Print environment variables
         run: |
           echo "TIMEOUT=${TIMEOUT}"
@@ -209,15 +231,15 @@ jobs:
       - name: Set up Ruby
         uses: ruby/setup-ruby@v1
         with:
-          ruby-version: "2.4"
+          ruby-version: "2.7"
           bundler-cache: true
       - name: Cache local temporary directory
-        uses: actions/cache@v3
+        uses: actions/cache@v4
         with:
           path: tmp
           key: "local-tmp-redis-7.0-on-ubuntu-latest"
       - name: Booting up Redis
-        run: make start_all
+        run: make start start_cluster create_cluster
       - name: Test
         run: bundle exec rake test:cluster
       - name: Shutting down Redis
diff --git a/.gitignore b/.gitignore
index a791b8e300d667f077add10919c67942b9849085..19ff57df03ed43e8e0d0b2e303785bca529094ac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@ Gemfile.lock
 /.idea
 /.yardoc
 /.bundle
+/cluster/.bundle
 /coverage/*
 /doc/
 /examples/sentinel/sentinel.conf
diff --git a/.rubocop.yml b/.rubocop.yml
index e9aa0b8fdb09f20cd6c520afc429da3460e38874..fc1171cddbd473582333a7d82b4440cbeec8fb93 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -1,7 +1,7 @@
 inherit_from: .rubocop_todo.yml
 
 AllCops:
-  TargetRubyVersion: 2.4
+  TargetRubyVersion: 2.6
 
 Layout/LineLength:
   Max: 120
@@ -56,9 +56,15 @@ Metrics/PerceivedComplexity:
 Style/PercentLiteralDelimiters:
   Enabled: false
 
+Style/SlicingWithRange:
+  Enabled: false
+
 Style/TrailingCommaInArrayLiteral:
   Enabled: false
 
+Style/TrailingCommaInArguments:
+  Enabled: false
+
 Style/ParallelAssignment:
   Enabled: false
 
@@ -68,6 +74,9 @@ Style/NumericPredicate:
 Style/IfUnlessModifier:
   Enabled: false
 
+Style/MutableConstant:
+  Enabled: false # false positives
+
 Style/SignalException:
   Exclude:
     - 'lib/redis/connection/synchrony.rb'
@@ -139,6 +148,16 @@ Metrics/BlockNesting:
 Style/HashTransformValues:
   Enabled: false
 
+Style/TrailingCommaInHashLiteral:
+  Enabled: false
+
 Style/SymbolProc:
   Exclude:
-    - 'test/**/*'
\ No newline at end of file
+    - 'test/**/*'
+
+Bundler/OrderedGems:
+  Enabled: false
+
+Gemspec/RequiredRubyVersion:
+  Exclude:
+    - cluster/redis-clustering.gemspec
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 85908a89a5ae2b78264da480dc0801a88c4ffaa2..2a3d18715d20f489dceecc70e353689282a60ebd 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -11,20 +11,6 @@ Lint/HashCompareByIdentity:
   Exclude:
     - 'lib/redis.rb'
 
-# Offense count: 1
-# Cop supports --auto-correct.
-Lint/RedundantStringCoercion:
-  Exclude:
-    - 'examples/consistency.rb'
-
-# Offense count: 2
-# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers.
-# SupportedStyles: snake_case, normalcase, non_integer
-# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339
-Naming/VariableNumber:
-  Exclude:
-    - 'test/remote_server_control_commands_test.rb'
-
 # Offense count: 6
 # Configuration parameters: AllowedMethods.
 # AllowedMethods: respond_to_missing?
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 224bebb58293c8adf0b24ce38d2c28215e5b5458..bbdc021bc137458dc3eb0aadd575cebeb9143e31 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,85 @@
 # Unreleased
 
+# 5.2.0
+
+- Now require Ruby 2.6 because `redis-client` does.
+- Eagerly close subscribed connection when using `subscribe_with_timeout`. See #1259.
+- Add `exception` flag in `pipelined` allowing failed commands to be returned in the result array when set to `false`.
+
+# 5.1.0
+
+- `multi` now accept a `watch` keyword argument like `redis-client`. See #1236.
+- `bitcount` and `bitpos` now accept a `scale:` argument on Redis 7+. See #1242
+- Added `expiretime` and `pexpiretime`. See #1248.
+
+# 5.0.8
+
+- Fix `Redis#without_reconnect` for sentinel clients. Fix #1212.
+- Add `sentinel_username`, `sentinel_password` for sentinel clients. Bump `redis-client` to `>=0.17.0`. See #1213
+
+# 5.0.7
+
+- Fix compatibility with `redis-client 0.15.0` when using Redis Sentinel. Fix #1209.
+
+# 5.0.6
+
+- Wait for an extra `config.read_timeout` in blocking commands rather than an arbitrary 100ms. See #1175.
+- Treat ReadOnlyError as ConnectionError. See #1168.
+
+# 5.0.5
+
+- Fix automatic disconnection when the process was forked. See #1157.
+
+# 5.0.4
+
+- Cast `ttl` argument to integer in `expire`, `setex` and a few others.
+
+# 5.0.3
+
+- Add `OutOfMemoryError` as a subclass of `CommandError`
+
+# 5.0.2
+
+- Fix `Redis#close` to properly reset the fork protection check.
+
+# 5.0.1
+
+- Added a fake `Redis::Connections.drivers` method to be compatible with older sidekiq versions.
+
+# 5.0.0
+
+- Default client timeout decreased from 5 seconds to 1 second.
+- Eagerly and strictly cast Integer and Float parameters.
+- Allow to call `subscribe`, `unsubscribe`, `psubscribe` and `punsubscribe` from a subscribed client. See #1131.
+- Use `MD5` for hashing server nodes in `Redis::Distributed`. This should improve keys distribution among servers. See #1089.
+- Changed `sadd` and `srem` to now always return an Integer.
+- Added `sadd?` and `srem?` which always return a Boolean.
+- Added support for `IDLE` paramter in `xpending`.
+- Cluster support has been moved to a `redis-clustering` companion gem.
+- `select` no longer record the current database. If the client has to reconnect after `select` was used, it will reconnect to the original database.
+- Better support Float timeout in blocking commands. See #977.
+- `Redis.new` will now raise an error if provided unknown options.
+- Removed positional timeout in blocking commands (`BLPOP`, etc). Timeout now must be passed as an option: `r.blpop("key", timeout: 2.5)`
+- Removed `logger` option.
+- Removed `reconnect_delay_max` and `reconnect_delay`, you can pass precise sleep durations to `reconnect_attempts` instead.
+- Require Ruby 2.5+.
+- Removed the deprecated `queue` and `commit` methods. Use `pipelined` instead.
+- Removed the deprecated `Redis::Future#==`.
+- Removed the deprecated `pipelined` and `multi` signature. Commands now MUST be called on the block argument, not the original redis instance.
+- Removed `Redis.current`. You shouldn't assume there is a single global Redis connection, use a connection pool instead,
+  and libaries using Redis should accept a Redis instance (or connection pool) as a config. E.g. `MyLibrary.redis = Redis.new(...)`.
+- Removed the `synchrony` driver.
+- Removed `Redis.exists_returns_integer`, it's now always enabled.
+
+# 4.8.1
+
+* Automatically reconnect after fork regardless of `reconnect_attempts`
+
 # 4.8.0
 
 * Introduce `sadd?` and `srem?` as boolean returning versions of `sadd` and `srem`.
 * Deprecate `sadd` and `srem` returning a boolean when called with a single argument.
-  To enable the redis 5.0 behavior you can set `Redis.sadd_returns_boolean = true`.
+  To enable the redis 5.0 behavior you can set `Redis.sadd_returns_boolean = false`.
 * Deprecate passing `timeout` as a positional argument in blocking commands (`brpop`, `blop`, etc).
 
 # 4.7.1
diff --git a/Gemfile b/Gemfile
index b997f52de4b76ff29be5ee4ddf8641aa815aaea4..00615dee1e628e9925d85f078af9a115e3dd6e30 100644
--- a/Gemfile
+++ b/Gemfile
@@ -6,4 +6,7 @@ gemspec
 
 gem 'minitest'
 gem 'rake'
-gem 'rubocop', '~> 1.0', '< 1.12'
+gem 'rubocop', '~> 1.25.1'
+gem 'mocha'
+
+gem 'hiredis-client'
diff --git a/README.md b/README.md
index 202ec869deef80e5288fdcfa5cebac5d88e3baed..d4ee0ab93c3c43809e7bf39ae91acc060956651d 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,6 @@
-# redis-rb [![Build Status][gh-actions-image]][gh-actions-link] [![Inline docs][inchpages-image]][inchpages-link]
+# redis-rb [![Build Status][gh-actions-image]][gh-actions-link] [![Inline docs][rdoc-master-image]][rdoc-master-link]
 
-A Ruby client that tries to match [Redis][redis-home]' API one-to-one, while still
-providing an idiomatic interface.
+A Ruby client that tries to match [Redis][redis-home]' API one-to-one, while still providing an idiomatic interface.
 
 See [RubyDoc.info][rubydoc] for the API docs of the latest published gem.
 
@@ -38,10 +37,6 @@ redis = Redis.new(url: "redis://:p4ssw0rd@10.0.1.1:6380/15")
 The client expects passwords with special chracters to be URL-encoded (i.e.
 `CGI.escape(password)`).
 
-By default, the client will try to read the `REDIS_URL` environment variable
-and use that as URL to connect to. The above statement is therefore equivalent
-to setting this environment variable and calling `Redis.new` without arguments.
-
 To connect to Redis listening on a Unix socket, try:
 
 ```ruby
@@ -76,6 +71,26 @@ redis.get("mykey")
 All commands, their arguments, and return values are documented and
 available on [RubyDoc.info][rubydoc].
 
+## Connection Pooling and Thread safety
+
+The client does not provide connection pooling. Each `Redis` instance
+has one and only one connection to the server, and use of this connection
+is protected by a mutex.
+
+As such it is heavilly recommended to use the [`connection_pool` gem](https://github.com/mperham/connection_pool), e.g.:
+
+```ruby
+module MyApp
+  def self.redis
+    @redis ||= ConnectionPool::Wrapper.new do
+      Redis.new(url: ENV["REDIS_URL"])
+    end
+  end
+end
+
+MyApp.redis.incr("some-counter")
+```
+
 ## Sentinel support
 
 The client is able to perform automatic failover by using [Redis
@@ -88,7 +103,7 @@ To connect using Sentinel, use:
 SENTINELS = [{ host: "127.0.0.1", port: 26380 },
              { host: "127.0.0.1", port: 26381 }]
 
-redis = Redis.new(url: "redis://mymaster", sentinels: SENTINELS, role: :master)
+redis = Redis.new(name: "mymaster", sentinels: SENTINELS, role: :master)
 ```
 
 * The master name identifies a group of Redis instances composed of a master
@@ -105,85 +120,44 @@ but a few so that if one is down the client will try the next one. The client
 is able to remember the last Sentinel that was able to reply correctly and will
 use it for the next requests.
 
-If you want to [authenticate](https://redis.io/topics/sentinel#configuring-sentinel-instances-with-authentication) Sentinel itself, you must specify the `password` option per instance.
+To [authenticate](https://redis.io/docs/management/sentinel/#configuring-sentinel-instances-with-authentication) Sentinel itself, you can specify the `sentinel_username` and `sentinel_password`. Exclude the `sentinel_username` option if you're using password-only authentication.
 
 ```ruby
-SENTINELS = [{ host: '127.0.0.1', port: 26380, password: 'mysecret' },
-             { host: '127.0.0.1', port: 26381, password: 'mysecret' }]
+SENTINELS = [{ host: '127.0.0.1', port: 26380},
+             { host: '127.0.0.1', port: 26381}]
 
-redis = Redis.new(host: 'mymaster', sentinels: SENTINELS, role: :master)
+redis = Redis.new(name: 'mymaster', sentinels: SENTINELS, sentinel_username: 'appuser', sentinel_password: 'mysecret', role: :master)
 ```
 
-## Cluster support
-
-`redis-rb` supports [clustering](https://redis.io/topics/cluster-spec).
+If you specify a username and/or password at the top level for your main Redis instance, Sentinel *will not* using thouse credentials
 
 ```ruby
-# Nodes can be passed to the client as an array of connection URLs.
-nodes = (7000..7005).map { |port| "redis://127.0.0.1:#{port}" }
-redis = Redis.new(cluster: nodes)
+# Use 'mysecret' to authenticate against the mymaster instance, but skip authentication for the sentinels:
+SENTINELS = [{ host: '127.0.0.1', port: 26380 },
+             { host: '127.0.0.1', port: 26381 }]
 
-# You can also specify the options as a Hash. The options are the same as for a single server connection.
-(7000..7005).map { |port| { host: '127.0.0.1', port: port } }
+redis = Redis.new(name: 'mymaster', sentinels: SENTINELS, role: :master, password: 'mysecret')
 ```
 
-You can also specify only a subset of the nodes, and the client will discover the missing ones using the [CLUSTER NODES](https://redis.io/commands/cluster-nodes) command.
+So you have to provide Sentinel credential and Redis explictly even they are the same
 
 ```ruby
-Redis.new(cluster: %w[redis://127.0.0.1:7000])
-```
-
-If you want [the connection to be able to read from any replica](https://redis.io/commands/readonly), you must pass the `replica: true`. Note that this connection won't be usable to write keys.
+# Use 'mysecret' to authenticate against the mymaster instance and sentinel
+SENTINELS = [{ host: '127.0.0.1', port: 26380 },
+             { host: '127.0.0.1', port: 26381 }]
 
-```ruby
-Redis.new(cluster: nodes, replica: true)
+redis = Redis.new(name: 'mymaster', sentinels: SENTINELS, role: :master, password: 'mysecret', sentinel_password: 'mysecret')
 ```
 
-The calling code is responsible for [avoiding cross slot commands](https://redis.io/topics/cluster-spec#keys-distribution-model).
+Also the `name`, `password`, `username` and `db` for Redis instance can be passed as an url:
 
 ```ruby
-redis = Redis.new(cluster: %w[redis://127.0.0.1:7000])
-
-redis.mget('key1', 'key2')
-#=> Redis::CommandError (CROSSSLOT Keys in request don't hash to the same slot)
-
-redis.mget('{key}1', '{key}2')
-#=> [nil, nil]
+redis = Redis.new(url: "redis://appuser:mysecret@mymaster/10", sentinels: SENTINELS, role: :master)
 ```
 
-* The client automatically reconnects after a failover occurred, but the caller is responsible for handling errors while it is happening.
-* The client support permanent node failures, and will reroute requests to promoted slaves.
-* The client supports `MOVED` and `ASK` redirections transparently.
-
-## Cluster mode with SSL/TLS
-Since Redis can return FQDN of nodes in reply to client since `7.*` with CLUSTER commands, we can use cluster feature with SSL/TLS connection like this:
-
-```ruby
-Redis.new(cluster: %w[rediss://foo.example.com:6379])
-```
-
-On the other hand, in Redis versions prior to `6.*`, you can specify options like the following if cluster mode is enabled and client has to connect to nodes via single endpoint with SSL/TLS.
-
-```ruby
-Redis.new(cluster: %w[rediss://foo-endpoint.example.com:6379], fixed_hostname: 'foo-endpoint.example.com')
-```
-
-In case of the above architecture, if you don't pass the `fixed_hostname` option to the client and servers return IP addresses of nodes, the client may fail to verify certificates.
-
-## Storing objects
-
-Redis "string" types can be used to store serialized Ruby objects, for
-example with JSON:
-
-```ruby
-require "json"
-
-redis.set "foo", [1, 2, 3].to_json
-# => OK
+## Cluster support
 
-JSON.parse(redis.get("foo"))
-# => [1, 2, 3]
-```
+[Clustering](https://redis.io/topics/cluster-spec). is supported via the [`redis-clustering` gem](cluster/).
 
 ## Pipelining
 
@@ -206,6 +180,39 @@ end
 # => ["OK", 1]
 ```
 
+Commands must be called on the yielded objects. If you call methods
+on the original client objects from inside a pipeline, they will be sent immediately:
+
+```ruby
+redis.pipelined do |pipeline|
+  pipeline.set "foo", "bar"
+  redis.incr "baz" # => 1
+end
+# => ["OK"]
+```
+
+### Exception management
+
+The `exception` flag in the `#pipelined` is a feature that modifies the pipeline execution behavior. When set
+to `false`, it doesn't raise an exception when a command error occurs. Instead, it allows the pipeline to execute all
+commands, and any failed command will be available in the returned array. (Defaults to `true`)
+
+```ruby
+results = redis.pipelined(exception: false) do |pipeline|
+  pipeline.set('key1', 'value1')
+  pipeline.lpush('key1', 'something') # This will fail
+  pipeline.set('key2', 'value2')
+end
+# results => ["OK", #<RedisClient::WrongTypeError: WRONGTYPE Operation against a key holding the wrong kind of value>, "OK"]
+
+results.each do |result|
+  if result.is_a?(Redis::CommandError)
+    # Do something with the failed result
+  end
+end
+```
+
+
 ### Executing commands atomically
 
 You can use `MULTI/EXEC` to run a number of commands in an atomic
@@ -225,21 +232,22 @@ end
 ### Futures
 
 Replies to commands in a pipeline can be accessed via the *futures* they
-emit (since redis-rb 3.0). All calls on the pipeline object return a
+emit. All calls on the pipeline object return a
 `Future` object, which responds to the `#value` method. When the
 pipeline has successfully executed, all futures are assigned their
 respective replies and can be used.
 
 ```ruby
+set = incr = nil
 redis.pipelined do |pipeline|
-  @set = pipeline.set "foo", "bar"
-  @incr = pipeline.incr "baz"
+  set = pipeline.set "foo", "bar"
+  incr = pipeline.incr "baz"
 end
 
-@set.value
+set.value
 # => "OK"
 
-@incr.value
+incr.value
 # => 1
 ```
 
@@ -251,7 +259,7 @@ it can't connect to the server a `Redis::CannotConnectError` error will be raise
 ```ruby
 begin
   redis.ping
-rescue StandardError => e
+rescue Redis::BaseError => e
   e.inspect
 # => #<Redis::CannotConnectError: Timed out connecting to Redis on 10.0.1.1:6380>
 
@@ -298,55 +306,37 @@ If no message is received after 5 seconds, the client will unsubscribe.
 
 ## Reconnections
 
-The client allows you to configure how many `reconnect_attempts` it should
-complete before declaring a connection as failed. Furthermore, you may want
-to control the maximum duration between reconnection attempts with
-`reconnect_delay` and `reconnect_delay_max`.
+**By default**, this gem will only **retry a connection once** and then fail, but
+the client allows you to configure how many `reconnect_attempts` it should
+complete before declaring a connection as failed.
 
 ```ruby
-Redis.new(
-  :reconnect_attempts => 10,
-  :reconnect_delay => 1.5,
-  :reconnect_delay_max => 10.0,
-)
+Redis.new(reconnect_attempts: 0)
+Redis.new(reconnect_attempts: 3)
 ```
 
-The delay values are specified in seconds. With the above configuration, the
-client would attempt 10 reconnections, exponentially increasing the duration
-between each attempt but it never waits longer than `reconnect_delay_max`.
-
-This is the retry algorithm:
+If you wish to wait between reconnection attempts, you can instead pass a list
+of durations:
 
 ```ruby
-attempt_wait_time = [(reconnect_delay * 2**(attempt-1)), reconnect_delay_max].min
+Redis.new(reconnect_attempts: [
+  0, # retry immediately
+  0.25, # retry a second time after 250ms
+  1, # retry a third and final time after another 1s
+])
 ```
 
-**By default**, this gem will only **retry a connection once** and then fail, but with the
-above configuration the reconnection attempt would look like this:
-
-#|Attempt wait time|Total wait time
-:-:|:-:|:-:
-1|1.5s|1.5s
-2|3.0s|4.5s
-3|6.0s|10.5s
-4|10.0s|20.5s
-5|10.0s|30.5s
-6|10.0s|40.5s
-7|10.0s|50.5s
-8|10.0s|60.5s
-9|10.0s|70.5s
-10|10.0s|80.5s
-
-So if the reconnection attempt #10 succeeds 70 seconds have elapsed trying
-to reconnect, this is likely fine in long-running background processes, but if
-you use Redis to drive your website you might want to have a lower
-`reconnect_delay_max` or have less `reconnect_attempts`.
+If you wish to disable reconnection only for some commands, you can use
+`disable_reconnection`:
 
-## SSL/TLS Support
+```ruby
+redis.get("some-key") # this may be retried
+redis.disable_reconnection do
+  redis.incr("some-counter") # this won't be retried.
+end
+```
 
-This library supports natively terminating client side SSL/TLS connections
-when talking to Redis via a server-side proxy such as [stunnel], [hitch],
-or [ghostunnel].
+## SSL/TLS Support
 
 To enable SSL support, pass the `:ssl => true` option when configuring the
 Redis client, or pass in `:url => "rediss://..."` (like HTTPS for Redis).
@@ -381,13 +371,7 @@ redis = Redis.new(
 )
 ```
 
-[stunnel]: https://www.stunnel.org/
-[hitch]: https://hitch-tls.org/
-[ghostunnel]: https://github.com/square/ghostunnel
-[OpenSSL::SSL::SSLContext documentation]: http://ruby-doc.org/stdlib-2.3.0/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html
-
-*NOTE:* SSL is only supported by the default "Ruby" driver
-
+[OpenSSL::SSL::SSLContext documentation]: http://ruby-doc.org/stdlib-2.5.0/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html
 
 ## Expert-Mode Options
 
@@ -401,17 +385,9 @@ redis = Redis.new(
    Improper use of `inherit_socket` will result in corrupted and/or incorrect
    responses.
 
-## Alternate drivers
+## hiredis binding
 
 By default, redis-rb uses Ruby's socket library to talk with Redis.
-To use an alternative connection driver it should be specified as option
-when instantiating the client object. These instructions are only valid
-for **redis-rb 3.0**. For instructions on how to use alternate drivers from
-**redis-rb 2.2**, please refer to an [older README][readme-2.2.2].
-
-[readme-2.2.2]: https://github.com/redis/redis-rb/blob/v2.2.2/README.md
-
-### hiredis
 
 The hiredis driver uses the connection facility of hiredis-rb. In turn,
 hiredis-rb is a binding to the official hiredis client library. It
@@ -421,41 +397,27 @@ extension, JRuby is not supported (by default).
 It is best to use hiredis when you have large replies (for example:
 `LRANGE`, `SMEMBERS`, `ZRANGE`, etc.) and/or use big pipelines.
 
-In your Gemfile, include hiredis:
+In your Gemfile, include `hiredis-client`:
 
 ```ruby
-gem "redis", "~> 3.0.1"
-gem "hiredis", "~> 0.4.5"
+gem "redis"
+gem "hiredis-client"
 ```
 
-When instantiating the client object, specify hiredis:
+If your application doesn't call `Bundler.require`, you may have
+to require it explictly:
 
 ```ruby
-redis = Redis.new(:driver => :hiredis)
-```
-
-### synchrony
-
-The synchrony driver adds support for [em-synchrony][em-synchrony].
-This makes redis-rb work with EventMachine's asynchronous I/O, while not
-changing the exposed API. The hiredis gem needs to be available as
-well, because the synchrony driver uses hiredis for parsing the Redis
-protocol.
+require "hiredis-client"
+````
 
-[em-synchrony]: https://github.com/igrigorik/em-synchrony
-
-In your Gemfile, include em-synchrony and hiredis:
-
-```ruby
-gem "redis", "~> 3.0.1"
-gem "hiredis", "~> 0.4.5"
-gem "em-synchrony"
-```
+This makes the hiredis driver the default.
 
-When instantiating the client object, specify synchrony:
+If you want to be certain hiredis is being used, when instantiating
+the client object, specify hiredis:
 
 ```ruby
-redis = Redis.new(:driver => :synchrony)
+redis = Redis.new(driver: :hiredis)
 ```
 
 ## Testing
@@ -480,11 +442,11 @@ client and evangelized Redis in Rubyland. Thank you, Ezra.
 requests.
 
 
-[inchpages-image]:  https://inch-ci.org/github/redis/redis-rb.svg
-[inchpages-link]:   https://inch-ci.org/github/redis/redis-rb
-[redis-commands]:   https://redis.io/commands
-[redis-home]:       https://redis.io
-[redis-url]:        http://www.iana.org/assignments/uri-schemes/prov/redis
-[gh-actions-image]: https://github.com/redis/redis-rb/workflows/Test/badge.svg
-[gh-actions-link]:  https://github.com/redis/redis-rb/actions
-[rubydoc]:          http://www.rubydoc.info/gems/redis
+[rdoc-master-image]: https://img.shields.io/badge/docs-rdoc.info-blue.svg
+[rdoc-master-link]:  https://rubydoc.info/github/redis/redis-rb
+[redis-commands]:    https://redis.io/commands
+[redis-home]:        https://redis.io
+[redis-url]:         https://www.iana.org/assignments/uri-schemes/prov/redis
+[gh-actions-image]:  https://github.com/redis/redis-rb/workflows/Test/badge.svg
+[gh-actions-link]:   https://github.com/redis/redis-rb/actions
+[rubydoc]:           https://rubydoc.info/gems/redis
diff --git a/Rakefile b/Rakefile
index c8d8c4d904674fda3644a660f18654a703d59d4f..15ff13e315022d2a649f18dbaf7310ea2d7b8524 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,10 +1,12 @@
 # frozen_string_literal: true
 
 require 'bundler/gem_tasks'
+Bundler::GemHelper.install_tasks(dir: "cluster", name: "redis-clustering")
+
 require 'rake/testtask'
 
 namespace :test do
-  groups = %i(redis distributed sentinel cluster)
+  groups = %i(redis distributed sentinel)
   groups.each do |group|
     Rake::TestTask.new(group) do |t|
       t.libs << "test"
@@ -18,6 +20,13 @@ namespace :test do
   unless lost_tests.empty?
     abort "The following test files are in no group:\n#{lost_tests.join("\n")}"
   end
+
+  Rake::TestTask.new(:cluster) do |t|
+    t.libs << "cluster/test" << "test"
+    t.libs << "cluster/lib" << "lib"
+    t.test_files = FileList["cluster/test/**/*_test.rb"]
+    t.options = '-v' if ENV['CI'] || ENV['VERBOSE']
+  end
 end
 
 task test: ["test:redis", "test:distributed", "test:sentinel", "test:cluster"]
diff --git a/benchmarking/cluster.rb b/benchmarking/cluster.rb
deleted file mode 100644
index 8a5b9ff5e39b802b8861576734c268412c4a50a8..0000000000000000000000000000000000000000
--- a/benchmarking/cluster.rb
+++ /dev/null
@@ -1,97 +0,0 @@
-# frozen_string_literal: true
-
-# Execute the following commands before execution
-#
-# `$ make start`
-# `$ make start_cluster`
-# `$ make create_cluster`
-
-require 'redis'
-require 'benchmark'
-
-HOST            = '127.0.0.1'
-STANDALONE_PORT = 6381
-CLUSTER_PORT    = 7000
-N               = (ARGV.first || 100_000).to_i
-
-rn = Redis.new(host: HOST, port: STANDALONE_PORT)
-rc = Redis.new(host: HOST, port: CLUSTER_PORT)
-rm = Redis.new(cluster: %W[redis://#{HOST}:#{CLUSTER_PORT}])
-rs = Redis.new(cluster: %W[redis://#{HOST}:#{CLUSTER_PORT}], replica: true)
-
-Benchmark.bmbm do |bm|
-  bm.report('client: normal,  server: standalone, command: SET,  key: fixed') do
-    N.times { rn.set('foo', '42') }
-  end
-
-  bm.report('client: normal,  server: standalone, command: GET,  key: fixed') do
-    N.times { rn.get('foo') }
-  end
-
-  bm.report('client: normal,  server: cluster,    command: SET,  key: fixed') do
-    N.times { rc.set('bar', '42') }
-  end
-
-  bm.report('client: normal,  server: cluster,    command: GET,  key: fixed') do
-    N.times { rc.get('bar') }
-  end
-
-  bm.report('client: cluster, server: cluster,    command: SET,  key: fixed') do
-    N.times { rm.set('baz', '42') }
-  end
-
-  rm.wait(1, 0)
-  bm.report('client: cluster, server: cluster,    command: GET,  key: fixed') do
-    N.times { rm.get('baz') }
-  end
-
-  bm.report('client: cluster, server: cluster,    command: SET,  key: fixed,    replica: true') do
-    N.times { rs.set('zap', '42') }
-  end
-
-  rs.wait(1, 0)
-  bm.report('client: cluster, server: cluster,    command: GET,  key: fixed,    replica: true') do
-    N.times { rs.get('zap') }
-  end
-
-  bm.report('client: normal,  server: standalone, command: SET,  key: variable') do
-    N.times { |i| rn.set("foo:#{i}", '42') }
-  end
-
-  bm.report('client: normal,  server: standalone, command: GET,  key: variable') do
-    N.times { |i| rn.get("foo:#{i}") }
-  end
-
-  bm.report('client: cluster, server: cluster,    command: SET,  key: variable') do
-    N.times { |i| rm.set("bar:#{i}", '42') }
-  end
-
-  rm.wait(1, 0)
-  bm.report('client: cluster, server: cluster,    command: GET,  key: variable') do
-    N.times { |i| rm.get("bar:#{i}") }
-  end
-
-  bm.report('client: cluster, server: cluster,    command: SET,  key: variable, replica: true') do
-    N.times { |i| rs.set("baz:#{i}", '42') }
-  end
-
-  rs.wait(1, 0)
-  bm.report('client: cluster, server: cluster,    command: GET,  key: variable, replica: true') do
-    N.times { |i| rs.get("baz:#{i}") }
-  end
-
-  rn.set('bar', 0)
-  bm.report('client: normal,  server: standalone, command: INCR, key: fixed') do
-    N.times { rn.incr('bar') }
-  end
-
-  rc.set('bar', 0)
-  bm.report('client: normal,  server: cluster,    command: INCR, key: fixed') do
-    N.times { rc.incr('bar') }
-  end
-
-  rm.set('bar', 0)
-  bm.report('client: cluster, server: cluster,    command: INCR, key: fixed') do
-    N.times { rm.incr('bar') }
-  end
-end
diff --git a/benchmarking/cluster_slot.rb b/benchmarking/cluster_slot.rb
deleted file mode 100644
index a374aff8627c279dccad4767ac939748e72ff559..0000000000000000000000000000000000000000
--- a/benchmarking/cluster_slot.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-require 'redis'
-require 'benchmark'
-
-N = (ARGV.first || 100_000).to_i
-
-available_slots = {
-  "127.0.0.1:7000" => [0..5460],
-  "127.0.0.1:7003" => [0..5460],
-  "127.0.0.1:7001" => [5461..10_922],
-  "127.0.0.1:7004" => [5461..10_922],
-  "127.0.0.1:7002" => [10_923..16_383],
-  "127.0.0.1:7005" => [10_923..16_383]
-}
-
-node_flags = {
-  "127.0.0.1:7000" => "master",
-  "127.0.0.1:7002" => "master",
-  "127.0.0.1:7001" => "master",
-  "127.0.0.1:7005" => "slave",
-  "127.0.0.1:7004" => "slave",
-  "127.0.0.1:7003" => "slave"
-}
-
-Benchmark.bmbm do |bm|
-  bm.report('Slot.new') do
-    allocs = GC.stat(:total_allocated_objects)
-
-    N.times do
-      Redis::Cluster::Slot.new(available_slots, node_flags, false)
-    end
-
-    puts GC.stat(:total_allocated_objects) - allocs
-  end
-end
diff --git a/benchmarking/logging.rb b/benchmarking/logging.rb
deleted file mode 100644
index 7b09a4609235fed17748d752ccc4014d8f9f7271..0000000000000000000000000000000000000000
--- a/benchmarking/logging.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-# frozen_string_literal: true
-
-# Run with
-#
-#   $ ruby -Ilib benchmarking/logging.rb
-#
-
-begin
-  require "bench"
-rescue LoadError
-  warn "`gem install bench` and try again."
-  exit 1
-end
-
-require "redis"
-require "logger"
-
-def log(level, namespace = nil)
-  logger = (namespace || Kernel).const_get(:Logger).new("/dev/null")
-  logger.level = (namespace || Logger).const_get(level)
-  logger
-end
-
-def stress(redis)
-  redis.flushdb
-
-  n = (ARGV.shift || 2000).to_i
-
-  n.times do |i|
-    key = "foo:#{i}"
-    redis.set key, i
-    redis.get key
-  end
-end
-
-default = Redis.new
-
-logging_redises = [
-  Redis.new(logger: log(:DEBUG)),
-  Redis.new(logger: log(:INFO))
-]
-
-begin
-  require "log4r"
-
-  logging_redises += [
-    Redis.new(logger: log(:DEBUG, Log4r)),
-    Redis.new(logger: log(:INFO, Log4r))
-  ]
-rescue LoadError
-  warn "Log4r not installed. `gem install log4r` if you want to compare it against Ruby's " \
-    "Logger (spoiler: it's much faster)."
-end
-
-benchmark "Default options (no logger)" do
-  stress(default)
-end
-
-logging_redises.each do |redis|
-  logger = redis._client.logger
-
-  case logger
-  when Logger
-    level = Logger::SEV_LABEL[logger.level]
-  when Log4r::Logger
-    level = logger.levels[logger.level]
-  end
-
-  benchmark "#{logger.class} on #{level}" do
-    stress(redis)
-  end
-end
-
-run 10
diff --git a/benchmarking/pipeline.rb b/benchmarking/pipeline.rb
deleted file mode 100644
index 84b20dcc70c5befef11e5b77402433f02331874f..0000000000000000000000000000000000000000
--- a/benchmarking/pipeline.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-# frozen_string_literal: true
-
-require "benchmark"
-
-$LOAD_PATH.push File.join(File.dirname(__FILE__), 'lib')
-
-require 'redis'
-
-ITERATIONS = 10_000
-
-@r = Redis.new
-
-Benchmark.bmbm do |benchmark|
-  benchmark.report("set") do
-    @r.flushdb
-
-    ITERATIONS.times do |i|
-      @r.set("foo#{i}", "Hello world!")
-      @r.get("foo#{i}")
-    end
-  end
-
-  benchmark.report("set (pipelined)") do
-    @r.flushdb
-
-    @r.pipelined do
-      ITERATIONS.times do |i|
-        @r.set("foo#{i}", "Hello world!")
-        @r.get("foo#{i}")
-      end
-    end
-  end
-
-  benchmark.report("lpush+ltrim") do
-    @r.flushdb
-
-    ITERATIONS.times do |i|
-      @r.lpush "lpush#{i}", i
-      @r.ltrim "ltrim#{i}", 0, 30
-    end
-  end
-
-  benchmark.report("lpush+ltrim (pipelined)") do
-    @r.flushdb
-
-    @r.pipelined do
-      ITERATIONS.times do |i|
-        @r.lpush "lpush#{i}", i
-        @r.ltrim "ltrim#{i}", 0, 30
-      end
-    end
-  end
-end
diff --git a/benchmarking/speed.rb b/benchmarking/speed.rb
deleted file mode 100644
index cb5bc46a74fff5791ff2bfb2bbf4ec3872829795..0000000000000000000000000000000000000000
--- a/benchmarking/speed.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-$LOAD_PATH.push File.join(__dir__, 'lib')
-
-require "benchmark"
-require "redis"
-
-r = Redis.new
-n = (ARGV.shift || 20_000).to_i
-
-elapsed = Benchmark.realtime do
-  # n sets, n gets
-  n.times do |i|
-    key = "foo#{i}"
-    r.set(key, key * 10)
-    r.get(key)
-  end
-end
-
-puts '%.2f Kops' % (2 * n / 1000 / elapsed)
diff --git a/benchmarking/suite.rb b/benchmarking/suite.rb
deleted file mode 100644
index de7dfe0efc525688df04e9375c2f6116be6aee24..0000000000000000000000000000000000000000
--- a/benchmarking/suite.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-require 'fileutils'
-
-def run_in_background(command)
-  fork { system command }
-end
-
-def with_all_segments(&block)
-  0.upto(9) do |segment_number|
-    block_size = 100_000
-    start_index = segment_number * block_size
-    end_index = start_index + block_size - 1
-    block.call(start_index, end_index)
-  end
-end
-
-# with_all_segments do |start_index, end_index|
-#  puts "Initializing keys from #{start_index} to #{end_index}"
-#  system "ruby worker.rb initialize #{start_index} #{end_index} 0"
-# end
-
-with_all_segments do |start_index, end_index|
-  run_in_background "ruby worker.rb write #{start_index} #{end_index} 10"
-  run_in_background "ruby worker.rb read #{start_index} #{end_index} 1"
-end
diff --git a/benchmarking/worker.rb b/benchmarking/worker.rb
deleted file mode 100644
index 15c6e5ac1c3a128cc173105e4fff151854961821..0000000000000000000000000000000000000000
--- a/benchmarking/worker.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-# frozen_string_literal: true
-
-BENCHMARK_ROOT = File.dirname(__FILE__)
-REDIS_ROOT = File.join(BENCHMARK_ROOT, "..", "lib")
-
-$LOAD_PATH << REDIS_ROOT
-require 'redis'
-require 'benchmark'
-
-def show_usage
-  puts <<-EOL
-    Usage: worker.rb [read:write] <start_index> <end_index> <sleep_msec>
-  EOL
-end
-
-def shift_from_argv
-  value = ARGV.shift
-  unless value
-    show_usage
-    exit(-1)
-  end
-  value
-end
-
-operation = shift_from_argv.to_sym
-start_index = shift_from_argv.to_i
-end_index = shift_from_argv.to_i
-sleep_msec = shift_from_argv.to_i
-sleep_duration = sleep_msec / 1000.0
-
-redis = Redis.new
-
-case operation
-when :initialize
-
-  start_index.upto(end_index) do |i|
-    redis[i] = 0
-  end
-
-when :clear
-
-  start_index.upto(end_index) do |i|
-    redis.delete(i)
-  end
-
-when :read, :write
-
-  puts "Starting to #{operation} at segment #{end_index + 1}"
-
-  loop do
-    t1 = Time.now
-    start_index.upto(end_index) do |i|
-      case operation
-      when :read
-        redis.get(i)
-      when :write
-        redis.incr(i)
-        else
-        raise "Unknown operation: #{operation}"
-      end
-      sleep sleep_duration
-    end
-    t2 = Time.now
-
-    requests_processed = end_index - start_index
-    time = t2 - t1
-    puts "#{t2.strftime('%H:%M')} [segment #{end_index + 1}] : Processed #{requests_processed} requests " \
-      "in #{time} seconds - #{(requests_processed / time).round} requests/sec"
-  end
-
-  else
-  raise "Unknown operation: #{operation}"
-end
diff --git a/bin/cluster_creator b/bin/cluster_creator
index cacca0c5f248e27fd9153820ad24e772b28ef60b..5e9761fa7ad233ea7d7c28f89a5e166d0e3f783a 100755
--- a/bin/cluster_creator
+++ b/bin/cluster_creator
@@ -1,11 +1,13 @@
 #!/usr/bin/env ruby
-
 # frozen_string_literal: true
 
+puts ARGV.join(" ")
+require 'bundler/setup'
+
 $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
-require_relative '../test/support/cluster/orchestrator'
+require_relative '../cluster/test/support/orchestrator'
 
 urls = ARGV.map { |host_port| "redis://#{host_port}" }
-orchestrator = ClusterOrchestrator.new(urls, timeout: 30.0)
+orchestrator = ClusterOrchestrator.new(urls, timeout: 3.0)
 orchestrator.rebuild
 orchestrator.close
diff --git a/bors.toml b/bors.toml
deleted file mode 100644
index e22ed32805cf9d3122efbc790195c50e0b18b52e..0000000000000000000000000000000000000000
--- a/bors.toml
+++ /dev/null
@@ -1,14 +0,0 @@
-# Gate on Travis CI
-status = ["continuous-integration/travis-ci/push"]
-
-# Set bors's timeout to 6 hours
-#
-# bors's timeout should always be twice a long as the test suite takes.
-# This is to allow Travis to fast-fail a test; if one of the builders
-# immediately reports a failure, then bors will move on to the next batch,
-# leaving the slower builders to work through the already-doomed run and
-# the next one.
-#
-# At the time it was written, a run had taken 3 hours.
-# bors's default timeout is 4 hours.
-timeout_sec = 14400
diff --git a/cluster/CHANGELOG.md b/cluster/CHANGELOG.md
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/cluster/Gemfile b/cluster/Gemfile
new file mode 100644
index 0000000000000000000000000000000000000000..ff5d5832afeb20b45760d04e3a5dadfe0949bb86
--- /dev/null
+++ b/cluster/Gemfile
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+source 'https://rubygems.org'
+
+gemspec
+
+gem 'redis', path: File.expand_path("..", __dir__)
+gem 'minitest'
+gem 'rake'
+gem 'rubocop', '~> 1.25.1'
+gem 'mocha'
diff --git a/cluster/LICENSE b/cluster/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..5e648fa9d911c0593ab260692786e57475bb7e90
--- /dev/null
+++ b/cluster/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2009 Ezra Zygmuntowicz
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/cluster/README.md b/cluster/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..d4722207716eff7721bd90aea541f7cb2dc078b0
--- /dev/null
+++ b/cluster/README.md
@@ -0,0 +1,77 @@
+# Redis::Cluster
+
+## Getting started
+
+Install with:
+
+```
+$ gem install redis-clustering
+```
+
+You can connect to Redis by instantiating the `Redis::Cluster` class:
+
+```ruby
+require "redis-clustering"
+
+redis = Redis::Cluster.new(nodes: (7000..7005).map { |port| "redis://127.0.0.1:#{port}" })
+```
+
+NB: Both `redis_cluster` and `redis-cluster` are unrelated and abandoned gems.
+
+```ruby
+# Nodes can be passed to the client as an array of connection URLs.
+nodes = (7000..7005).map { |port| "redis://127.0.0.1:#{port}" }
+redis = Redis::Cluster.new(nodes: nodes)
+
+# You can also specify the options as a Hash. The options are the same as for a single server connection.
+(7000..7005).map { |port| { host: '127.0.0.1', port: port } }
+```
+
+You can also specify only a subset of the nodes, and the client will discover the missing ones using the [CLUSTER NODES](https://redis.io/commands/cluster-nodes) command.
+
+```ruby
+Redis::Cluster.new(nodes: %w[redis://127.0.0.1:7000])
+```
+
+If you want [the connection to be able to read from any replica](https://redis.io/commands/readonly), you must pass the `replica: true`. Note that this connection won't be usable to write keys.
+
+```ruby
+Redis::Cluster.new(nodes: nodes, replica: true)
+```
+
+Also, you can specify the `:replica_affinity` option if you want to prevent accessing cross availability zones.
+
+```ruby
+Redis::Cluster.new(nodes: nodes, replica: true, replica_affinity: :latency)
+```
+
+The calling code is responsible for [avoiding cross slot commands](https://redis.io/topics/cluster-spec#keys-distribution-model).
+
+```ruby
+redis = Redis::Cluster.new(nodes: %w[redis://127.0.0.1:7000])
+
+redis.mget('key1', 'key2')
+#=> Redis::CommandError (CROSSSLOT Keys in request don't hash to the same slot)
+
+redis.mget('{key}1', '{key}2')
+#=> [nil, nil]
+```
+
+* The client automatically reconnects after a failover occurred, but the caller is responsible for handling errors while it is happening.
+* The client support permanent node failures, and will reroute requests to promoted slaves.
+* The client supports `MOVED` and `ASK` redirections transparently.
+
+## Cluster mode with SSL/TLS
+Since Redis can return FQDN of nodes in reply to client since `7.*` with CLUSTER commands, we can use cluster feature with SSL/TLS connection like this:
+
+```ruby
+Redis::Cluster.new(nodes: %w[rediss://foo.example.com:6379])
+```
+
+On the other hand, in Redis versions prior to `6.*`, you can specify options like the following if cluster mode is enabled and client has to connect to nodes via single endpoint with SSL/TLS.
+
+```ruby
+Redis::Cluster.new(nodes: %w[rediss://foo-endpoint.example.com:6379], fixed_hostname: 'foo-endpoint.example.com')
+```
+
+In case of the above architecture, if you don't pass the `fixed_hostname` option to the client and servers return IP addresses of nodes, the client may fail to verify certificates.
diff --git a/cluster/lib/redis-clustering.rb b/cluster/lib/redis-clustering.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d0343944035491ea25439e64d3052a2b62847f6b
--- /dev/null
+++ b/cluster/lib/redis-clustering.rb
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+require "redis/cluster"
diff --git a/cluster/lib/redis/cluster.rb b/cluster/lib/redis/cluster.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5abbb628c72975672d64c7573245eb978ad1826c
--- /dev/null
+++ b/cluster/lib/redis/cluster.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+require "redis"
+
+class Redis
+  class Cluster < ::Redis
+    # Raised when client connected to redis as cluster mode
+    # and failed to fetch cluster state information by commands.
+    class InitialSetupError < BaseError
+    end
+
+    # Raised when client connected to redis as cluster mode
+    # and some cluster subcommands were called.
+    class OrchestrationCommandNotSupported < BaseError
+      def initialize(command, subcommand = '')
+        str = [command, subcommand].map(&:to_s).reject(&:empty?).join(' ').upcase
+        msg = "#{str} command should be used with care "\
+              'only by applications orchestrating Redis Cluster, like redis-trib, '\
+              'and the command if used out of the right context can leave the cluster '\
+              'in a wrong state or cause data loss.'
+        super(msg)
+      end
+    end
+
+    # Raised when error occurs on any node of cluster.
+    class CommandErrorCollection < BaseError
+      attr_reader :errors
+
+      # @param errors [Hash{String => Redis::CommandError}]
+      # @param error_message [String]
+      def initialize(errors, error_message = 'Command errors were replied on any node')
+        @errors = errors
+        super(error_message)
+      end
+    end
+
+    # Raised when cluster client can't select node.
+    class AmbiguousNodeError < BaseError
+    end
+
+    class TransactionConsistencyError < BaseError
+    end
+
+    class NodeMightBeDown < BaseError
+    end
+
+    def connection
+      raise NotImplementedError, "Redis::Cluster doesn't implement #connection"
+    end
+
+    # Create a new client instance
+    #
+    # @param [Hash] options
+    # @option options [Float] :timeout (5.0) timeout in seconds
+    # @option options [Float] :connect_timeout (same as timeout) timeout for initial connect in seconds
+    # @option options [Symbol] :driver Driver to use, currently supported: `:ruby`, `:hiredis`
+    # @option options [Integer, Array<Integer, Float>] :reconnect_attempts Number of attempts trying to connect,
+    #   or a list of sleep duration between attempts.
+    # @option options [Boolean] :inherit_socket (false) Whether to use socket in forked process or not
+    # @option options [Array<String, Hash{Symbol => String, Integer}>] :nodes List of cluster nodes to contact
+    # @option options [Boolean] :replica Whether to use readonly replica nodes in Redis Cluster or not
+    # @option options [Symbol] :replica_affinity scale reading strategy, currently supported: `:random`, `:latency`
+    # @option options [String] :fixed_hostname Specify a FQDN if cluster mode enabled and
+    #   client has to connect nodes via single endpoint with SSL/TLS
+    # @option options [Class] :connector Class of custom connector
+    #
+    # @return [Redis::Cluster] a new client instance
+    def initialize(*) # rubocop:disable Lint/UselessMethodDefinition
+      super
+    end
+    ruby2_keywords :initialize if respond_to?(:ruby2_keywords, true)
+
+    # Sends `CLUSTER *` command to random node and returns its reply.
+    #
+    # @see https://redis.io/commands#cluster Reference of cluster command
+    #
+    # @param subcommand [String, Symbol] the subcommand of cluster command
+    #   e.g. `:slots`, `:nodes`, `:slaves`, `:info`
+    #
+    # @return [Object] depends on the subcommand
+    def cluster(subcommand, *args)
+      subcommand = subcommand.to_s.downcase
+      block = case subcommand
+      when 'slots'
+        HashifyClusterSlots
+      when 'nodes'
+        HashifyClusterNodes
+      when 'slaves'
+        HashifyClusterSlaves
+      when 'info'
+        HashifyInfo
+      else
+        Noop
+      end
+
+      send_command([:cluster, subcommand] + args, &block)
+    end
+
+    def watch(*keys, &block)
+      synchronize { |c| c.call_v([:watch] + keys, &block) }
+    end
+
+    private
+
+    def initialize_client(options)
+      cluster_config = RedisClient.cluster(**options, protocol: 2, client_implementation: ::Redis::Cluster::Client)
+      cluster_config.new_client
+    end
+  end
+end
+
+require "redis/cluster/client"
diff --git a/cluster/lib/redis/cluster/client.rb b/cluster/lib/redis/cluster/client.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a0e889926496b181e64761bb76e9113f61b01595
--- /dev/null
+++ b/cluster/lib/redis/cluster/client.rb
@@ -0,0 +1,110 @@
+# frozen_string_literal: true
+
+require 'redis-cluster-client'
+
+class Redis
+  class Cluster
+    class Client < RedisClient::Cluster
+      ERROR_MAPPING = ::Redis::Client::ERROR_MAPPING.merge(
+        RedisClient::Cluster::InitialSetupError => Redis::Cluster::InitialSetupError,
+        RedisClient::Cluster::OrchestrationCommandNotSupported => Redis::Cluster::OrchestrationCommandNotSupported,
+        RedisClient::Cluster::AmbiguousNodeError => Redis::Cluster::AmbiguousNodeError,
+        RedisClient::Cluster::ErrorCollection => Redis::Cluster::CommandErrorCollection,
+        RedisClient::Cluster::Transaction::ConsistencyError => Redis::Cluster::TransactionConsistencyError,
+        RedisClient::Cluster::NodeMightBeDown => Redis::Cluster::NodeMightBeDown,
+      )
+
+      class << self
+        def config(**kwargs)
+          super(protocol: 2, **kwargs)
+        end
+
+        def sentinel(**kwargs)
+          super(protocol: 2, **kwargs)
+        end
+
+        def translate_error!(error, mapping: ERROR_MAPPING)
+          case error
+          when RedisClient::Cluster::ErrorCollection
+            error.errors.each do |_node, node_error|
+              if node_error.is_a?(RedisClient::AuthenticationError)
+                raise mapping.fetch(node_error.class), node_error.message, node_error.backtrace
+              end
+            end
+
+            remapped_node_errors = error.errors.map do |node_key, node_error|
+              remapped = mapping.fetch(node_error.class, node_error.class).new(node_error.message)
+              remapped.set_backtrace node_error.backtrace
+              [node_key, remapped]
+            end.to_h
+
+            raise(Redis::Cluster::CommandErrorCollection.new(remapped_node_errors, error.message).tap do |remapped|
+              remapped.set_backtrace error.backtrace
+            end)
+          else
+            Redis::Client.translate_error!(error, mapping: mapping)
+          end
+        end
+      end
+
+      def initialize(*)
+        handle_errors { super }
+      end
+      ruby2_keywords :initialize if respond_to?(:ruby2_keywords, true)
+
+      def id
+        @router.node_keys.join(' ')
+      end
+
+      def server_url
+        @router.node_keys
+      end
+
+      def connected?
+        true
+      end
+
+      def disable_reconnection
+        yield # TODO: do we need this, is it doable?
+      end
+
+      def timeout
+        config.read_timeout
+      end
+
+      def db
+        0
+      end
+
+      undef_method :call
+      undef_method :call_once
+      undef_method :call_once_v
+      undef_method :blocking_call
+
+      def call_v(command, &block)
+        handle_errors { super(command, &block) }
+      end
+
+      def blocking_call_v(timeout, command, &block)
+        timeout += self.timeout if timeout && timeout > 0
+        handle_errors { super(timeout, command, &block) }
+      end
+
+      def pipelined(exception: true, &block)
+        handle_errors { super(exception: exception, &block) }
+      end
+
+      def multi(watch: nil, &block)
+        handle_errors { super(watch: watch, &block) }
+      end
+
+      private
+
+      def handle_errors
+        yield
+      rescue ::RedisClient::Error => error
+        Redis::Cluster::Client.translate_error!(error)
+      end
+    end
+  end
+end
diff --git a/cluster/lib/redis/cluster/version.rb b/cluster/lib/redis/cluster/version.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1e5507a70c3ef79b380a8159463c0918f6b3ea73
--- /dev/null
+++ b/cluster/lib/redis/cluster/version.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require "redis/version"
+
+class Redis
+  class Cluster
+    VERSION = Redis::VERSION
+  end
+end
diff --git a/cluster/redis-clustering.gemspec b/cluster/redis-clustering.gemspec
new file mode 100644
index 0000000000000000000000000000000000000000..05194217bd52874268cfaa93f5f14778af95cf47
--- /dev/null
+++ b/cluster/redis-clustering.gemspec
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require_relative "../lib/redis/version"
+
+Gem::Specification.new do |s|
+  s.name = "redis-clustering"
+
+  s.version = Redis::VERSION
+
+  github_root = "https://github.com/redis/redis-rb"
+  s.homepage = "#{github_root}/blob/master/cluster"
+
+  s.summary = "A Ruby client library for Redis Cluster"
+
+  s.description = <<-EOS
+  A Ruby client that tries to match Redis' Cluster API one-to-one, while still
+    providing an idiomatic interface.
+  EOS
+
+  s.license = "MIT"
+
+  s.authors = [
+    "Ezra Zygmuntowicz",
+    "Taylor Weibley",
+    "Matthew Clark",
+    "Brian McKinney",
+    "Salvatore Sanfilippo",
+    "Luca Guidi",
+    "Michel Martens",
+    "Damian Janowski",
+    "Pieter Noordhuis"
+  ]
+
+  s.email = ["redis-db@googlegroups.com"]
+
+  s.metadata = {
+    "bug_tracker_uri" => "#{github_root}/issues",
+    "changelog_uri" => "#{s.homepage}/CHANGELOG.md",
+    "documentation_uri" => "https://www.rubydoc.info/gems/redis/#{s.version}",
+    "homepage_uri" => s.homepage,
+    "source_code_uri" => "#{github_root}/tree/v#{s.version}/cluster"
+  }
+
+  s.files         = Dir["CHANGELOG.md", "LICENSE", "README.md", "lib/**/*"]
+  s.executables   = `git ls-files -- exe/*`.split("\n").map { |f| File.basename(f) }
+
+  s.required_ruby_version = '>= 2.7.0'
+
+  s.add_runtime_dependency('redis', s.version)
+  s.add_runtime_dependency('redis-cluster-client', '>= 0.7.11')
+end
diff --git a/test/cluster/blocking_commands_test.rb b/cluster/test/blocking_commands_test.rb
similarity index 78%
rename from test/cluster/blocking_commands_test.rb
rename to cluster/test/blocking_commands_test.rb
index 1e6acf6f51134e4424f6d6232c2560fbcb9709b0..6adc0265639f1070da80277c81d1ed072624edba 100644
--- a/test/cluster/blocking_commands_test.rb
+++ b/cluster/test/blocking_commands_test.rb
@@ -9,6 +9,6 @@ class TestClusterBlockingCommands < Minitest::Test
 
   def mock(options = {}, &blk)
     commands = build_mock_commands(options)
-    redis_cluster_mock(commands, { timeout: LOW_TIMEOUT }, &blk)
+    redis_cluster_mock(commands, { timeout: LOW_TIMEOUT, concurrent: true }, &blk)
   end
 end
diff --git a/test/cluster/client_internals_test.rb b/cluster/test/client_internals_test.rb
similarity index 50%
rename from test/cluster/client_internals_test.rb
rename to cluster/test/client_internals_test.rb
index 02e3eeb21cab8854bddf29d84b17d36ffcbb847b..2f5d660d52b7222e28015afcfae86e7c7cdd0423 100644
--- a/test/cluster/client_internals_test.rb
+++ b/cluster/test/client_internals_test.rb
@@ -21,24 +21,16 @@ class TestClusterClientInternals < Minitest::Test
     end
   end
 
-  def test_with_reconnect
-    assert_equal('Hello World', redis.with_reconnect { 'Hello World' })
-  end
-
-  def test_without_reconnect
-    assert_equal('Hello World', redis.without_reconnect { 'Hello World' })
-  end
-
   def test_connected?
     assert_equal true, redis.connected?
   end
 
   def test_close
-    assert_equal true, redis.close
+    redis.close
   end
 
   def test_disconnect!
-    assert_equal true, redis.disconnect!
+    redis.disconnect!
   end
 
   def test_asking
@@ -46,39 +38,25 @@ class TestClusterClientInternals < Minitest::Test
   end
 
   def test_id
-    expected = 'redis://127.0.0.1:7000/0 '\
-               'redis://127.0.0.1:7001/0 '\
-               'redis://127.0.0.1:7002/0'
+    expected = '127.0.0.1:16380 '\
+               '127.0.0.1:16381 '\
+               '127.0.0.1:16382'
     assert_equal expected, redis.id
   end
 
   def test_inspect
     expected = "#<Redis client v#{Redis::VERSION} for "\
-               'redis://127.0.0.1:7000/0 '\
-               'redis://127.0.0.1:7001/0 '\
-               'redis://127.0.0.1:7002/0>'
+                  '127.0.0.1:16380 '\
+                  '127.0.0.1:16381 '\
+                  '127.0.0.1:16382>'
 
     assert_equal expected, redis.inspect
   end
 
-  def test_dup
-    assert_instance_of Redis, redis.dup
-  end
-
-  def test_connection
-    expected = [
-      { host: '127.0.0.1', port: 7000, db: 0, id: 'redis://127.0.0.1:7000/0', location: '127.0.0.1:7000' },
-      { host: '127.0.0.1', port: 7001, db: 0, id: 'redis://127.0.0.1:7001/0', location: '127.0.0.1:7001' },
-      { host: '127.0.0.1', port: 7002, db: 0, id: 'redis://127.0.0.1:7002/0', location: '127.0.0.1:7002' }
-    ]
-
-    assert_equal expected, redis.connection
-  end
-
   def test_acl_auth_success
     target_version "6.0.0" do
       with_acl do |username, password|
-        r = _new_client(cluster: DEFAULT_PORTS.map { |port| "redis://#{username}:#{password}@#{DEFAULT_HOST}:#{port}" })
+        r = _new_client(nodes: DEFAULT_PORTS.map { |port| "redis://#{username}:#{password}@#{DEFAULT_HOST}:#{port}" })
         assert_equal('PONG', r.ping)
       end
     end
@@ -88,7 +66,7 @@ class TestClusterClientInternals < Minitest::Test
     target_version "6.0.0" do
       with_acl do |username, _|
         assert_raises(Redis::Cluster::InitialSetupError) do
-          _new_client(cluster: DEFAULT_PORTS.map { |port| "redis://#{username}:wrongpassword@#{DEFAULT_HOST}:#{port}" })
+          _new_client(nodes: DEFAULT_PORTS.map { |port| "redis://#{username}:wrongpassword@#{DEFAULT_HOST}:#{port}" })
         end
       end
     end
diff --git a/test/cluster/client_pipelining_test.rb b/cluster/test/client_pipelining_test.rb
similarity index 61%
rename from test/cluster/client_pipelining_test.rb
rename to cluster/test/client_pipelining_test.rb
index 62d6bcbae8d2694a6dc151a22843801998d2c286..4116c9a536e6de16b80fffc588fd9779b27b3f6f 100644
--- a/test/cluster/client_pipelining_test.rb
+++ b/cluster/test/client_pipelining_test.rb
@@ -38,35 +38,42 @@ class TestClusterClientPipelining < Minitest::Test
   end
 
   def test_pipelining_without_hash_tags
-    assert_raises(Redis::Cluster::CrossSlotPipeliningError) do
-      redis.pipelined do
-        redis.set(:a, 1)
-        redis.set(:b, 2)
-        redis.set(:c, 3)
-        redis.set(:d, 4)
-        redis.set(:e, 5)
-        redis.set(:f, 6)
+    result = redis.pipelined do |pipeline|
+      pipeline.set(:a, 1)
+      pipeline.set(:b, 2)
+      pipeline.set(:c, 3)
+      pipeline.set(:d, 4)
+      pipeline.set(:e, 5)
+      pipeline.set(:f, 6)
+    end
+    assert_equal ["OK"] * 6, result
 
-        redis.get(:a)
-        redis.get(:b)
-        redis.get(:c)
-        redis.get(:d)
-        redis.get(:e)
-        redis.get(:f)
-      end
+    result = redis.pipelined do |pipeline|
+      pipeline.get(:a)
+      pipeline.get(:b)
+      pipeline.get(:c)
+      pipeline.get(:d)
+      pipeline.get(:e)
+      pipeline.get(:f)
     end
+    assert_equal 1.upto(6).map(&:to_s), result
   end
 
-  def test_pipelining_with_multiple_replicas
-    rc = build_another_client(replica: true)
-    rc.instance_variable_get(:@client).instance_variable_get(:@slot).instance_variable_get(:@map).each do |_, v|
-      v[:slaves] << v[:master] if v[:slaves].size < 2 # reproducing multiple replicas
+  def test_pipeline_unmapped_errors_are_bubbled_up
+    ex = Class.new(StandardError)
+    assert_raises(ex) do
+      redis.pipelined do |_pipe|
+        raise ex, "boom"
+      end
     end
+  end
 
-    rc.pipelined do |r|
-      10.times { r.get('key1') }
+  def test_pipeline_error_subclasses_are_mapped
+    ex = Class.new(RedisClient::ConnectionError)
+    assert_raises(Redis::ConnectionError) do
+      redis.pipelined do |_pipe|
+        raise ex, "tick tock"
+      end
     end
-
-    rc.close
   end
 end
diff --git a/cluster/test/client_replicas_test.rb b/cluster/test/client_replicas_test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e6926d52e3000fc21315ab51b429e8edaf2f5790
--- /dev/null
+++ b/cluster/test/client_replicas_test.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require "helper"
+
+# ruby -w -Itest test/cluster_client_replicas_test.rb
+class TestClusterClientReplicas < Minitest::Test
+  include Helper::Cluster
+
+  def test_client_can_command_with_replica
+    r = build_another_client(replica: true)
+
+    100.times do |i|
+      assert_equal 'OK', r.set("key#{i}", i)
+    end
+
+    r.wait(1, TIMEOUT.to_i * 1000)
+
+    100.times do |i|
+      assert_equal i.to_s, r.get("key#{i}")
+    end
+  end
+end
diff --git a/cluster/test/client_transactions_test.rb b/cluster/test/client_transactions_test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4258200e7f6de138f90340b5159c9cb40a20c27c
--- /dev/null
+++ b/cluster/test/client_transactions_test.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require "helper"
+
+# ruby -w -Itest test/cluster_client_transactions_test.rb
+class TestClusterClientTransactions < Minitest::Test
+  include Helper::Cluster
+
+  def test_cluster_client_does_support_transaction_by_single_key
+    actual = redis.multi do |r|
+      r.set('counter', '0')
+      r.incr('counter')
+      r.incr('counter')
+    end
+
+    assert_equal(['OK', 1, 2], actual)
+    assert_equal('2', redis.get('counter'))
+  end
+
+  def test_cluster_client_does_support_transaction_by_hashtag
+    actual = redis.multi do |r|
+      r.mset('{key}1', 1, '{key}2', 2)
+      r.mset('{key}3', 3, '{key}4', 4)
+    end
+
+    assert_equal(%w[OK OK], actual)
+    assert_equal(%w[1 2 3 4], redis.mget('{key}1', '{key}2', '{key}3', '{key}4'))
+  end
+
+  def test_cluster_client_does_not_support_transaction_by_multiple_keys
+    assert_raises(Redis::Cluster::TransactionConsistencyError) do
+      redis.multi do |r|
+        r.set('key1', 1)
+        r.set('key2', 2)
+        r.set('key3', 3)
+        r.set('key4', 4)
+      end
+    end
+
+    assert_raises(Redis::Cluster::TransactionConsistencyError) do
+      redis.multi do |r|
+        r.mset('key1', 1, 'key2', 2)
+        r.mset('key3', 3, 'key4', 4)
+      end
+    end
+
+    (1..4).each do |i|
+      assert_nil(redis.get("key#{i}"))
+    end
+  end
+
+  def test_cluster_client_does_support_transaction_with_optimistic_locking
+    redis.mset('{key}1', '1', '{key}2', '2')
+
+    another = Fiber.new do
+      cli = build_another_client
+      cli.mset('{key}1', '3', '{key}2', '4')
+      cli.close
+      Fiber.yield
+    end
+
+    redis.watch('{key}1', '{key}2') do |tx|
+      another.resume
+      v1 = redis.get('{key}1')
+      v2 = redis.get('{key}2')
+      tx.call('SET', '{key}1', v2)
+      tx.call('SET', '{key}2', v1)
+    end
+
+    assert_equal %w[3 4], redis.mget('{key}1', '{key}2')
+  end
+end
diff --git a/test/cluster/commands_on_cluster_test.rb b/cluster/test/commands_on_cluster_test.rb
similarity index 86%
rename from test/cluster/commands_on_cluster_test.rb
rename to cluster/test/commands_on_cluster_test.rb
index 4500e35e2726582656f7d68d74067f9056370429..a6fea1bbdd9c51344dc268fba407b0ca1b21f337 100644
--- a/test/cluster/commands_on_cluster_test.rb
+++ b/cluster/test/commands_on_cluster_test.rb
@@ -63,14 +63,6 @@ class TestClusterCommandsOnCluster < Minitest::Test
     assert_equal '3', info.fetch('cluster_size')
   end
 
-  def test_cluster_keyslot
-    assert_equal Redis::Cluster::KeySlotConverter.convert('hogehoge'), redis.cluster(:keyslot, 'hogehoge')
-    assert_equal Redis::Cluster::KeySlotConverter.convert('12345'), redis.cluster(:keyslot, '12345')
-    assert_equal Redis::Cluster::KeySlotConverter.convert('foo'), redis.cluster(:keyslot, 'boo{foo}woo')
-    assert_equal Redis::Cluster::KeySlotConverter.convert('antirez.is.cool'), redis.cluster(:keyslot, 'antirez.is.cool')
-    assert_equal Redis::Cluster::KeySlotConverter.convert(''), redis.cluster(:keyslot, '')
-  end
-
   def test_cluster_meet
     assert_raises(Redis::Cluster::OrchestrationCommandNotSupported, 'CLUSTER MEET command should be...') do
       redis.cluster(:meet, '127.0.0.1', 11_211)
@@ -146,9 +138,11 @@ class TestClusterCommandsOnCluster < Minitest::Test
     assert_equal true, sample_slot.fetch('master').key?('node_id')
     assert_equal true, sample_slot.key?('replicas')
     assert_equal true, sample_slot.fetch('replicas').is_a?(Array)
-    assert_equal true, sample_slot.fetch('replicas').first.key?('ip')
-    assert_equal true, sample_slot.fetch('replicas').first.key?('port')
-    assert_equal true, sample_slot.fetch('replicas').first.key?('node_id')
+    sample_slot.fetch('replicas').each do |replica|
+      assert_equal true, replica.key?('ip')
+      assert_equal true, replica.key?('port')
+      assert_equal true, replica.key?('node_id')
+    end
   end
 
   def test_readonly
diff --git a/test/cluster/commands_on_connection_test.rb b/cluster/test/commands_on_connection_test.rb
similarity index 100%
rename from test/cluster/commands_on_connection_test.rb
rename to cluster/test/commands_on_connection_test.rb
diff --git a/cluster/test/commands_on_geo_test.rb b/cluster/test/commands_on_geo_test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6dc94a9675f2b43382b54ac39192ed1517ba70a6
--- /dev/null
+++ b/cluster/test/commands_on_geo_test.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require "helper"
+
+# ruby -w -Itest test/cluster_commands_on_geo_test.rb
+# @see https://redis.io/commands#geo
+class TestClusterCommandsOnGeo < Minitest::Test
+  include Helper::Cluster
+
+  def add_sicily
+    redis.geoadd('Sicily',
+                 13.361389, 38.115556, 'Palermo',
+                 15.087269, 37.502669, 'Catania')
+  end
+
+  def test_geoadd
+    assert_equal 2, add_sicily
+  end
+
+  def test_geohash
+    add_sicily
+    assert_equal %w[sqc8b49rny0 sqdtr74hyu0], redis.geohash('Sicily', %w[Palermo Catania])
+  end
+
+  def test_geopos
+    add_sicily
+    expected = [%w[13.36138933897018433 38.11555639549629859],
+                %w[15.08726745843887329 37.50266842333162032],
+                nil]
+    assert_equal expected, redis.geopos('Sicily', %w[Palermo Catania NonExisting])
+  end
+
+  def test_geodist
+    add_sicily
+    assert_equal '166274.1516', redis.geodist('Sicily', 'Palermo', 'Catania')
+    assert_equal '166.2742', redis.geodist('Sicily', 'Palermo', 'Catania', 'km')
+    assert_equal '103.3182', redis.geodist('Sicily', 'Palermo', 'Catania', 'mi')
+  end
+
+  def test_georadius
+    add_sicily
+
+    expected = [%w[Palermo 190.4424], %w[Catania 56.4413]]
+    assert_equal expected, redis.georadius('Sicily', 15, 37, 200, 'km', 'WITHDIST')
+
+    expected = [['Palermo', %w[13.36138933897018433 38.11555639549629859]],
+                ['Catania', %w[15.08726745843887329 37.50266842333162032]]]
+    assert_equal expected, redis.georadius('Sicily', 15, 37, 200, 'km', 'WITHCOORD')
+
+    expected = [['Palermo', '190.4424', %w[13.36138933897018433 38.11555639549629859]],
+                ['Catania', '56.4413', %w[15.08726745843887329 37.50266842333162032]]]
+    assert_equal expected, redis.georadius('Sicily', 15, 37, 200, 'km', 'WITHDIST', 'WITHCOORD')
+  end
+
+  def test_georadiusbymember
+    redis.geoadd('Sicily', 13.583333, 37.316667, 'Agrigento')
+    add_sicily
+    assert_equal %w[Agrigento Palermo], redis.georadiusbymember('Sicily', 'Agrigento', 100, 'km')
+  end
+end
diff --git a/test/cluster/commands_on_hashes_test.rb b/cluster/test/commands_on_hashes_test.rb
similarity index 100%
rename from test/cluster/commands_on_hashes_test.rb
rename to cluster/test/commands_on_hashes_test.rb
diff --git a/test/cluster/commands_on_hyper_log_log_test.rb b/cluster/test/commands_on_hyper_log_log_test.rb
similarity index 100%
rename from test/cluster/commands_on_hyper_log_log_test.rb
rename to cluster/test/commands_on_hyper_log_log_test.rb
diff --git a/test/cluster/commands_on_keys_test.rb b/cluster/test/commands_on_keys_test.rb
similarity index 82%
rename from test/cluster/commands_on_keys_test.rb
rename to cluster/test/commands_on_keys_test.rb
index 0d319e74d9cddd0d0f21aeb77128e8090aa494e0..6227e5b46e3a9ec75638494eca471366fcb6f59f 100644
--- a/test/cluster/commands_on_keys_test.rb
+++ b/cluster/test/commands_on_keys_test.rb
@@ -47,8 +47,6 @@ class TestClusterCommandsOnKeys < Minitest::Test
   def test_object
     redis.lpush('mylist', 'Hello World')
     assert_equal 1, redis.object('refcount', 'mylist')
-    expected_encoding = version < '3.2.0' ? 'ziplist' : 'quicklist'
-    assert_equal expected_encoding, redis.object('encoding', 'mylist')
     assert(redis.object('idletime', 'mylist') >= 0)
 
     redis.set('foo', 1000)
@@ -93,29 +91,25 @@ class TestClusterCommandsOnKeys < Minitest::Test
   end
 
   def test_touch
-    target_version('3.2.1') do
-      set_some_keys
-      assert_equal 1, redis.touch('key1')
-      assert_equal 1, redis.touch('key2')
-      if version < '6'
-        assert_equal 1, redis.touch('key1', 'key2')
-      else
-        assert_raises(Redis::CommandError, "CROSSSLOT Keys in request don't hash to the same slot") do
-          redis.touch('key1', 'key2')
-        end
+    set_some_keys
+    assert_equal 1, redis.touch('key1')
+    assert_equal 1, redis.touch('key2')
+    if version < '6'
+      assert_equal 1, redis.touch('key1', 'key2')
+    else
+      assert_raises(Redis::CommandError, "CROSSSLOT Keys in request don't hash to the same slot") do
+        redis.touch('key1', 'key2')
       end
-      assert_equal 2, redis.touch('{key}1', '{key}2')
     end
+    assert_equal 2, redis.touch('{key}1', '{key}2')
   end
 
   def test_unlink
-    target_version('4.0.0') do
-      set_some_keys
-      assert_raises(Redis::CommandError, "CROSSSLOT Keys in request don't hash to the same slot") do
-        redis.unlink('key1', 'key2', 'key3')
-      end
-      assert_equal 2, redis.unlink('{key}1', '{key}2', '{key}3')
+    set_some_keys
+    assert_raises(Redis::CommandError, "CROSSSLOT Keys in request don't hash to the same slot") do
+      redis.unlink('key1', 'key2', 'key3')
     end
+    assert_equal 2, redis.unlink('{key}1', '{key}2', '{key}3')
   end
 
   def test_wait
diff --git a/test/cluster/commands_on_lists_test.rb b/cluster/test/commands_on_lists_test.rb
similarity index 100%
rename from test/cluster/commands_on_lists_test.rb
rename to cluster/test/commands_on_lists_test.rb
diff --git a/cluster/test/commands_on_pub_sub_test.rb b/cluster/test/commands_on_pub_sub_test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..107f35a7b73142eec2580f2e53933904cc9abc15
--- /dev/null
+++ b/cluster/test/commands_on_pub_sub_test.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+require "helper"
+
+# ruby -w -Itest test/cluster_commands_on_pub_sub_test.rb
+# @see https://redis.io/commands#pubsub
+class TestClusterCommandsOnPubSub < Minitest::Test
+  include Helper::Cluster
+
+  def test_publish_subscribe_unsubscribe_pubsub
+    sub_cnt = 0
+    messages = {}
+
+    thread = Thread.new do
+      redis.subscribe('channel1', 'channel2') do |on|
+        on.subscribe { sub_cnt += 1 }
+        on.message do |c, msg|
+          messages[c] = msg
+          redis.unsubscribe if messages.size == 2
+        end
+      end
+    end
+
+    Thread.pass until sub_cnt == 2
+
+    publisher = build_another_client
+
+    assert_equal %w[channel1 channel2], publisher.pubsub(:channels, 'channel*')
+    assert_equal({ 'channel1' => 1, 'channel2' => 1, 'channel3' => 0 },
+                 publisher.pubsub(:numsub, 'channel1', 'channel2', 'channel3'))
+
+    publisher.publish('channel1', 'one')
+    publisher.publish('channel2', 'two')
+    publisher.publish('channel3', 'three')
+
+    thread.join
+
+    assert_equal(2, messages.size)
+    assert_equal('one', messages['channel1'])
+    assert_equal('two', messages['channel2'])
+  end
+
+  def test_publish_psubscribe_punsubscribe_pubsub
+    sub_cnt = 0
+    messages = {}
+
+    thread = Thread.new do
+      redis.psubscribe('guc*', 'her*') do |on|
+        on.psubscribe { sub_cnt += 1 }
+        on.pmessage do |_ptn, c, msg|
+          messages[c] = msg
+          redis.punsubscribe if messages.size == 2
+        end
+      end
+    end
+
+    Thread.pass until sub_cnt == 2
+
+    publisher = build_another_client
+
+    assert_equal 2, publisher.pubsub(:numpat)
+
+    publisher.publish('burberry1', 'one')
+    publisher.publish('gucci2', 'two')
+    publisher.publish('hermes3', 'three')
+
+    thread.join
+
+    assert_equal(2, messages.size)
+    assert_equal('two', messages['gucci2'])
+    assert_equal('three', messages['hermes3'])
+  end
+
+  def test_spublish_ssubscribe_sunsubscribe_pubsub
+    omit_version('7.0.0')
+
+    sub_cnt = 0
+    messages = {}
+
+    thread = Thread.new do
+      redis.ssubscribe('channel1', 'channel2') do |on|
+        on.ssubscribe { sub_cnt += 1 }
+        on.smessage do |c, msg|
+          messages[c] = msg
+          redis.sunsubscribe if messages.size == 2
+        end
+      end
+    end
+
+    Thread.pass until sub_cnt == 2
+
+    publisher = build_another_client
+
+    assert_equal %w[channel1 channel2], publisher.pubsub(:shardchannels, 'channel*')
+    assert_equal({ 'channel1' => 1, 'channel2' => 1, 'channel3' => 0 },
+                 publisher.pubsub(:shardnumsub, 'channel1', 'channel2', 'channel3'))
+
+    publisher.spublish('channel1', 'one')
+    publisher.spublish('channel2', 'two')
+    publisher.spublish('channel3', 'three')
+
+    thread.join
+
+    assert_equal(2, messages.size)
+    assert_equal('one', messages['channel1'])
+    assert_equal('two', messages['channel2'])
+  end
+end
diff --git a/test/cluster/commands_on_scripting_test.rb b/cluster/test/commands_on_scripting_test.rb
similarity index 91%
rename from test/cluster/commands_on_scripting_test.rb
rename to cluster/test/commands_on_scripting_test.rb
index 0ad8a6abc7998702e225bfe065e254d279316a88..b0af1dda09be9b968314805a117925a078814b87 100644
--- a/test/cluster/commands_on_scripting_test.rb
+++ b/cluster/test/commands_on_scripting_test.rb
@@ -28,10 +28,8 @@ class TestClusterCommandsOnScripting < Minitest::Test
   end
 
   def test_script_debug
-    target_version('3.2.0') do
-      assert_equal 'OK', redis.script(:debug, 'yes')
-      assert_equal 'OK', redis.script(:debug, 'no')
-    end
+    assert_equal 'OK', redis.script(:debug, 'yes')
+    assert_equal 'OK', redis.script(:debug, 'no')
   end
 
   def test_script_exists
diff --git a/test/cluster/commands_on_server_test.rb b/cluster/test/commands_on_server_test.rb
similarity index 78%
rename from test/cluster/commands_on_server_test.rb
rename to cluster/test/commands_on_server_test.rb
index f52400a7172413def5004da373ecbfa7f46fa134..3fc36a7a15441e1e16f1f2a832c438b164fe5afd 100644
--- a/test/cluster/commands_on_server_test.rb
+++ b/cluster/test/commands_on_server_test.rb
@@ -21,9 +21,11 @@ class TestClusterCommandsOnServer < Minitest::Test
               'Use BGSAVE SCHEDULE in order to schedule a BGSAVE whenever possible.'
 
     redis_cluster_mock(bgsave: ->(*_) { "-Error #{err_msg}" }) do |redis|
-      assert_raises(Redis::Cluster::CommandErrorCollection, 'Command error replied on any node') do
+      err = assert_raises(Redis::Cluster::CommandErrorCollection, 'Command error replied on any node') do
         redis.bgsave
       end
+      assert_includes err.message, err_msg
+      assert_kind_of Redis::CommandError, err.errors.values.first
     end
   end
 
@@ -41,12 +43,8 @@ class TestClusterCommandsOnServer < Minitest::Test
 
   def test_client_list
     a_client_info = redis.client(:list).first
-    actual = a_client_info.keys.sort
-    expected = %w[addr age cmd db events fd flags id idle multi name obl oll omem psub qbuf qbuf-free sub]
-    expected << 'user' << 'argv-mem' << 'tot-mem' if version >= '6'
-    expected << 'laddr' << 'redir' if version >= '6.2'
-    expected << "multi-mem" << "rbp" << "rbs" << "resp" << "ssub" if version >= '7.0'
-    assert_equal expected.sort, actual.sort
+    assert_instance_of Hash, a_client_info
+    assert_includes a_client_info, 'addr'
   end
 
   def test_client_getname
@@ -59,9 +57,7 @@ class TestClusterCommandsOnServer < Minitest::Test
   end
 
   def test_client_reply
-    target_version('3.2.0') do
-      assert_equal 'OK', redis.client(:reply, 'ON')
-    end
+    assert_equal 'OK', redis.client(:reply, 'ON')
   end
 
   def test_client_setname
@@ -143,40 +139,28 @@ class TestClusterCommandsOnServer < Minitest::Test
   end
 
   def test_memory_doctor
-    target_version('4.0.0') do
-      assert_instance_of String, redis.memory(:doctor)
-    end
+    assert_instance_of String, redis.memory(:doctor)
   end
 
   def test_memory_help
-    target_version('4.0.0') do
-      assert_instance_of Array, redis.memory(:help)
-    end
+    assert_instance_of Array, redis.memory(:help)
   end
 
   def test_memory_malloc_stats
-    target_version('4.0.0') do
-      assert_instance_of String, redis.memory('malloc-stats')
-    end
+    assert_instance_of String, redis.memory('malloc-stats')
   end
 
   def test_memory_purge
-    target_version('4.0.0') do
-      assert_equal 'OK', redis.memory(:purge)
-    end
+    assert_equal 'OK', redis.memory(:purge)
   end
 
   def test_memory_stats
-    target_version('4.0.0') do
-      assert_instance_of Array, redis.memory(:stats)
-    end
+    assert_instance_of Array, redis.memory(:stats)
   end
 
   def test_memory_usage
-    target_version('4.0.0') do
-      redis.set('key1', 'Hello World')
-      assert_operator redis.memory(:usage, 'key1'), :>, 0
-    end
+    redis.set('key1', 'Hello World')
+    assert_operator redis.memory(:usage, 'key1'), :>, 0
   end
 
   def test_monitor
diff --git a/test/cluster/commands_on_sets_test.rb b/cluster/test/commands_on_sets_test.rb
similarity index 100%
rename from test/cluster/commands_on_sets_test.rb
rename to cluster/test/commands_on_sets_test.rb
diff --git a/test/cluster/commands_on_sorted_sets_test.rb b/cluster/test/commands_on_sorted_sets_test.rb
similarity index 100%
rename from test/cluster/commands_on_sorted_sets_test.rb
rename to cluster/test/commands_on_sorted_sets_test.rb
diff --git a/test/cluster/commands_on_streams_test.rb b/cluster/test/commands_on_streams_test.rb
similarity index 100%
rename from test/cluster/commands_on_streams_test.rb
rename to cluster/test/commands_on_streams_test.rb
diff --git a/test/cluster/commands_on_strings_test.rb b/cluster/test/commands_on_strings_test.rb
similarity index 100%
rename from test/cluster/commands_on_strings_test.rb
rename to cluster/test/commands_on_strings_test.rb
diff --git a/cluster/test/commands_on_transactions_test.rb b/cluster/test/commands_on_transactions_test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0877f4c7beea8951801b7607c4ad483369165bf2
--- /dev/null
+++ b/cluster/test/commands_on_transactions_test.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require "helper"
+
+# ruby -w -Itest test/cluster_commands_on_transactions_test.rb
+# @see https://redis.io/commands#transactions
+class TestClusterCommandsOnTransactions < Minitest::Test
+  include Helper::Cluster
+
+  def test_discard
+    assert_raises(Redis::Cluster::AmbiguousNodeError) do
+      redis.discard
+    end
+  end
+
+  def test_exec
+    assert_raises(Redis::Cluster::AmbiguousNodeError) do
+      redis.exec
+    end
+  end
+
+  def test_multi
+    assert_raises(LocalJumpError) do
+      redis.multi
+    end
+
+    assert_raises(ArgumentError) do
+      redis.multi {}
+    end
+
+    assert_equal([1], redis.multi { |r| r.incr('counter') })
+  end
+
+  def test_unwatch
+    assert_raises(Redis::Cluster::AmbiguousNodeError) do
+      redis.unwatch
+    end
+  end
+
+  def test_watch
+    assert_raises(Redis::Cluster::TransactionConsistencyError) do
+      redis.watch('{key}1', '{key}2')
+    end
+
+    assert_raises(Redis::Cluster::TransactionConsistencyError) do
+      redis.watch('key1', 'key2') do |tx|
+        tx.call('SET', 'key1', '1')
+        tx.call('SET', 'key2', '2')
+      end
+    end
+
+    assert_raises(Redis::Cluster::TransactionConsistencyError) do
+      redis.watch('{hey}1', '{hey}2') do |tx|
+        tx.call('SET', '{key}1', '1')
+        tx.call('SET', '{key}2', '2')
+      end
+    end
+
+    redis.watch('{key}1', '{key}2') do |tx|
+      tx.call('SET', '{key}1', '1')
+      tx.call('SET', '{key}2', '2')
+    end
+
+    assert_equal %w[1 2], redis.mget('{key}1', '{key}2')
+  end
+end
diff --git a/test/cluster/commands_on_value_types_test.rb b/cluster/test/commands_on_value_types_test.rb
similarity index 100%
rename from test/cluster/commands_on_value_types_test.rb
rename to cluster/test/commands_on_value_types_test.rb
diff --git a/cluster/test/helper.rb b/cluster/test/helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b6ba79aa2d1bf7b00df81f33b473e66b5b71b4a6
--- /dev/null
+++ b/cluster/test/helper.rb
@@ -0,0 +1,161 @@
+# frozen_string_literal: true
+
+require_relative "../../test/helper"
+$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
+
+require "redis-clustering"
+require_relative 'support/orchestrator'
+
+module Helper
+  module Cluster
+    include Generic
+
+    DEFAULT_HOST = '127.0.0.1'
+    DEFAULT_PORTS = (16_380..16_385).freeze
+
+    ClusterSlotsRawReply = lambda { |host, port|
+      # @see https://redis.io/topics/protocol
+      <<-REPLY.delete(' ')
+        *1\r
+        *4\r
+        :0\r
+        :16383\r
+        *3\r
+        $#{host.size}\r
+        #{host}\r
+        :#{port}\r
+        $40\r
+        649fa246273043021a05f547a79478597d3f1dc5\r
+        *3\r
+        $#{host.size}\r
+        #{host}\r
+        :#{port}\r
+        $40\r
+        649fa246273043021a05f547a79478597d3f1dc5\r
+      REPLY
+    }
+
+    ClusterNodesRawReply = lambda { |host, port|
+      line = "649fa246273043021a05f547a79478597d3f1dc5 #{host}:#{port}@17000 "\
+             'myself,master - 0 1530797742000 1 connected 0-16383'
+      "$#{line.size}\r\n#{line}\r\n"
+    }
+
+    def init(redis)
+      redis.flushall
+      redis
+    rescue Redis::CannotConnectError
+      puts <<-MSG
+
+        Cannot connect to Redis Cluster.
+
+        Make sure Redis is running on localhost, port #{DEFAULT_PORTS}.
+
+        Try this once:
+
+          $ make stop_cluster
+
+        Then run the build again:
+
+          $ make
+
+      MSG
+      exit! 1
+    end
+
+    def build_another_client(options = {})
+      _new_client(options)
+    end
+
+    def redis_cluster_mock(commands, options = {})
+      host = DEFAULT_HOST
+      port = nil
+
+      cluster_subcommands = if commands.key?(:cluster)
+        commands.delete(:cluster).transform_keys { |k| k.to_s.downcase }
+      else
+        {}
+      end
+
+      commands[:cluster] = lambda { |subcommand, *args|
+        subcommand = subcommand.downcase
+        if cluster_subcommands.key?(subcommand)
+          cluster_subcommands[subcommand].call(*args)
+        else
+          case subcommand.downcase
+          when 'slots' then ClusterSlotsRawReply.call(host, port)
+          when 'nodes' then ClusterNodesRawReply.call(host, port)
+          else '+OK'
+          end
+        end
+      }
+
+      commands[:command] = ->(*_) { "*0\r\n" }
+
+      RedisMock.start(commands, options) do |po|
+        port = po
+        scheme = options[:ssl] ? 'rediss' : 'redis'
+        nodes = %W[#{scheme}://#{host}:#{port}]
+        yield _new_client(options.merge(nodes: nodes))
+      end
+    end
+
+    def redis_cluster_down
+      trib = ClusterOrchestrator.new(_default_nodes, timeout: TIMEOUT)
+      trib.down
+      yield
+    ensure
+      trib.rebuild
+      trib.close
+    end
+
+    def redis_cluster_failover
+      trib = ClusterOrchestrator.new(_default_nodes, timeout: TIMEOUT)
+      trib.failover
+      yield
+    ensure
+      trib.rebuild
+      trib.close
+    end
+
+    def redis_cluster_fail_master
+      trib = ClusterOrchestrator.new(_default_nodes, timeout: TIMEOUT)
+      trib.fail_serving_master
+      yield
+    ensure
+      trib.restart_cluster_nodes
+      trib.rebuild
+      trib.close
+    end
+
+    # @param slot [Integer]
+    # @param src [String] <ip>:<port>
+    # @param dest [String] <ip>:<port>
+    def redis_cluster_resharding(slot, src:, dest:)
+      trib = ClusterOrchestrator.new(_default_nodes, timeout: TIMEOUT)
+      trib.start_resharding(slot, src, dest)
+      yield
+      trib.finish_resharding(slot, dest)
+    ensure
+      trib.rebuild
+      trib.close
+    end
+
+    private
+
+    def _default_nodes(host: DEFAULT_HOST, ports: DEFAULT_PORTS)
+      ports.map { |port| "redis://#{host}:#{port}" }
+    end
+
+    def _format_options(options)
+      {
+        timeout: OPTIONS[:timeout],
+        nodes: _default_nodes
+      }.merge(options)
+    end
+
+    def _new_client(options = {})
+      Redis::Cluster.new(_format_options(options).merge(driver: ENV['DRIVER']))
+    end
+  end
+end
diff --git a/test/support/cluster/orchestrator.rb b/cluster/test/support/orchestrator.rb
similarity index 83%
rename from test/support/cluster/orchestrator.rb
rename to cluster/test/support/orchestrator.rb
index 85336aaf29f0d6e27a74be644938695ddff86efb..fd52dec35e9ea55a5f5701432388fe762b87cada 100644
--- a/test/support/cluster/orchestrator.rb
+++ b/cluster/test/support/orchestrator.rb
@@ -9,11 +9,7 @@ class ClusterOrchestrator
     raise 'Redis Cluster requires at least 3 master nodes.' if node_addrs.size < 3
 
     @clients = node_addrs.map do |addr|
-      Redis.new(url: addr,
-                timeout: timeout,
-                reconnect_attempts: 10,
-                reconnect_delay: 1.5,
-                reconnect_delay_max: 10.0)
+      Redis.new(url: addr, timeout: timeout, reconnect_attempts: [0, 0.5, 1, 1.5])
     end
     @timeout = timeout
   end
@@ -82,15 +78,13 @@ class ClusterOrchestrator
       break if keys.empty?
 
       keys.each do |k|
-        begin
-          src_client.migrate(k, host: dest_host, port: dest_port)
-        rescue Redis::CommandError => err
-          raise unless err.message.start_with?('IOERR')
+        src_client.migrate(k, host: dest_host, port: dest_port)
+      rescue Redis::CommandError => err
+        raise unless err.message.start_with?('IOERR')
 
-          src_client.migrate(k, host: dest_host, port: dest_port, replace: true) # retry once
-        ensure
-          keys_count -= 1
-        end
+        src_client.migrate(k, host: dest_host, port: dest_port, replace: true) # retry once
+      ensure
+        keys_count -= 1
       end
     end
   end
@@ -108,12 +102,10 @@ class ClusterOrchestrator
 
   def flush_all_data(clients)
     clients.each do |c|
-      begin
-        c.flushall
-      rescue Redis::CommandError
-        # READONLY You can't write against a read only slave.
-        nil
-      end
+      c.flushall(async: true)
+    rescue Redis::CommandError
+      # READONLY You can't write against a read only slave.
+      nil
     end
   end
 
@@ -138,12 +130,10 @@ class ClusterOrchestrator
 
   def save_config_epoch(clients)
     clients.each_with_index do |c, i|
-      begin
-        c.cluster('set-config-epoch', i + 1)
-      rescue Redis::CommandError
-        # ERR Node config epoch is already non-zero
-        nil
-      end
+      c.cluster('set-config-epoch', i + 1)
+    rescue Redis::CommandError
+      # ERR Node config epoch is already non-zero
+      nil
     end
   end
 
@@ -160,7 +150,7 @@ class ClusterOrchestrator
     end
   end
 
-  def wait_meeting(clients, max_attempts: 600)
+  def wait_meeting(clients, max_attempts: 60)
     size = clients.size.to_s
 
     wait_for_state(clients, max_attempts) do |client|
@@ -198,21 +188,21 @@ class ClusterOrchestrator
     clients.each { |c| c.cluster(:saveconfig) }
   end
 
-  def wait_cluster_building(clients, max_attempts: 600)
+  def wait_cluster_building(clients, max_attempts: 60)
     wait_for_state(clients, max_attempts) do |client|
       info = hashify_cluster_info(client)
       info['cluster_state'] == 'ok'
     end
   end
 
-  def wait_replication(clients, max_attempts: 600)
+  def wait_replication(clients, max_attempts: 60)
     wait_for_state(clients, max_attempts) do |client|
       flags = hashify_cluster_node_flags(client)
       flags.values.select { |f| f == 'slave' }.size == 3
     end
   end
 
-  def wait_failover(master_key, slave_key, clients, max_attempts: 600)
+  def wait_failover(master_key, slave_key, clients, max_attempts: 60)
     wait_for_state(clients, max_attempts) do |client|
       flags = hashify_cluster_node_flags(client)
       flags[master_key] == 'slave' && flags[slave_key] == 'master'
@@ -227,21 +217,19 @@ class ClusterOrchestrator
     end
   end
 
-  def wait_cluster_recovering(clients, max_attempts: 600)
+  def wait_cluster_recovering(clients, max_attempts: 60)
     key = 0
     wait_for_state(clients, max_attempts) do |client|
-      begin
-        client.get(key) if client.role.first == 'master'
+      client.get(key) if client.role.first == 'master'
+      true
+    rescue Redis::CommandError => err
+      if err.message.start_with?('CLUSTERDOWN')
+        false
+      elsif err.message.start_with?('MOVED')
+        key += 1
+        false
+      else
         true
-      rescue Redis::CommandError => err
-        if err.message.start_with?('CLUSTERDOWN')
-          false
-        elsif err.message.start_with?('MOVED')
-          key += 1
-          false
-        else
-          true
-        end
       end
     end
   end
diff --git a/examples/consistency.rb b/examples/consistency.rb
index 3c54eaf5fdbcfc821745db5ec21681569a8b1843..e56e049df7d2802e027363c30a5c1bd495a5a761 100644
--- a/examples/consistency.rb
+++ b/examples/consistency.rb
@@ -52,7 +52,7 @@ class ConsistencyTester
   def genkey
     # Write more often to a small subset of keys
     ks = rand > 0.5 ? @keyspace : @working_set
-    "#{@prefix}key_#{rand(ks).to_s}"
+    "#{@prefix}key_#{rand(ks)}"
   end
 
   def check_consistency(key, value)
diff --git a/examples/sentinel/start b/examples/sentinel/start
index 2ffbfa307d06967cc45b985591102024419a3f81..547d63bb5fa70330ce0c97e5252430119fa977bc 100755
--- a/examples/sentinel/start
+++ b/examples/sentinel/start
@@ -13,10 +13,8 @@ pids = []
 
 at_exit do
   pids.each do |pid|
-    begin
-      Process.kill(:INT, pid)
-    rescue Errno::ESRCH
-    end
+    Process.kill(:INT, pid)
+  rescue Errno::ESRCH
   end
 
   Process.waitall
diff --git a/examples/unicorn/config.ru b/examples/unicorn/config.ru
index 81e5a523d3af2a5548fef1d8a4a7112be1f1e82a..d49d5690470c9da7b5eeb679d7db56b5992a1182 100644
--- a/examples/unicorn/config.ru
+++ b/examples/unicorn/config.ru
@@ -1,5 +1,5 @@
 # frozen_string_literal: true
 
 run lambda { |_env|
-  [200, { "Content-Type" => "text/plain" }, [Redis.current.randomkey]]
+  [200, { "Content-Type" => "text/plain" }, [MyApp.redis.randomkey]]
 }
diff --git a/examples/unicorn/unicorn.rb b/examples/unicorn/unicorn.rb
index 939e0e6dc175268dd504d3414779ef38d9d1ae61..a47f1a0fb49f6ea3f6830428ee0ef4e7d5b430f8 100644
--- a/examples/unicorn/unicorn.rb
+++ b/examples/unicorn/unicorn.rb
@@ -18,5 +18,5 @@ worker_processes 3
 # worker processes.
 
 after_fork do |_server, _worker|
-  Redis.current.disconnect!
+  MyApp.redis.disconnect!
 end
diff --git a/lib/redis.rb b/lib/redis.rb
index 1bb1eb377a77ef37ba2e2a6cff3a0d719eac9f76..09d8fd95c98087cdddff9c1be4a63bdc7509e1d4 100644
--- a/lib/redis.rb
+++ b/lib/redis.rb
@@ -6,26 +6,10 @@ require "redis/commands"
 
 class Redis
   BASE_PATH = __dir__
-  @exists_returns_integer = true
-  @sadd_returns_boolean = true
-
   Deprecated = Class.new(StandardError)
 
   class << self
-    attr_reader :exists_returns_integer
-    attr_accessor :silence_deprecations, :raise_deprecations, :sadd_returns_boolean
-
-    def exists_returns_integer=(value)
-      unless value
-        deprecate!(
-          "`Redis#exists(key)` will return an Integer by default in redis-rb 4.3. The option to explicitly " \
-          "disable this behaviour via `Redis.exists_returns_integer` will be removed in 5.0. You should use " \
-          "`exists?` instead."
-        )
-      end
-
-      @exists_returns_integer = value
-    end
+    attr_accessor :silence_deprecations, :raise_deprecations
 
     def deprecate!(message)
       unless silence_deprecations
@@ -36,20 +20,22 @@ class Redis
         end
       end
     end
+  end
 
-    def current
-      deprecate!("`Redis.current` is deprecated and will be removed in 5.0. (called from: #{caller(1, 1).first})")
-      @current ||= Redis.new
-    end
-
-    def current=(redis)
-      deprecate!("`Redis.current=` is deprecated and will be removed in 5.0. (called from: #{caller(1, 1).first})")
-      @current = redis
+  # soft-deprecated
+  # We added this back for older sidekiq releases
+  module Connection
+    class << self
+      def drivers
+        [RedisClient.default_driver]
+      end
     end
   end
 
   include Commands
 
+  SERVER_URL_OPTIONS = %i(url host port path).freeze
+
   # Create a new client instance
   #
   # @param [Hash] options
@@ -59,56 +45,49 @@ class Redis
   # @option options [String] :host ("127.0.0.1") server hostname
   # @option options [Integer] :port (6379) server port
   # @option options [String] :path path to server socket (overrides host and port)
-  # @option options [Float] :timeout (5.0) timeout in seconds
+  # @option options [Float] :timeout (1.0) timeout in seconds
   # @option options [Float] :connect_timeout (same as timeout) timeout for initial connect in seconds
   # @option options [String] :username Username to authenticate against server
   # @option options [String] :password Password to authenticate against server
-  # @option options [Integer] :db (0) Database to select after initial connect
-  # @option options [Symbol] :driver Driver to use, currently supported: `:ruby`, `:hiredis`, `:synchrony`
+  # @option options [Integer] :db (0) Database to select after connect and on reconnects
+  # @option options [Symbol] :driver Driver to use, currently supported: `:ruby`, `:hiredis`
   # @option options [String] :id ID for the client connection, assigns name to current connection by sending
   #   `CLIENT SETNAME`
-  # @option options [Hash, Integer] :tcp_keepalive Keepalive values, if Integer `intvl` and `probe` are calculated
-  #   based on the value, if Hash `time`, `intvl` and `probes` can be specified as a Integer
-  # @option options [Integer] :reconnect_attempts Number of attempts trying to connect
+  # @option options [Integer, Array<Integer, Float>] :reconnect_attempts Number of attempts trying to connect,
+  #   or a list of sleep duration between attempts.
   # @option options [Boolean] :inherit_socket (false) Whether to use socket in forked process or not
+  # @option options [String] :name The name of the server group to connect to.
   # @option options [Array] :sentinels List of sentinels to contact
-  # @option options [Symbol] :role (:master) Role to fetch via Sentinel, either `:master` or `:slave`
-  # @option options [Array<String, Hash{Symbol => String, Integer}>] :cluster List of cluster nodes to contact
-  # @option options [Boolean] :replica Whether to use readonly replica nodes in Redis Cluster or not
-  # @option options [String] :fixed_hostname Specify a FQDN if cluster mode enabled and
-  #   client has to connect nodes via single endpoint with SSL/TLS
-  # @option options [Class] :connector Class of custom connector
   #
   # @return [Redis] a new client instance
   def initialize(options = {})
-    @options = options.dup
-    @cluster_mode = options.key?(:cluster)
-    client = @cluster_mode ? Cluster : Client
-    @original_client = @client = client.new(options)
-    @queue = Hash.new { |h, k| h[k] = [] }
     @monitor = Monitor.new
-  end
-
-  # Run code with the client reconnecting
-  def with_reconnect(val = true, &blk)
-    synchronize do |client|
-      client.with_reconnect(val, &blk)
+    @options = options.dup
+    @options[:reconnect_attempts] = 1 unless @options.key?(:reconnect_attempts)
+    if ENV["REDIS_URL"] && SERVER_URL_OPTIONS.none? { |o| @options.key?(o) }
+      @options[:url] = ENV["REDIS_URL"]
     end
+    inherit_socket = @options.delete(:inherit_socket)
+    @subscription_client = nil
+
+    @client = initialize_client(@options)
+    @client.inherit_socket! if inherit_socket
   end
 
   # Run code without the client reconnecting
-  def without_reconnect(&blk)
-    with_reconnect(false, &blk)
+  def without_reconnect(&block)
+    @client.disable_reconnection(&block)
   end
 
   # Test whether or not the client is connected
   def connected?
-    @original_client.connected?
+    @client.connected? || @subscription_client&.connected?
   end
 
   # Disconnect the client as quickly and silently as possible.
   def close
-    @original_client.disconnect
+    @client.close
+    @subscription_client&.close
   end
   alias disconnect! close
 
@@ -116,127 +95,20 @@ class Redis
     yield self
   end
 
-  # @deprecated Queues a command for pipelining.
-  #
-  # Commands in the queue are executed with the Redis#commit method.
-  #
-  # See http://redis.io/topics/pipelining for more details.
-  #
-  def queue(*command)
-    ::Redis.deprecate!(
-      "Redis#queue is deprecated and will be removed in Redis 5.0.0. Use Redis#pipelined instead." \
-      "(called from: #{caller(1, 1).first})"
-    )
-
-    synchronize do
-      @queue[Thread.current.object_id] << command
-    end
-  end
-
-  # @deprecated Sends all commands in the queue.
-  #
-  # See http://redis.io/topics/pipelining for more details.
-  #
-  def commit
-    ::Redis.deprecate!(
-      "Redis#commit is deprecated and will be removed in Redis 5.0.0. Use Redis#pipelined instead. " \
-      "(called from: #{Kernel.caller(1, 1).first})"
-    )
-
-    synchronize do |client|
-      begin
-        pipeline = Pipeline.new(client)
-        @queue[Thread.current.object_id].each do |command|
-          pipeline.call(command)
-        end
-
-        client.call_pipelined(pipeline)
-      ensure
-        @queue.delete(Thread.current.object_id)
-      end
-    end
-  end
-
   def _client
     @client
   end
 
-  def pipelined(&block)
-    deprecation_displayed = false
-    if block&.arity == 0
-      Pipeline.deprecation_warning("pipelined", Kernel.caller_locations(1, 5))
-      deprecation_displayed = true
-    end
-
-    synchronize do |prior_client|
-      begin
-        pipeline = Pipeline.new(prior_client)
-        @client = deprecation_displayed ? pipeline : DeprecatedPipeline.new(pipeline)
-        pipelined_connection = PipelinedConnection.new(pipeline)
-        yield pipelined_connection
-        prior_client.call_pipeline(pipeline)
-      ensure
-        @client = prior_client
-      end
-    end
-  end
-
-  # Mark the start of a transaction block.
-  #
-  # Passing a block is optional.
-  #
-  # @example With a block
-  #   redis.multi do |multi|
-  #     multi.set("key", "value")
-  #     multi.incr("counter")
-  #   end # => ["OK", 6]
-  #
-  # @example Without a block
-  #   redis.multi
-  #     # => "OK"
-  #   redis.set("key", "value")
-  #     # => "QUEUED"
-  #   redis.incr("counter")
-  #     # => "QUEUED"
-  #   redis.exec
-  #     # => ["OK", 6]
-  #
-  # @yield [multi] the commands that are called inside this block are cached
-  #   and written to the server upon returning from it
-  # @yieldparam [Redis] multi `self`
-  #
-  # @return [String, Array<...>]
-  #   - when a block is not given, `OK`
-  #   - when a block is given, an array with replies
-  #
-  # @see #watch
-  # @see #unwatch
-  def multi(&block)
-    if block_given?
-      deprecation_displayed = false
-      if block&.arity == 0
-        Pipeline.deprecation_warning("multi", Kernel.caller_locations(1, 5))
-        deprecation_displayed = true
-      end
-
-      synchronize do |prior_client|
-        begin
-          pipeline = Pipeline::Multi.new(prior_client)
-          @client = deprecation_displayed ? pipeline : DeprecatedMulti.new(pipeline)
-          pipelined_connection = PipelinedConnection.new(pipeline)
-          yield pipelined_connection
-          prior_client.call_pipeline(pipeline)
-        ensure
-          @client = prior_client
-        end
+  def pipelined(exception: true)
+    synchronize do |client|
+      client.pipelined(exception: exception) do |raw_pipeline|
+        yield PipelinedConnection.new(raw_pipeline, exception: exception)
       end
-    else
-      send_command([:multi])
     end
   end
 
   def id
-    @original_client.id
+    @client.id || @client.server_url
   end
 
   def inspect
@@ -248,54 +120,75 @@ class Redis
   end
 
   def connection
-    return @original_client.connection_info if @cluster_mode
-
     {
-      host: @original_client.host,
-      port: @original_client.port,
-      db: @original_client.db,
-      id: @original_client.id,
-      location: @original_client.location
+      host: @client.host,
+      port: @client.port,
+      db: @client.db,
+      id: id,
+      location: "#{@client.host}:#{@client.port}"
     }
   end
 
   private
 
+  def initialize_client(options)
+    if options.key?(:cluster)
+      raise "Redis Cluster support was moved to the `redis-clustering` gem."
+    end
+
+    if options.key?(:sentinels)
+      Client.sentinel(**options).new_client
+    else
+      Client.config(**options).new_client
+    end
+  end
+
   def synchronize
     @monitor.synchronize { yield(@client) }
   end
 
   def send_command(command, &block)
     @monitor.synchronize do
-      @client.call(command, &block)
+      @client.call_v(command, &block)
     end
+  rescue ::RedisClient::Error => error
+    Client.translate_error!(error)
   end
 
   def send_blocking_command(command, timeout, &block)
     @monitor.synchronize do
-      @client.call_with_timeout(command, timeout, &block)
+      @client.blocking_call_v(timeout, command, &block)
     end
   end
 
   def _subscription(method, timeout, channels, block)
-    return @client.call([method] + channels) if subscribed?
+    if block
+      if @subscription_client
+        raise SubscriptionError, "This client is already subscribed"
+      end
 
-    begin
-      original, @client = @client, SubscribedClient.new(@client)
-      if timeout > 0
-        @client.send(method, timeout, *channels, &block)
-      else
-        @client.send(method, *channels, &block)
+      begin
+        @subscription_client = SubscribedClient.new(@client.pubsub)
+        if timeout > 0
+          @subscription_client.send(method, timeout, *channels, &block)
+        else
+          @subscription_client.send(method, *channels, &block)
+        end
+      ensure
+        @subscription_client&.close
+        @subscription_client = nil
+      end
+    else
+      unless @subscription_client
+        raise SubscriptionError, "This client is not subscribed"
       end
-    ensure
-      @client = original
+
+      @subscription_client.call_v([method].concat(channels))
     end
   end
 end
 
 require "redis/version"
-require "redis/connection"
 require "redis/client"
-require "redis/cluster"
 require "redis/pipeline"
 require "redis/subscribe"
diff --git a/lib/redis/client.rb b/lib/redis/client.rb
index 3e620faa9e33e1765cef8ba5dcbbae7bd9e35b6a..d23ed9122cba6fc42cd9351690d6edc90a6188d5 100644
--- a/lib/redis/client.rb
+++ b/lib/redis/client.rb
@@ -1,667 +1,124 @@
 # frozen_string_literal: true
 
-require "socket"
-require "cgi"
-require "redis/errors"
+require 'redis-client'
 
 class Redis
-  class Client
-    # Defaults are also used for converting string keys to symbols.
-    DEFAULTS = {
-      url: -> { ENV["REDIS_URL"] },
-      scheme: "redis",
-      host: "127.0.0.1",
-      port: 6379,
-      path: nil,
-      read_timeout: nil,
-      write_timeout: nil,
-      connect_timeout: nil,
-      timeout: 5.0,
-      username: nil,
-      password: nil,
-      db: 0,
-      driver: nil,
-      id: nil,
-      tcp_keepalive: 0,
-      reconnect_attempts: 1,
-      reconnect_delay: 0,
-      reconnect_delay_max: 0.5,
-      inherit_socket: false,
-      logger: nil,
-      sentinels: nil,
-      role: nil
-    }.freeze
-
-    attr_reader :options, :connection, :command_map
-
-    def scheme
-      @options[:scheme]
-    end
-
-    def host
-      @options[:host]
-    end
-
-    def port
-      @options[:port]
-    end
-
-    def path
-      @options[:path]
-    end
-
-    def read_timeout
-      @options[:read_timeout]
-    end
-
-    def connect_timeout
-      @options[:connect_timeout]
-    end
-
-    def timeout
-      @options[:read_timeout]
-    end
-
-    def username
-      @options[:username]
-    end
-
-    def password
-      @options[:password]
-    end
-
-    def db
-      @options[:db]
-    end
-
-    def db=(db)
-      @options[:db] = db.to_i
-    end
-
-    def driver
-      @options[:driver]
-    end
-
-    def inherit_socket?
-      @options[:inherit_socket]
-    end
-
-    attr_accessor :logger
-
-    def initialize(options = {})
-      @options = _parse_options(options)
-      @reconnect = true
-      @logger = @options[:logger]
-      @connection = nil
-      @command_map = {}
-
-      @pending_reads = 0
-
-      @connector =
-        if !@options[:sentinels].nil?
-          Connector::Sentinel.new(@options)
-        elsif options.include?(:connector) && options[:connector].respond_to?(:new)
-          options.delete(:connector).new(@options)
+  class Client < ::RedisClient
+    ERROR_MAPPING = {
+      RedisClient::ConnectionError => Redis::ConnectionError,
+      RedisClient::CommandError => Redis::CommandError,
+      RedisClient::ReadTimeoutError => Redis::TimeoutError,
+      RedisClient::CannotConnectError => Redis::CannotConnectError,
+      RedisClient::AuthenticationError => Redis::CannotConnectError,
+      RedisClient::FailoverError => Redis::CannotConnectError,
+      RedisClient::PermissionError => Redis::PermissionError,
+      RedisClient::WrongTypeError => Redis::WrongTypeError,
+      RedisClient::ReadOnlyError => Redis::ReadOnlyError,
+      RedisClient::ProtocolError => Redis::ProtocolError,
+      RedisClient::OutOfMemoryError => Redis::OutOfMemoryError,
+    }
+
+    class << self
+      def config(**kwargs)
+        super(protocol: 2, **kwargs)
+      end
+
+      def sentinel(**kwargs)
+        super(protocol: 2, **kwargs, client_implementation: ::RedisClient)
+      end
+
+      def translate_error!(error, mapping: ERROR_MAPPING)
+        redis_error = translate_error_class(error.class, mapping: mapping)
+        raise redis_error, error.message, error.backtrace
+      end
+
+      private
+
+      def translate_error_class(error_class, mapping: ERROR_MAPPING)
+        mapping.fetch(error_class)
+      rescue IndexError
+        if (client_error = error_class.ancestors.find { |a| mapping[a] })
+          mapping[error_class] = mapping[client_error]
         else
-          Connector.new(@options)
-        end
-    end
-
-    def connect
-      @pid = Process.pid
-
-      # Don't try to reconnect when the connection is fresh
-      with_reconnect(false) do
-        establish_connection
-        if password
-          if username
-            begin
-              call [:auth, username, password]
-            rescue CommandError => err # Likely on Redis < 6
-              case err.message
-              when /ERR wrong number of arguments for 'auth' command/
-                call [:auth, password]
-              when /WRONGPASS invalid username-password pair/
-                begin
-                  call [:auth, password]
-                rescue CommandError
-                  raise err
-                end
-                ::Redis.deprecate!(
-                  "[redis-rb] The Redis connection was configured with username #{username.inspect}, but" \
-                  " the provided password was for the default user. This will start failing in redis-rb 5.0.0."
-                )
-              else
-                raise
-              end
-            end
-          else
-            call [:auth, password]
-          end
+          raise
         end
-
-        call [:readonly] if @options[:readonly]
-        call [:select, db] if db != 0
-        call [:client, :setname, @options[:id]] if @options[:id]
-        @connector.check(self)
       end
-
-      self
     end
 
     def id
-      @options[:id] || "#{@options[:ssl] ? 'rediss' : @options[:scheme]}://#{location}/#{db}"
+      config.id
     end
 
-    def location
-      path || "#{host}:#{port}"
+    def server_url
+      config.server_url
     end
 
-    def call(command)
-      reply = process([command]) { read }
-      raise reply if reply.is_a?(CommandError)
-
-      if block_given? && reply != 'QUEUED'
-        yield reply
-      else
-        reply
-      end
-    end
-
-    def call_loop(command, timeout = 0)
-      error = nil
-
-      result = with_socket_timeout(timeout) do
-        process([command]) do
-          loop do
-            reply = read
-            if reply.is_a?(CommandError)
-              error = reply
-              break
-            else
-              yield reply
-            end
-          end
-        end
-      end
-
-      # Raise error when previous block broke out of the loop.
-      raise error if error
-
-      # Result is set to the value that the provided block used to break.
-      result
-    end
-
-    def call_pipeline(pipeline)
-      return [] if pipeline.futures.empty?
-
-      with_reconnect pipeline.with_reconnect? do
-        begin
-          pipeline.finish(call_pipelined(pipeline)).tap do
-            self.db = pipeline.db if pipeline.db
-          end
-        rescue ConnectionError => e
-          return nil if pipeline.shutdown?
-
-          # Assume the pipeline was sent in one piece, but execution of
-          # SHUTDOWN caused none of the replies for commands that were executed
-          # prior to it from coming back around.
-          raise e
-        end
-      end
-    end
-
-    def call_pipelined(pipeline)
-      return [] if pipeline.futures.empty?
-
-      # The method #ensure_connected (called from #process) reconnects once on
-      # I/O errors. To make an effort in making sure that commands are not
-      # executed more than once, only allow reconnection before the first reply
-      # has been read. When an error occurs after the first reply has been
-      # read, retrying would re-execute the entire pipeline, thus re-issuing
-      # already successfully executed commands. To circumvent this, don't retry
-      # after the first reply has been read successfully.
-
-      commands = pipeline.commands
-
-      result = Array.new(commands.size)
-      reconnect = @reconnect
-
-      begin
-        exception = nil
-
-        process(commands) do
-          pipeline.timeouts.each_with_index do |timeout, i|
-            reply = if timeout
-              with_socket_timeout(timeout) { read }
-            else
-              read
-            end
-            result[i] = reply
-            @reconnect = false
-            exception = reply if exception.nil? && reply.is_a?(CommandError)
-          end
-        end
-
-        raise exception if exception
-      ensure
-        @reconnect = reconnect
-      end
-
-      result
-    end
-
-    def call_with_timeout(command, extra_timeout, &blk)
-      timeout = extra_timeout == 0 ? 0 : self.timeout + extra_timeout
-      with_socket_timeout(timeout) do
-        call(command, &blk)
-      end
-    rescue ConnectionError
-      retry
-    end
-
-    def call_without_timeout(command, &blk)
-      call_with_timeout(command, 0, &blk)
-    end
-
-    def process(commands)
-      logging(commands) do
-        ensure_connected do
-          commands.each do |command|
-            if command_map[command.first]
-              command = command.dup
-              command[0] = command_map[command.first]
-            end
-
-            write(command)
-          end
-
-          yield if block_given?
-        end
-      end
-    end
-
-    def connected?
-      !!(connection && connection.connected?)
-    end
-
-    def disconnect
-      connection.disconnect if connected?
+    def timeout
+      config.read_timeout
     end
-    alias close disconnect
 
-    def reconnect
-      disconnect
-      connect
+    def db
+      config.db
     end
 
-    def io
-      yield
-    rescue TimeoutError => e1
-      # Add a message to the exception without destroying the original stack
-      e2 = TimeoutError.new("Connection timed out")
-      e2.set_backtrace(e1.backtrace)
-      raise e2
-    rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF, Errno::EINVAL, EOFError => e
-      raise ConnectionError, "Connection lost (%s)" % [e.class.name.split("::").last]
+    def host
+      config.host unless config.path
     end
 
-    def read
-      io do
-        value = connection.read
-        @pending_reads -= 1
-        value
-      end
+    def port
+      config.port unless config.path
     end
 
-    def write(command)
-      io do
-        @pending_reads += 1
-        connection.write(command)
-      end
+    def path
+      config.path
     end
 
-    def with_socket_timeout(timeout)
-      connect unless connected?
-      original = @options[:read_timeout]
-
-      begin
-        connection.timeout = timeout
-        @options[:read_timeout] = timeout # for reconnection
-        yield
-      ensure
-        connection.timeout = self.timeout if connected?
-        @options[:read_timeout] = original
-      end
+    def username
+      config.username
     end
 
-    def without_socket_timeout(&blk)
-      with_socket_timeout(0, &blk)
+    def password
+      config.password
     end
 
-    def with_reconnect(val = true)
-      original, @reconnect = @reconnect, val
-      yield
-    ensure
-      @reconnect = original
-    end
+    undef_method :call
+    undef_method :call_once
+    undef_method :call_once_v
+    undef_method :blocking_call
 
-    def without_reconnect(&blk)
-      with_reconnect(false, &blk)
+    def call_v(command, &block)
+      super(command, &block)
+    rescue ::RedisClient::Error => error
+      Client.translate_error!(error)
     end
 
-    protected
-
-    def logging(commands)
-      return yield unless @logger&.debug?
-
-      begin
-        commands.each do |name, *args|
-          logged_args = args.map do |a|
-            if a.respond_to?(:inspect) then a.inspect
-            elsif a.respond_to?(:to_s) then a.to_s
-            else
-              # handle poorly-behaved descendants of BasicObject
-              klass = a.instance_exec { (class << self; self end).superclass }
-              "\#<#{klass}:#{a.__id__}>"
-            end
-          end
-          @logger.debug("[Redis] command=#{name.to_s.upcase} args=#{logged_args.join(' ')}")
-        end
-
-        t1 = Time.now
-        yield
-      ensure
-        @logger.debug("[Redis] call_time=%0.2f ms" % ((Time.now - t1) * 1000)) if t1
+    def blocking_call_v(timeout, command, &block)
+      if timeout && timeout > 0
+        # Can't use the command timeout argument as the connection timeout
+        # otherwise it would be very racy. So we add the regular read_timeout on top
+        # to account for the network delay.
+        timeout += config.read_timeout
       end
-    end
-
-    def establish_connection
-      server = @connector.resolve.dup
-
-      @options[:host] = server[:host]
-      @options[:port] = Integer(server[:port]) if server.include?(:port)
 
-      @connection = @options[:driver].connect(@options)
-      @pending_reads = 0
-    rescue TimeoutError,
-           SocketError,
-           Errno::EADDRNOTAVAIL,
-           Errno::ECONNREFUSED,
-           Errno::EHOSTDOWN,
-           Errno::EHOSTUNREACH,
-           Errno::ENETUNREACH,
-           Errno::ENOENT,
-           Errno::ETIMEDOUT,
-           Errno::EINVAL => error
-
-      raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})"
+      super(timeout, command, &block)
+    rescue ::RedisClient::Error => error
+      Client.translate_error!(error)
     end
 
-    def ensure_connected
-      disconnect if @pending_reads > 0
-
-      attempts = 0
-
-      begin
-        attempts += 1
-
-        if connected?
-          unless inherit_socket? || Process.pid == @pid
-            raise InheritedError,
-                  "Tried to use a connection from a child process without reconnecting. " \
-                  "You need to reconnect to Redis after forking " \
-                  "or set :inherit_socket to true."
-          end
-        else
-          connect
-        end
-
-        yield
-      rescue BaseConnectionError
-        disconnect
-
-        if attempts <= @options[:reconnect_attempts] && @reconnect
-          sleep_t = [(@options[:reconnect_delay] * 2**(attempts - 1)),
-                     @options[:reconnect_delay_max]].min
-
-          Kernel.sleep(sleep_t)
-          retry
-        else
-          raise
-        end
-      rescue Exception
-        disconnect
-        raise
-      end
-    end
-
-    def _parse_options(options)
-      return options if options[:_parsed]
-
-      defaults = DEFAULTS.dup
-      options = options.dup
-
-      defaults.each_key do |key|
-        # Fill in defaults if needed
-        defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
-
-        # Symbolize only keys that are needed
-        options[key] = options[key.to_s] if options.key?(key.to_s)
-      end
-
-      url = options[:url]
-      url = defaults[:url] if url.nil?
-
-      # Override defaults from URL if given
-      if url
-        require "uri"
-
-        uri = URI(url)
-
-        case uri.scheme
-        when "unix"
-          defaults[:path] = uri.path
-        when "redis", "rediss"
-          defaults[:scheme]   = uri.scheme
-          defaults[:host]     = uri.host.sub(/\A\[(.*)\]\z/, '\1') if uri.host
-          defaults[:port]     = uri.port if uri.port
-          defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty?
-          defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty?
-          defaults[:db]       = uri.path[1..-1].to_i if uri.path
-          defaults[:role] = :master
-        else
-          raise ArgumentError, "invalid uri scheme '#{uri.scheme}'"
-        end
-
-        defaults[:ssl] = true if uri.scheme == "rediss"
-      end
-
-      # Use default when option is not specified or nil
-      defaults.each_key do |key|
-        options[key] = defaults[key] if options[key].nil?
-      end
-
-      if options[:path]
-        # Unix socket
-        options[:scheme] = "unix"
-        options.delete(:host)
-        options.delete(:port)
-      else
-        # TCP socket
-        options[:host] = options[:host].to_s
-        options[:port] = options[:port].to_i
-      end
-
-      if options.key?(:timeout)
-        options[:connect_timeout] ||= options[:timeout]
-        options[:read_timeout]    ||= options[:timeout]
-        options[:write_timeout]   ||= options[:timeout]
-      end
-
-      options[:connect_timeout] = Float(options[:connect_timeout])
-      options[:read_timeout]    = Float(options[:read_timeout])
-      options[:write_timeout]   = Float(options[:write_timeout])
-
-      options[:reconnect_attempts] = options[:reconnect_attempts].to_i
-      options[:reconnect_delay] = options[:reconnect_delay].to_f
-      options[:reconnect_delay_max] = options[:reconnect_delay_max].to_f
-
-      options[:db] = options[:db].to_i
-      options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last
-
-      case options[:tcp_keepalive]
-      when Hash
-        %i[time intvl probes].each do |key|
-          unless options[:tcp_keepalive][key].is_a?(Integer)
-            raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
-          end
-        end
-
-      when Integer
-        if options[:tcp_keepalive] >= 60
-          options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
-
-        elsif options[:tcp_keepalive] >= 30
-          options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
-
-        elsif options[:tcp_keepalive] >= 5
-          options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
-        end
-      end
-
-      options[:_parsed] = true
-
-      options
+    def pipelined(exception: true)
+      super
+    rescue ::RedisClient::Error => error
+      Client.translate_error!(error)
     end
 
-    def _parse_driver(driver)
-      driver = driver.to_s if driver.is_a?(Symbol)
-
-      if driver.is_a?(String)
-        begin
-          require_relative "connection/#{driver}"
-        rescue LoadError, NameError
-          begin
-            require "redis/connection/#{driver}"
-          rescue LoadError, NameError => error
-            raise "Cannot load driver #{driver.inspect}: #{error.message}"
-          end
-        end
-
-        driver = Connection.const_get(driver.capitalize)
-      end
-
-      driver
+    def multi(watch: nil)
+      super
+    rescue ::RedisClient::Error => error
+      Client.translate_error!(error)
     end
 
-    class Connector
-      def initialize(options)
-        @options = options.dup
-      end
-
-      def resolve
-        @options
-      end
-
-      def check(client); end
-
-      class Sentinel < Connector
-        def initialize(options)
-          super(options)
-
-          @options[:db] = DEFAULTS.fetch(:db)
-
-          @sentinels = @options.delete(:sentinels).dup
-          @role = (@options[:role] || "master").to_s
-          @master = @options[:host]
-        end
-
-        def check(client)
-          # Check the instance is really of the role we are looking for.
-          # We can't assume the command is supported since it was introduced
-          # recently and this client should work with old stuff.
-          begin
-            role = client.call([:role])[0]
-          rescue Redis::CommandError
-            # Assume the test is passed if we can't get a reply from ROLE...
-            role = @role
-          end
-
-          if role != @role
-            client.disconnect
-            raise ConnectionError, "Instance role mismatch. Expected #{@role}, got #{role}."
-          end
-        end
-
-        def resolve
-          result = case @role
-          when "master"
-            resolve_master
-          when "slave"
-            resolve_slave
-          else
-            raise ArgumentError, "Unknown instance role #{@role}"
-          end
-
-          result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
-        end
-
-        def sentinel_detect
-          @sentinels.each do |sentinel|
-            client = Client.new(@options.merge({
-                                                 host: sentinel[:host] || sentinel["host"],
-                                                 port: sentinel[:port] || sentinel["port"],
-                                                 username: sentinel[:username] || sentinel["username"],
-                                                 password: sentinel[:password] || sentinel["password"],
-                                                 reconnect_attempts: 0
-                                               }))
-
-            begin
-              if result = yield(client)
-                # This sentinel responded. Make sure we ask it first next time.
-                @sentinels.delete(sentinel)
-                @sentinels.unshift(sentinel)
-
-                return result
-              end
-            rescue BaseConnectionError
-            ensure
-              client.disconnect
-            end
-          end
-
-          raise CannotConnectError, "No sentinels available."
-        end
-
-        def resolve_master
-          sentinel_detect do |client|
-            if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
-              { host: reply[0], port: reply[1] }
-            end
-          end
-        end
-
-        def resolve_slave
-          sentinel_detect do |client|
-            if reply = client.call(["sentinel", "slaves", @master])
-              slaves = reply.map { |s| s.each_slice(2).to_h }
-              slaves.each { |s| s['flags'] = s.fetch('flags').split(',') }
-              slaves.reject! { |s| s.fetch('flags').include?('s_down') }
-
-              if slaves.empty?
-                raise CannotConnectError, 'No slaves available.'
-              else
-                slave = slaves.sample
-                {
-                  host: slave.fetch('ip'),
-                  port: slave.fetch('port')
-                }
-              end
-            end
-          end
-        end
-      end
+    def inherit_socket!
+      @inherit_socket = true
     end
   end
 end
diff --git a/lib/redis/cluster.rb b/lib/redis/cluster.rb
deleted file mode 100644
index a71a45f1dabfcf9c9b31fc7a7997e411c23ade99..0000000000000000000000000000000000000000
--- a/lib/redis/cluster.rb
+++ /dev/null
@@ -1,315 +0,0 @@
-# frozen_string_literal: true
-
-require_relative 'errors'
-require_relative 'client'
-require_relative 'cluster/command'
-require_relative 'cluster/command_loader'
-require_relative 'cluster/key_slot_converter'
-require_relative 'cluster/node'
-require_relative 'cluster/node_key'
-require_relative 'cluster/node_loader'
-require_relative 'cluster/option'
-require_relative 'cluster/slot'
-require_relative 'cluster/slot_loader'
-
-class Redis
-  # Redis Cluster client
-  #
-  # @see https://github.com/antirez/redis-rb-cluster POC implementation
-  # @see https://redis.io/topics/cluster-spec Redis Cluster specification
-  # @see https://redis.io/topics/cluster-tutorial Redis Cluster tutorial
-  #
-  # Copyright (C) 2013 Salvatore Sanfilippo <antirez@gmail.com>
-  class Cluster
-    def initialize(options = {})
-      @option = Option.new(options)
-      @node, @slot = fetch_cluster_info!(@option)
-      @command = fetch_command_details(@node)
-    end
-
-    def id
-      @node.map(&:id).sort.join(' ')
-    end
-
-    # db feature is disabled in cluster mode
-    def db
-      0
-    end
-
-    # db feature is disabled in cluster mode
-    def db=(_db); end
-
-    def timeout
-      @node.first.timeout
-    end
-
-    def connected?
-      @node.any?(&:connected?)
-    end
-
-    def disconnect
-      @node.each(&:disconnect)
-      true
-    end
-
-    def connection_info
-      @node.sort_by(&:id).map do |client|
-        {
-          host: client.host,
-          port: client.port,
-          db: client.db,
-          id: client.id,
-          location: client.location
-        }
-      end
-    end
-
-    def with_reconnect(val = true, &block)
-      try_send(@node.sample, :with_reconnect, val, &block)
-    end
-
-    def call(command, &block)
-      send_command(command, &block)
-    end
-
-    def call_loop(command, timeout = 0, &block)
-      node = assign_node(command)
-      try_send(node, :call_loop, command, timeout, &block)
-    end
-
-    def call_pipeline(pipeline)
-      node_keys = pipeline.commands.map { |cmd| find_node_key(cmd, primary_only: true) }.compact.uniq
-      if node_keys.size > 1
-        raise(CrossSlotPipeliningError,
-              pipeline.commands.map { |cmd| @command.extract_first_key(cmd) }.reject(&:empty?).uniq)
-      end
-
-      try_send(find_node(node_keys.first), :call_pipeline, pipeline)
-    end
-
-    def call_with_timeout(command, timeout, &block)
-      node = assign_node(command)
-      try_send(node, :call_with_timeout, command, timeout, &block)
-    end
-
-    def call_without_timeout(command, &block)
-      call_with_timeout(command, 0, &block)
-    end
-
-    def process(commands, &block)
-      if commands.size == 1 &&
-         %w[unsubscribe punsubscribe].include?(commands.first.first.to_s.downcase) &&
-         commands.first.size == 1
-
-        # Node is indeterminate. We do just a best-effort try here.
-        @node.process_all(commands, &block)
-      else
-        node = assign_node(commands.first)
-        try_send(node, :process, commands, &block)
-      end
-    end
-
-    private
-
-    def fetch_cluster_info!(option)
-      node = Node.new(option.per_node_key)
-      available_slots = SlotLoader.load(node)
-      node_flags = NodeLoader.load_flags(node)
-      option.update_node(available_slots.keys.map { |k| NodeKey.optionize(k) })
-      [Node.new(option.per_node_key, node_flags, option.use_replica?),
-       Slot.new(available_slots, node_flags, option.use_replica?)]
-    ensure
-      node&.each(&:disconnect)
-    end
-
-    def fetch_command_details(nodes)
-      details = CommandLoader.load(nodes)
-      Command.new(details)
-    end
-
-    def send_command(command, &block)
-      cmd = command.first.to_s.downcase
-      case cmd
-      when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
-        @node.call_all(command, &block).first
-      when 'flushall', 'flushdb'
-        @node.call_master(command, &block).first
-      when 'wait'     then @node.call_master(command, &block).reduce(:+)
-      when 'keys'     then @node.call_slave(command, &block).flatten.sort
-      when 'dbsize'   then @node.call_slave(command, &block).reduce(:+)
-      when 'scan'     then _scan(command, &block)
-      when 'lastsave' then @node.call_all(command, &block).sort
-      when 'role'     then @node.call_all(command, &block)
-      when 'config'   then send_config_command(command, &block)
-      when 'client'   then send_client_command(command, &block)
-      when 'cluster'  then send_cluster_command(command, &block)
-      when 'readonly', 'readwrite', 'shutdown'
-        raise OrchestrationCommandNotSupported, cmd
-      when 'memory'   then send_memory_command(command, &block)
-      when 'script'   then send_script_command(command, &block)
-      when 'pubsub'   then send_pubsub_command(command, &block)
-      when 'discard', 'exec', 'multi', 'unwatch'
-        raise AmbiguousNodeError, cmd
-      else
-        node = assign_node(command)
-        try_send(node, :call, command, &block)
-      end
-    end
-
-    def send_config_command(command, &block)
-      case command[1].to_s.downcase
-      when 'resetstat', 'rewrite', 'set'
-        @node.call_all(command, &block).first
-      else assign_node(command).call(command, &block)
-      end
-    end
-
-    def send_memory_command(command, &block)
-      case command[1].to_s.downcase
-      when 'stats' then @node.call_all(command, &block)
-      when 'purge' then @node.call_all(command, &block).first
-      else assign_node(command).call(command, &block)
-      end
-    end
-
-    def send_client_command(command, &block)
-      case command[1].to_s.downcase
-      when 'list' then @node.call_all(command, &block).flatten
-      when 'pause', 'reply', 'setname'
-        @node.call_all(command, &block).first
-      else assign_node(command).call(command, &block)
-      end
-    end
-
-    def send_cluster_command(command, &block)
-      subcommand = command[1].to_s.downcase
-      case subcommand
-      when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
-           'reset', 'set-config-epoch', 'setslot'
-        raise OrchestrationCommandNotSupported, 'cluster', subcommand
-      when 'saveconfig' then @node.call_all(command, &block).first
-      else assign_node(command).call(command, &block)
-      end
-    end
-
-    def send_script_command(command, &block)
-      case command[1].to_s.downcase
-      when 'debug', 'kill'
-        @node.call_all(command, &block).first
-      when 'flush', 'load'
-        @node.call_master(command, &block).first
-      else assign_node(command).call(command, &block)
-      end
-    end
-
-    def send_pubsub_command(command, &block)
-      case command[1].to_s.downcase
-      when 'channels' then @node.call_all(command, &block).flatten.uniq.sort
-      when 'numsub'
-        @node.call_all(command, &block).reject(&:empty?).map { |e| Hash[*e] }
-             .reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } }
-      when 'numpat' then @node.call_all(command, &block).reduce(:+)
-      else assign_node(command).call(command, &block)
-      end
-    end
-
-    # @see https://redis.io/topics/cluster-spec#redirection-and-resharding
-    #   Redirection and resharding
-    def try_send(node, method_name, *args, retry_count: 3, &block)
-      node.public_send(method_name, *args, &block)
-    rescue CommandError => err
-      if err.message.start_with?('MOVED')
-        raise if retry_count <= 0
-
-        node = assign_redirection_node(err.message)
-        retry_count -= 1
-        retry
-      elsif err.message.start_with?('ASK')
-        raise if retry_count <= 0
-
-        node = assign_asking_node(err.message)
-        node.call(%i[asking])
-        retry_count -= 1
-        retry
-      else
-        raise
-      end
-    rescue CannotConnectError
-      update_cluster_info!
-      raise
-    end
-
-    def _scan(command, &block)
-      input_cursor = Integer(command[1])
-
-      client_index = input_cursor % 256
-      raw_cursor = input_cursor >> 8
-
-      clients = @node.scale_reading_clients
-
-      client = clients[client_index]
-      return ['0', []] unless client
-
-      command[1] = raw_cursor.to_s
-
-      result_cursor, result_keys = client.call(command, &block)
-      result_cursor = Integer(result_cursor)
-
-      if result_cursor == 0
-        client_index += 1
-      end
-
-      [((result_cursor << 8) + client_index).to_s, result_keys]
-    end
-
-    def assign_redirection_node(err_msg)
-      _, slot, node_key = err_msg.split(' ')
-      slot = slot.to_i
-      @slot.put(slot, node_key)
-      find_node(node_key)
-    end
-
-    def assign_asking_node(err_msg)
-      _, _, node_key = err_msg.split(' ')
-      find_node(node_key)
-    end
-
-    def assign_node(command)
-      node_key = find_node_key(command)
-      find_node(node_key)
-    end
-
-    def find_node_key(command, primary_only: false)
-      key = @command.extract_first_key(command)
-      return if key.empty?
-
-      slot = KeySlotConverter.convert(key)
-      return unless @slot.exists?(slot)
-
-      if @command.should_send_to_master?(command) || primary_only
-        @slot.find_node_key_of_master(slot)
-      else
-        @slot.find_node_key_of_slave(slot)
-      end
-    end
-
-    def find_node(node_key)
-      return @node.sample if node_key.nil?
-
-      @node.find_by(node_key)
-    rescue Node::ReloadNeeded
-      update_cluster_info!(node_key)
-      @node.find_by(node_key)
-    end
-
-    def update_cluster_info!(node_key = nil)
-      unless node_key.nil?
-        host, port = NodeKey.split(node_key)
-        @option.add_node(host, port)
-      end
-
-      @node.map(&:disconnect)
-      @node, @slot = fetch_cluster_info!(@option)
-    end
-  end
-end
diff --git a/lib/redis/cluster/command.rb b/lib/redis/cluster/command.rb
deleted file mode 100644
index 9386136e079bfc75a6d388da038b556817b00c9d..0000000000000000000000000000000000000000
--- a/lib/redis/cluster/command.rb
+++ /dev/null
@@ -1,79 +0,0 @@
-# frozen_string_literal: true
-
-require_relative '../errors'
-
-class Redis
-  class Cluster
-    # Keep details about Redis commands for Redis Cluster Client.
-    # @see https://redis.io/commands/command
-    class Command
-      def initialize(details)
-        @details = pick_details(details)
-      end
-
-      def extract_first_key(command)
-        i = determine_first_key_position(command)
-        return '' if i == 0
-
-        key = command[i].to_s
-        hash_tag = extract_hash_tag(key)
-        hash_tag.empty? ? key : hash_tag
-      end
-
-      def should_send_to_master?(command)
-        dig_details(command, :write)
-      end
-
-      def should_send_to_slave?(command)
-        dig_details(command, :readonly)
-      end
-
-      private
-
-      def pick_details(details)
-        details.transform_values do |detail|
-          {
-            first_key_position: detail[:first],
-            write: detail[:flags].include?('write'),
-            readonly: detail[:flags].include?('readonly')
-          }
-        end
-      end
-
-      def dig_details(command, key)
-        name = command.first.to_s
-        return unless @details.key?(name)
-
-        @details.fetch(name).fetch(key)
-      end
-
-      def determine_first_key_position(command)
-        case command.first.to_s.downcase
-        when 'eval', 'evalsha', 'migrate', 'zinterstore', 'zunionstore' then 3
-        when 'object' then 2
-        when 'memory'
-          command[1].to_s.casecmp('usage').zero? ? 2 : 0
-        when 'xread', 'xreadgroup'
-          determine_optional_key_position(command, 'streams')
-        else
-          dig_details(command, :first_key_position).to_i
-        end
-      end
-
-      def determine_optional_key_position(command, option_name)
-        idx = command.map(&:to_s).map(&:downcase).index(option_name)
-        idx.nil? ? 0 : idx + 1
-      end
-
-      # @see https://redis.io/topics/cluster-spec#keys-hash-tags Keys hash tags
-      def extract_hash_tag(key)
-        s = key.index('{')
-        e = key.index('}', s.to_i + 1)
-
-        return '' if s.nil? || e.nil?
-
-        key[s + 1..e - 1]
-      end
-    end
-  end
-end
diff --git a/lib/redis/cluster/command_loader.rb b/lib/redis/cluster/command_loader.rb
deleted file mode 100644
index a840ae672b8a7f977ce0eca8fec534a1088c8fa3..0000000000000000000000000000000000000000
--- a/lib/redis/cluster/command_loader.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-require 'redis/errors'
-
-class Redis
-  class Cluster
-    # Load details about Redis commands for Redis Cluster Client
-    # @see https://redis.io/commands/command
-    module CommandLoader
-      module_function
-
-      def load(nodes)
-        errors = nodes.map do |node|
-          begin
-            return fetch_command_details(node)
-          rescue CannotConnectError, ConnectionError, CommandError => error
-            error
-          end
-        end
-
-        raise InitialSetupError, errors
-      end
-
-      def fetch_command_details(node)
-        node.call(%i[command]).map do |reply|
-          [reply[0], { arity: reply[1], flags: reply[2], first: reply[3], last: reply[4], step: reply[5] }]
-        end.to_h
-      end
-
-      private_class_method :fetch_command_details
-    end
-  end
-end
diff --git a/lib/redis/cluster/key_slot_converter.rb b/lib/redis/cluster/key_slot_converter.rb
deleted file mode 100644
index 72ae3f6742d082839327f84bc2b8b2622e57791e..0000000000000000000000000000000000000000
--- a/lib/redis/cluster/key_slot_converter.rb
+++ /dev/null
@@ -1,72 +0,0 @@
-# frozen_string_literal: true
-
-class Redis
-  class Cluster
-    # Key to slot converter for Redis Cluster Client
-    #
-    # We can test it by `CLUSTER KEYSLOT` command.
-    #
-    # @see https://github.com/antirez/redis-rb-cluster
-    #   Reference implementation in Ruby
-    # @see https://redis.io/topics/cluster-spec#appendix
-    #   Reference implementation in ANSI C
-    # @see https://redis.io/commands/cluster-keyslot
-    #   CLUSTER KEYSLOT command reference
-    #
-    # Copyright (C) 2013 Salvatore Sanfilippo <antirez@gmail.com>
-    module KeySlotConverter
-      XMODEM_CRC16_LOOKUP = [
-        0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
-        0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
-        0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
-        0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
-        0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
-        0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
-        0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
-        0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
-        0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
-        0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
-        0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
-        0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
-        0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
-        0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
-        0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
-        0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
-        0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
-        0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
-        0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
-        0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
-        0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
-        0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
-        0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
-        0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
-        0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
-        0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
-        0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
-        0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
-        0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
-        0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
-        0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
-        0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
-      ].freeze
-
-      HASH_SLOTS = 16_384
-
-      module_function
-
-      # Convert key into slot.
-      #
-      # @param key [String] the key of the redis command
-      #
-      # @return [Integer] slot number
-      def convert(key)
-        crc = 0
-        key.each_byte do |b|
-          crc = ((crc << 8) & 0xffff) ^ XMODEM_CRC16_LOOKUP[((crc >> 8) ^ b) & 0xff]
-        end
-
-        crc % HASH_SLOTS
-      end
-    end
-  end
-end
diff --git a/lib/redis/cluster/node.rb b/lib/redis/cluster/node.rb
deleted file mode 100644
index 30ab54275a2170d564cd536289f59ca9768c5010..0000000000000000000000000000000000000000
--- a/lib/redis/cluster/node.rb
+++ /dev/null
@@ -1,120 +0,0 @@
-# frozen_string_literal: true
-
-require_relative '../errors'
-
-class Redis
-  class Cluster
-    # Keep client list of node for Redis Cluster Client
-    class Node
-      include Enumerable
-
-      ReloadNeeded = Class.new(StandardError)
-
-      ROLE_SLAVE = 'slave'
-
-      def initialize(options, node_flags = {}, with_replica = false)
-        @with_replica = with_replica
-        @node_flags = node_flags
-        @clients = build_clients(options)
-      end
-
-      def each(&block)
-        @clients.values.each(&block)
-      end
-
-      def sample
-        @clients.values.sample
-      end
-
-      def find_by(node_key)
-        @clients.fetch(node_key)
-      rescue KeyError
-        raise ReloadNeeded
-      end
-
-      def call_all(command, &block)
-        try_map { |_, client| client.call(command, &block) }.values
-      end
-
-      def call_master(command, &block)
-        try_map do |node_key, client|
-          next if slave?(node_key)
-
-          client.call(command, &block)
-        end.values
-      end
-
-      def call_slave(command, &block)
-        return call_master(command, &block) if replica_disabled?
-
-        try_map do |node_key, client|
-          next if master?(node_key)
-
-          client.call(command, &block)
-        end.values
-      end
-
-      def process_all(commands, &block)
-        try_map { |_, client| client.process(commands, &block) }.values
-      end
-
-      def scale_reading_clients
-        reading_clients = []
-
-        @clients.each do |node_key, client|
-          next unless replica_disabled? ? master?(node_key) : slave?(node_key)
-
-          reading_clients << client
-        end
-
-        reading_clients
-      end
-
-      private
-
-      def replica_disabled?
-        !@with_replica
-      end
-
-      def master?(node_key)
-        !slave?(node_key)
-      end
-
-      def slave?(node_key)
-        @node_flags[node_key] == ROLE_SLAVE
-      end
-
-      def build_clients(options)
-        clients = options.map do |node_key, option|
-          next if replica_disabled? && slave?(node_key)
-
-          option = option.merge(readonly: true) if slave?(node_key)
-
-          client = Client.new(option)
-          [node_key, client]
-        end
-
-        clients.compact.to_h
-      end
-
-      def try_map
-        errors = {}
-        results = {}
-
-        @clients.each do |node_key, client|
-          begin
-            reply = yield(node_key, client)
-            results[node_key] = reply unless reply.nil?
-          rescue CommandError => err
-            errors[node_key] = err
-            next
-          end
-        end
-
-        return results if errors.empty?
-
-        raise CommandErrorCollection, errors
-      end
-    end
-  end
-end
diff --git a/lib/redis/cluster/node_key.rb b/lib/redis/cluster/node_key.rb
deleted file mode 100644
index f0e825a6009dbe51e71a3a44ec6e3de873095765..0000000000000000000000000000000000000000
--- a/lib/redis/cluster/node_key.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-class Redis
-  class Cluster
-    # Node key's format is `<ip>:<port>`.
-    # It is different from node id.
-    # Node id is internal identifying code in Redis Cluster.
-    module NodeKey
-      DELIMITER = ':'
-
-      module_function
-
-      def optionize(node_key)
-        host, port = split(node_key)
-        { host: host, port: port }
-      end
-
-      def split(node_key)
-        node_key.split(DELIMITER)
-      end
-
-      def build_from_uri(uri)
-        "#{uri.host}#{DELIMITER}#{uri.port}"
-      end
-
-      def build_from_host_port(host, port)
-        "#{host}#{DELIMITER}#{port}"
-      end
-    end
-  end
-end
diff --git a/lib/redis/cluster/node_loader.rb b/lib/redis/cluster/node_loader.rb
deleted file mode 100644
index 689aafcb6b30cc07a5c550c900c43b93eefea1ae..0000000000000000000000000000000000000000
--- a/lib/redis/cluster/node_loader.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-# frozen_string_literal: true
-
-require 'redis/errors'
-
-class Redis
-  class Cluster
-    # Load and hashify node info for Redis Cluster Client
-    module NodeLoader
-      module_function
-
-      def load_flags(nodes)
-        errors = nodes.map do |node|
-          begin
-            return fetch_node_info(node)
-          rescue CannotConnectError, ConnectionError, CommandError => error
-            error
-          end
-        end
-
-        raise InitialSetupError, errors
-      end
-
-      def fetch_node_info(node)
-        node.call(%i[cluster nodes])
-            .split("\n")
-            .map { |str| str.split(' ') }
-            .map { |arr| [arr[1].split('@').first, (arr[2].split(',') & %w[master slave]).first] }
-            .to_h
-      end
-
-      private_class_method :fetch_node_info
-    end
-  end
-end
diff --git a/lib/redis/cluster/option.rb b/lib/redis/cluster/option.rb
deleted file mode 100644
index 7721149c37b1c9935a864d02f446d96303055a85..0000000000000000000000000000000000000000
--- a/lib/redis/cluster/option.rb
+++ /dev/null
@@ -1,100 +0,0 @@
-# frozen_string_literal: true
-
-require_relative '../errors'
-require_relative 'node_key'
-require 'uri'
-
-class Redis
-  class Cluster
-    # Keep options for Redis Cluster Client
-    class Option
-      DEFAULT_SCHEME = 'redis'
-      SECURE_SCHEME = 'rediss'
-      VALID_SCHEMES = [DEFAULT_SCHEME, SECURE_SCHEME].freeze
-
-      def initialize(options)
-        options = options.dup
-        node_addrs = options.delete(:cluster)
-        @node_opts = build_node_options(node_addrs)
-        @replica = options.delete(:replica) == true
-        @fixed_hostname = options.delete(:fixed_hostname)
-        add_common_node_option_if_needed(options, @node_opts, :scheme)
-        add_common_node_option_if_needed(options, @node_opts, :username)
-        add_common_node_option_if_needed(options, @node_opts, :password)
-        @options = options
-      end
-
-      def per_node_key
-        @node_opts.map do |opt|
-          node_key = NodeKey.build_from_host_port(opt[:host], opt[:port])
-          options = @options.merge(opt)
-          options = options.merge(host: @fixed_hostname) if @fixed_hostname && !@fixed_hostname.empty?
-          [node_key, options]
-        end.to_h
-      end
-
-      def use_replica?
-        @replica
-      end
-
-      def update_node(addrs)
-        @node_opts = build_node_options(addrs)
-      end
-
-      def add_node(host, port)
-        @node_opts << { host: host, port: port }
-      end
-
-      private
-
-      def build_node_options(addrs)
-        raise InvalidClientOptionError, 'Redis option of `cluster` must be an Array' unless addrs.is_a?(Array)
-
-        addrs.map { |addr| parse_node_addr(addr) }
-      end
-
-      def parse_node_addr(addr)
-        case addr
-        when String
-          parse_node_url(addr)
-        when Hash
-          parse_node_option(addr)
-        else
-          raise InvalidClientOptionError, 'Redis option of `cluster` must includes String or Hash'
-        end
-      end
-
-      def parse_node_url(addr)
-        uri = URI(addr)
-        raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme)
-
-        db = uri.path.split('/')[1]&.to_i
-        username = uri.user ? URI.decode_www_form_component(uri.user) : nil
-        password = uri.password ? URI.decode_www_form_component(uri.password) : nil
-
-        { scheme: uri.scheme, username: username, password: password, host: uri.host, port: uri.port, db: db }
-          .reject { |_, v| v.nil? || v == '' }
-      rescue URI::InvalidURIError => err
-        raise InvalidClientOptionError, err.message
-      end
-
-      def parse_node_option(addr)
-        addr = addr.map { |k, v| [k.to_sym, v] }.to_h
-        if addr.values_at(:host, :port).any?(&:nil?)
-          raise InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys'
-        end
-
-        addr
-      end
-
-      # Redis cluster node returns only host and port information.
-      # So we should complement additional information such as:
-      #   scheme, username, password and so on.
-      def add_common_node_option_if_needed(options, node_opts, key)
-        return options if options[key].nil? && node_opts.first[key].nil?
-
-        options[key] ||= node_opts.first[key]
-      end
-    end
-  end
-end
diff --git a/lib/redis/cluster/slot.rb b/lib/redis/cluster/slot.rb
deleted file mode 100644
index c89cc1a978b3cca98c326a84a0d47e00d79b6a6d..0000000000000000000000000000000000000000
--- a/lib/redis/cluster/slot.rb
+++ /dev/null
@@ -1,86 +0,0 @@
-# frozen_string_literal: true
-
-class Redis
-  class Cluster
-    # Keep slot and node key map for Redis Cluster Client
-    class Slot
-      ROLE_SLAVE = 'slave'
-
-      def initialize(available_slots, node_flags = {}, with_replica = false)
-        @with_replica = with_replica
-        @node_flags = node_flags
-        @map = build_slot_node_key_map(available_slots)
-      end
-
-      def exists?(slot)
-        @map.key?(slot)
-      end
-
-      def find_node_key_of_master(slot)
-        return nil unless exists?(slot)
-
-        @map[slot][:master]
-      end
-
-      def find_node_key_of_slave(slot)
-        return nil unless exists?(slot)
-        return find_node_key_of_master(slot) if replica_disabled?
-
-        @map[slot][:slaves].sample
-      end
-
-      def put(slot, node_key)
-        # Since we're sharing a hash for build_slot_node_key_map, duplicate it
-        # if it already exists instead of preserving as-is.
-        @map[slot] = @map[slot] ? @map[slot].dup : { master: nil, slaves: [] }
-
-        if master?(node_key)
-          @map[slot][:master] = node_key
-        elsif !@map[slot][:slaves].include?(node_key)
-          @map[slot][:slaves] << node_key
-        end
-
-        nil
-      end
-
-      private
-
-      def replica_disabled?
-        !@with_replica
-      end
-
-      def master?(node_key)
-        !slave?(node_key)
-      end
-
-      def slave?(node_key)
-        @node_flags[node_key] == ROLE_SLAVE
-      end
-
-      # available_slots is mapping of node_key to list of slot ranges
-      def build_slot_node_key_map(available_slots)
-        by_ranges = {}
-        available_slots.each do |node_key, slots_arr|
-          by_ranges[slots_arr] ||= { master: nil, slaves: [] }
-
-          if master?(node_key)
-            by_ranges[slots_arr][:master] = node_key
-          elsif !by_ranges[slots_arr][:slaves].include?(node_key)
-            by_ranges[slots_arr][:slaves] << node_key
-          end
-        end
-
-        by_slot = {}
-        by_ranges.each do |slots_arr, nodes|
-          slots_arr.each do |slots|
-            slots.each do |slot|
-              by_slot[slot] = nodes
-            end
-          end
-        end
-
-        by_slot
-      end
-    end
-  end
-end
diff --git a/lib/redis/cluster/slot_loader.rb b/lib/redis/cluster/slot_loader.rb
deleted file mode 100644
index ee5af2c60cac95ab1bd33522c5481f15c06ff719..0000000000000000000000000000000000000000
--- a/lib/redis/cluster/slot_loader.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-# frozen_string_literal: true
-
-require 'redis/errors'
-require 'redis/cluster/node_key'
-
-class Redis
-  class Cluster
-    # Load and hashify slot info for Redis Cluster Client
-    module SlotLoader
-      module_function
-
-      def load(nodes)
-        errors = nodes.map do |node|
-          begin
-            return fetch_slot_info(node)
-          rescue CannotConnectError, ConnectionError, CommandError => error
-            error
-          end
-        end
-
-        raise InitialSetupError, errors
-      end
-
-      def fetch_slot_info(node)
-        hash_with_default_arr = Hash.new { |h, k| h[k] = [] }
-        node.call(%i[cluster slots])
-            .flat_map { |arr| parse_slot_info(arr, default_ip: node.host) }
-            .each_with_object(hash_with_default_arr) { |arr, h| h[arr[0]] << arr[1] }
-      end
-
-      def parse_slot_info(arr, default_ip:)
-        first_slot, last_slot = arr[0..1]
-        slot_range = (first_slot..last_slot).freeze
-        arr[2..-1].map { |addr| [stringify_node_key(addr, default_ip), slot_range] }
-      end
-
-      def stringify_node_key(arr, default_ip)
-        ip, port = arr
-        ip = default_ip if ip.empty? # When cluster is down
-        NodeKey.build_from_host_port(ip, port)
-      end
-
-      private_class_method :fetch_slot_info, :parse_slot_info, :stringify_node_key
-    end
-  end
-end
diff --git a/lib/redis/commands.rb b/lib/redis/commands.rb
index 8a153e14c361fbe59fc59ecf4d3938d21e818c1d..ddf264d952c7289493d01c0f69ec574c2d0e11bd 100644
--- a/lib/redis/commands.rb
+++ b/lib/redis/commands.rb
@@ -40,12 +40,7 @@ class Redis
     # where the method call will return nil. Propagate the nil instead of falsely
     # returning false.
     Boolify = lambda { |value|
-      case value
-      when Integer
-        value > 0
-      else
-        value
-      end
+      value != 0 unless value.nil?
     }
 
     BoolifySet = lambda { |value|
@@ -124,7 +119,9 @@ class Redis
     HashifyStreamAutoclaim = lambda { |reply|
       {
         'next' => reply[0],
-        'entries' => reply[1].map { |entry| [entry[0], entry[1].each_slice(2).to_h] }
+        'entries' => reply[1].compact.map do |entry, values|
+          [entry, values.each_slice(2)&.to_h]
+        end
       }
     }
 
diff --git a/lib/redis/commands/bitmaps.rb b/lib/redis/commands/bitmaps.rb
index a8d9fdfe40fc1ecc8d64ad0e1df0cdf99542adb0..c9f172825e3e5711e91318bd91129ceb8038bedc 100644
--- a/lib/redis/commands/bitmaps.rb
+++ b/lib/redis/commands/bitmaps.rb
@@ -27,9 +27,13 @@ class Redis
       # @param [String] key
       # @param [Integer] start start index
       # @param [Integer] stop stop index
+      # @param [String, Symbol] scale the scale of the offset range
+      #     e.g. 'BYTE' - interpreted as a range of bytes, 'BIT' - interpreted as a range of bits
       # @return [Integer] the number of bits set to 1
-      def bitcount(key, start = 0, stop = -1)
-        send_command([:bitcount, key, start, stop])
+      def bitcount(key, start = 0, stop = -1, scale: nil)
+        command = [:bitcount, key, start, stop]
+        command << scale if scale
+        send_command(command)
       end
 
       # Perform a bitwise operation between strings and store the resulting string in a key.
@@ -39,7 +43,10 @@ class Redis
       # @param [String, Array<String>] keys one or more source keys to perform `operation`
       # @return [Integer] the length of the string stored in `destkey`
       def bitop(operation, destkey, *keys)
-        send_command([:bitop, operation, destkey, *keys])
+        keys.flatten!(1)
+        command = [:bitop, operation, destkey]
+        command.concat(keys)
+        send_command(command)
       end
 
       # Return the position of the first bit set to 1 or 0 in a string.
@@ -48,14 +55,17 @@ class Redis
       # @param [Integer] bit whether to look for the first 1 or 0 bit
       # @param [Integer] start start index
       # @param [Integer] stop stop index
+      # @param [String, Symbol] scale the scale of the offset range
+      #     e.g. 'BYTE' - interpreted as a range of bytes, 'BIT' - interpreted as a range of bits
       # @return [Integer] the position of the first 1/0 bit.
       #                  -1 if looking for 1 and it is not found or start and stop are given.
-      def bitpos(key, bit, start = nil, stop = nil)
+      def bitpos(key, bit, start = nil, stop = nil, scale: nil)
         raise(ArgumentError, 'stop parameter specified without start parameter') if stop && !start
 
         command = [:bitpos, key, bit]
         command << start if start
         command << stop if stop
+        command << scale if scale
         send_command(command)
       end
     end
diff --git a/lib/redis/commands/cluster.rb b/lib/redis/commands/cluster.rb
index 49c5776a49df91737ec1e00bf080c844dd5df18e..390b4974645765d12e33fbc03268a58aacd8bf04 100644
--- a/lib/redis/commands/cluster.rb
+++ b/lib/redis/commands/cluster.rb
@@ -12,24 +12,7 @@ class Redis
       #
       # @return [Object] depends on the subcommand
       def cluster(subcommand, *args)
-        subcommand = subcommand.to_s.downcase
-        block = case subcommand
-        when 'slots'
-          HashifyClusterSlots
-        when 'nodes'
-          HashifyClusterNodes
-        when 'slaves'
-          HashifyClusterSlaves
-        when 'info'
-          HashifyInfo
-        else
-          Noop
-        end
-
-        # @see https://github.com/antirez/redis/blob/unstable/src/redis-trib.rb#L127 raw reply expected
-        block = Noop unless @cluster_mode
-
-        send_command([:cluster, subcommand] + args, &block)
+        send_command([:cluster, subcommand] + args)
       end
 
       # Sends `ASKING` command to random node and returns its reply.
diff --git a/lib/redis/commands/connection.rb b/lib/redis/commands/connection.rb
index c63dfbc9d356c1fe43f2e35cbdeaa9a8e8401a11..50c39e8e70b982823983b17124cf9483d4038b89 100644
--- a/lib/redis/commands/connection.rb
+++ b/lib/redis/commands/connection.rb
@@ -34,10 +34,7 @@ class Redis
       # @param [Integer] db zero-based index of the DB to use (0 to 15)
       # @return [String] `OK`
       def select(db)
-        synchronize do |client|
-          client.db = db
-          client.call([:select, db])
-        end
+        send_command([:select, db])
       end
 
       # Close the connection.
@@ -45,12 +42,10 @@ class Redis
       # @return [String] `OK`
       def quit
         synchronize do |client|
-          begin
-            client.call([:quit])
-          rescue ConnectionError
-          ensure
-            client.disconnect
-          end
+          client.call_v([:quit])
+        rescue ConnectionError
+        ensure
+          client.close
         end
       end
     end
diff --git a/lib/redis/commands/geo.rb b/lib/redis/commands/geo.rb
index 003858fd0a049f51e2d819654298f9f1c08ba952..fe0d211cec49f7d87ea5e10acac2f0dd2c039d02 100644
--- a/lib/redis/commands/geo.rb
+++ b/lib/redis/commands/geo.rb
@@ -74,9 +74,9 @@ class Redis
       private
 
       def _geoarguments(*args, options: nil, sort: nil, count: nil)
-        args.push sort if sort
-        args.push 'count', count if count
-        args.push options if options
+        args << sort if sort
+        args << 'COUNT' << Integer(count) if count
+        args << options if options
         args
       end
     end
diff --git a/lib/redis/commands/hashes.rb b/lib/redis/commands/hashes.rb
index 5531ad0698306a761665ab3496cb6ddda79295b7..a9a505afe60931a87633eb4248e49043b8c6c0d8 100644
--- a/lib/redis/commands/hashes.rb
+++ b/lib/redis/commands/hashes.rb
@@ -63,7 +63,7 @@ class Redis
       #
       # @see #hmset
       def mapped_hmset(key, hash)
-        hmset(key, hash.to_a.flatten)
+        hmset(key, hash.flatten)
       end
 
       # Get the value of a hash field.
@@ -87,7 +87,8 @@ class Redis
       #
       # @see #mapped_hmget
       def hmget(key, *fields, &blk)
-        send_command([:hmget, key] + fields, &blk)
+        fields.flatten!(1)
+        send_command([:hmget, key].concat(fields), &blk)
       end
 
       # Get the values of all the given hash fields.
@@ -102,7 +103,8 @@ class Redis
       #
       # @see #hmget
       def mapped_hmget(key, *fields)
-        hmget(key, *fields) do |reply|
+        fields.flatten!(1)
+        hmget(key, fields) do |reply|
           if reply.is_a?(Array)
             Hash[fields.zip(reply)]
           else
@@ -152,7 +154,8 @@ class Redis
       # @param [String, Array<String>] field
       # @return [Integer] the number of fields that were removed from the hash
       def hdel(key, *fields)
-        send_command([:hdel, key, *fields])
+        fields.flatten!(1)
+        send_command([:hdel, key].concat(fields))
       end
 
       # Determine if a hash field exists.
@@ -171,7 +174,7 @@ class Redis
       # @param [Integer] increment
       # @return [Integer] value of the field after incrementing it
       def hincrby(key, field, increment)
-        send_command([:hincrby, key, field, increment])
+        send_command([:hincrby, key, field, Integer(increment)])
       end
 
       # Increment the numeric value of a hash field by the given float number.
@@ -181,7 +184,7 @@ class Redis
       # @param [Float] increment
       # @return [Float] value of the field after incrementing it
       def hincrbyfloat(key, field, increment)
-        send_command([:hincrbyfloat, key, field, increment], &Floatify)
+        send_command([:hincrbyfloat, key, field, Float(increment)], &Floatify)
       end
 
       # Get all the fields in a hash.
diff --git a/lib/redis/commands/hyper_log_log.rb b/lib/redis/commands/hyper_log_log.rb
index 39e31c7daa9488574c7d70557ccf0882d24ddc63..c7834b7109de578dc0b4083919651c9d8adead4f 100644
--- a/lib/redis/commands/hyper_log_log.rb
+++ b/lib/redis/commands/hyper_log_log.rb
@@ -20,7 +20,7 @@ class Redis
       # @param [String, Array<String>] keys
       # @return [Integer]
       def pfcount(*keys)
-        send_command([:pfcount] + keys)
+        send_command([:pfcount] + keys.flatten(1))
       end
 
       # Merge multiple HyperLogLog values into an unique value that will approximate the cardinality of the union of
diff --git a/lib/redis/commands/keys.rb b/lib/redis/commands/keys.rb
index 75d8c6f07d34f763af788251680a93b6e21a54fe..f507a924f3c4e82554f764515c5bf6a7bd648201 100644
--- a/lib/redis/commands/keys.rb
+++ b/lib/redis/commands/keys.rb
@@ -76,7 +76,7 @@ class Redis
       #   - `:lt => true`: Set expiry only when the new expiry is less than current one.
       # @return [Boolean] whether the timeout was set or not
       def expire(key, seconds, nx: nil, xx: nil, gt: nil, lt: nil)
-        args = [:expire, key, seconds]
+        args = [:expire, key, Integer(seconds)]
         args << "NX" if nx
         args << "XX" if xx
         args << "GT" if gt
@@ -96,7 +96,7 @@ class Redis
       #   - `:lt => true`: Set expiry only when the new expiry is less than current one.
       # @return [Boolean] whether the timeout was set or not
       def expireat(key, unix_time, nx: nil, xx: nil, gt: nil, lt: nil)
-        args = [:expireat, key, unix_time]
+        args = [:expireat, key, Integer(unix_time)]
         args << "NX" if nx
         args << "XX" if xx
         args << "GT" if gt
@@ -105,6 +105,14 @@ class Redis
         send_command(args, &Boolify)
       end
 
+      # Get a key's expiry time specified as number of seconds from UNIX Epoch
+      #
+      # @param  [String] key
+      # @return [Integer] expiry time specified as number of seconds from UNIX Epoch
+      def expiretime(key)
+        send_command([:expiretime, key])
+      end
+
       # Get the time to live (in seconds) for a key.
       #
       # @param [String] key
@@ -132,7 +140,7 @@ class Redis
       #   - `:lt => true`: Set expiry only when the new expiry is less than current one.
       # @return [Boolean] whether the timeout was set or not
       def pexpire(key, milliseconds, nx: nil, xx: nil, gt: nil, lt: nil)
-        args = [:pexpire, key, milliseconds]
+        args = [:pexpire, key, Integer(milliseconds)]
         args << "NX" if nx
         args << "XX" if xx
         args << "GT" if gt
@@ -152,7 +160,7 @@ class Redis
       #   - `:lt => true`: Set expiry only when the new expiry is less than current one.
       # @return [Boolean] whether the timeout was set or not
       def pexpireat(key, ms_unix_time, nx: nil, xx: nil, gt: nil, lt: nil)
-        args = [:pexpireat, key, ms_unix_time]
+        args = [:pexpireat, key, Integer(ms_unix_time)]
         args << "NX" if nx
         args << "XX" if xx
         args << "GT" if gt
@@ -161,6 +169,14 @@ class Redis
         send_command(args, &Boolify)
       end
 
+      # Get a key's expiry time specified as number of milliseconds from UNIX Epoch
+      #
+      # @param  [String] key
+      # @return [Integer] expiry time specified as number of milliseconds from UNIX Epoch
+      def pexpiretime(key)
+        send_command([:pexpiretime, key])
+      end
+
       # Get the time to live (in milliseconds) for a key.
       #
       # @param [String] key
@@ -249,24 +265,6 @@ class Redis
       # @param [String, Array<String>] keys
       # @return [Integer]
       def exists(*keys)
-        if !Redis.exists_returns_integer && keys.size == 1
-          if Redis.exists_returns_integer.nil?
-            message = "`Redis#exists(key)` will return an Integer in redis-rb 4.3. `exists?` returns a boolean, you " \
-              "should use it instead. To opt-in to the new behavior now you can set Redis.exists_returns_integer =  " \
-              "true. To disable this message and keep the current (boolean) behaviour of 'exists' you can set " \
-              "`Redis.exists_returns_integer = false`, but this option will be removed in 5.0.0. " \
-              "(#{::Kernel.caller(1, 1).first})\n"
-
-            ::Redis.deprecate!(message)
-          end
-
-          exists?(*keys)
-        else
-          _exists(*keys)
-        end
-      end
-
-      def _exists(*keys)
         send_command([:exists, *keys])
       end
 
@@ -445,7 +443,7 @@ class Redis
 
         args << cursor
         args << "MATCH" << match if match
-        args << "COUNT" << count if count
+        args << "COUNT" << Integer(count) if count
         args << "TYPE" << type if type
 
         send_command([command] + args, &block)
diff --git a/lib/redis/commands/lists.rb b/lib/redis/commands/lists.rb
index bf04bb303b37f2287296365a01e008510393a0ee..08a619a2106e23ec9878a1beba7d97c08ec65a57 100644
--- a/lib/redis/commands/lists.rb
+++ b/lib/redis/commands/lists.rb
@@ -48,7 +48,7 @@ class Redis
       # @param [String, Symbol] where_destination where to push the element to the source list
       #     e.g. 'LEFT' - to head, 'RIGHT' - to tail
       # @param [Hash] options
-      #   - `:timeout => Numeric`: timeout in seconds, defaults to no timeout
+      #   - `:timeout => [Float, Integer]`: timeout in seconds, defaults to no timeout
       #
       # @return [nil, String] the element, or nil when the source key does not exist or the timeout expired
       #
@@ -99,10 +99,10 @@ class Redis
       #
       # @param [String] key
       # @param [Integer] count number of elements to remove
-      # @return [String, Array<String>] the values of the first elements
+      # @return [nil, String, Array<String>] the values of the first elements
       def lpop(key, count = nil)
         command = [:lpop, key]
-        command << count if count
+        command << Integer(count) if count
         send_command(command)
       end
 
@@ -110,10 +110,10 @@ class Redis
       #
       # @param [String] key
       # @param [Integer] count number of elements to remove
-      # @return [String, Array<String>] the values of the last elements
+      # @return [nil, String, Array<String>] the values of the last elements
       def rpop(key, count = nil)
         command = [:rpop, key]
-        command << count if count
+        command << Integer(count) if count
         send_command(command)
       end
 
@@ -142,7 +142,7 @@ class Redis
       # @param [String, Array<String>] keys one or more keys to perform the
       #   blocking pop on
       # @param [Hash] options
-      #   - `:timeout => Integer`: timeout in seconds, defaults to no timeout
+      #   - `:timeout => [Float, Integer]`: timeout in seconds, defaults to no timeout
       #
       # @return [nil, [String, String]]
       #   - `nil` when the operation timed out
@@ -156,7 +156,7 @@ class Redis
       # @param [String, Array<String>] keys one or more keys to perform the
       #   blocking pop on
       # @param [Hash] options
-      #   - `:timeout => Integer`: timeout in seconds, defaults to no timeout
+      #   - `:timeout => [Float, Integer]`: timeout in seconds, defaults to no timeout
       #
       # @return [nil, [String, String]]
       #   - `nil` when the operation timed out
@@ -173,23 +173,77 @@ class Redis
       # @param [String] source source key
       # @param [String] destination destination key
       # @param [Hash] options
-      #   - `:timeout => Integer`: timeout in seconds, defaults to no timeout
+      #   - `:timeout => [Float, Integer]`: timeout in seconds, defaults to no timeout
       #
       # @return [nil, String]
       #   - `nil` when the operation timed out
       #   - the element was popped and pushed otherwise
-      def brpoplpush(source, destination, deprecated_timeout = 0, timeout: deprecated_timeout)
+      def brpoplpush(source, destination, timeout: 0)
         command = [:brpoplpush, source, destination, timeout]
         send_blocking_command(command, timeout)
       end
 
+      # Pops one or more elements from the first non-empty list key from the list
+      # of provided key names. If lists are empty, blocks until timeout has passed.
+      #
+      # @example Popping a element
+      #   redis.blmpop(1.0, 'list')
+      #   #=> ['list', ['a']]
+      # @example With count option
+      #   redis.blmpop(1.0, 'list', count: 2)
+      #   #=> ['list', ['a', 'b']]
+      #
+      # @params timeout [Float] a float value specifying the maximum number of seconds to block) elapses.
+      #   A timeout of zero can be used to block indefinitely.
+      # @params key [String, Array<String>] one or more keys with lists
+      # @params modifier [String]
+      #  - when `"LEFT"` - the elements popped are those from the left of the list
+      #  - when `"RIGHT"` - the elements popped are those from the right of the list
+      # @params count [Integer] a number of elements to pop
+      #
+      # @return [Array<String, Array<String, Float>>] list of popped elements or nil
+      def blmpop(timeout, *keys, modifier: "LEFT", count: nil)
+        raise ArgumentError, "Pick either LEFT or RIGHT" unless modifier == "LEFT" || modifier == "RIGHT"
+
+        args = [:lmpop, keys.size, *keys, modifier]
+        args << "COUNT" << Integer(count) if count
+
+        send_blocking_command(args, timeout)
+      end
+
+      # Pops one or more elements from the first non-empty list key from the list
+      # of provided key names.
+      #
+      # @example Popping a element
+      #   redis.lmpop('list')
+      #   #=> ['list', ['a']]
+      # @example With count option
+      #   redis.lmpop('list', count: 2)
+      #   #=> ['list', ['a', 'b']]
+      #
+      # @params key [String, Array<String>] one or more keys with lists
+      # @params modifier [String]
+      #  - when `"LEFT"` - the elements popped are those from the left of the list
+      #  - when `"RIGHT"` - the elements popped are those from the right of the list
+      # @params count [Integer] a number of elements to pop
+      #
+      # @return [Array<String, Array<String, Float>>] list of popped elements or nil
+      def lmpop(*keys, modifier: "LEFT", count: nil)
+        raise ArgumentError, "Pick either LEFT or RIGHT" unless modifier == "LEFT" || modifier == "RIGHT"
+
+        args = [:lmpop, keys.size, *keys, modifier]
+        args << "COUNT" << Integer(count) if count
+
+        send_command(args)
+      end
+
       # Get an element from a list by its index.
       #
       # @param [String] key
       # @param [Integer] index
       # @return [String]
       def lindex(key, index)
-        send_command([:lindex, key, index])
+        send_command([:lindex, key, Integer(index)])
       end
 
       # Insert an element before or after another element in a list.
@@ -211,7 +265,7 @@ class Redis
       # @param [Integer] stop stop index
       # @return [Array<String>]
       def lrange(key, start, stop)
-        send_command([:lrange, key, start, stop])
+        send_command([:lrange, key, Integer(start), Integer(stop)])
       end
 
       # Remove elements from a list.
@@ -224,7 +278,7 @@ class Redis
       # @param [String] value
       # @return [Integer] the number of removed elements
       def lrem(key, count, value)
-        send_command([:lrem, key, count, value])
+        send_command([:lrem, key, Integer(count), value])
       end
 
       # Set the value of an element in a list by its index.
@@ -234,7 +288,7 @@ class Redis
       # @param [String] value
       # @return [String] `OK`
       def lset(key, index, value)
-        send_command([:lset, key, index, value])
+        send_command([:lset, key, Integer(index), value])
       end
 
       # Trim a list to the specified range.
@@ -244,7 +298,7 @@ class Redis
       # @param [Integer] stop stop index
       # @return [String] `OK`
       def ltrim(key, start, stop)
-        send_command([:ltrim, key, start, stop])
+        send_command([:ltrim, key, Integer(start), Integer(stop)])
       end
 
       private
@@ -253,21 +307,16 @@ class Redis
         timeout = if args.last.is_a?(Hash)
           options = args.pop
           options[:timeout]
-        elsif args.last.respond_to?(:to_int)
-          last_arg = args.pop
-          ::Redis.deprecate!(
-            "Passing the timeout as a positional argument is deprecated, it should be passed as a keyword argument:\n" \
-            "  redis.#{cmd}(#{args.map(&:inspect).join(', ')}, timeout: #{last_arg.to_int})" \
-            "(called from: #{caller(2, 1).first})"
-          )
-          last_arg.to_int
         end
 
         timeout ||= 0
+        unless timeout.is_a?(Integer) || timeout.is_a?(Float)
+          raise ArgumentError, "timeout must be an Integer or Float, got: #{timeout.class}"
+        end
 
-        keys = args.flatten
-
-        command = [cmd, keys, timeout]
+        args.flatten!(1)
+        command = [cmd].concat(args)
+        command << timeout
         send_blocking_command(command, timeout, &blk)
       end
 
diff --git a/lib/redis/commands/pubsub.rb b/lib/redis/commands/pubsub.rb
index 09da592fb2c47f492699506d7d7222d2d5260fa8..ccdababff71bb3c6328103c16146c07cf4ab3c66 100644
--- a/lib/redis/commands/pubsub.rb
+++ b/lib/redis/commands/pubsub.rb
@@ -9,57 +9,39 @@ class Redis
       end
 
       def subscribed?
-        synchronize do |client|
-          client.is_a? SubscribedClient
-        end
+        !@subscription_client.nil?
       end
 
       # Listen for messages published to the given channels.
       def subscribe(*channels, &block)
-        synchronize do |_client|
-          _subscription(:subscribe, 0, channels, block)
-        end
+        _subscription(:subscribe, 0, channels, block)
       end
 
       # Listen for messages published to the given channels. Throw a timeout error
       # if there is no messages for a timeout period.
       def subscribe_with_timeout(timeout, *channels, &block)
-        synchronize do |_client|
-          _subscription(:subscribe_with_timeout, timeout, channels, block)
-        end
+        _subscription(:subscribe_with_timeout, timeout, channels, block)
       end
 
       # Stop listening for messages posted to the given channels.
       def unsubscribe(*channels)
-        synchronize do |client|
-          raise "Can't unsubscribe if not subscribed." unless subscribed?
-
-          client.unsubscribe(*channels)
-        end
+        _subscription(:unsubscribe, 0, channels, nil)
       end
 
       # Listen for messages published to channels matching the given patterns.
       def psubscribe(*channels, &block)
-        synchronize do |_client|
-          _subscription(:psubscribe, 0, channels, block)
-        end
+        _subscription(:psubscribe, 0, channels, block)
       end
 
       # Listen for messages published to channels matching the given patterns.
       # Throw a timeout error if there is no messages for a timeout period.
       def psubscribe_with_timeout(timeout, *channels, &block)
-        synchronize do |_client|
-          _subscription(:psubscribe_with_timeout, timeout, channels, block)
-        end
+        _subscription(:psubscribe_with_timeout, timeout, channels, block)
       end
 
       # Stop listening for messages posted to channels matching the given patterns.
       def punsubscribe(*channels)
-        synchronize do |client|
-          raise "Can't unsubscribe if not subscribed." unless subscribed?
-
-          client.punsubscribe(*channels)
-        end
+        _subscription(:punsubscribe, 0, channels, nil)
       end
 
       # Inspect the state of the Pub/Sub subsystem.
@@ -67,6 +49,27 @@ class Redis
       def pubsub(subcommand, *args)
         send_command([:pubsub, subcommand] + args)
       end
+
+      # Post a message to a channel in a shard.
+      def spublish(channel, message)
+        send_command([:spublish, channel, message])
+      end
+
+      # Listen for messages published to the given channels in a shard.
+      def ssubscribe(*channels, &block)
+        _subscription(:ssubscribe, 0, channels, block)
+      end
+
+      # Listen for messages published to the given channels in a shard.
+      # Throw a timeout error if there is no messages for a timeout period.
+      def ssubscribe_with_timeout(timeout, *channels, &block)
+        _subscription(:ssubscribe_with_timeout, timeout, channels, block)
+      end
+
+      # Stop listening for messages posted to the given channels in a shard.
+      def sunsubscribe(*channels)
+        _subscription(:sunsubscribe, 0, channels, nil)
+      end
     end
   end
 end
diff --git a/lib/redis/commands/server.rb b/lib/redis/commands/server.rb
index 327d0c80e1ef307be34a7e6610e31aa2ef6e9a1f..eca57782f02f2ef6e8e9a381a7954fb1cebe8d81 100644
--- a/lib/redis/commands/server.rb
+++ b/lib/redis/commands/server.rb
@@ -36,7 +36,7 @@ class Redis
       #
       # @param [String, Symbol] subcommand e.g. `kill`, `list`, `getname`, `setname`
       # @return [String, Hash] depends on subcommand
-      def client(subcommand = nil, *args)
+      def client(subcommand, *args)
         send_command([:client, subcommand] + args) do |reply|
           if subcommand.to_s == "list"
             reply.lines.map do |line|
@@ -117,9 +117,13 @@ class Redis
       #
       # @yield a block to be called for every line of output
       # @yieldparam [String] line timestamp and command that was executed
-      def monitor(&block)
+      def monitor
         synchronize do |client|
-          client.call_loop([:monitor], &block)
+          client = client.pubsub
+          client.call_v([:monitor])
+          loop do
+            yield client.next_event
+          end
         end
       end
 
@@ -133,13 +137,11 @@ class Redis
       # Synchronously save the dataset to disk and then shut down the server.
       def shutdown
         synchronize do |client|
-          client.with_reconnect(false) do
-            begin
-              client.call([:shutdown])
-            rescue ConnectionError
-              # This means Redis has probably exited.
-              nil
-            end
+          client.disable_reconnection do
+            client.call_v([:shutdown])
+          rescue ConnectionError
+            # This means Redis has probably exited.
+            nil
           end
         end
       end
@@ -155,11 +157,9 @@ class Redis
       # @param [Integer] length maximum number of entries to return
       # @return [Array<String>, Integer, String] depends on subcommand
       def slowlog(subcommand, length = nil)
-        synchronize do |client|
-          args = [:slowlog, subcommand]
-          args << length if length
-          client.call args
-        end
+        args = [:slowlog, subcommand]
+        args << Integer(length) if length
+        send_command(args)
       end
 
       # Internal command used for replication.
diff --git a/lib/redis/commands/sets.rb b/lib/redis/commands/sets.rb
index 71e15dbe1d945112e740801d576efea692469dcf..e0575139725c88fe3561620b708f23524669cd9c 100644
--- a/lib/redis/commands/sets.rb
+++ b/lib/redis/commands/sets.rb
@@ -15,56 +15,40 @@ class Redis
       #
       # @param [String] key
       # @param [String, Array<String>] member one member, or array of members
-      # @return [Boolean, Integer] `Boolean` when a single member is specified,
-      #   holding whether or not adding the member succeeded, or `Integer` when an
-      #   array of members is specified, holding the number of members that were
-      #   successfully added
-      def sadd(key, member)
-        block = if Redis.sadd_returns_boolean && !member.is_a?(Array)
-          ::Redis.deprecate!(
-            "Redis#sadd will always return an Integer in Redis 5.0.0. Use Redis#sadd? instead." \
-            "(called from: #{caller(1, 1).first})"
-          )
-          Boolify
-        end
-        send_command([:sadd, key, member], &block)
+      # @return [Integer] The number of members that were successfully added
+      def sadd(key, *members)
+        members.flatten!(1)
+        send_command([:sadd, key].concat(members))
       end
 
       # Add one or more members to a set.
       #
       # @param [String] key
       # @param [String, Array<String>] member one member, or array of members
-      # @return [Boolean] Whether or not at least one member was added.
-      def sadd?(key, member)
-        send_command([:sadd, key, member], &Boolify)
+      # @return [Boolean] Wether at least one member was successfully added.
+      def sadd?(key, *members)
+        members.flatten!(1)
+        send_command([:sadd, key].concat(members), &Boolify)
       end
 
       # Remove one or more members from a set.
       #
       # @param [String] key
       # @param [String, Array<String>] member one member, or array of members
-      # @return [Boolean, Integer] `Boolean` when a single member is specified,
-      #   holding whether or not removing the member succeeded, or `Integer` when an
-      #   array of members is specified, holding the number of members that were
-      #   successfully removed
-      def srem(key, member)
-        block = if Redis.sadd_returns_boolean && !member.is_a?(Array)
-          ::Redis.deprecate!(
-            "Redis#sadd will always return an Integer in Redis 5.0.0. Use Redis#sadd? instead." \
-            "(called from: #{caller(1, 1).first})"
-          )
-          Boolify
-        end
-        send_command([:srem, key, member], &block)
+      # @return [Integer] The number of members that were successfully removed
+      def srem(key, *members)
+        members.flatten!(1)
+        send_command([:srem, key].concat(members))
       end
 
       # Remove one or more members from a set.
       #
       # @param [String] key
       # @param [String, Array<String>] member one member, or array of members
-      # @return [Boolean] `Boolean` Whether or not a member was removed.
-      def srem?(key, member)
-        send_command([:srem, key, member], &Boolify)
+      # @return [Boolean] Wether at least one member was successfully removed.
+      def srem?(key, *members)
+        members.flatten!(1)
+        send_command([:srem, key].concat(members), &Boolify)
       end
 
       # Remove and return one or more random member from a set.
@@ -76,7 +60,7 @@ class Redis
         if count.nil?
           send_command([:spop, key])
         else
-          send_command([:spop, key, count])
+          send_command([:spop, key, Integer(count)])
         end
       end
 
@@ -118,7 +102,8 @@ class Redis
       # @param [String, Array<String>] members
       # @return [Array<Boolean>]
       def smismember(key, *members)
-        send_command([:smismember, key, *members]) do |reply|
+        members.flatten!(1)
+        send_command([:smismember, key].concat(members)) do |reply|
           reply.map(&Boolify)
         end
       end
@@ -136,7 +121,8 @@ class Redis
       # @param [String, Array<String>] keys keys pointing to sets to subtract
       # @return [Array<String>] members in the difference
       def sdiff(*keys)
-        send_command([:sdiff, *keys])
+        keys.flatten!(1)
+        send_command([:sdiff].concat(keys))
       end
 
       # Subtract multiple sets and store the resulting set in a key.
@@ -145,7 +131,8 @@ class Redis
       # @param [String, Array<String>] keys keys pointing to sets to subtract
       # @return [Integer] number of elements in the resulting set
       def sdiffstore(destination, *keys)
-        send_command([:sdiffstore, destination, *keys])
+        keys.flatten!(1)
+        send_command([:sdiffstore, destination].concat(keys))
       end
 
       # Intersect multiple sets.
@@ -153,7 +140,8 @@ class Redis
       # @param [String, Array<String>] keys keys pointing to sets to intersect
       # @return [Array<String>] members in the intersection
       def sinter(*keys)
-        send_command([:sinter, *keys])
+        keys.flatten!(1)
+        send_command([:sinter].concat(keys))
       end
 
       # Intersect multiple sets and store the resulting set in a key.
@@ -162,7 +150,8 @@ class Redis
       # @param [String, Array<String>] keys keys pointing to sets to intersect
       # @return [Integer] number of elements in the resulting set
       def sinterstore(destination, *keys)
-        send_command([:sinterstore, destination, *keys])
+        keys.flatten!(1)
+        send_command([:sinterstore, destination].concat(keys))
       end
 
       # Add multiple sets.
@@ -170,7 +159,8 @@ class Redis
       # @param [String, Array<String>] keys keys pointing to sets to unify
       # @return [Array<String>] members in the union
       def sunion(*keys)
-        send_command([:sunion, *keys])
+        keys.flatten!(1)
+        send_command([:sunion].concat(keys))
       end
 
       # Add multiple sets and store the resulting set in a key.
@@ -179,7 +169,8 @@ class Redis
       # @param [String, Array<String>] keys keys pointing to sets to unify
       # @return [Integer] number of elements in the resulting set
       def sunionstore(destination, *keys)
-        send_command([:sunionstore, destination, *keys])
+        keys.flatten!(1)
+        send_command([:sunionstore, destination].concat(keys))
       end
 
       # Scan a set
diff --git a/lib/redis/commands/sorted_sets.rb b/lib/redis/commands/sorted_sets.rb
index c77e41b3f771c90a812b7240c77131e75d51e302..f07f952758425a3d2ba0be85fea30e88cc1f8767 100644
--- a/lib/redis/commands/sorted_sets.rb
+++ b/lib/redis/commands/sorted_sets.rb
@@ -136,7 +136,9 @@ class Redis
       # @return [Array<String, Float>] element and score pair if count is not specified
       # @return [Array<Array<String, Float>>] list of popped elements and scores
       def zpopmax(key, count = nil)
-        send_command([:zpopmax, key, count].compact) do |members|
+        command = [:zpopmax, key]
+        command << Integer(count) if count
+        send_command(command) do |members|
           members = FloatifyPairs.call(members)
           count.to_i > 1 ? members : members.first
         end
@@ -157,12 +159,80 @@ class Redis
       # @return [Array<String, Float>] element and score pair if count is not specified
       # @return [Array<Array<String, Float>>] list of popped elements and scores
       def zpopmin(key, count = nil)
-        send_command([:zpopmin, key, count].compact) do |members|
+        command = [:zpopmin, key]
+        command << Integer(count) if count
+        send_command(command) do |members|
           members = FloatifyPairs.call(members)
           count.to_i > 1 ? members : members.first
         end
       end
 
+      # Removes and returns up to count members with scores in the sorted set stored at key.
+      #
+      # @example Popping a member
+      #   redis.bzmpop('zset')
+      #   #=> ['zset', ['a', 1.0]]
+      # @example With count option
+      #   redis.bzmpop('zset', count: 2)
+      #   #=> ['zset', [['a', 1.0], ['b', 2.0]]
+      #
+      # @params timeout [Float] a float value specifying the maximum number of seconds to block) elapses.
+      #   A timeout of zero can be used to block indefinitely.
+      # @params key [String, Array<String>] one or more keys with sorted sets
+      # @params modifier [String]
+      #  - when `"MIN"` - the elements popped are those with lowest scores
+      #  - when `"MAX"` - the elements popped are those with the highest scores
+      # @params count [Integer] a number of members to pop
+      #
+      # @return [Array<String, Array<String, Float>>] list of popped elements and scores
+      def bzmpop(timeout, *keys, modifier: "MIN", count: nil)
+        raise ArgumentError, "Pick either MIN or MAX" unless modifier == "MIN" || modifier == "MAX"
+
+        args = [:bzmpop, timeout, keys.size, *keys, modifier]
+        args << "COUNT" << Integer(count) if count
+
+        send_blocking_command(args, timeout) do |response|
+          response&.map do |entry|
+            case entry
+            when String then entry
+            when Array then entry.map { |pair| FloatifyPairs.call(pair) }.flatten(1)
+            end
+          end
+        end
+      end
+
+      # Removes and returns up to count members with scores in the sorted set stored at key.
+      #
+      # @example Popping a member
+      #   redis.zmpop('zset')
+      #   #=> ['zset', ['a', 1.0]]
+      # @example With count option
+      #   redis.zmpop('zset', count: 2)
+      #   #=> ['zset', [['a', 1.0], ['b', 2.0]]
+      #
+      # @params key [String, Array<String>] one or more keys with sorted sets
+      # @params modifier [String]
+      #  - when `"MIN"` - the elements popped are those with lowest scores
+      #  - when `"MAX"` - the elements popped are those with the highest scores
+      # @params count [Integer] a number of members to pop
+      #
+      # @return [Array<String, Array<String, Float>>] list of popped elements and scores
+      def zmpop(*keys, modifier: "MIN", count: nil)
+        raise ArgumentError, "Pick either MIN or MAX" unless modifier == "MIN" || modifier == "MAX"
+
+        args = [:zmpop, keys.size, *keys, modifier]
+        args << "COUNT" << Integer(count) if count
+
+        send_command(args) do |response|
+          response&.map do |entry|
+            case entry
+            when String then entry
+            when Array then entry.map { |pair| FloatifyPairs.call(pair) }.flatten(1)
+            end
+          end
+        end
+      end
+
       # Removes and returns up to count members with the highest scores in the sorted set stored at keys,
       #   or block until one is available.
       #
@@ -261,7 +331,7 @@ class Redis
         end
 
         args = [:zrandmember, key]
-        args << count if count
+        args << Integer(count) if count
 
         if with_scores
           args << "WITHSCORES"
@@ -313,7 +383,7 @@ class Redis
 
         if limit
           args << "LIMIT"
-          args.concat(limit)
+          args.concat(limit.map { |l| Integer(l) })
         end
 
         if with_scores
@@ -354,7 +424,7 @@ class Redis
 
         if limit
           args << "LIMIT"
-          args.concat(limit)
+          args.concat(limit.map { |l| Integer(l) })
         end
 
         send_command(args)
@@ -372,7 +442,7 @@ class Redis
       #
       # @see #zrange
       def zrevrange(key, start, stop, withscores: false, with_scores: withscores)
-        args = [:zrevrange, key, start, stop]
+        args = [:zrevrange, key, Integer(start), Integer(stop)]
 
         if with_scores
           args << "WITHSCORES"
@@ -466,7 +536,7 @@ class Redis
 
         if limit
           args << "LIMIT"
-          args.concat(limit)
+          args.concat(limit.map { |l| Integer(l) })
         end
 
         send_command(args)
@@ -488,7 +558,7 @@ class Redis
 
         if limit
           args << "LIMIT"
-          args.concat(limit)
+          args.concat(limit.map { |l| Integer(l) })
         end
 
         send_command(args)
@@ -531,7 +601,7 @@ class Redis
 
         if limit
           args << "LIMIT"
-          args.concat(limit)
+          args.concat(limit.map { |l| Integer(l) })
         end
 
         send_command(args, &block)
@@ -561,7 +631,7 @@ class Redis
 
         if limit
           args << "LIMIT"
-          args.concat(limit)
+          args.concat(limit.map { |l| Integer(l) })
         end
 
         send_command(args, &block)
@@ -778,7 +848,8 @@ class Redis
       private
 
       def _zsets_operation(cmd, *keys, weights: nil, aggregate: nil, with_scores: false)
-        command = [cmd, keys.size, *keys]
+        keys.flatten!(1)
+        command = [cmd, keys.size].concat(keys)
 
         if weights
           command << "WEIGHTS"
@@ -796,7 +867,8 @@ class Redis
       end
 
       def _zsets_operation_store(cmd, destination, keys, weights: nil, aggregate: nil)
-        command = [cmd, destination, keys.size, *keys]
+        keys.flatten!(1)
+        command = [cmd, destination, keys.size].concat(keys)
 
         if weights
           command << "WEIGHTS"
diff --git a/lib/redis/commands/streams.rb b/lib/redis/commands/streams.rb
index 1cd34dd7352bc0a53f14f2b572f4e03e51a98353..e3c7506884d7acc1ccb92df5617da6bb29f41ca9 100644
--- a/lib/redis/commands/streams.rb
+++ b/lib/redis/commands/streams.rb
@@ -21,15 +21,12 @@ class Redis
       # @return [Array<Hash>] information of the consumers if subcommand is `consumers`
       def xinfo(subcommand, key, group = nil)
         args = [:xinfo, subcommand, key, group].compact
-        synchronize do |client|
-          client.call(args) do |reply|
-            case subcommand.to_s.downcase
-            when 'stream'              then Hashify.call(reply)
-            when 'groups', 'consumers' then reply.map { |arr| Hashify.call(arr) }
-            else reply
-            end
-          end
+        block = case subcommand.to_s.downcase
+        when 'stream'              then Hashify
+        when 'groups', 'consumers' then proc { |r| r.map(&Hashify) }
         end
+
+        send_command(args, &block)
       end
 
       # Add new entry to the stream.
@@ -37,7 +34,7 @@ class Redis
       # @example Without options
       #   redis.xadd('mystream', f1: 'v1', f2: 'v2')
       # @example With options
-      #   redis.xadd('mystream', { f1: 'v1', f2: 'v2' }, id: '0-0', maxlen: 1000, approximate: true)
+      #   redis.xadd('mystream', { f1: 'v1', f2: 'v2' }, id: '0-0', maxlen: 1000, approximate: true, nomkstream: true)
       #
       # @param key   [String] the stream key
       # @param entry [Hash]   one or multiple field-value pairs
@@ -46,17 +43,19 @@ class Redis
       # @option opts [String]  :id          the entry id, default value is `*`, it means auto generation
       # @option opts [Integer] :maxlen      max length of entries
       # @option opts [Boolean] :approximate whether to add `~` modifier of maxlen or not
+      # @option opts [Boolean] :nomkstream  whether to add NOMKSTREAM, default is not to add
       #
       # @return [String] the entry id
-      def xadd(key, entry, approximate: nil, maxlen: nil, id: '*')
+      def xadd(key, entry, approximate: nil, maxlen: nil, nomkstream: nil, id: '*')
         args = [:xadd, key]
+        args << 'NOMKSTREAM' if nomkstream
         if maxlen
           args << "MAXLEN"
           args << "~" if approximate
           args << maxlen
         end
         args << id
-        args.concat(entry.to_a.flatten)
+        args.concat(entry.flatten)
         send_command(args)
       end
 
@@ -66,14 +65,30 @@ class Redis
       #   redis.xtrim('mystream', 1000)
       # @example With options
       #   redis.xtrim('mystream', 1000, approximate: true)
-      #
-      # @param key         [String]  the stream key
-      # @param mexlen      [Integer] max length of entries
-      # @param approximate [Boolean] whether to add `~` modifier of maxlen or not
+      # @example With strategy
+      #   redis.xtrim('mystream', '1-0', strategy: 'MINID')
+      #
+      # @overload xtrim(key, maxlen, strategy: 'MAXLEN', approximate: true)
+      #   @param key         [String]  the stream key
+      #   @param maxlen      [Integer] max length of entries
+      #   @param strategy    [String]  the limit strategy, must be MAXLEN
+      #   @param approximate [Boolean] whether to add `~` modifier of maxlen or not
+      #   @param limit       [Integer] maximum count of entries to be evicted
+      # @overload xtrim(key, minid, strategy: 'MINID', approximate: true)
+      #   @param key         [String]  the stream key
+      #   @param minid       [String]  minimum id of entries
+      #   @param strategy    [String]  the limit strategy, must be MINID
+      #   @param approximate [Boolean] whether to add `~` modifier of minid or not
+      #   @param limit       [Integer] maximum count of entries to be evicted
       #
       # @return [Integer] the number of entries actually deleted
-      def xtrim(key, maxlen, approximate: false)
-        args = [:xtrim, key, 'MAXLEN', (approximate ? '~' : nil), maxlen].compact
+      def xtrim(key, len_or_id, strategy: 'MAXLEN', approximate: false, limit: nil)
+        strategy = strategy.to_s.upcase
+
+        args = [:xtrim, key, strategy]
+        args << '~' if approximate
+        args << len_or_id
+        args.concat(['LIMIT', limit]) if limit
         send_command(args)
       end
 
@@ -113,7 +128,7 @@ class Redis
       def xrange(key, start = '-', range_end = '+', count: nil)
         args = [:xrange, key, start, range_end]
         args.concat(['COUNT', count]) if count
-        synchronize { |client| client.call(args, &HashifyStreamEntries) }
+        send_command(args, &HashifyStreamEntries)
       end
 
       # Fetches entries of the stream in descending order.
@@ -334,6 +349,8 @@ class Redis
       #   redis.xpending('mystream', 'mygroup')
       # @example With range options
       #   redis.xpending('mystream', 'mygroup', '-', '+', 10)
+      # @example With range and idle time options
+      #   redis.xpending('mystream', 'mygroup', '-', '+', 10, idle: 9000)
       # @example With range and consumer options
       #   redis.xpending('mystream', 'mygroup', '-', '+', 10, 'consumer1')
       #
@@ -344,10 +361,13 @@ class Redis
       # @param count    [Integer] count the number of entries as limit
       # @param consumer [String]  the consumer name
       #
+      # @option opts [Integer] :idle       pending message minimum idle time in milliseconds
+      #
       # @return [Hash]        the summary of pending entries
       # @return [Array<Hash>] the pending entries details if options were specified
-      def xpending(key, group, *args)
+      def xpending(key, group, *args, idle: nil)
         command_args = [:xpending, key, group]
+        command_args << 'IDLE' << Integer(idle) if idle
         case args.size
         when 0, 3, 4
           command_args.concat(args)
diff --git a/lib/redis/commands/strings.rb b/lib/redis/commands/strings.rb
index 69f92852f7fd12522ecb0e1e3af0796316c8615c..6080a5d6dbf0acb1888a5598d7430c8599ecd474 100644
--- a/lib/redis/commands/strings.rb
+++ b/lib/redis/commands/strings.rb
@@ -25,7 +25,7 @@ class Redis
       # @param [Integer] decrement
       # @return [Integer] value after decrementing it
       def decrby(key, decrement)
-        send_command([:decrby, key, decrement])
+        send_command([:decrby, key, Integer(decrement)])
       end
 
       # Increment the integer value of a key by one.
@@ -50,7 +50,7 @@ class Redis
       # @param [Integer] increment
       # @return [Integer] value after incrementing it
       def incrby(key, increment)
-        send_command([:incrby, key, increment])
+        send_command([:incrby, key, Integer(increment)])
       end
 
       # Increment the numeric value of a key by the given float number.
@@ -63,7 +63,7 @@ class Redis
       # @param [Float] increment
       # @return [Float] value after incrementing it
       def incrbyfloat(key, increment)
-        send_command([:incrbyfloat, key, increment], &Floatify)
+        send_command([:incrbyfloat, key, Float(increment)], &Floatify)
       end
 
       # Set the string value of a key.
@@ -82,10 +82,10 @@ class Redis
       # @return [String, Boolean] `"OK"` or true, false if `:nx => true` or `:xx => true`
       def set(key, value, ex: nil, px: nil, exat: nil, pxat: nil, nx: nil, xx: nil, keepttl: nil, get: nil)
         args = [:set, key, value.to_s]
-        args << "EX" << ex if ex
-        args << "PX" << px if px
-        args << "EXAT" << exat if exat
-        args << "PXAT" << pxat if pxat
+        args << "EX" << Integer(ex) if ex
+        args << "PX" << Integer(px) if px
+        args << "EXAT" << Integer(exat) if exat
+        args << "PXAT" << Integer(pxat) if pxat
         args << "NX" if nx
         args << "XX" if xx
         args << "KEEPTTL" if keepttl
@@ -105,7 +105,7 @@ class Redis
       # @param [String] value
       # @return [String] `"OK"`
       def setex(key, ttl, value)
-        send_command([:setex, key, ttl, value.to_s])
+        send_command([:setex, key, Integer(ttl), value.to_s])
       end
 
       # Set the time to live in milliseconds of a key.
@@ -115,7 +115,7 @@ class Redis
       # @param [String] value
       # @return [String] `"OK"`
       def psetex(key, ttl, value)
-        send_command([:psetex, key, ttl, value.to_s])
+        send_command([:psetex, key, Integer(ttl), value.to_s])
       end
 
       # Set the value of a key, only if the key does not exist.
@@ -152,7 +152,7 @@ class Redis
       #
       # @see #mset
       def mapped_mset(hash)
-        mset(hash.to_a.flatten)
+        mset(hash.flatten)
       end
 
       # Set one or more values, only if none of the keys exist.
@@ -180,7 +180,7 @@ class Redis
       #
       # @see #msetnx
       def mapped_msetnx(hash)
-        msetnx(hash.to_a.flatten)
+        msetnx(hash.flatten)
       end
 
       # Get the value of a key.
@@ -202,6 +202,7 @@ class Redis
       #
       # @see #mapped_mget
       def mget(*keys, &blk)
+        keys.flatten!(1)
         send_command([:mget, *keys], &blk)
       end
 
@@ -232,7 +233,7 @@ class Redis
       # @param [String] value
       # @return [Integer] length of the string after it was modified
       def setrange(key, offset, value)
-        send_command([:setrange, key, offset, value.to_s])
+        send_command([:setrange, key, Integer(offset), value.to_s])
       end
 
       # Get a substring of the string stored at a key.
@@ -243,7 +244,7 @@ class Redis
       #   the end of the string
       # @return [Integer] `0` or `1`
       def getrange(key, start, stop)
-        send_command([:getrange, key, start, stop])
+        send_command([:getrange, key, Integer(start), Integer(stop)])
       end
 
       # Append a value to a key.
@@ -291,10 +292,10 @@ class Redis
       # @return [String] The value of key, or nil when key does not exist.
       def getex(key, ex: nil, px: nil, exat: nil, pxat: nil, persist: false)
         args = [:getex, key]
-        args << "EX" << ex if ex
-        args << "PX" << px if px
-        args << "EXAT" << exat if exat
-        args << "PXAT" << pxat if pxat
+        args << "EX" << Integer(ex) if ex
+        args << "PX" << Integer(px) if px
+        args << "EXAT" << Integer(exat) if exat
+        args << "PXAT" << Integer(pxat) if pxat
         args << "PERSIST" if persist
 
         send_command(args)
diff --git a/lib/redis/commands/transactions.rb b/lib/redis/commands/transactions.rb
index ac294abf8f0a45a60a87a9d909be116e2d99cab5..c0dcf984d985fa2464633adef61939b47847ec16 100644
--- a/lib/redis/commands/transactions.rb
+++ b/lib/redis/commands/transactions.rb
@@ -5,48 +5,26 @@ class Redis
     module Transactions
       # Mark the start of a transaction block.
       #
-      # Passing a block is optional.
-      #
       # @example With a block
       #   redis.multi do |multi|
       #     multi.set("key", "value")
       #     multi.incr("counter")
       #   end # => ["OK", 6]
       #
-      # @example Without a block
-      #   redis.multi
-      #     # => "OK"
-      #   redis.set("key", "value")
-      #     # => "QUEUED"
-      #   redis.incr("counter")
-      #     # => "QUEUED"
-      #   redis.exec
-      #     # => ["OK", 6]
-      #
       # @yield [multi] the commands that are called inside this block are cached
       #   and written to the server upon returning from it
       # @yieldparam [Redis] multi `self`
       #
-      # @return [String, Array<...>]
-      #   - when a block is not given, `OK`
-      #   - when a block is given, an array with replies
+      # @return [Array<...>]
+      #   - an array with replies
       #
       # @see #watch
       # @see #unwatch
-      def multi(&block) # :nodoc:
-        if block_given?
-          if block&.arity == 0
-            Pipeline.deprecation_warning("multi", Kernel.caller_locations(1, 5))
-          end
-
-          synchronize do |prior_client|
-            pipeline = Pipeline::Multi.new(prior_client)
-            pipelined_connection = PipelinedConnection.new(pipeline)
-            yield pipelined_connection
-            prior_client.call_pipeline(pipeline)
+      def multi
+        synchronize do |client|
+          client.multi do |raw_transaction|
+            yield MultiConnection.new(raw_transaction)
           end
-        else
-          send_command([:multi])
         end
       end
 
@@ -82,7 +60,7 @@ class Redis
       # @see #multi
       def watch(*keys)
         synchronize do |client|
-          res = client.call([:watch, *keys])
+          res = client.call_v([:watch] + keys)
 
           if block_given?
             begin
@@ -125,8 +103,6 @@ class Redis
 
       # Discard all commands issued after MULTI.
       #
-      # Only call this method when `#multi` was called **without** a block.
-      #
       # @return [String] `"OK"`
       #
       # @see #multi
diff --git a/lib/redis/connection.rb b/lib/redis/connection.rb
deleted file mode 100644
index b90a921499e0ac3dcb1119a2ddf9778dadda0804..0000000000000000000000000000000000000000
--- a/lib/redis/connection.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-require "redis/connection/registry"
-
-# If a connection driver was required before this file, the array
-# Redis::Connection.drivers will contain one or more classes. The last driver
-# in this array will be used as default driver. If this array is empty, we load
-# the plain Ruby driver as our default. Another driver can be required at a
-# later point in time, causing it to be the last element of the #drivers array
-# and therefore be chosen by default.
-require_relative "connection/ruby" if Redis::Connection.drivers.empty?
diff --git a/lib/redis/connection/command_helper.rb b/lib/redis/connection/command_helper.rb
deleted file mode 100644
index f14f90a875b4de9e653aa6bd739969d609353de4..0000000000000000000000000000000000000000
--- a/lib/redis/connection/command_helper.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-# frozen_string_literal: true
-
-class Redis
-  module Connection
-    module CommandHelper
-      COMMAND_DELIMITER = "\r\n"
-
-      def build_command(args)
-        command = [nil]
-
-        args.each do |i|
-          if i.is_a? Array
-            i.each do |j|
-              j = j.to_s
-              j = j.encoding == Encoding::BINARY ? j : j.b
-              command << "$#{j.bytesize}"
-              command << j
-            end
-          else
-            i = i.to_s
-            i = i.encoding == Encoding::BINARY ? i : i.b
-            command << "$#{i.bytesize}"
-            command << i
-          end
-        end
-
-        command[0] = "*#{(command.length - 1) / 2}"
-
-        # Trailing delimiter
-        command << ""
-        command.join(COMMAND_DELIMITER)
-      end
-
-      protected
-
-      def encode(string)
-        string.force_encoding(Encoding.default_external)
-      end
-    end
-  end
-end
diff --git a/lib/redis/connection/hiredis.rb b/lib/redis/connection/hiredis.rb
deleted file mode 100644
index 2e0a472221c488eac181e4fd2dfbf418f69193eb..0000000000000000000000000000000000000000
--- a/lib/redis/connection/hiredis.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-# frozen_string_literal: true
-
-require "redis/connection/registry"
-require "redis/errors"
-
-require "hiredis/connection"
-require "timeout"
-
-class Redis
-  module Connection
-    class Hiredis
-      def self.connect(config)
-        connection = ::Hiredis::Connection.new
-        connect_timeout = (config.fetch(:connect_timeout, 0) * 1_000_000).to_i
-
-        if config[:scheme] == "unix"
-          connection.connect_unix(config[:path], connect_timeout)
-        elsif config[:scheme] == "rediss" || config[:ssl]
-          raise NotImplementedError, "SSL not supported by hiredis driver"
-        else
-          connection.connect(config[:host], config[:port], connect_timeout)
-        end
-
-        instance = new(connection)
-        instance.timeout = config[:read_timeout]
-        instance
-      rescue Errno::ETIMEDOUT
-        raise TimeoutError
-      end
-
-      def initialize(connection)
-        @connection = connection
-      end
-
-      def connected?
-        @connection&.connected?
-      end
-
-      def timeout=(timeout)
-        # Hiredis works with microsecond timeouts
-        @connection.timeout = Integer(timeout * 1_000_000)
-      end
-
-      def disconnect
-        @connection.disconnect
-        @connection = nil
-      end
-
-      def write(command)
-        @connection.write(command.flatten(1))
-      rescue Errno::EAGAIN
-        raise TimeoutError
-      end
-
-      def read
-        reply = @connection.read
-        reply = CommandError.new(reply.message) if reply.is_a?(RuntimeError)
-        reply
-      rescue Errno::EAGAIN
-        raise TimeoutError
-      rescue RuntimeError => err
-        raise ProtocolError, err.message
-      end
-    end
-  end
-end
-
-Redis::Connection.drivers << Redis::Connection::Hiredis
diff --git a/lib/redis/connection/registry.rb b/lib/redis/connection/registry.rb
deleted file mode 100644
index 26ff8335d4a8a9f0683aeaa9d881c0b43cc361a0..0000000000000000000000000000000000000000
--- a/lib/redis/connection/registry.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-class Redis
-  module Connection
-    # Store a list of loaded connection drivers in the Connection module.
-    # Redis::Client uses the last required driver by default, and will be aware
-    # of the loaded connection drivers if the user chooses to override the
-    # default connection driver.
-    def self.drivers
-      @drivers ||= []
-    end
-  end
-end
diff --git a/lib/redis/connection/ruby.rb b/lib/redis/connection/ruby.rb
deleted file mode 100644
index f3740453b8ed667fc3a0f14a183921e2391dc3f1..0000000000000000000000000000000000000000
--- a/lib/redis/connection/ruby.rb
+++ /dev/null
@@ -1,437 +0,0 @@
-# frozen_string_literal: true
-
-require "redis/connection/registry"
-require "redis/connection/command_helper"
-require "redis/errors"
-
-require "socket"
-require "timeout"
-
-begin
-  require "openssl"
-rescue LoadError
-  # Not all systems have OpenSSL support
-end
-
-class Redis
-  module Connection
-    module SocketMixin
-      CRLF = "\r\n"
-
-      def initialize(*args)
-        super(*args)
-
-        @timeout = @write_timeout = nil
-        @buffer = "".b
-      end
-
-      def timeout=(timeout)
-        @timeout = (timeout if timeout && timeout > 0)
-      end
-
-      def write_timeout=(timeout)
-        @write_timeout = (timeout if timeout && timeout > 0)
-      end
-
-      def read(nbytes)
-        result = @buffer.slice!(0, nbytes)
-
-        buffer = String.new(capacity: nbytes, encoding: Encoding::ASCII_8BIT)
-        result << _read_from_socket(nbytes - result.bytesize, buffer) while result.bytesize < nbytes
-
-        result
-      end
-
-      def gets
-        while (crlf = @buffer.index(CRLF)).nil?
-          @buffer << _read_from_socket(16_384)
-        end
-
-        @buffer.slice!(0, crlf + CRLF.bytesize)
-      end
-
-      def _read_from_socket(nbytes, buffer = nil)
-        loop do
-          case chunk = read_nonblock(nbytes, buffer, exception: false)
-          when :wait_readable
-            unless wait_readable(@timeout)
-              raise Redis::TimeoutError
-            end
-          when :wait_writable
-            unless wait_writable(@timeout)
-              raise Redis::TimeoutError
-            end
-          when nil
-            raise Errno::ECONNRESET
-          when String
-            return chunk
-          end
-        end
-      end
-
-      def write(buffer)
-        return super(buffer) unless @write_timeout
-
-        bytes_to_write = buffer.bytesize
-        total_bytes_written = 0
-        loop do
-          case bytes_written = write_nonblock(buffer, exception: false)
-          when :wait_readable
-            unless wait_readable(@write_timeout)
-              raise Redis::TimeoutError
-            end
-          when :wait_writable
-            unless wait_writable(@write_timeout)
-              raise Redis::TimeoutError
-            end
-          when nil
-            raise Errno::ECONNRESET
-          when Integer
-            total_bytes_written += bytes_written
-
-            if total_bytes_written >= bytes_to_write
-              return total_bytes_written
-            end
-
-            buffer = buffer.byteslice(bytes_written..-1)
-          end
-        end
-      end
-    end
-
-    if defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby"
-
-      require "timeout"
-
-      class TCPSocket < ::TCPSocket
-        include SocketMixin
-
-        def self.connect(host, port, timeout)
-          Timeout.timeout(timeout) do
-            sock = new(host, port)
-            sock
-          end
-        rescue Timeout::Error
-          raise TimeoutError
-        end
-      end
-
-      if defined?(::UNIXSocket)
-
-        class UNIXSocket < ::UNIXSocket
-          include SocketMixin
-
-          def self.connect(path, timeout)
-            Timeout.timeout(timeout) do
-              sock = new(path)
-              sock
-            end
-          rescue Timeout::Error
-            raise TimeoutError
-          end
-
-          # JRuby raises Errno::EAGAIN on #read_nonblock even when it
-          # says it is readable (1.6.6, in both 1.8 and 1.9 mode).
-          # Use the blocking #readpartial method instead.
-
-          def _read_from_socket(nbytes, _buffer = nil)
-            # JRuby: Throw away the buffer as we won't need it
-            # but still need to support the max arity of 2
-            readpartial(nbytes)
-          rescue EOFError
-            raise Errno::ECONNRESET
-          end
-        end
-
-      end
-
-    else
-
-      class TCPSocket < ::Socket
-        include SocketMixin
-
-        def self.connect_addrinfo(addrinfo, port, timeout)
-          sock = new(::Socket.const_get(addrinfo[0]), Socket::SOCK_STREAM, 0)
-          sockaddr = ::Socket.pack_sockaddr_in(port, addrinfo[3])
-
-          begin
-            sock.connect_nonblock(sockaddr)
-          rescue Errno::EINPROGRESS
-            raise TimeoutError unless sock.wait_writable(timeout)
-
-            begin
-              sock.connect_nonblock(sockaddr)
-            rescue Errno::EISCONN
-            end
-          end
-
-          sock
-        end
-
-        def self.connect(host, port, timeout)
-          # Don't pass AI_ADDRCONFIG as flag to getaddrinfo(3)
-          #
-          # From the man page for getaddrinfo(3):
-          #
-          #   If hints.ai_flags includes the AI_ADDRCONFIG flag, then IPv4
-          #   addresses are returned in the list pointed to by res only if the
-          #   local system has at least one IPv4 address configured, and IPv6
-          #   addresses are returned only if the local system has at least one
-          #   IPv6 address configured. The loopback address is not considered
-          #   for this case as valid as a configured address.
-          #
-          # We do want the IPv6 loopback address to be returned if applicable,
-          # even if it is the only configured IPv6 address on the machine.
-          # Also see: https://github.com/redis/redis-rb/pull/394.
-          addrinfo = ::Socket.getaddrinfo(host, nil, Socket::AF_UNSPEC, Socket::SOCK_STREAM)
-
-          # From the man page for getaddrinfo(3):
-          #
-          #   Normally, the application should try using the addresses in the
-          #   order in which they are returned. The sorting function used
-          #   within getaddrinfo() is defined in RFC 3484 [...].
-          #
-          addrinfo.each_with_index do |ai, i|
-            begin
-              return connect_addrinfo(ai, port, timeout)
-            rescue SystemCallError
-              # Raise if this was our last attempt.
-              raise if addrinfo.length == i + 1
-            end
-          end
-        end
-      end
-
-      class UNIXSocket < ::Socket
-        include SocketMixin
-
-        def self.connect(path, timeout)
-          sock = new(::Socket::AF_UNIX, Socket::SOCK_STREAM, 0)
-          sockaddr = ::Socket.pack_sockaddr_un(path)
-
-          begin
-            sock.connect_nonblock(sockaddr)
-          rescue Errno::EINPROGRESS
-            raise TimeoutError unless sock.wait_writable(timeout)
-
-            begin
-              sock.connect_nonblock(sockaddr)
-            rescue Errno::EISCONN
-            end
-          end
-
-          sock
-        end
-      end
-
-    end
-
-    if defined?(OpenSSL)
-      class SSLSocket < ::OpenSSL::SSL::SSLSocket
-        include SocketMixin
-
-        unless method_defined?(:wait_readable)
-          def wait_readable(timeout = nil)
-            to_io.wait_readable(timeout)
-          end
-        end
-
-        unless method_defined?(:wait_writable)
-          def wait_writable(timeout = nil)
-            to_io.wait_writable(timeout)
-          end
-        end
-
-        def self.connect(host, port, timeout, ssl_params)
-          # NOTE: this is using Redis::Connection::TCPSocket
-          tcp_sock = TCPSocket.connect(host, port, timeout)
-
-          ctx = OpenSSL::SSL::SSLContext.new
-
-          # The provided parameters are merged into OpenSSL::SSL::SSLContext::DEFAULT_PARAMS
-          ctx.set_params(ssl_params || {})
-
-          ssl_sock = new(tcp_sock, ctx)
-          ssl_sock.hostname = host
-
-          begin
-            # Initiate the socket connection in the background. If it doesn't fail
-            # immediately it will raise an IO::WaitWritable (Errno::EINPROGRESS)
-            # indicating the connection is in progress.
-            # Unlike waiting for a tcp socket to connect, you can't time out ssl socket
-            # connections during the connect phase properly, because IO.select only partially works.
-            # Instead, you have to retry.
-            ssl_sock.connect_nonblock
-          rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitReadable
-            if ssl_sock.wait_readable(timeout)
-              retry
-            else
-              raise TimeoutError
-            end
-          rescue IO::WaitWritable
-            if ssl_sock.wait_writable(timeout)
-              retry
-            else
-              raise TimeoutError
-            end
-          end
-
-          unless ctx.verify_mode == OpenSSL::SSL::VERIFY_NONE || (
-            ctx.respond_to?(:verify_hostname) &&
-            !ctx.verify_hostname
-          )
-            ssl_sock.post_connection_check(host)
-          end
-
-          ssl_sock
-        end
-      end
-    end
-
-    class Ruby
-      include Redis::Connection::CommandHelper
-
-      MINUS    = "-"
-      PLUS     = "+"
-      COLON    = ":"
-      DOLLAR   = "$"
-      ASTERISK = "*"
-
-      def self.connect(config)
-        if config[:scheme] == "unix"
-          raise ArgumentError, "SSL incompatible with unix sockets" if config[:ssl]
-
-          sock = UNIXSocket.connect(config[:path], config[:connect_timeout])
-        elsif config[:scheme] == "rediss" || config[:ssl]
-          sock = SSLSocket.connect(config[:host], config[:port], config[:connect_timeout], config[:ssl_params])
-        else
-          sock = TCPSocket.connect(config[:host], config[:port], config[:connect_timeout])
-        end
-
-        instance = new(sock)
-        instance.timeout = config[:read_timeout]
-        instance.write_timeout = config[:write_timeout]
-        instance.set_tcp_keepalive config[:tcp_keepalive]
-        instance.set_tcp_nodelay if sock.is_a? TCPSocket
-        instance
-      end
-
-      if %i[SOL_SOCKET SO_KEEPALIVE SOL_TCP TCP_KEEPIDLE TCP_KEEPINTVL TCP_KEEPCNT].all? { |c| Socket.const_defined? c }
-        def set_tcp_keepalive(keepalive)
-          return unless keepalive.is_a?(Hash)
-
-          @sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE,  true)
-          @sock.setsockopt(Socket::SOL_TCP,    Socket::TCP_KEEPIDLE,  keepalive[:time])
-          @sock.setsockopt(Socket::SOL_TCP,    Socket::TCP_KEEPINTVL, keepalive[:intvl])
-          @sock.setsockopt(Socket::SOL_TCP,    Socket::TCP_KEEPCNT,   keepalive[:probes])
-        end
-
-        def get_tcp_keepalive
-          {
-            time: @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE).int,
-            intvl: @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL).int,
-            probes: @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT).int
-          }
-        end
-      else
-        def set_tcp_keepalive(keepalive); end
-
-        def get_tcp_keepalive
-          {
-          }
-        end
-      end
-
-      # disables Nagle's Algorithm, prevents multiple round trips with MULTI
-      if %i[IPPROTO_TCP TCP_NODELAY].all? { |c| Socket.const_defined? c }
-        def set_tcp_nodelay
-          @sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
-        end
-      else
-        def set_tcp_nodelay; end
-      end
-
-      def initialize(sock)
-        @sock = sock
-      end
-
-      def connected?
-        !!@sock
-      end
-
-      def disconnect
-        @sock.close
-      rescue
-      ensure
-        @sock = nil
-      end
-
-      def timeout=(timeout)
-        @sock.timeout = timeout if @sock.respond_to?(:timeout=)
-      end
-
-      def write_timeout=(timeout)
-        @sock.write_timeout = timeout
-      end
-
-      def write(command)
-        @sock.write(build_command(command))
-      end
-
-      def read
-        line = @sock.gets
-        reply_type = line.slice!(0, 1)
-        format_reply(reply_type, line)
-      rescue Errno::EAGAIN
-        raise TimeoutError
-      rescue OpenSSL::SSL::SSLError => ssl_error
-        if ssl_error.message.match?(/SSL_read: unexpected eof while reading/i)
-          raise EOFError, ssl_error.message
-        else
-          raise
-        end
-      end
-
-      def format_reply(reply_type, line)
-        case reply_type
-        when MINUS    then format_error_reply(line)
-        when PLUS     then format_status_reply(line)
-        when COLON    then format_integer_reply(line)
-        when DOLLAR   then format_bulk_reply(line)
-        when ASTERISK then format_multi_bulk_reply(line)
-        else raise ProtocolError, reply_type
-        end
-      end
-
-      def format_error_reply(line)
-        CommandError.new(line.strip)
-      end
-
-      def format_status_reply(line)
-        line.strip
-      end
-
-      def format_integer_reply(line)
-        line.to_i
-      end
-
-      def format_bulk_reply(line)
-        bulklen = line.to_i
-        return if bulklen == -1
-
-        reply = encode(@sock.read(bulklen))
-        @sock.read(2) # Discard CRLF.
-        reply
-      end
-
-      def format_multi_bulk_reply(line)
-        n = line.to_i
-        return if n == -1
-
-        Array.new(n) { read }
-      end
-    end
-  end
-end
-
-Redis::Connection.drivers << Redis::Connection::Ruby
diff --git a/lib/redis/connection/synchrony.rb b/lib/redis/connection/synchrony.rb
deleted file mode 100644
index 35eeaa34caeb7dbc0537bda3d8f209c06d115523..0000000000000000000000000000000000000000
--- a/lib/redis/connection/synchrony.rb
+++ /dev/null
@@ -1,148 +0,0 @@
-# frozen_string_literal: true
-
-require "redis/connection/registry"
-require "redis/connection/command_helper"
-require "redis/errors"
-
-require "em-synchrony"
-require "hiredis/reader"
-
-::Redis.deprecate!(
-  "The redis synchrony driver is deprecated and will be removed in redis-rb 5.0.0. " \
-  "We're looking for people to maintain it as a separate gem, see https://github.com/redis/redis-rb/issues/915"
-)
-
-class Redis
-  module Connection
-    class RedisClient < EventMachine::Connection
-      include EventMachine::Deferrable
-
-      attr_accessor :timeout
-
-      def post_init
-        @req = nil
-        @connected = false
-        @reader = ::Hiredis::Reader.new
-      end
-
-      def connection_completed
-        @connected = true
-        succeed
-      end
-
-      def connected?
-        @connected
-      end
-
-      def receive_data(data)
-        @reader.feed(data)
-
-        loop do
-          begin
-            reply = @reader.gets
-          rescue RuntimeError => err
-            @req.fail [:error, ProtocolError.new(err.message)]
-            break
-          end
-
-          break if reply == false
-
-          reply = CommandError.new(reply.message) if reply.is_a?(RuntimeError)
-          @req.succeed [:reply, reply]
-        end
-      end
-
-      def read
-        @req = EventMachine::DefaultDeferrable.new
-        @req.timeout(@timeout, :timeout) if @timeout > 0
-        EventMachine::Synchrony.sync @req
-      end
-
-      def send(data)
-        callback { send_data data }
-      end
-
-      def unbind
-        @connected = false
-        if @req
-          @req.fail [:error, Errno::ECONNRESET]
-          @req = nil
-        else
-          fail
-        end
-      end
-    end
-
-    class Synchrony
-      include Redis::Connection::CommandHelper
-
-      def self.connect(config)
-        if config[:scheme] == "unix"
-          begin
-            conn = EventMachine.connect_unix_domain(config[:path], RedisClient)
-          rescue RuntimeError => e
-            if e.message == "no connection"
-              raise Errno::ECONNREFUSED
-            else
-              raise e
-            end
-          end
-        elsif config[:scheme] == "rediss" || config[:ssl]
-          raise NotImplementedError, "SSL not supported by synchrony driver"
-        else
-          conn = EventMachine.connect(config[:host], config[:port], RedisClient) do |c|
-            c.pending_connect_timeout = [config[:connect_timeout], 0.1].max
-          end
-        end
-
-        fiber = Fiber.current
-        conn.callback { fiber.resume }
-        conn.errback { fiber.resume :refused }
-
-        raise Errno::ECONNREFUSED if Fiber.yield == :refused
-
-        instance = new(conn)
-        instance.timeout = config[:read_timeout]
-        instance
-      end
-
-      def initialize(connection)
-        @connection = connection
-      end
-
-      def connected?
-        @connection&.connected?
-      end
-
-      def timeout=(timeout)
-        @connection.timeout = timeout
-      end
-
-      def disconnect
-        @connection.close_connection
-        @connection = nil
-      end
-
-      def write(command)
-        @connection.send(build_command(command))
-      end
-
-      def read
-        type, payload = @connection.read
-
-        case type
-        when :reply
-          payload
-        when :error
-          raise payload
-        when :timeout
-          raise TimeoutError
-        else
-          raise "Unknown type #{type.inspect}"
-        end
-      end
-    end
-  end
-end
-
-Redis::Connection.drivers << Redis::Connection::Synchrony
diff --git a/lib/redis/distributed.rb b/lib/redis/distributed.rb
index 3e7ccc95fc1dec2a160d5c598ae044377a099b30..33185d7c35a2638e3dfc86d58e9c70e12895afaf 100644
--- a/lib/redis/distributed.rb
+++ b/lib/redis/distributed.rb
@@ -20,7 +20,7 @@ class Redis
     def initialize(node_configs, options = {})
       @tag = options[:tag] || /^\{(.+?)\}/
       @ring = options[:ring] || HashRing.new
-      @node_configs = node_configs.dup
+      @node_configs = node_configs.map(&:dup)
       @default_options = options.dup
       node_configs.each { |node_config| add_node(node_config) }
       @subscribed_node = nil
@@ -41,6 +41,8 @@ class Redis
     def add_node(options)
       options = { url: options } if options.is_a?(String)
       options = @default_options.merge(options)
+      options.delete(:tag)
+      options.delete(:ring)
       @ring.add_node Redis.new(options)
     end
 
@@ -64,6 +66,10 @@ class Redis
       on_each_node :quit
     end
 
+    def close
+      on_each_node :close
+    end
+
     # Asynchronously save the dataset to disk.
     def bgsave
       on_each_node :bgsave
@@ -124,6 +130,11 @@ class Redis
       node_for(key).expireat(key, unix_time, **kwargs)
     end
 
+    # Get the expiration for a key as a UNIX timestamp.
+    def expiretime(key)
+      node_for(key).expiretime(key)
+    end
+
     # Get the time to live (in seconds) for a key.
     def ttl(key)
       node_for(key).ttl(key)
@@ -139,6 +150,11 @@ class Redis
       node_for(key).pexpireat(key, ms_unix_time, **kwarg)
     end
 
+    # Get the expiration for a key as number of milliseconds from UNIX Epoch.
+    def pexpiretime(key)
+      node_for(key).pexpiretime(key)
+    end
+
     # Get the time to live (in milliseconds) for a key.
     def pttl(key)
       node_for(key).pttl(key)
@@ -161,6 +177,7 @@ class Redis
 
     # Delete a key.
     def del(*args)
+      args.flatten!(1)
       keys_per_node = args.group_by { |key| node_for(key) }
       keys_per_node.inject(0) do |sum, (node, keys)|
         sum + node.del(*keys)
@@ -169,6 +186,7 @@ class Redis
 
     # Unlink keys.
     def unlink(*args)
+      args.flatten!(1)
       keys_per_node = args.group_by { |key| node_for(key) }
       keys_per_node.inject(0) do |sum, (node, keys)|
         sum + node.unlink(*keys)
@@ -177,23 +195,16 @@ class Redis
 
     # Determine if a key exists.
     def exists(*args)
-      if !Redis.exists_returns_integer && args.size == 1
-        ::Redis.deprecate!(
-          "`Redis#exists(key)` will return an Integer in redis-rb 4.3, if you want to keep the old behavior, " \
-          "use `exists?` instead. To opt-in to the new behavior now you can set Redis.exists_returns_integer = true. " \
-          "(#{::Kernel.caller(1, 1).first})\n"
-        )
-        exists?(*args)
-      else
-        keys_per_node = args.group_by { |key| node_for(key) }
-        keys_per_node.inject(0) do |sum, (node, keys)|
-          sum + node._exists(*keys)
-        end
+      args.flatten!(1)
+      keys_per_node = args.group_by { |key| node_for(key) }
+      keys_per_node.inject(0) do |sum, (node, keys)|
+        sum + node.exists(*keys)
       end
     end
 
     # Determine if any of the keys exists.
     def exists?(*args)
+      args.flatten!(1)
       keys_per_node = args.group_by { |key| node_for(key) }
       keys_per_node.each do |node, keys|
         return true if node.exists?(*keys)
@@ -297,7 +308,7 @@ class Redis
     end
 
     # Set multiple keys to multiple values.
-    def mset(*_args)
+    def mset(*)
       raise CannotDistribute, :mset
     end
 
@@ -306,7 +317,7 @@ class Redis
     end
 
     # Set multiple keys to multiple values, only if none of the keys exist.
-    def msetnx(*_args)
+    def msetnx(*)
       raise CannotDistribute, :msetnx
     end
 
@@ -331,11 +342,13 @@ class Redis
 
     # Get the values of all the given keys as an Array.
     def mget(*keys)
+      keys.flatten!(1)
       mapped_mget(*keys).values_at(*keys)
     end
 
     # Get the values of all the given keys as a Hash.
     def mapped_mget(*keys)
+      keys.flatten!(1)
       keys.group_by { |k| node_for k }.inject({}) do |results, (node, subkeys)|
         results.merge! node.mapped_mget(*subkeys)
       end
@@ -367,20 +380,21 @@ class Redis
     end
 
     # Count the number of set bits in a range of the string value stored at key.
-    def bitcount(key, start = 0, stop = -1)
-      node_for(key).bitcount(key, start, stop)
+    def bitcount(key, start = 0, stop = -1, scale: nil)
+      node_for(key).bitcount(key, start, stop, scale: scale)
     end
 
     # Perform a bitwise operation between strings and store the resulting string in a key.
     def bitop(operation, destkey, *keys)
+      keys.flatten!(1)
       ensure_same_node(:bitop, [destkey] + keys) do |node|
-        node.bitop(operation, destkey, *keys)
+        node.bitop(operation, destkey, keys)
       end
     end
 
     # Return the position of the first bit set to 1 or 0 in a string.
-    def bitpos(key, bit, start = nil, stop = nil)
-      node_for(key).bitpos(key, bit, start, stop)
+    def bitpos(key, bit, start = nil, stop = nil, scale: nil)
+      node_for(key).bitpos(key, bit, start, stop, scale: scale)
     end
 
     # Set the string value of a key and return its old value.
@@ -463,23 +477,15 @@ class Redis
       timeout = if args.last.is_a?(Hash)
         options = args.pop
         options[:timeout]
-      elsif args.last.respond_to?(:to_int)
-        last_arg = args.pop
-        ::Redis.deprecate!(
-          "Passing the timeout as a positional argument is deprecated, it should be passed as a keyword argument:\n" \
-          "  redis.#{cmd}(#{args.map(&:inspect).join(', ')}, timeout: #{last_arg.to_int})" \
-          "(called from: #{caller(2, 1).first})"
-        )
-        last_arg.to_int
       end
 
-      keys = args.flatten
+      args.flatten!(1)
 
-      ensure_same_node(cmd, keys) do |node|
+      ensure_same_node(cmd, args) do |node|
         if timeout
-          node.__send__(cmd, keys, timeout: timeout)
+          node.__send__(cmd, args, timeout: timeout)
         else
-          node.__send__(cmd, keys)
+          node.__send__(cmd, args)
         end
       end
     end
@@ -490,6 +496,18 @@ class Redis
       _bpop(:blpop, args)
     end
 
+    def bzpopmax(*args)
+      _bpop(:bzpopmax, args) do |reply|
+        reply.is_a?(Array) ? [reply[0], reply[1], Floatify.call(reply[2])] : reply
+      end
+    end
+
+    def bzpopmin(*args)
+      _bpop(:bzpopmin, args) do |reply|
+        reply.is_a?(Array) ? [reply[0], reply[1], Floatify.call(reply[2])] : reply
+      end
+    end
+
     # Remove and get the last element in a list, or block until one is
     # available.
     def brpop(*args)
@@ -498,9 +516,9 @@ class Redis
 
     # Pop a value from a list, push it to another list and return it; or block
     # until one is available.
-    def brpoplpush(source, destination, deprecated_timeout = 0, **options)
+    def brpoplpush(source, destination, **options)
       ensure_same_node(:brpoplpush, [source, destination]) do |node|
-        node.brpoplpush(source, destination, deprecated_timeout, **options)
+        node.brpoplpush(source, destination, **options)
       end
     end
 
@@ -534,29 +552,43 @@ class Redis
       node_for(key).ltrim(key, start, stop)
     end
 
+    # Iterate over keys, blocking and removing elements from the first non empty liist found.
+    def blmpop(timeout, *keys, modifier: "LEFT", count: nil)
+      ensure_same_node(:blmpop, keys) do |node|
+        node.blmpop(timeout, *keys, modifier: modifier, count: count)
+      end
+    end
+
+    # Iterate over keys, removing elements from the first non list found.
+    def lmpop(*keys, modifier: "LEFT", count: nil)
+      ensure_same_node(:lmpop, keys) do |node|
+        node.lmpop(*keys, modifier: modifier, count: count)
+      end
+    end
+
     # Get the number of members in a set.
     def scard(key)
       node_for(key).scard(key)
     end
 
     # Add one or more members to a set.
-    def sadd(key, member)
-      node_for(key).sadd(key, member)
+    def sadd(key, *members)
+      node_for(key).sadd(key, *members)
     end
 
     # Add one or more members to a set.
-    def sadd?(key, member)
-      node_for(key).sadd?(key, member)
+    def sadd?(key, *members)
+      node_for(key).sadd?(key, *members)
     end
 
     # Remove one or more members from a set.
-    def srem(key, member)
-      node_for(key).srem(key, member)
+    def srem(key, *members)
+      node_for(key).srem(key, *members)
     end
 
     # Remove one or more members from a set.
-    def srem?(key, member)
-      node_for(key).srem?(key, member)
+    def srem?(key, *members)
+      node_for(key).srem?(key, *members)
     end
 
     # Remove and return a random member from a set.
@@ -603,43 +635,49 @@ class Redis
 
     # Subtract multiple sets.
     def sdiff(*keys)
+      keys.flatten!(1)
       ensure_same_node(:sdiff, keys) do |node|
-        node.sdiff(*keys)
+        node.sdiff(keys)
       end
     end
 
     # Subtract multiple sets and store the resulting set in a key.
     def sdiffstore(destination, *keys)
-      ensure_same_node(:sdiffstore, [destination] + keys) do |node|
-        node.sdiffstore(destination, *keys)
+      keys.flatten!(1)
+      ensure_same_node(:sdiffstore, [destination].concat(keys)) do |node|
+        node.sdiffstore(destination, keys)
       end
     end
 
     # Intersect multiple sets.
     def sinter(*keys)
+      keys.flatten!(1)
       ensure_same_node(:sinter, keys) do |node|
-        node.sinter(*keys)
+        node.sinter(keys)
       end
     end
 
     # Intersect multiple sets and store the resulting set in a key.
     def sinterstore(destination, *keys)
-      ensure_same_node(:sinterstore, [destination] + keys) do |node|
-        node.sinterstore(destination, *keys)
+      keys.flatten!(1)
+      ensure_same_node(:sinterstore, [destination].concat(keys)) do |node|
+        node.sinterstore(destination, keys)
       end
     end
 
     # Add multiple sets.
     def sunion(*keys)
+      keys.flatten!(1)
       ensure_same_node(:sunion, keys) do |node|
-        node.sunion(*keys)
+        node.sunion(keys)
       end
     end
 
     # Add multiple sets and store the resulting set in a key.
     def sunionstore(destination, *keys)
-      ensure_same_node(:sunionstore, [destination] + keys) do |node|
-        node.sunionstore(destination, *keys)
+      keys.flatten!(1)
+      ensure_same_node(:sunionstore, [destination].concat(keys)) do |node|
+        node.sunionstore(destination, keys)
       end
     end
 
@@ -680,6 +718,20 @@ class Redis
       node_for(key).zmscore(key, *members)
     end
 
+    # Iterate over keys, blocking and removing members from the first non empty sorted set found.
+    def bzmpop(timeout, *keys, modifier: "MIN", count: nil)
+      ensure_same_node(:bzmpop, keys) do |node|
+        node.bzmpop(timeout, *keys, modifier: modifier, count: count)
+      end
+    end
+
+    # Iterate over keys, removing members from the first non empty sorted set found.
+    def zmpop(*keys, modifier: "MIN", count: nil)
+      ensure_same_node(:zmpop, keys) do |node|
+        node.zmpop(*keys, modifier: modifier, count: count)
+      end
+    end
+
     # Return a range of members in a sorted set, by index, score or lexicographical ordering.
     def zrange(key, start, stop, **options)
       node_for(key).zrange(key, start, stop, **options)
@@ -738,43 +790,49 @@ class Redis
 
     # Get the intersection of multiple sorted sets
     def zinter(*keys, **options)
+      keys.flatten!(1)
       ensure_same_node(:zinter, keys) do |node|
-        node.zinter(*keys, **options)
+        node.zinter(keys, **options)
       end
     end
 
     # Intersect multiple sorted sets and store the resulting sorted set in a new
     # key.
-    def zinterstore(destination, keys, **options)
-      ensure_same_node(:zinterstore, [destination] + keys) do |node|
+    def zinterstore(destination, *keys, **options)
+      keys.flatten!(1)
+      ensure_same_node(:zinterstore, [destination].concat(keys)) do |node|
         node.zinterstore(destination, keys, **options)
       end
     end
 
     # Return the union of multiple sorted sets.
     def zunion(*keys, **options)
+      keys.flatten!(1)
       ensure_same_node(:zunion, keys) do |node|
-        node.zunion(*keys, **options)
+        node.zunion(keys, **options)
       end
     end
 
     # Add multiple sorted sets and store the resulting sorted set in a new key.
-    def zunionstore(destination, keys, **options)
-      ensure_same_node(:zunionstore, [destination] + keys) do |node|
+    def zunionstore(destination, *keys, **options)
+      keys.flatten!(1)
+      ensure_same_node(:zunionstore, [destination].concat(keys)) do |node|
         node.zunionstore(destination, keys, **options)
       end
     end
 
     # Return the difference between the first and all successive input sorted sets.
     def zdiff(*keys, **options)
+      keys.flatten!(1)
       ensure_same_node(:zdiff, keys) do |node|
-        node.zdiff(*keys, **options)
+        node.zdiff(keys, **options)
       end
     end
 
     # Compute the difference between the first and all successive input sorted sets
     # and store the resulting sorted set in a new key.
-    def zdiffstore(destination, keys, **options)
+    def zdiffstore(destination, *keys, **options)
+      keys.flatten!(1)
       ensure_same_node(:zdiffstore, [destination] + keys) do |node|
         node.zdiffstore(destination, keys, **options)
       end
@@ -801,7 +859,7 @@ class Redis
     end
 
     def mapped_hmset(key, hash)
-      node_for(key).hmset(key, *hash.to_a.flatten)
+      node_for(key).hmset(key, hash)
     end
 
     # Get the value of a hash field.
@@ -811,11 +869,13 @@ class Redis
 
     # Get the values of all the given hash fields.
     def hmget(key, *fields)
-      node_for(key).hmget(key, *fields)
+      fields.flatten!(1)
+      node_for(key).hmget(key, fields)
     end
 
     def mapped_hmget(key, *fields)
-      Hash[*fields.zip(hmget(key, *fields)).flatten]
+      fields.flatten!(1)
+      node_for(key).mapped_hmget(key, fields)
     end
 
     def hrandfield(key, count = nil, **options)
@@ -824,7 +884,8 @@ class Redis
 
     # Delete one or more hash fields.
     def hdel(key, *fields)
-      node_for(key).hdel(key, *fields)
+      fields.flatten!(1)
+      node_for(key).hdel(key, fields)
     end
 
     # Determine if a hash field exists.
@@ -881,7 +942,7 @@ class Redis
 
     # Stop listening for messages posted to the given channels.
     def unsubscribe(*channels)
-      raise "Can't unsubscribe if not subscribed." unless subscribed?
+      raise SubscriptionError, "Can't unsubscribe if not subscribed." unless subscribed?
 
       @subscribed_node.unsubscribe(*channels)
     end
@@ -928,9 +989,7 @@ class Redis
     def multi(&block)
       raise CannotDistribute, :multi unless @watch_key
 
-      result = node_for(@watch_key).multi(&block)
-      @watch_key = nil if block_given?
-      result
+      node_for(@watch_key).multi(&block)
     end
 
     # Execute all commands issued after MULTI.
@@ -1020,7 +1079,8 @@ class Redis
     end
 
     def key_tag(key)
-      key.to_s[@tag, 1] if @tag
+      key = key.to_s
+      key[@tag, 1] if key.match?(@tag)
     end
 
     def ensure_same_node(command, keys)
diff --git a/lib/redis/errors.rb b/lib/redis/errors.rb
index 74a77271309d0cbe1498c1a66e462370b8a0df99..bef5a8d4516fbdf836b6a18e739230c456fbc920 100644
--- a/lib/redis/errors.rb
+++ b/lib/redis/errors.rb
@@ -2,7 +2,7 @@
 
 class Redis
   # Base error for all redis-rb errors.
-  class BaseError < RuntimeError
+  class BaseError < StandardError
   end
 
   # Raised by the connection when a protocol error occurs.
@@ -20,6 +20,15 @@ class Redis
   class CommandError < BaseError
   end
 
+  class PermissionError < CommandError
+  end
+
+  class WrongTypeError < CommandError
+  end
+
+  class OutOfMemoryError < CommandError
+  end
+
   # Base error for connection related errors.
   class BaseConnectionError < BaseError
   end
@@ -40,58 +49,14 @@ class Redis
   class InheritedError < BaseConnectionError
   end
 
+  # Generally raised during Redis failover scenarios
+  class ReadOnlyError < BaseConnectionError
+  end
+
   # Raised when client options are invalid.
   class InvalidClientOptionError < BaseError
   end
 
-  class Cluster
-    # Raised when client connected to redis as cluster mode
-    # and failed to fetch cluster state information by commands.
-    class InitialSetupError < BaseError
-      # @param errors [Array<Redis::BaseError>]
-      def initialize(errors)
-        super("Redis client could not fetch cluster information: #{errors.map(&:message).uniq.join(',')}")
-      end
-    end
-
-    # Raised when client connected to redis as cluster mode
-    # and some cluster subcommands were called.
-    class OrchestrationCommandNotSupported < BaseError
-      def initialize(command, subcommand = '')
-        str = [command, subcommand].map(&:to_s).reject(&:empty?).join(' ').upcase
-        msg = "#{str} command should be used with care "\
-              'only by applications orchestrating Redis Cluster, like redis-trib, '\
-              'and the command if used out of the right context can leave the cluster '\
-              'in a wrong state or cause data loss.'
-        super(msg)
-      end
-    end
-
-    # Raised when error occurs on any node of cluster.
-    class CommandErrorCollection < BaseError
-      attr_reader :errors
-
-      # @param errors [Hash{String => Redis::CommandError}]
-      # @param error_message [String]
-      def initialize(errors, error_message = 'Command errors were replied on any node')
-        @errors = errors
-        super(error_message)
-      end
-    end
-
-    # Raised when cluster client can't select node.
-    class AmbiguousNodeError < BaseError
-      def initialize(command)
-        super("Cluster client doesn't know which node the #{command} command should be sent to.")
-      end
-    end
-
-    # Raised when commands in pipelining include cross slot keys.
-    class CrossSlotPipeliningError < BaseError
-      def initialize(keys)
-        super("Cluster client couldn't send pipelining to single node. "\
-              "The commands include cross slot keys. #{keys}")
-      end
-    end
+  class SubscriptionError < BaseError
   end
 end
diff --git a/lib/redis/hash_ring.rb b/lib/redis/hash_ring.rb
index d84084cfdff169080ad217a3f307effbc3148556..15f38d55647fb451bc91a9b450c06f1ab9f73e02 100644
--- a/lib/redis/hash_ring.rb
+++ b/lib/redis/hash_ring.rb
@@ -1,6 +1,7 @@
 # frozen_string_literal: true
 
 require 'zlib'
+require 'digest/md5'
 
 class Redis
   class HashRing
@@ -25,7 +26,7 @@ class Redis
     def add_node(node)
       @nodes << node
       @replicas.times do |i|
-        key = Zlib.crc32("#{node.id}:#{i}")
+        key = server_hash_for("#{node.id}:#{i}")
         @ring[key] = node
         @sorted_keys << key
       end
@@ -35,7 +36,7 @@ class Redis
     def remove_node(node)
       @nodes.reject! { |n| n.id == node.id }
       @replicas.times do |i|
-        key = Zlib.crc32("#{node.id}:#{i}")
+        key = server_hash_for("#{node.id}:#{i}")
         @ring.delete(key)
         @sorted_keys.reject! { |k| k == key }
       end
@@ -43,47 +44,46 @@ class Redis
 
     # get the node in the hash ring for this key
     def get_node(key)
-      get_node_pos(key)[0]
-    end
-
-    def get_node_pos(key)
-      return [nil, nil] if @ring.empty?
-
-      crc = Zlib.crc32(key)
-      idx = HashRing.binary_search(@sorted_keys, crc)
-      [@ring[@sorted_keys[idx]], idx]
+      hash = hash_for(key)
+      idx = binary_search(@sorted_keys, hash)
+      @ring[@sorted_keys[idx]]
     end
 
     def iter_nodes(key)
       return [nil, nil] if @ring.empty?
 
-      _, pos = get_node_pos(key)
+      crc = hash_for(key)
+      pos = binary_search(@sorted_keys, crc)
       @ring.size.times do |n|
         yield @ring[@sorted_keys[(pos + n) % @ring.size]]
       end
     end
 
+    private
+
+    def hash_for(key)
+      Zlib.crc32(key)
+    end
+
+    def server_hash_for(key)
+      Digest::MD5.digest(key).unpack1("L>")
+    end
+
     # Find the closest index in HashRing with value <= the given value
-    def self.binary_search(ary, value)
-      upper = ary.size - 1
+    def binary_search(ary, value)
+      upper = ary.size
       lower = 0
-      idx = 0
-
-      while lower <= upper
-        idx = (lower + upper) / 2
-        comp = ary[idx] <=> value
 
-        if comp == 0
-          return idx
-        elsif comp > 0
-          upper = idx - 1
+      while lower < upper
+        mid = (lower + upper) / 2
+        if ary[mid] > value
+          upper = mid
         else
-          lower = idx + 1
+          lower = mid + 1
         end
       end
 
-      upper = ary.size - 1 if upper < 0
-      upper
+      upper - 1
     end
   end
 end
diff --git a/lib/redis/pipeline.rb b/lib/redis/pipeline.rb
index 11ed00095c2156cd7e8b9028a1f680f097ef1db8..0e415aa984381be934619588523be1fd8d40a827 100644
--- a/lib/redis/pipeline.rb
+++ b/lib/redis/pipeline.rb
@@ -4,27 +4,31 @@ require "delegate"
 
 class Redis
   class PipelinedConnection
-    def initialize(pipeline)
+    attr_accessor :db
+
+    def initialize(pipeline, futures = [], exception: true)
       @pipeline = pipeline
+      @futures = futures
+      @exception = exception
     end
 
     include Commands
 
-    def db
-      @pipeline.db
-    end
-
-    def db=(db)
-      @pipeline.db = db
-    end
-
     def pipelined
       yield self
     end
 
-    def call_pipeline(pipeline)
-      @pipeline.call_pipeline(pipeline)
-      nil
+    def multi
+      transaction = MultiConnection.new(@pipeline, @futures)
+      send_command([:multi])
+      size = @futures.size
+      yield transaction
+      multi_future = MultiFuture.new(@futures[size..-1])
+      @pipeline.call_v([:exec]) do |result|
+        multi_future._set(result)
+      end
+      @futures << multi_future
+      multi_future
     end
 
     private
@@ -34,204 +38,36 @@ class Redis
     end
 
     def send_command(command, &block)
-      @pipeline.call(command, &block)
-    end
-
-    def send_blocking_command(command, timeout, &block)
-      @pipeline.call_with_timeout(command, timeout, &block)
-    end
-  end
-
-  class Pipeline
-    REDIS_INTERNAL_PATH = File.expand_path("..", __dir__).freeze
-    # Redis use MonitorMixin#synchronize and this class use DelegateClass which we want to filter out.
-    # Both are in the stdlib so we can simply filter the entire stdlib out.
-    STDLIB_PATH = File.expand_path("..", MonitorMixin.instance_method(:synchronize).source_location.first).freeze
-
-    class << self
-      def deprecation_warning(method, caller_locations) # :nodoc:
-        callsite = caller_locations.find { |l| !l.path.start_with?(REDIS_INTERNAL_PATH, STDLIB_PATH) }
-        callsite ||= caller_locations.last # The caller_locations should be large enough, but just in case.
-        ::Redis.deprecate! <<~MESSAGE
-          Pipelining commands on a Redis instance is deprecated and will be removed in Redis 5.0.0.
-
-          redis.#{method} do
-            redis.get("key")
-          end
-
-          should be replaced by
-
-          redis.#{method} do |pipeline|
-            pipeline.get("key")
-          end
-
-          (called from #{callsite}}
-        MESSAGE
+      future = Future.new(command, block, @exception)
+      @pipeline.call_v(command) do |result|
+        future._set(result)
       end
-    end
-
-    attr_accessor :db
-    attr_reader :client
-
-    attr :futures
-    alias materialized_futures futures
-
-    def initialize(client)
-      @client = client.is_a?(Pipeline) ? client.client : client
-      @with_reconnect = true
-      @shutdown = false
-      @futures = []
-    end
-
-    def timeout
-      client.timeout
-    end
-
-    def with_reconnect?
-      @with_reconnect
-    end
-
-    def without_reconnect?
-      !@with_reconnect
-    end
-
-    def shutdown?
-      @shutdown
-    end
-
-    def empty?
-      @futures.empty?
-    end
-
-    def call(command, timeout: nil, &block)
-      # A pipeline that contains a shutdown should not raise ECONNRESET when
-      # the connection is gone.
-      @shutdown = true if command.first == :shutdown
-      future = Future.new(command, block, timeout)
       @futures << future
       future
     end
 
-    def call_with_timeout(command, timeout, &block)
-      call(command, timeout: timeout, &block)
-    end
-
-    def call_pipeline(pipeline)
-      @shutdown = true if pipeline.shutdown?
-      @futures.concat(pipeline.materialized_futures)
-      @db = pipeline.db
-      nil
-    end
-
-    def commands
-      @futures.map(&:_command)
-    end
-
-    def timeouts
-      @futures.map(&:timeout)
-    end
-
-    def with_reconnect(val = true)
-      @with_reconnect = false unless val
-      yield
-    end
-
-    def without_reconnect(&blk)
-      with_reconnect(false, &blk)
-    end
-
-    def finish(replies, &blk)
-      if blk
-        futures.each_with_index.map do |future, i|
-          future._set(blk.call(replies[i]))
-        end
-      else
-        futures.each_with_index.map do |future, i|
-          future._set(replies[i])
-        end
-      end
-    end
-
-    class Multi < self
-      def finish(replies)
-        exec = replies.last
-
-        return if exec.nil? # The transaction failed because of WATCH.
-
-        # EXEC command failed.
-        raise exec if exec.is_a?(CommandError)
-
-        if exec.size < futures.size
-          # Some command wasn't recognized by Redis.
-          command_error = replies.detect { |r| r.is_a?(CommandError) }
-          raise command_error
-        end
-
-        super(exec) do |reply|
-          # Because an EXEC returns nested replies, hiredis won't be able to
-          # convert an error reply to a CommandError instance itself. This is
-          # specific to MULTI/EXEC, so we solve this here.
-          reply.is_a?(::RuntimeError) ? CommandError.new(reply.message) : reply
-        end
-      end
-
-      def materialized_futures
-        if empty?
-          []
-        else
-          [
-            Future.new([:multi], nil, 0),
-            *futures,
-            MultiFuture.new(futures)
-          ]
-        end
-      end
-
-      def timeouts
-        if empty?
-          []
-        else
-          [nil, *super, nil]
-        end
-      end
-
-      def commands
-        if empty?
-          []
-        else
-          [[:multi]] + super + [[:exec]]
-        end
+    def send_blocking_command(command, timeout, &block)
+      future = Future.new(command, block, @exception)
+      @pipeline.blocking_call_v(timeout, command) do |result|
+        future._set(result)
       end
+      @futures << future
+      future
     end
   end
 
-  class DeprecatedPipeline < DelegateClass(Pipeline)
-    def initialize(pipeline)
-      super(pipeline)
-      @deprecation_displayed = false
+  class MultiConnection < PipelinedConnection
+    def multi
+      raise Redis::Error, "Can't nest multi transaction"
     end
 
-    def __getobj__
-      unless @deprecation_displayed
-        Pipeline.deprecation_warning("pipelined", Kernel.caller_locations(1, 10))
-        @deprecation_displayed = true
-      end
-      @delegate_dc_obj
-    end
-  end
+    private
 
-  class DeprecatedMulti < DelegateClass(Pipeline::Multi)
-    def initialize(pipeline)
-      super(pipeline)
-      @deprecation_displayed = false
-    end
-
-    def __getobj__
-      unless @deprecation_displayed
-        Pipeline.deprecation_warning("multi", Kernel.caller_locations(1, 10))
-        @deprecation_displayed = true
-      end
-      @delegate_dc_obj
+    # Blocking commands inside transaction behave like non-blocking.
+    # It shouldn't be done though.
+    # https://redis.io/commands/blpop/#blpop-inside-a-multi--exec-transaction
+    def send_blocking_command(command, _timeout, &block)
+      send_command(command, &block)
     end
   end
 
@@ -244,23 +80,11 @@ class Redis
   class Future < BasicObject
     FutureNotReady = ::Redis::FutureNotReady.new
 
-    attr_reader :timeout
-
-    def initialize(command, transformation, timeout)
+    def initialize(command, coerce, exception)
       @command = command
-      @transformation = transformation
-      @timeout = timeout
       @object = FutureNotReady
-    end
-
-    def ==(_other)
-      message = +"The methods == and != are deprecated for Redis::Future and will be removed in 5.0.0"
-      message << " - You probably meant to call .value == or .value !="
-      message << " (#{::Kernel.caller(1, 1).first})\n"
-
-      ::Redis.deprecate!(message)
-
-      super
+      @coerce = coerce
+      @exception = exception
     end
 
     def inspect
@@ -268,16 +92,12 @@ class Redis
     end
 
     def _set(object)
-      @object = @transformation ? @transformation.call(object) : object
+      @object = @coerce ? @coerce.call(object) : object
       value
     end
 
-    def _command
-      @command
-    end
-
     def value
-      ::Kernel.raise(@object) if @object.is_a?(::RuntimeError)
+      ::Kernel.raise(@object) if @exception && @object.is_a?(::StandardError)
       @object
     end
 
@@ -294,13 +114,16 @@ class Redis
     def initialize(futures)
       @futures = futures
       @command = [:exec]
+      @object = FutureNotReady
     end
 
     def _set(replies)
-      @futures.each_with_index do |future, index|
-        future._set(replies[index])
+      if replies
+        @futures.each_with_index do |future, index|
+          future._set(replies[index])
+        end
       end
-      replies
+      @object = replies
     end
   end
 end
diff --git a/lib/redis/subscribe.rb b/lib/redis/subscribe.rb
index 5573b98f1c5d87365bbab0e8b3bf59b7131867fe..f95e29dfb255529a062f7e035e3fd7dacdead15e 100644
--- a/lib/redis/subscribe.rb
+++ b/lib/redis/subscribe.rb
@@ -4,10 +4,13 @@ class Redis
   class SubscribedClient
     def initialize(client)
       @client = client
+      @write_monitor = Monitor.new
     end
 
-    def call(command)
-      @client.process([command])
+    def call_v(command)
+      @write_monitor.synchronize do
+        @client.call_v(command)
+      end
     end
 
     def subscribe(*channels, &block)
@@ -26,12 +29,28 @@ class Redis
       subscription("psubscribe", "punsubscribe", channels, block, timeout)
     end
 
+    def ssubscribe(*channels, &block)
+      subscription("ssubscribe", "sunsubscribe", channels, block)
+    end
+
+    def ssubscribe_with_timeout(timeout, *channels, &block)
+      subscription("ssubscribe", "sunsubscribe", channels, block, timeout)
+    end
+
     def unsubscribe(*channels)
-      call([:unsubscribe, *channels])
+      call_v([:unsubscribe, *channels])
     end
 
     def punsubscribe(*channels)
-      call([:punsubscribe, *channels])
+      call_v([:punsubscribe, *channels])
+    end
+
+    def sunsubscribe(*channels)
+      call_v([:sunsubscribe, *channels])
+    end
+
+    def close
+      @client.close
     end
 
     protected
@@ -39,13 +58,21 @@ class Redis
     def subscription(start, stop, channels, block, timeout = 0)
       sub = Subscription.new(&block)
 
-      unsubscribed = false
+      case start
+      when "ssubscribe" then channels.each { |c| call_v([start, c]) } # avoid cross-slot keys
+      else call_v([start, *channels])
+      end
 
-      @client.call_loop([start, *channels], timeout) do |line|
-        type, *rest = line
-        sub.callbacks[type].call(*rest)
-        unsubscribed = type == stop && rest.last == 0
-        break if unsubscribed
+      while event = @client.next_event(timeout)
+        if event.is_a?(::RedisClient::CommandError)
+          raise Client::ERROR_MAPPING.fetch(event.class), event.message
+        end
+
+        type, *rest = event
+        if callback = sub.callbacks[type]
+          callback.call(*rest)
+        end
+        break if type == stop && rest.last == 0
       end
       # No need to unsubscribe here. The real client closes the connection
       # whenever an exception is raised (see #ensure_connected).
@@ -56,10 +83,7 @@ class Redis
     attr :callbacks
 
     def initialize
-      @callbacks = Hash.new do |hash, key|
-        hash[key] = ->(*_) {}
-      end
-
+      @callbacks = {}
       yield(self)
     end
 
@@ -86,5 +110,17 @@ class Redis
     def pmessage(&block)
       @callbacks["pmessage"] = block
     end
+
+    def ssubscribe(&block)
+      @callbacks["ssubscribe"] = block
+    end
+
+    def sunsubscribe(&block)
+      @callbacks["sunsubscribe"] = block
+    end
+
+    def smessage(&block)
+      @callbacks["smessage"] = block
+    end
   end
 end
diff --git a/lib/redis/version.rb b/lib/redis/version.rb
index b0cbed457354281a6f08d1955e04d7e31cd181bc..4d34fe45db19935e115ecb03a3a4bc580e959818 100644
--- a/lib/redis/version.rb
+++ b/lib/redis/version.rb
@@ -1,5 +1,5 @@
 # frozen_string_literal: true
 
 class Redis
-  VERSION = '4.8.0'
+  VERSION = '5.2.0'
 end
diff --git a/makefile b/makefile
index df25e4b215cad1d5939ae723cd30126a0fdffa29..9d9079f75db2376bfed6f26da7d6fdd6650f5486 100644
--- a/makefile
+++ b/makefile
@@ -1,4 +1,4 @@
-REDIS_BRANCH       ?= 7.0
+REDIS_BRANCH       ?= 7.2
 ROOT_DIR           :=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
 TMP                := tmp
 CONF               := ${ROOT_DIR}/test/support/conf/redis-${REDIS_BRANCH}.conf
@@ -16,7 +16,7 @@ SLAVE_SOCKET_PATH  := ${BUILD_DIR}/redis_slave.sock
 HA_GROUP_NAME      := master1
 SENTINEL_PORTS     := 6400 6401 6402
 SENTINEL_PID_PATHS := $(addprefix ${TMP}/redis,$(addsuffix .pid,${SENTINEL_PORTS}))
-CLUSTER_PORTS      := 7000 7001 7002 7003 7004 7005
+CLUSTER_PORTS      := 16380 16381 16382 16383 16384 16385
 CLUSTER_PID_PATHS  := $(addprefix ${TMP}/redis,$(addsuffix .pid,${CLUSTER_PORTS}))
 CLUSTER_CONF_PATHS := $(addprefix ${TMP}/nodes,$(addsuffix .conf,${CLUSTER_PORTS}))
 CLUSTER_ADDRS      := $(addprefix 127.0.0.1:,${CLUSTER_PORTS})
@@ -54,7 +54,7 @@ start: ${BINARY}
 stop_slave:
 	@$(call kill-redis,${SLAVE_PID_PATH})
 
-start_slave: ${BINARY}
+start_slave: start
 	@${BINARY}\
 		--daemonize  yes\
 		--pidfile    ${SLAVE_PID_PATH}\
@@ -62,11 +62,11 @@ start_slave: ${BINARY}
 		--unixsocket ${SLAVE_SOCKET_PATH}\
 		--slaveof    127.0.0.1 ${PORT}
 
-stop_sentinel:
+stop_sentinel: stop_slave stop
 	@$(call kill-redis,${SENTINEL_PID_PATHS})
 	@rm -f ${TMP}/sentinel*.conf || true
 
-start_sentinel: ${BINARY}
+start_sentinel: start start_slave
 	@for port in ${SENTINEL_PORTS}; do\
 		conf=${TMP}/sentinel$$port.conf;\
 		touch $$conf;\
@@ -109,7 +109,7 @@ start_cluster: ${BINARY}
 	@for port in ${CLUSTER_PORTS}; do\
 		${BINARY}\
 			--daemonize            yes\
-			--appendonly           yes\
+			--appendonly           no\
 			--cluster-enabled      yes\
 			--cluster-config-file  ${TMP}/nodes$$port.conf\
 			--cluster-node-timeout 5000\
diff --git a/redis.gemspec b/redis.gemspec
index afdcaab8b053b302ee63272b3efb11f1e892f210..90711d76c60d5412d2b1ffb8c26e50cf40660ca4 100644
--- a/redis.gemspec
+++ b/redis.gemspec
@@ -43,9 +43,7 @@ Gem::Specification.new do |s|
   s.files         = Dir["CHANGELOG.md", "LICENSE", "README.md", "lib/**/*"]
   s.executables   = `git ls-files -- exe/*`.split("\n").map { |f| File.basename(f) }
 
-  s.required_ruby_version = '>= 2.4.0'
+  s.required_ruby_version = '>= 2.6.0'
 
-  s.add_development_dependency("em-synchrony")
-  s.add_development_dependency("hiredis")
-  s.add_development_dependency("mocha")
+  s.add_runtime_dependency('redis-client', '>= 0.22.0')
 end
diff --git a/test/cluster/abnormal_state_test.rb b/test/cluster/abnormal_state_test.rb
deleted file mode 100644
index 081ecc99b60ac54389e3bd8cf031fa3880b89157..0000000000000000000000000000000000000000
--- a/test/cluster/abnormal_state_test.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-# frozen_string_literal: true
-
-require "helper"
-
-# ruby -w -Itest test/cluster_abnormal_state_test.rb
-class TestClusterAbnormalState < Minitest::Test
-  include Helper::Cluster
-
-  def test_the_state_of_cluster_down
-    redis_cluster_down do
-      assert_raises(Redis::CommandError, 'CLUSTERDOWN Hash slot not served') do
-        redis.set('key1', 1)
-      end
-
-      assert_equal 'fail', redis.cluster(:info).fetch('cluster_state')
-    end
-  end
-
-  def test_the_state_of_cluster_failover
-    redis_cluster_failover do
-      10.times do |i|
-        assert_equal 'OK', r.set("key#{i}", i)
-      end
-
-      10.times do |i|
-        assert_equal i.to_s, r.get("key#{i}")
-      end
-
-      assert_equal 'ok', redis.cluster(:info).fetch('cluster_state')
-    end
-  end
-
-  def test_the_state_of_cluster_node_failure
-    redis_cluster_fail_master do
-      assert_raises(Redis::CannotConnectError, 'Error connecting to Redis on 127.0.0.1:7002') do
-        r.set('key0', 0)
-      end
-
-      10.times do |i|
-        assert_equal 'OK', r.set("key#{i}", i)
-      end
-
-      10.times do |i|
-        assert_equal i.to_s, r.get("key#{i}")
-      end
-
-      assert_equal 'ok', redis.cluster(:info).fetch('cluster_state')
-    end
-  end
-
-  def test_raising_error_when_nodes_are_not_cluster_mode
-    assert_raises(Redis::Cluster::InitialSetupError) do
-      build_another_client(cluster: %W[redis://127.0.0.1:#{PORT}])
-    end
-  end
-end
diff --git a/test/cluster/client_key_hash_tags_test.rb b/test/cluster/client_key_hash_tags_test.rb
deleted file mode 100644
index b436739eaeb852424ef79b9d331ff9c4d844cd2a..0000000000000000000000000000000000000000
--- a/test/cluster/client_key_hash_tags_test.rb
+++ /dev/null
@@ -1,107 +0,0 @@
-# frozen_string_literal: true
-
-require "helper"
-
-# ruby -w -Itest test/cluster_client_key_hash_tags_test.rb
-class TestClusterClientKeyHashTags < Minitest::Test
-  include Helper::Cluster
-
-  def build_described_class(urls = ['redis://127.0.0.1:7000'])
-    option = Redis::Cluster::Option.new(cluster: urls)
-    node = Redis::Cluster::Node.new(option.per_node_key)
-    details = Redis::Cluster::CommandLoader.load(node)
-    Redis::Cluster::Command.new(details)
-  end
-
-  def test_key_extraction
-    described_class = build_described_class
-
-    assert_equal 'dogs:1', described_class.extract_first_key(%w[get dogs:1])
-    assert_equal 'user1000', described_class.extract_first_key(%w[get {user1000}.following])
-    assert_equal 'user1000', described_class.extract_first_key(%w[get {user1000}.followers])
-    assert_equal 'foo{}{bar}', described_class.extract_first_key(%w[get foo{}{bar}])
-    assert_equal '{bar', described_class.extract_first_key(%w[get foo{{bar}}zap])
-    assert_equal 'bar', described_class.extract_first_key(%w[get foo{bar}{zap}])
-    assert_equal 'dogs', described_class.extract_first_key([:sscan, 'dogs', 0])
-    assert_equal 'dogs', described_class.extract_first_key([:sscan, 'dogs', 0, 'MATCH', '/poodle/', 'COUNT', 10])
-    assert_equal 'dogs', described_class.extract_first_key([:hscan, 'dogs', 0])
-    assert_equal 'dogs', described_class.extract_first_key([:hscan, 'dogs', 0, 'MATCH', '/poodle/', 'COUNT', 10])
-    assert_equal 'dogs', described_class.extract_first_key([:zscan, 'dogs', 0])
-    assert_equal 'dogs', described_class.extract_first_key([:zscan, 'dogs', 0, 'MATCH', '/poodle/', 'COUNT', 10])
-
-    assert_equal '', described_class.extract_first_key([:get, ''])
-    assert_equal '', described_class.extract_first_key([:get, nil])
-    assert_equal '', described_class.extract_first_key([:get])
-
-    assert_equal '', described_class.extract_first_key([:set, '', 1])
-    assert_equal '', described_class.extract_first_key([:set, nil, 1])
-    assert_equal '', described_class.extract_first_key([:set])
-
-    # Keyless commands
-    assert_equal '', described_class.extract_first_key([:scan, 0])
-    assert_equal '', described_class.extract_first_key([:scan, 0, 'MATCH', '/poodle/', 'COUNT', 10])
-    assert_equal '', described_class.extract_first_key([:auth, 'password'])
-    assert_equal '', described_class.extract_first_key(%i[client kill])
-    assert_equal '', described_class.extract_first_key(%i[cluster addslots])
-    assert_equal '', described_class.extract_first_key(%i[command])
-    assert_equal '', described_class.extract_first_key(%i[command count])
-    assert_equal '', described_class.extract_first_key(%i[config get])
-    assert_equal '', described_class.extract_first_key(%i[debug segfault])
-    assert_equal '', described_class.extract_first_key([:echo, 'Hello World'])
-    assert_equal '', described_class.extract_first_key([:flushall, 'ASYNC'])
-    assert_equal '', described_class.extract_first_key([:flushdb, 'ASYNC'])
-    assert_equal '', described_class.extract_first_key([:info, 'cluster'])
-    assert_equal '', described_class.extract_first_key(%i[memory doctor])
-    assert_equal '', described_class.extract_first_key([:ping, 'Hi'])
-    assert_equal '', described_class.extract_first_key([:psubscribe, 'channel'])
-    assert_equal '', described_class.extract_first_key([:pubsub, 'channels', '*'])
-    assert_equal '', described_class.extract_first_key([:publish, 'channel', 'Hi'])
-    assert_equal '', described_class.extract_first_key([:punsubscribe, 'channel'])
-    assert_equal '', described_class.extract_first_key([:subscribe, 'channel'])
-    assert_equal '', described_class.extract_first_key([:unsubscribe, 'channel'])
-    assert_equal '', described_class.extract_first_key(%w[script exists sha1 sha1])
-    assert_equal '', described_class.extract_first_key([:select, 1])
-    assert_equal '', described_class.extract_first_key([:shutdown, 'SAVE'])
-    assert_equal '', described_class.extract_first_key([:slaveof, '127.0.0.1', 6379])
-    assert_equal '', described_class.extract_first_key([:slowlog, 'get', 2])
-    assert_equal '', described_class.extract_first_key([:swapdb, 0, 1])
-    assert_equal '', described_class.extract_first_key([:wait, 1, 0])
-
-    # 2nd argument is not a key
-    assert_equal 'key1', described_class.extract_first_key([:eval, 'script', 2, 'key1', 'key2', 'first', 'second'])
-    assert_equal '', described_class.extract_first_key([:eval, 'return 0', 0])
-    assert_equal 'key1', described_class.extract_first_key([:evalsha, 'sha1', 2, 'key1', 'key2', 'first', 'second'])
-    assert_equal '', described_class.extract_first_key([:evalsha, 'return 0', 0])
-    assert_equal 'key1', described_class.extract_first_key([:migrate, '127.0.0.1', 6379, 'key1', 0, 5000])
-    assert_equal 'key1', described_class.extract_first_key([:memory, :usage, 'key1'])
-    assert_equal 'key1', described_class.extract_first_key([:object, 'refcount', 'key1'])
-    assert_equal 'mystream', described_class.extract_first_key([:xread, 'COUNT', 2, 'STREAMS', 'mystream', 0])
-    assert_equal 'mystream', described_class.extract_first_key([:xreadgroup, 'GROUP', 'mygroup', 'Bob', 'COUNT', 2, 'STREAMS', 'mystream', '>'])
-  end
-
-  def test_whether_the_command_effect_is_readonly_or_not
-    described_class = build_described_class
-
-    assert_equal true,  described_class.should_send_to_master?([:set])
-    assert_equal false, described_class.should_send_to_slave?([:set])
-
-    assert_equal false, described_class.should_send_to_master?([:get])
-    assert_equal true,  described_class.should_send_to_slave?([:get])
-
-    target_version('3.2.0') do
-      assert_equal false, described_class.should_send_to_master?([:info])
-      assert_equal false, described_class.should_send_to_slave?([:info])
-    end
-  end
-
-  def test_cannot_build_details_from_bad_urls
-    assert_raises(Redis::Cluster::InitialSetupError) do
-      build_described_class(['redis://127.0.0.1:7006'])
-    end
-  end
-
-  def test_builds_details_from_a_mix_of_good_and_bad_urls
-    described_class = build_described_class(['redis://127.0.0.1:7006', 'redis://127.0.0.1:7000'])
-    assert_equal 'dogs:1', described_class.extract_first_key(%w[get dogs:1])
-  end
-end
diff --git a/test/cluster/client_options_test.rb b/test/cluster/client_options_test.rb
deleted file mode 100644
index 01ba7311fe2515be60eb8dd279c64eae6c019215..0000000000000000000000000000000000000000
--- a/test/cluster/client_options_test.rb
+++ /dev/null
@@ -1,181 +0,0 @@
-# frozen_string_literal: true
-
-require 'uri'
-require 'helper'
-
-# ruby -w -Itest test/cluster_client_options_test.rb
-class TestClusterClientOptions < Minitest::Test
-  include Helper::Cluster
-
-  def test_option_class
-    option = Redis::Cluster::Option.new(cluster: %w[redis://127.0.0.1:7000], replica: true)
-    assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', host: '127.0.0.1', port: 7000 } }, option.per_node_key)
-    assert_equal true, option.use_replica?
-
-    option = Redis::Cluster::Option.new(cluster: %w[redis://127.0.0.1:7000], replica: false)
-    assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', host: '127.0.0.1', port: 7000 } }, option.per_node_key)
-    assert_equal false, option.use_replica?
-
-    option = Redis::Cluster::Option.new(cluster: %w[redis://127.0.0.1:7000])
-    assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', host: '127.0.0.1', port: 7000 } }, option.per_node_key)
-    assert_equal false, option.use_replica?
-
-    option = Redis::Cluster::Option.new(cluster: %w[rediss://johndoe:foobar@127.0.0.1:7000/1/namespace])
-    assert_equal({ '127.0.0.1:7000' => { scheme: 'rediss', username: 'johndoe', password: 'foobar', host: '127.0.0.1', port: 7000, db: 1 } }, option.per_node_key)
-
-    option = Redis::Cluster::Option.new(cluster: %w[rediss://127.0.0.1:7000], scheme: 'redis')
-    assert_equal({ '127.0.0.1:7000' => { scheme: 'rediss', host: '127.0.0.1', port: 7000 } }, option.per_node_key)
-
-    option = Redis::Cluster::Option.new(cluster: %w[redis://bazzap:@127.0.0.1:7000], username: 'foobar')
-    assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', username: 'bazzap', host: '127.0.0.1', port: 7000 } }, option.per_node_key)
-
-    option = Redis::Cluster::Option.new(cluster: %w[redis://:bazzap@127.0.0.1:7000], password: 'foobar')
-    assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', password: 'bazzap', host: '127.0.0.1', port: 7000 } }, option.per_node_key)
-
-    option = Redis::Cluster::Option.new(cluster: %W[redis://#{URI.encode_www_form_component('!&<123-abc>')}:@127.0.0.1:7000])
-    assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', username: '!&<123-abc>', host: '127.0.0.1', port: 7000 } }, option.per_node_key)
-
-    option = Redis::Cluster::Option.new(cluster: %W[redis://:#{URI.encode_www_form_component('!&<123-abc>')}@127.0.0.1:7000])
-    assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', password: '!&<123-abc>', host: '127.0.0.1', port: 7000 } }, option.per_node_key)
-
-    option = Redis::Cluster::Option.new(cluster: %w[redis://127.0.0.1:7000/0], db: 1)
-    assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', host: '127.0.0.1', port: 7000, db: 0 } }, option.per_node_key)
-
-    option = Redis::Cluster::Option.new(cluster: [{ host: '127.0.0.1', port: 7000 }])
-    assert_equal({ '127.0.0.1:7000' => { host: '127.0.0.1', port: 7000 } }, option.per_node_key)
-
-    option = Redis::Cluster::Option.new(cluster: %w[redis://127.0.0.1:7000], fixed_hostname: 'foo-endpoint.example.com')
-    assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', host: 'foo-endpoint.example.com', port: 7000 } }, option.per_node_key)
-
-    option = Redis::Cluster::Option.new(cluster: %w[redis://127.0.0.1:7000], fixed_hostname: '')
-    assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', host: '127.0.0.1', port: 7000 } }, option.per_node_key)
-
-    assert_raises(Redis::InvalidClientOptionError) do
-      Redis::Cluster::Option.new(cluster: nil)
-    end
-
-    assert_raises(Redis::InvalidClientOptionError) do
-      Redis::Cluster::Option.new(cluster: %w[invalid_uri])
-    end
-
-    assert_raises(Redis::InvalidClientOptionError) do
-      Redis::Cluster::Option.new(cluster: [{ host: '127.0.0.1' }])
-    end
-  end
-
-  def test_client_accepts_valid_node_configs
-    nodes = ['redis://127.0.0.1:7000',
-             'redis://127.0.0.1:7001',
-             { host: '127.0.0.1', port: '7002' },
-             { 'host' => '127.0.0.1', port: 7003 },
-             'redis://127.0.0.1:7004',
-             'redis://127.0.0.1:7005']
-
-    build_another_client(cluster: nodes)
-  end
-
-  def test_client_accepts_valid_options
-    build_another_client(timeout: TIMEOUT)
-  end
-
-  def test_client_ignores_invalid_options
-    build_another_client(invalid_option: true)
-  end
-
-  def test_client_works_even_if_so_many_unavailable_nodes_specified
-    min = 7000
-    max = min + Process.getrlimit(Process::RLIMIT_NOFILE).first / 3 * 2
-    nodes = (min..max).map { |port| "redis://127.0.0.1:#{port}" }
-    redis = build_another_client(cluster: nodes)
-
-    assert_equal 'PONG', redis.ping
-  end
-
-  def test_client_does_not_accept_db_specified_url
-    assert_raises(Redis::Cluster::InitialSetupError) do
-      build_another_client(cluster: ['redis://127.0.0.1:7000/1/namespace'])
-    end
-
-    assert_raises(Redis::Cluster::InitialSetupError) do
-      build_another_client(cluster: [{ host: '127.0.0.1', port: '7000' }], db: 1)
-    end
-  end
-
-  def test_client_does_not_accept_unconnectable_node_url_only
-    nodes = ['redis://127.0.0.1:7006']
-
-    assert_raises(Redis::Cluster::InitialSetupError) do
-      build_another_client(cluster: nodes)
-    end
-  end
-
-  def test_client_accepts_unconnectable_node_url_included
-    nodes = ['redis://127.0.0.1:7000', 'redis://127.0.0.1:7006']
-
-    build_another_client(cluster: nodes)
-  end
-
-  def test_client_does_not_accept_http_scheme_url
-    nodes = ['http://127.0.0.1:80']
-
-    assert_raises(Redis::InvalidClientOptionError, "invalid uri scheme 'http'") do
-      build_another_client(cluster: nodes)
-    end
-  end
-
-  def test_client_does_not_accept_blank_included_config
-    nodes = ['']
-
-    assert_raises(Redis::InvalidClientOptionError, "invalid uri scheme ''") do
-      build_another_client(cluster: nodes)
-    end
-  end
-
-  def test_client_does_not_accept_bool_included_config
-    nodes = [true]
-
-    assert_raises(Redis::InvalidClientOptionError, "invalid uri scheme ''") do
-      build_another_client(cluster: nodes)
-    end
-  end
-
-  def test_client_does_not_accept_nil_included_config
-    nodes = [nil]
-
-    assert_raises(Redis::InvalidClientOptionError, "invalid uri scheme ''") do
-      build_another_client(cluster: nodes)
-    end
-  end
-
-  def test_client_does_not_accept_array_included_config
-    nodes = [[]]
-
-    assert_raises(Redis::InvalidClientOptionError, "invalid uri scheme ''") do
-      build_another_client(cluster: nodes)
-    end
-  end
-
-  def test_client_does_not_accept_empty_hash_included_config
-    nodes = [{}]
-
-    assert_raises(Redis::InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys') do
-      build_another_client(cluster: nodes)
-    end
-  end
-
-  def test_client_does_not_accept_object_included_config
-    nodes = [Object.new]
-
-    assert_raises(Redis::InvalidClientOptionError, 'Redis Cluster node config must includes String or Hash') do
-      build_another_client(cluster: nodes)
-    end
-  end
-
-  def test_client_does_not_accept_not_array_config
-    nodes = :not_array
-
-    assert_raises(Redis::InvalidClientOptionError, 'Redis Cluster node config must be Array') do
-      build_another_client(cluster: nodes)
-    end
-  end
-end
diff --git a/test/cluster/client_replicas_test.rb b/test/cluster/client_replicas_test.rb
deleted file mode 100644
index 7cb8602a4116bd2319d00cb7a659b7c57642b507..0000000000000000000000000000000000000000
--- a/test/cluster/client_replicas_test.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-# frozen_string_literal: true
-
-require "helper"
-
-# ruby -w -Itest test/cluster_client_replicas_test.rb
-class TestClusterClientReplicas < Minitest::Test
-  include Helper::Cluster
-
-  def test_client_can_command_with_replica
-    r = build_another_client(replica: true)
-
-    100.times do |i|
-      assert_equal 'OK', r.set("key#{i}", i)
-    end
-
-    r.wait(1, TIMEOUT.to_i * 1000)
-
-    100.times do |i|
-      assert_equal i.to_s, r.get("key#{i}")
-    end
-  end
-
-  def test_client_can_flush_with_replica
-    r = build_another_client(replica: true)
-
-    assert_equal 'OK', r.flushall
-    assert_equal 'OK', r.flushdb
-  end
-
-  def test_some_reference_commands_are_sent_to_slaves_if_needed
-    skip("This test is very flaky") if ENV["CI"]
-    r = build_another_client(replica: true)
-
-    5.times { |i| r.set("key#{i}", i) }
-
-    r.wait(1, TIMEOUT.to_i * 1000)
-
-    assert_equal %w[key0 key1 key2 key3 key4], r.keys
-    assert_equal 5, r.dbsize
-  end
-end
diff --git a/test/cluster/client_slots_test.rb b/test/cluster/client_slots_test.rb
deleted file mode 100644
index 95cf28abacaf232002c7cf8536c1d1a417fc6e54..0000000000000000000000000000000000000000
--- a/test/cluster/client_slots_test.rb
+++ /dev/null
@@ -1,150 +0,0 @@
-# frozen_string_literal: true
-
-require "helper"
-
-# ruby -w -Itest test/cluster_client_slots_test.rb
-class TestClusterClientSlots < Minitest::Test
-  include Helper::Cluster
-
-  def test_slot_class
-    slot = Redis::Cluster::Slot.new('127.0.0.1:7000' => [1..10])
-
-    assert_equal false, slot.exists?(0)
-    assert_equal true, slot.exists?(1)
-    assert_equal true, slot.exists?(10)
-    assert_equal false, slot.exists?(11)
-
-    assert_nil slot.find_node_key_of_master(0)
-    assert_nil slot.find_node_key_of_slave(0)
-    assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(1)
-    assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(1)
-    assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(10)
-    assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(10)
-    assert_nil slot.find_node_key_of_master(11)
-    assert_nil slot.find_node_key_of_slave(11)
-
-    assert_nil slot.put(1, '127.0.0.1:7001')
-  end
-
-  def test_slot_class_with_multiple_slot_ranges
-    slot = Redis::Cluster::Slot.new('127.0.0.1:7000' => [1..10, 30..40])
-
-    assert_equal false, slot.exists?(0)
-    assert_equal true, slot.exists?(1)
-    assert_equal true, slot.exists?(10)
-    assert_equal false, slot.exists?(11)
-    assert_equal true, slot.exists?(30)
-    assert_equal true, slot.exists?(40)
-    assert_equal false, slot.exists?(41)
-
-    assert_nil slot.find_node_key_of_master(0)
-    assert_nil slot.find_node_key_of_slave(0)
-    assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(1)
-    assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(1)
-    assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(10)
-    assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(10)
-    assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(30)
-    assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(40)
-    assert_nil slot.find_node_key_of_master(11)
-    assert_nil slot.find_node_key_of_slave(11)
-    assert_nil slot.find_node_key_of_master(41)
-    assert_nil slot.find_node_key_of_slave(41)
-
-    assert_nil slot.put(1, '127.0.0.1:7001')
-    assert_nil slot.put(30, '127.0.0.1:7001')
-  end
-
-  def test_slot_class_with_node_flags_and_replicas
-    slot = Redis::Cluster::Slot.new({ '127.0.0.1:7000' => [1..10], '127.0.0.1:7001' => [1..10] },
-                                    { '127.0.0.1:7000' => 'master', '127.0.0.1:7001' => 'slave' },
-                                    true)
-
-    assert_equal false, slot.exists?(0)
-    assert_equal true, slot.exists?(1)
-    assert_equal true, slot.exists?(10)
-    assert_equal false, slot.exists?(11)
-
-    assert_nil slot.find_node_key_of_master(0)
-    assert_nil slot.find_node_key_of_slave(0)
-    assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(1)
-    assert_equal '127.0.0.1:7001', slot.find_node_key_of_slave(1)
-    assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(10)
-    assert_equal '127.0.0.1:7001', slot.find_node_key_of_slave(10)
-    assert_nil slot.find_node_key_of_master(11)
-    assert_nil slot.find_node_key_of_slave(11)
-
-    assert_nil slot.put(1, '127.0.0.1:7002')
-  end
-
-  def test_slot_class_with_node_flags_replicas_and_slot_range
-    slot = Redis::Cluster::Slot.new({ '127.0.0.1:7000' => [1..10, 30..40], '127.0.0.1:7001' => [1..10, 30..40] },
-                                    { '127.0.0.1:7000' => 'master', '127.0.0.1:7001' => 'slave' },
-                                    true)
-
-    assert_equal false, slot.exists?(0)
-    assert_equal true, slot.exists?(1)
-    assert_equal true, slot.exists?(10)
-    assert_equal false, slot.exists?(11)
-    assert_equal true, slot.exists?(30)
-    assert_equal false, slot.exists?(41)
-
-    assert_nil slot.find_node_key_of_master(0)
-    assert_nil slot.find_node_key_of_slave(0)
-    assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(1)
-    assert_equal '127.0.0.1:7001', slot.find_node_key_of_slave(1)
-    assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(10)
-    assert_equal '127.0.0.1:7001', slot.find_node_key_of_slave(10)
-    assert_nil slot.find_node_key_of_master(11)
-    assert_nil slot.find_node_key_of_slave(11)
-    assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(30)
-    assert_equal '127.0.0.1:7001', slot.find_node_key_of_slave(30)
-    assert_nil slot.find_node_key_of_master(41)
-    assert_nil slot.find_node_key_of_slave(41)
-
-    assert_nil slot.put(1, '127.0.0.1:7002')
-  end
-
-  def test_slot_class_with_node_flags_and_without_replicas
-    slot = Redis::Cluster::Slot.new({ '127.0.0.1:7000' => [1..10], '127.0.0.1:7001' => [1..10] },
-                                    { '127.0.0.1:7000' => 'master', '127.0.0.1:7001' => 'slave' },
-                                    false)
-
-    assert_equal false, slot.exists?(0)
-    assert_equal true, slot.exists?(1)
-    assert_equal true, slot.exists?(10)
-    assert_equal false, slot.exists?(11)
-
-    assert_nil slot.find_node_key_of_master(0)
-    assert_nil slot.find_node_key_of_slave(0)
-    assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(1)
-    assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(1)
-    assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(10)
-    assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(10)
-    assert_nil slot.find_node_key_of_master(11)
-    assert_nil slot.find_node_key_of_slave(11)
-
-    assert_nil slot.put(1, '127.0.0.1:7002')
-  end
-
-  def test_slot_class_with_empty_slots
-    slot = Redis::Cluster::Slot.new({})
-
-    assert_equal false, slot.exists?(0)
-    assert_equal false, slot.exists?(1)
-
-    assert_nil slot.find_node_key_of_master(0)
-    assert_nil slot.find_node_key_of_slave(0)
-    assert_nil slot.find_node_key_of_master(1)
-    assert_nil slot.find_node_key_of_slave(1)
-
-    assert_nil slot.put(1, '127.0.0.1:7001')
-  end
-
-  def test_redirection_when_slot_is_resharding
-    100.times { |i| redis.set("{key}#{i}", i) }
-
-    redis_cluster_resharding(12_539, src: '127.0.0.1:7002', dest: '127.0.0.1:7000') do
-      100.times { |i| assert_equal i.to_s, redis.get("{key}#{i}") }
-    end
-  end
-end
diff --git a/test/cluster/client_transactions_test.rb b/test/cluster/client_transactions_test.rb
deleted file mode 100644
index bce56fc7e0fc24523f079ac19dba0dc866b9864d..0000000000000000000000000000000000000000
--- a/test/cluster/client_transactions_test.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-# frozen_string_literal: true
-
-require "helper"
-
-# ruby -w -Itest test/cluster_client_transactions_test.rb
-class TestClusterClientTransactions < Minitest::Test
-  include Helper::Cluster
-
-  def test_transaction_with_hash_tag
-    rc1 = redis
-    rc2 = build_another_client
-
-    rc1.multi do |cli|
-      100.times { |i| cli.set("{key}#{i}", i) }
-    end
-
-    100.times { |i| assert_equal i.to_s, rc1.get("{key}#{i}") }
-    100.times { |i| assert_equal i.to_s, rc2.get("{key}#{i}") }
-  end
-
-  def test_transaction_without_hash_tag
-    rc1 = redis
-    rc2 = build_another_client
-
-    assert_raises(Redis::Cluster::CrossSlotPipeliningError) do
-      rc1.multi do |cli|
-        100.times { |i| cli.set("key#{i}", i) }
-      end
-    end
-
-    100.times { |i| assert_nil rc1.get("key#{i}") }
-    100.times { |i| assert_nil rc2.get("key#{i}") }
-  end
-
-  def test_transaction_with_replicas
-    rc1 = build_another_client(replica: true)
-    rc2 = build_another_client(replica: true)
-
-    rc1.multi do |cli|
-      100.times { |i| cli.set("{key}#{i}", i) }
-    end
-
-    rc1.wait(1, TIMEOUT.to_i * 1000)
-
-    100.times { |i| assert_equal i.to_s, rc1.get("{key}#{i}") }
-    100.times { |i| assert_equal i.to_s, rc2.get("{key}#{i}") }
-  end
-
-  def test_transaction_with_watch
-    rc1 = redis
-    rc2 = build_another_client
-
-    rc1.set('{key}1', 100)
-    rc1.watch('{key}1')
-
-    rc2.set('{key}1', 200)
-    val = rc1.get('{key}1').to_i
-    val += 1
-
-    rc1.multi do |cli|
-      cli.set('{key}1', val)
-      cli.set('{key}2', 300)
-    end
-
-    assert_equal '200', rc1.get('{key}1')
-    assert_equal '200', rc2.get('{key}1')
-
-    assert_nil rc1.get('{key}2')
-    assert_nil rc2.get('{key}2')
-  end
-end
diff --git a/test/cluster/commands_on_geo_test.rb b/test/cluster/commands_on_geo_test.rb
deleted file mode 100644
index 23a6050bf9bf0bea6ea700e3f43866bfb8af83d0..0000000000000000000000000000000000000000
--- a/test/cluster/commands_on_geo_test.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-# frozen_string_literal: true
-
-require "helper"
-
-# ruby -w -Itest test/cluster_commands_on_geo_test.rb
-# @see https://redis.io/commands#geo
-class TestClusterCommandsOnGeo < Minitest::Test
-  include Helper::Cluster
-
-  MIN_REDIS_VERSION = '3.2.0'
-
-  def add_sicily
-    redis.geoadd('Sicily',
-                 13.361389, 38.115556, 'Palermo',
-                 15.087269, 37.502669, 'Catania')
-  end
-
-  def test_geoadd
-    target_version(MIN_REDIS_VERSION) do
-      assert_equal 2, add_sicily
-    end
-  end
-
-  def test_geohash
-    target_version(MIN_REDIS_VERSION) do
-      add_sicily
-      assert_equal %w[sqc8b49rny0 sqdtr74hyu0], redis.geohash('Sicily', %w[Palermo Catania])
-    end
-  end
-
-  def test_geopos
-    target_version(MIN_REDIS_VERSION) do
-      add_sicily
-      expected = [%w[13.36138933897018433 38.11555639549629859],
-                  %w[15.08726745843887329 37.50266842333162032],
-                  nil]
-      assert_equal expected, redis.geopos('Sicily', %w[Palermo Catania NonExisting])
-    end
-  end
-
-  def test_geodist
-    target_version(MIN_REDIS_VERSION) do
-      add_sicily
-      assert_equal '166274.1516', redis.geodist('Sicily', 'Palermo', 'Catania')
-      assert_equal '166.2742', redis.geodist('Sicily', 'Palermo', 'Catania', 'km')
-      assert_equal '103.3182', redis.geodist('Sicily', 'Palermo', 'Catania', 'mi')
-    end
-  end
-
-  def test_georadius
-    target_version(MIN_REDIS_VERSION) do
-      add_sicily
-
-      expected = [%w[Palermo 190.4424], %w[Catania 56.4413]]
-      assert_equal expected, redis.georadius('Sicily', 15, 37, 200, 'km', 'WITHDIST')
-
-      expected = [['Palermo', %w[13.36138933897018433 38.11555639549629859]],
-                  ['Catania', %w[15.08726745843887329 37.50266842333162032]]]
-      assert_equal expected, redis.georadius('Sicily', 15, 37, 200, 'km', 'WITHCOORD')
-
-      expected = [['Palermo', '190.4424', %w[13.36138933897018433 38.11555639549629859]],
-                  ['Catania', '56.4413', %w[15.08726745843887329 37.50266842333162032]]]
-      assert_equal expected, redis.georadius('Sicily', 15, 37, 200, 'km', 'WITHDIST', 'WITHCOORD')
-    end
-  end
-
-  def test_georadiusbymember
-    target_version(MIN_REDIS_VERSION) do
-      redis.geoadd('Sicily', 13.583333, 37.316667, 'Agrigento')
-      add_sicily
-      assert_equal %w[Agrigento Palermo], redis.georadiusbymember('Sicily', 'Agrigento', 100, 'km')
-    end
-  end
-end
diff --git a/test/cluster/commands_on_pub_sub_test.rb b/test/cluster/commands_on_pub_sub_test.rb
deleted file mode 100644
index 8f4c2ca53bec0f4d125117bd116c82ecb1dc1577..0000000000000000000000000000000000000000
--- a/test/cluster/commands_on_pub_sub_test.rb
+++ /dev/null
@@ -1,98 +0,0 @@
-# frozen_string_literal: true
-
-require "helper"
-
-# ruby -w -Itest test/cluster_commands_on_pub_sub_test.rb
-# @see https://redis.io/commands#pubsub
-class TestClusterCommandsOnPubSub < Minitest::Test
-  include Helper::Cluster
-
-  def test_publish_subscribe_unsubscribe_pubsub
-    sub_cnt = 0
-    messages = {}
-
-    wire = Wire.new do
-      redis.subscribe('channel1', 'channel2') do |on|
-        on.subscribe { |_c, t| sub_cnt = t }
-        on.unsubscribe { |_c, t| sub_cnt = t }
-        on.message do |c, msg|
-          messages[c] = msg
-          # FIXME: blocking occurs when `unsubscribe` method was called with channel arguments
-          redis.unsubscribe if messages.size == 2
-        end
-      end
-    end
-
-    Wire.pass until sub_cnt == 2
-
-    publisher = build_another_client
-
-    assert_equal %w[channel1 channel2], publisher.pubsub(:channels)
-    assert_equal %w[channel1 channel2], publisher.pubsub(:channels, 'cha*')
-    assert_equal [], publisher.pubsub(:channels, 'chachacha*')
-    assert_equal({}, publisher.pubsub(:numsub))
-    assert_equal({ 'channel1' => 1, 'channel2' => 1, 'channel3' => 0 },
-                 publisher.pubsub(:numsub, 'channel1', 'channel2', 'channel3'))
-    assert_equal 0, publisher.pubsub(:numpat)
-
-    publisher.publish('channel1', 'one')
-    publisher.publish('channel2', 'two')
-
-    wire.join
-
-    assert_equal({ 'channel1' => 'one', 'channel2' => 'two' }, messages.sort.to_h)
-
-    assert_equal [], publisher.pubsub(:channels)
-    assert_equal [], publisher.pubsub(:channels, 'cha*')
-    assert_equal [], publisher.pubsub(:channels, 'chachacha*')
-    assert_equal({}, publisher.pubsub(:numsub))
-    assert_equal({ 'channel1' => 0, 'channel2' => 0, 'channel3' => 0 },
-                 publisher.pubsub(:numsub, 'channel1', 'channel2', 'channel3'))
-    assert_equal 0, publisher.pubsub(:numpat)
-  end
-
-  def test_publish_psubscribe_punsubscribe_pubsub
-    sub_cnt = 0
-    messages = {}
-
-    wire = Wire.new do
-      redis.psubscribe('guc*', 'her*') do |on|
-        on.psubscribe { |_c, t| sub_cnt = t }
-        on.punsubscribe { |_c, t| sub_cnt = t }
-        on.pmessage do |_ptn, chn, msg|
-          messages[chn] = msg
-          # FIXME: blocking occurs when `unsubscribe` method was called with channel arguments
-          redis.punsubscribe if messages.size == 2
-        end
-      end
-    end
-
-    Wire.pass until sub_cnt == 2
-
-    publisher = build_another_client
-
-    assert_equal [], publisher.pubsub(:channels)
-    assert_equal [], publisher.pubsub(:channels, 'bur*')
-    assert_equal [], publisher.pubsub(:channels, 'guc*')
-    assert_equal [], publisher.pubsub(:channels, 'her*')
-    assert_equal({}, publisher.pubsub(:numsub))
-    assert_equal({ 'burberry1' => 0, 'gucci2' => 0, 'hermes3' => 0 }, publisher.pubsub(:numsub, 'burberry1', 'gucci2', 'hermes3'))
-    assert_equal 2, publisher.pubsub(:numpat)
-
-    publisher.publish('burberry1', 'one')
-    publisher.publish('gucci2', 'two')
-    publisher.publish('hermes3', 'three')
-
-    wire.join
-
-    assert_equal({ 'gucci2' => 'two', 'hermes3' => 'three' }, messages.sort.to_h)
-
-    assert_equal [], publisher.pubsub(:channels)
-    assert_equal [], publisher.pubsub(:channels, 'bur*')
-    assert_equal [], publisher.pubsub(:channels, 'guc*')
-    assert_equal [], publisher.pubsub(:channels, 'her*')
-    assert_equal({}, publisher.pubsub(:numsub))
-    assert_equal({ 'burberry1' => 0, 'gucci2' => 0, 'hermes3' => 0 }, publisher.pubsub(:numsub, 'burberry1', 'gucci2', 'hermes3'))
-    assert_equal 0, publisher.pubsub(:numpat)
-  end
-end
diff --git a/test/cluster/commands_on_transactions_test.rb b/test/cluster/commands_on_transactions_test.rb
deleted file mode 100644
index e2964e027d432de1ad8010ec286ea970e5d05785..0000000000000000000000000000000000000000
--- a/test/cluster/commands_on_transactions_test.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-# frozen_string_literal: true
-
-require "helper"
-
-# ruby -w -Itest test/cluster_commands_on_transactions_test.rb
-# @see https://redis.io/commands#transactions
-class TestClusterCommandsOnTransactions < Minitest::Test
-  include Helper::Cluster
-
-  def test_discard
-    assert_raises(Redis::Cluster::AmbiguousNodeError) do
-      redis.discard
-    end
-  end
-
-  def test_exec
-    assert_raises(Redis::Cluster::AmbiguousNodeError) do
-      redis.exec
-    end
-  end
-
-  def test_multi
-    assert_raises(Redis::Cluster::AmbiguousNodeError) do
-      redis.multi
-    end
-  end
-
-  def test_unwatch
-    assert_raises(Redis::Cluster::AmbiguousNodeError) do
-      redis.unwatch
-    end
-  end
-
-  def test_watch
-    assert_raises(Redis::CommandError, "CROSSSLOT Keys in request don't hash to the same slot") do
-      redis.watch('key1', 'key2')
-    end
-
-    assert_equal 'OK', redis.watch('{key}1', '{key}2')
-  end
-end
diff --git a/test/distributed/blocking_commands_test.rb b/test/distributed/blocking_commands_test.rb
index e7e09997577f81b646e26578e7ca4e1d585e6d80..2d1c538e79e9e22246cbfe1bd56954231ab61f3d 100644
--- a/test/distributed/blocking_commands_test.rb
+++ b/test/distributed/blocking_commands_test.rb
@@ -20,41 +20,15 @@ class TestDistributedBlockingCommands < Minitest::Test
     end
   end
 
-  def test_blpop_raises_with_old_prototype
-    assert_raises(Redis::Distributed::CannotDistribute) do
-      r.blpop('foo', 'bar', 0)
-    end
-  end
-
   def test_brpop_raises
     assert_raises(Redis::Distributed::CannotDistribute) do
       r.brpop(%w[foo bar])
     end
   end
 
-  def test_brpop_raises_with_old_prototype
-    assert_raises(Redis::Distributed::CannotDistribute) do
-      r.brpop('foo', 'bar', 0)
-    end
-  end
-
   def test_brpoplpush_raises
     assert_raises(Redis::Distributed::CannotDistribute) do
       r.brpoplpush('foo', 'bar')
     end
   end
-
-  def test_brpoplpush_raises_with_old_prototype
-    assert_raises(Redis::Distributed::CannotDistribute) do
-      r.brpoplpush('foo', 'bar', 0)
-    end
-  end
-
-  def test_bzpopmin
-    # Not implemented yet
-  end
-
-  def test_bzpopmax
-    # Not implemented yet
-  end
 end
diff --git a/test/distributed/commands_on_hyper_log_log_test.rb b/test/distributed/commands_on_hyper_log_log_test.rb
index 38dc85dd640df047cb701abef3e62f21d16c4608..4d91427f55ce717f5a110c95c0a0cfb322fd7d75 100644
--- a/test/distributed/commands_on_hyper_log_log_test.rb
+++ b/test/distributed/commands_on_hyper_log_log_test.rb
@@ -7,21 +7,17 @@ class TestDistributedCommandsOnHyperLogLog < Minitest::Test
   include Lint::HyperLogLog
 
   def test_pfmerge
-    target_version '2.8.9' do
-      assert_raises Redis::Distributed::CannotDistribute do
-        super
-      end
+    assert_raises Redis::Distributed::CannotDistribute do
+      super
     end
   end
 
   def test_pfcount_multiple_keys_diff_nodes
-    target_version '2.8.9' do
-      assert_raises Redis::Distributed::CannotDistribute do
-        r.pfadd 'foo', 's1'
-        r.pfadd 'bar', 's2'
+    assert_raises Redis::Distributed::CannotDistribute do
+      r.pfadd 'foo', 's1'
+      r.pfadd 'bar', 's2'
 
-        assert r.pfcount('res', 'foo', 'bar')
-      end
+      assert r.pfcount('res', 'foo', 'bar')
     end
   end
 end
diff --git a/test/distributed/commands_on_strings_test.rb b/test/distributed/commands_on_strings_test.rb
index 5d66ec0a3c7a30c9ab797725822c9cbcbc41f7cf..85e439a0c3973664f28080ac4c3a0d447a0217e7 100644
--- a/test/distributed/commands_on_strings_test.rb
+++ b/test/distributed/commands_on_strings_test.rb
@@ -12,6 +12,7 @@ class TestDistributedCommandsOnStrings < Minitest::Test
 
     assert_equal ["s1", "s2"], r.mget("foo", "bar")
     assert_equal ["s1", "s2", nil], r.mget("foo", "bar", "baz")
+    assert_equal ["s1", "s2", nil], r.mget(["foo", "bar", "baz"])
   end
 
   def test_mget_mapped
@@ -57,13 +58,11 @@ class TestDistributedCommandsOnStrings < Minitest::Test
   end
 
   def test_bitop
-    target_version "2.5.10" do
-      assert_raises Redis::Distributed::CannotDistribute do
-        r.set("foo", "a")
-        r.set("bar", "b")
+    assert_raises Redis::Distributed::CannotDistribute do
+      r.set("foo", "a")
+      r.set("bar", "b")
 
-        r.bitop(:and, "foo&bar", "foo", "bar")
-      end
+      r.bitop(:and, "foo&bar", "foo", "bar")
     end
   end
 
diff --git a/test/distributed/commands_on_value_types_test.rb b/test/distributed/commands_on_value_types_test.rb
index 12797cd66b2c3dff6bff12b441e30c0db31e00d4..876641b7440cc0b2aa7db9ebdde22fd67457e802 100644
--- a/test/distributed/commands_on_value_types_test.rb
+++ b/test/distributed/commands_on_value_types_test.rb
@@ -39,39 +39,35 @@ class TestDistributedCommandsOnValueTypes < Minitest::Test
   end
 
   def test_unlink
-    target_version "4.0.0" do
-      r.set "foo", "s1"
-      r.set "bar", "s2"
-      r.set "baz", "s3"
+    r.set "foo", "s1"
+    r.set "bar", "s2"
+    r.set "baz", "s3"
 
-      assert_equal ["bar", "baz", "foo"], r.keys("*").sort
+    assert_equal ["bar", "baz", "foo"], r.keys("*").sort
 
-      assert_equal 1, r.unlink("foo")
+    assert_equal 1, r.unlink("foo")
 
-      assert_equal ["bar", "baz"], r.keys("*").sort
+    assert_equal ["bar", "baz"], r.keys("*").sort
 
-      assert_equal 2, r.unlink("bar", "baz")
+    assert_equal 2, r.unlink("bar", "baz")
 
-      assert_equal [], r.keys("*").sort
-    end
+    assert_equal [], r.keys("*").sort
   end
 
   def test_unlink_with_array_argument
-    target_version "4.0.0" do
-      r.set "foo", "s1"
-      r.set "bar", "s2"
-      r.set "baz", "s3"
+    r.set "foo", "s1"
+    r.set "bar", "s2"
+    r.set "baz", "s3"
 
-      assert_equal ["bar", "baz", "foo"], r.keys("*").sort
+    assert_equal ["bar", "baz", "foo"], r.keys("*").sort
 
-      assert_equal 1, r.unlink(["foo"])
+    assert_equal 1, r.unlink(["foo"])
 
-      assert_equal ["bar", "baz"], r.keys("*").sort
+    assert_equal ["bar", "baz"], r.keys("*").sort
 
-      assert_equal 2, r.unlink(["bar", "baz"])
+    assert_equal 2, r.unlink(["bar", "baz"])
 
-      assert_equal [], r.keys("*").sort
-    end
+    assert_equal [], r.keys("*").sort
   end
 
   def test_randomkey
diff --git a/test/distributed/commands_requiring_clustering_test.rb b/test/distributed/commands_requiring_clustering_test.rb
index 9711b10e9274b093cde03e4033b01f531a68a32e..db90025233aa25fb26f7ed9faf08f08a7aee8ab3 100644
--- a/test/distributed/commands_requiring_clustering_test.rb
+++ b/test/distributed/commands_requiring_clustering_test.rb
@@ -159,18 +159,16 @@ class TestDistributedCommandsRequiringClustering < Minitest::Test
   end
 
   def test_bitop
-    target_version "2.5.10" do
-      r.set("{qux}foo", "a")
-      r.set("{qux}bar", "b")
-
-      r.bitop(:and, "{qux}foo&bar", "{qux}foo", "{qux}bar")
-      assert_equal "\x60", r.get("{qux}foo&bar")
-      r.bitop(:or, "{qux}foo|bar", "{qux}foo", "{qux}bar")
-      assert_equal "\x63", r.get("{qux}foo|bar")
-      r.bitop(:xor, "{qux}foo^bar", "{qux}foo", "{qux}bar")
-      assert_equal "\x03", r.get("{qux}foo^bar")
-      r.bitop(:not, "{qux}~foo", "{qux}foo")
-      assert_equal "\x9E", r.get("{qux}~foo")
-    end
+    r.set("{qux}foo", "a")
+    r.set("{qux}bar", "b")
+
+    r.bitop(:and, "{qux}foo&bar", "{qux}foo", "{qux}bar")
+    assert_equal "\x60", r.get("{qux}foo&bar")
+    r.bitop(:or, "{qux}foo|bar", "{qux}foo", "{qux}bar")
+    assert_equal "\x63", r.get("{qux}foo|bar")
+    r.bitop(:xor, "{qux}foo^bar", "{qux}foo", "{qux}bar")
+    assert_equal "\x03", r.get("{qux}foo^bar")
+    r.bitop(:not, "{qux}~foo", "{qux}foo")
+    assert_equal "\x9E".b, r.get("{qux}~foo")
   end
 end
diff --git a/test/distributed/distributed_test.rb b/test/distributed/distributed_test.rb
index d03b52d6060db877afc331a572e20d8d3ba351fc..8a5878498f83c34eb5c2a1ac8303b6054dc5c839 100644
--- a/test/distributed/distributed_test.rb
+++ b/test/distributed/distributed_test.rb
@@ -21,15 +21,12 @@ class TestDistributed < Minitest::Test
   end
 
   def test_add_nodes
-    logger = Logger.new("/dev/null")
-
-    @r = Redis::Distributed.new NODES, logger: logger, timeout: 10
+    @r = Redis::Distributed.new NODES, timeout: 10
 
     assert_equal "127.0.0.1", @r.nodes[0]._client.host
     assert_equal PORT, @r.nodes[0]._client.port
     assert_equal 15, @r.nodes[0]._client.db
     assert_equal 10, @r.nodes[0]._client.timeout
-    assert_equal logger, @r.nodes[0]._client.logger
 
     @r.add_node("redis://127.0.0.1:6380/14")
 
@@ -37,7 +34,6 @@ class TestDistributed < Minitest::Test
     assert_equal 6380, @r.nodes[1]._client.port
     assert_equal 14, @r.nodes[1]._client.db
     assert_equal 10, @r.nodes[1]._client.timeout
-    assert_equal logger, @r.nodes[1]._client.logger
   end
 
   def test_pipelining_commands_cannot_be_distributed
diff --git a/test/distributed/internals_test.rb b/test/distributed/internals_test.rb
index ac3fad205b34c447eaf880243fdd0b784292af0f..454828021411f3af5dc5da42a8b66641ff7c6c8b 100644
--- a/test/distributed/internals_test.rb
+++ b/test/distributed/internals_test.rb
@@ -15,19 +15,19 @@ class TestDistributedInternals < Minitest::Test
   def test_default_as_urls
     nodes = ["redis://127.0.0.1:#{PORT}/15", *NODES]
     redis = Redis::Distributed.new nodes
-    assert_equal(["redis://127.0.0.1:#{PORT}/15", *NODES], redis.nodes.map { |node| node._client.id })
+    assert_equal(["redis://127.0.0.1:#{PORT}/15", *NODES], redis.nodes.map { |node| node._client.server_url })
   end
 
   def test_default_as_config_hashes
     nodes = [OPTIONS.merge(host: '127.0.0.1'), OPTIONS.merge(host: 'somehost', port: PORT.next)]
     redis = Redis::Distributed.new nodes
-    assert_equal(["redis://127.0.0.1:#{PORT}/15", "redis://somehost:#{PORT.next}/15"], redis.nodes.map { |node| node._client.id })
+    assert_equal(["redis://127.0.0.1:#{PORT}/15", "redis://somehost:#{PORT.next}/15"], redis.nodes.map { |node| node._client.server_url })
   end
 
   def test_as_mix_and_match
     nodes = ["redis://127.0.0.1:7389/15", OPTIONS.merge(host: 'somehost'), OPTIONS.merge(host: 'somehost', port: PORT.next)]
     redis = Redis::Distributed.new nodes
-    assert_equal(["redis://127.0.0.1:7389/15", "redis://somehost:#{PORT}/15", "redis://somehost:#{PORT.next}/15"], redis.nodes.map { |node| node._client.id })
+    assert_equal(["redis://127.0.0.1:7389/15", "redis://somehost:#{PORT}/15", "redis://somehost:#{PORT.next}/15"], redis.nodes.map { |node| node._client.server_url })
   end
 
   def test_override_id
diff --git a/test/distributed/key_tags_test.rb b/test/distributed/key_tags_test.rb
index b36e0c1d1609157635fda0505b5f99780b6df350..415779ad6f57b86615049385ee05883c06645790 100644
--- a/test/distributed/key_tags_test.rb
+++ b/test/distributed/key_tags_test.rb
@@ -28,7 +28,7 @@ class TestDistributedKeyTags < Minitest::Test
   end
 
   def test_distributes_keys_if_no_clustering_is_used
-    r.add_node("redis://127.0.0.1:#{PORT}/14")
+    r.add_node("redis://127.0.0.1:#{PORT}/13")
     r.flushdb
 
     r.set "users:1", 1
diff --git a/test/distributed/publish_subscribe_test.rb b/test/distributed/publish_subscribe_test.rb
index 93d1d2b7d862bcdb829e4d5a858b5aa7aebc85a3..34f8d69b67ffd0bebd4fdc1818a3a1a5b02fef1c 100644
--- a/test/distributed/publish_subscribe_test.rb
+++ b/test/distributed/publish_subscribe_test.rb
@@ -19,7 +19,7 @@ class TestDistributedPublishSubscribe < Minitest::Test
     @subscribed = false
     @unsubscribed = false
 
-    wire = Wire.new do
+    thread = Thread.new do
       r.subscribe("foo") do |on|
         on.subscribe do |_channel, total|
           @subscribed = true
@@ -41,11 +41,11 @@ class TestDistributedPublishSubscribe < Minitest::Test
     end
 
     # Wait until the subscription is active before publishing
-    Wire.pass until @subscribed
+    Thread.pass until @subscribed
 
     Redis::Distributed.new(NODES).publish("foo", "s1")
 
-    wire.join
+    thread.join
 
     assert @subscribed
     assert_equal 1, @t1
@@ -57,7 +57,7 @@ class TestDistributedPublishSubscribe < Minitest::Test
   def test_subscribe_within_subscribe
     @channels = []
 
-    wire = Wire.new do
+    thread = Thread.new do
       r.subscribe("foo") do |on|
         on.subscribe do |channel, _total|
           @channels << channel
@@ -68,23 +68,22 @@ class TestDistributedPublishSubscribe < Minitest::Test
       end
     end
 
-    wire.join
+    thread.join
 
     assert_equal ["foo", "bar"], @channels
   end
 
   def test_other_commands_within_a_subscribe
-    assert_raises Redis::CommandError do
-      r.subscribe("foo") do |on|
-        on.subscribe do |_channel, _total|
-          r.set("bar", "s2")
-        end
+    r.subscribe("foo") do |on|
+      on.subscribe do |_channel, _total|
+        r.set("bar", "s2")
+        r.unsubscribe("foo")
       end
     end
   end
 
   def test_subscribe_without_a_block
-    assert_raises LocalJumpError do
+    assert_raises Redis::SubscriptionError do
       r.subscribe("foo")
     end
   end
diff --git a/test/distributed/remote_server_control_commands_test.rb b/test/distributed/remote_server_control_commands_test.rb
index 4c20c7d7846a84264851f0de3b950cab10293b64..2d2516d01efda3c4d2b473efc67115629513f55e 100644
--- a/test/distributed/remote_server_control_commands_test.rb
+++ b/test/distributed/remote_server_control_commands_test.rb
@@ -27,16 +27,14 @@ class TestDistributedRemoteServerControlCommands < Minitest::Test
   end
 
   def test_info_commandstats
-    target_version "2.5.7" do
-      r.nodes.each do |n|
-        n.config(:resetstat)
-        n.get("foo")
-        n.get("bar")
-      end
+    r.nodes.each do |n|
+      n.config(:resetstat)
+      n.get("foo")
+      n.get("bar")
+    end
 
-      r.info(:commandstats).each do |info|
-        assert_equal '2', info['get']['calls']
-      end
+    r.info(:commandstats).each do |info|
+      assert_equal '2', info['get']['calls']
     end
   end
 
@@ -52,15 +50,13 @@ class TestDistributedRemoteServerControlCommands < Minitest::Test
   end
 
   def test_time
-    target_version "2.5.4" do
-      # Test that the difference between the time that Ruby reports and the time
-      # that Redis reports is minimal (prevents the test from being racy).
-      r.time.each do |rv|
-        redis_usec = rv[0] * 1_000_000 + rv[1]
-        ruby_usec = Integer(Time.now.to_f * 1_000_000)
+    # Test that the difference between the time that Ruby reports and the time
+    # that Redis reports is minimal (prevents the test from being racy).
+    r.time.each do |rv|
+      redis_usec = rv[0] * 1_000_000 + rv[1]
+      ruby_usec = Integer(Time.now.to_f * 1_000_000)
 
-        assert((ruby_usec - redis_usec).abs < 500_000)
-      end
+      assert((ruby_usec - redis_usec).abs < 500_000)
     end
   end
 end
diff --git a/test/distributed/scripting_test.rb b/test/distributed/scripting_test.rb
index cf69ac0a02df9c4f9ca2488dd38967a1c621b409..eda11d78a475966e497b58afb29ed74c4b0fc618 100644
--- a/test/distributed/scripting_test.rb
+++ b/test/distributed/scripting_test.rb
@@ -10,92 +10,78 @@ class TestDistributedScripting < Minitest::Test
   end
 
   def test_script_exists
-    target_version "2.5.9" do # 2.6-rc1
-      a = to_sha("return 1")
-      b = a.succ
-
-      assert_equal [true], r.script(:exists, a)
-      assert_equal [false], r.script(:exists, b)
-      assert_equal [[true]], r.script(:exists, [a])
-      assert_equal [[false]], r.script(:exists, [b])
-      assert_equal [[true, false]], r.script(:exists, [a, b])
-    end
+    a = to_sha("return 1")
+    b = a.succ
+
+    assert_equal [true], r.script(:exists, a)
+    assert_equal [false], r.script(:exists, b)
+    assert_equal [[true]], r.script(:exists, [a])
+    assert_equal [[false]], r.script(:exists, [b])
+    assert_equal [[true, false]], r.script(:exists, [a, b])
   end
 
   def test_script_flush
-    target_version "2.5.9" do # 2.6-rc1
-      sha = to_sha("return 1")
-      assert r.script(:exists, sha).first
-      assert_equal ["OK"], r.script(:flush)
-      assert !r.script(:exists, sha).first
-    end
+    sha = to_sha("return 1")
+    assert r.script(:exists, sha).first
+    assert_equal ["OK"], r.script(:flush)
+    assert !r.script(:exists, sha).first
   end
 
   def test_script_kill
-    target_version "2.5.9" do # 2.6-rc1
-      redis_mock(script: ->(arg) { "+#{arg.upcase}" }) do |redis|
-        assert_equal ["KILL"], redis.script(:kill)
-      end
+    redis_mock(script: ->(arg) { "+#{arg.upcase}" }) do |redis|
+      assert_equal ["KILL"], redis.script(:kill)
     end
   end
 
   def test_eval
-    target_version "2.5.9" do # 2.6-rc1
-      assert_raises(Redis::Distributed::CannotDistribute) do
-        r.eval("return #KEYS")
-      end
-
-      assert_raises(Redis::Distributed::CannotDistribute) do
-        r.eval("return KEYS", ["k1", "k2"])
-      end
+    assert_raises(Redis::Distributed::CannotDistribute) do
+      r.eval("return #KEYS")
+    end
 
-      assert_equal ["k1"], r.eval("return KEYS", ["k1"])
-      assert_equal ["a1", "a2"], r.eval("return ARGV", ["k1"], ["a1", "a2"])
+    assert_raises(Redis::Distributed::CannotDistribute) do
+      r.eval("return KEYS", ["k1", "k2"])
     end
+
+    assert_equal ["k1"], r.eval("return KEYS", ["k1"])
+    assert_equal ["a1", "a2"], r.eval("return ARGV", ["k1"], ["a1", "a2"])
   end
 
   def test_eval_with_options_hash
-    target_version "2.5.9" do # 2.6-rc1
-      assert_raises(Redis::Distributed::CannotDistribute) do
-        r.eval("return #KEYS", {})
-      end
-
-      assert_raises(Redis::Distributed::CannotDistribute) do
-        r.eval("return KEYS", { keys: ["k1", "k2"] })
-      end
+    assert_raises(Redis::Distributed::CannotDistribute) do
+      r.eval("return #KEYS", {})
+    end
 
-      assert_equal ["k1"], r.eval("return KEYS", { keys: ["k1"] })
-      assert_equal ["a1", "a2"], r.eval("return ARGV", { keys: ["k1"], argv: ["a1", "a2"] })
+    assert_raises(Redis::Distributed::CannotDistribute) do
+      r.eval("return KEYS", { keys: ["k1", "k2"] })
     end
+
+    assert_equal ["k1"], r.eval("return KEYS", { keys: ["k1"] })
+    assert_equal ["a1", "a2"], r.eval("return ARGV", { keys: ["k1"], argv: ["a1", "a2"] })
   end
 
   def test_evalsha
-    target_version "2.5.9" do # 2.6-rc1
-      assert_raises(Redis::Distributed::CannotDistribute) do
-        r.evalsha(to_sha("return #KEYS"))
-      end
-
-      assert_raises(Redis::Distributed::CannotDistribute) do
-        r.evalsha(to_sha("return KEYS"), ["k1", "k2"])
-      end
+    assert_raises(Redis::Distributed::CannotDistribute) do
+      r.evalsha(to_sha("return #KEYS"))
+    end
 
-      assert_equal ["k1"], r.evalsha(to_sha("return KEYS"), ["k1"])
-      assert_equal ["a1", "a2"], r.evalsha(to_sha("return ARGV"), ["k1"], ["a1", "a2"])
+    assert_raises(Redis::Distributed::CannotDistribute) do
+      r.evalsha(to_sha("return KEYS"), ["k1", "k2"])
     end
+
+    assert_equal ["k1"], r.evalsha(to_sha("return KEYS"), ["k1"])
+    assert_equal ["a1", "a2"], r.evalsha(to_sha("return ARGV"), ["k1"], ["a1", "a2"])
   end
 
   def test_evalsha_with_options_hash
-    target_version "2.5.9" do # 2.6-rc1
-      assert_raises(Redis::Distributed::CannotDistribute) do
-        r.evalsha(to_sha("return #KEYS"), {})
-      end
-
-      assert_raises(Redis::Distributed::CannotDistribute) do
-        r.evalsha(to_sha("return KEYS"), { keys: ["k1", "k2"] })
-      end
+    assert_raises(Redis::Distributed::CannotDistribute) do
+      r.evalsha(to_sha("return #KEYS"), {})
+    end
 
-      assert_equal ["k1"], r.evalsha(to_sha("return KEYS"), { keys: ["k1"] })
-      assert_equal ["a1", "a2"], r.evalsha(to_sha("return ARGV"), { keys: ["k1"], argv: ["a1", "a2"] })
+    assert_raises(Redis::Distributed::CannotDistribute) do
+      r.evalsha(to_sha("return KEYS"), { keys: ["k1", "k2"] })
     end
+
+    assert_equal ["k1"], r.evalsha(to_sha("return KEYS"), { keys: ["k1"] })
+    assert_equal ["a1", "a2"], r.evalsha(to_sha("return ARGV"), { keys: ["k1"], argv: ["a1", "a2"] })
   end
 end
diff --git a/test/distributed/transactions_test.rb b/test/distributed/transactions_test.rb
index dfe9e565fdb59675d40ac2df6d929e1d65fd2164..6be3101346a950bf1436fedb54639e52a36b565a 100644
--- a/test/distributed/transactions_test.rb
+++ b/test/distributed/transactions_test.rb
@@ -5,22 +5,6 @@ require "helper"
 class TestDistributedTransactions < Minitest::Test
   include Helper::Distributed
 
-  def test_multi_discard
-    r.set("foo", 1)
-
-    r.watch("foo")
-    r.multi
-    r.set("foo", 2)
-
-    assert_raises Redis::Distributed::CannotDistribute do
-      r.set("bar", 1)
-    end
-
-    r.discard
-
-    assert_equal('1', r.get("foo"))
-  end
-
   def test_multi_discard_without_watch
     @foo = nil
 
@@ -74,31 +58,13 @@ class TestDistributedTransactions < Minitest::Test
     r.watch("{qux}foo", "{qux}bar", "{qux}baz") do
       assert_equal '1', r.get("{qux}baz")
 
-      result = r.multi do
-        r.incrby("{qux}foo", 3)
-        r.incrby("{qux}bar", 6)
-        r.incrby("{qux}baz", 9)
+      result = r.multi do |transaction|
+        transaction.incrby("{qux}foo", 3)
+        transaction.incrby("{qux}bar", 6)
+        transaction.incrby("{qux}baz", 9)
       end
 
       assert_equal [3, 6, 10], result
     end
   end
-
-  def test_watch_multi_exec_without_block
-    r.set("{qux}baz", 1)
-
-    assert_equal "OK", r.watch("{qux}foo", "{qux}bar", "{qux}baz")
-    assert_equal '1', r.get("{qux}baz")
-
-    assert_raises Redis::Distributed::CannotDistribute do
-      r.get("{foo}baz")
-    end
-
-    assert_equal "OK", r.multi
-    assert_equal "QUEUED", r.incrby("{qux}baz", 1)
-    assert_equal "QUEUED", r.incrby("{qux}baz", 1)
-    assert_equal [2, 3], r.exec
-
-    assert_equal "OK", r.set("{other}baz", 1)
-  end
 end
diff --git a/test/helper.rb b/test/helper.rb
index 609d74303b13cbdc067431025fe87256bd0f12b9..d3fb5290dd3a9fcf259f7e8e093bb22b2b16454a 100644
--- a/test/helper.rb
+++ b/test/helper.rb
@@ -1,9 +1,9 @@
 # frozen_string_literal: true
 
+$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
+
 require "minitest/autorun"
 require "mocha/minitest"
-require "logger"
-require "stringio"
 
 $VERBOSE = true
 
@@ -13,11 +13,12 @@ require "redis"
 Redis.silence_deprecations = true
 
 require "redis/distributed"
-require "redis/connection/#{ENV['DRIVER']}"
 
 require_relative "support/redis_mock"
-require_relative "support/connection/#{ENV['DRIVER']}"
-require_relative 'support/cluster/orchestrator'
+
+if ENV["DRIVER"] == "hiredis"
+  require "hiredis-client"
+end
 
 PORT        = 6381
 DB          = 15
@@ -35,10 +36,6 @@ if ENV['REDIS_SOCKET_PATH'].nil?
   ENV['REDIS_SOCKET_PATH'] = sock_file
 end
 
-def driver(*drivers, &blk)
-  class_eval(&blk) if drivers.map(&:to_s).include?(ENV["DRIVER"])
-end
-
 Dir[File.expand_path('lint/**/*.rb', __dir__)].sort.each do |f|
   require f
 end
@@ -62,17 +59,6 @@ module Helper
     end
   end
 
-  def with_external_encoding(encoding)
-    original_encoding = Encoding.default_external
-
-    begin
-      silent { Encoding.default_external = Encoding.find(encoding) }
-      yield
-    ensure
-      silent { Encoding.default_external = original_encoding }
-    end
-  end
-
   class Version
     include Comparable
 
@@ -110,7 +96,6 @@ module Helper
     alias r redis
 
     def setup
-      @log = StringIO.new
       @redis = init _new_client
 
       # Run GC to make sure orphaned connections are closed.
@@ -119,7 +104,7 @@ module Helper
     end
 
     def teardown
-      redis&.quit
+      redis&.close
       super
     end
 
@@ -189,7 +174,7 @@ module Helper
     def with_acl
       admin = _new_client
       admin.acl('SETUSER', 'johndoe', 'on',
-                '+ping', '+select', '+command', '+cluster|slots', '+cluster|nodes',
+                '+ping', '+select', '+command', '+cluster|slots', '+cluster|nodes', '+readonly',
                 '>mysecret')
       yield('johndoe', 'mysecret')
     ensure
@@ -213,7 +198,7 @@ module Helper
     private
 
     def _format_options(options)
-      OPTIONS.merge(logger: ::Logger.new(@log)).merge(options)
+      OPTIONS.merge(options)
     end
 
     def _new_client(options = {})
@@ -232,7 +217,7 @@ module Helper
     LOCALHOST = '127.0.0.1'
 
     def build_sentinel_client(options = {})
-      opts = { host: LOCALHOST, port: SENTINEL_PORT, timeout: TIMEOUT, logger: ::Logger.new(@log) }
+      opts = { host: LOCALHOST, port: SENTINEL_PORT, timeout: TIMEOUT }
       Redis.new(opts.merge(options))
     end
 
@@ -242,11 +227,25 @@ module Helper
 
     private
 
+    def wait_for_quorum
+      redis = build_sentinel_client
+      50.times do
+        if redis.sentinel('ckquorum', MASTER_NAME).start_with?('OK 3 usable Sentinels')
+          return
+        else
+          sleep 0.1
+        end
+      rescue
+        sleep 0.1
+      end
+      raise "ckquorum timeout"
+    end
+
     def _format_options(options = {})
       {
         url: "redis://#{MASTER_NAME}",
         sentinels: [{ host: LOCALHOST, port: SENTINEL_PORT }],
-        role: :master, timeout: TIMEOUT, logger: ::Logger.new(@log)
+        role: :master, timeout: TIMEOUT,
       }.merge(options)
     end
 
@@ -269,7 +268,6 @@ module Helper
     def _format_options(options)
       {
         timeout: OPTIONS[:timeout],
-        logger: ::Logger.new(@log)
       }.merge(options)
     end
 
@@ -277,158 +275,4 @@ module Helper
       Redis::Distributed.new(NODES, _format_options(options).merge(driver: ENV["conn"]))
     end
   end
-
-  module Cluster
-    include Generic
-
-    DEFAULT_HOST = '127.0.0.1'
-    DEFAULT_PORTS = (7000..7005).freeze
-
-    ClusterSlotsRawReply = lambda { |host, port|
-      # @see https://redis.io/topics/protocol
-      <<-REPLY.delete(' ')
-        *1\r
-        *4\r
-        :0\r
-        :16383\r
-        *3\r
-        $#{host.size}\r
-        #{host}\r
-        :#{port}\r
-        $40\r
-        649fa246273043021a05f547a79478597d3f1dc5\r
-        *3\r
-        $#{host.size}\r
-        #{host}\r
-        :#{port}\r
-        $40\r
-        649fa246273043021a05f547a79478597d3f1dc5\r
-      REPLY
-    }
-
-    ClusterNodesRawReply = lambda { |host, port|
-      line = "649fa246273043021a05f547a79478597d3f1dc5 #{host}:#{port}@17000 "\
-             'myself,master - 0 1530797742000 1 connected 0-16383'
-      "$#{line.size}\r\n#{line}\r\n"
-    }
-
-    def init(redis)
-      redis.flushall
-      redis
-    rescue Redis::CannotConnectError
-      puts <<-MSG
-
-        Cannot connect to Redis Cluster.
-
-        Make sure Redis is running on localhost, port #{DEFAULT_PORTS}.
-
-        Try this once:
-
-          $ make stop_cluster
-
-        Then run the build again:
-
-          $ make
-
-      MSG
-      exit! 1
-    end
-
-    def build_another_client(options = {})
-      _new_client(options)
-    end
-
-    def redis_cluster_mock(commands, options = {})
-      host = DEFAULT_HOST
-      port = nil
-
-      cluster_subcommands = if commands.key?(:cluster)
-        commands.delete(:cluster)
-                .map { |k, v| [k.to_s.downcase, v] }
-                .to_h
-      else
-        {}
-      end
-
-      commands[:cluster] = lambda { |subcommand, *args|
-        if cluster_subcommands.key?(subcommand)
-          cluster_subcommands[subcommand].call(*args)
-        else
-          case subcommand
-          when 'slots' then ClusterSlotsRawReply.call(host, port)
-          when 'nodes' then ClusterNodesRawReply.call(host, port)
-          else '+OK'
-          end
-        end
-      }
-
-      commands[:command] = ->(*_) { "*0\r\n" }
-
-      RedisMock.start(commands, options) do |po|
-        port = po
-        scheme = options[:ssl] ? 'rediss' : 'redis'
-        nodes = %W[#{scheme}://#{host}:#{port}]
-        yield _new_client(options.merge(cluster: nodes))
-      end
-    end
-
-    def redis_cluster_down
-      trib = ClusterOrchestrator.new(_default_nodes, timeout: TIMEOUT)
-      trib.down
-      yield
-    ensure
-      trib.rebuild
-      trib.close
-    end
-
-    def redis_cluster_failover
-      trib = ClusterOrchestrator.new(_default_nodes, timeout: TIMEOUT)
-      trib.failover
-      yield
-    ensure
-      trib.rebuild
-      trib.close
-    end
-
-    def redis_cluster_fail_master
-      trib = ClusterOrchestrator.new(_default_nodes, timeout: TIMEOUT)
-      trib.fail_serving_master
-      yield
-    ensure
-      trib.restart_cluster_nodes
-      trib.rebuild
-      trib.close
-    end
-
-    # @param slot [Integer]
-    # @param src [String] <ip>:<port>
-    # @param dest [String] <ip>:<port>
-    def redis_cluster_resharding(slot, src:, dest:)
-      trib = ClusterOrchestrator.new(_default_nodes, timeout: TIMEOUT)
-      trib.start_resharding(slot, src, dest)
-      yield
-      trib.finish_resharding(slot, dest)
-    ensure
-      trib.rebuild
-      trib.close
-    end
-
-    private
-
-    def _default_nodes(host: DEFAULT_HOST, ports: DEFAULT_PORTS)
-      ports.map { |port| "redis://#{host}:#{port}" }
-    end
-
-    def _format_options(options)
-      {
-        timeout: OPTIONS[:timeout],
-        logger: ::Logger.new(@log),
-        cluster: _default_nodes
-      }.merge(options)
-    end
-
-    def _new_client(options = {})
-      Redis.new(_format_options(options).merge(driver: ENV['DRIVER']))
-    end
-  end
 end
diff --git a/test/lint/authentication.rb b/test/lint/authentication.rb
index 6dad12ef20e1f2a90c2c97710d12619faf1393c7..7f960e0317e5f0d7b1c50148de3fb012a2012ddf 100644
--- a/test/lint/authentication.rb
+++ b/test/lint/authentication.rb
@@ -15,7 +15,7 @@ module Lint
     def test_auth_for_acl
       target_version "6.0.0" do
         with_acl do |username, password|
-          assert_raises(Redis::BaseError) { redis.auth(username, 'wrongpassword') }
+          assert_raises(Redis::CannotConnectError) { redis.auth(username, 'wrongpassword') }
           assert_equal 'OK', redis.auth(username, password)
           assert_equal 'PONG', redis.ping
           assert_raises(Redis::BaseError) { redis.echo('foo') }
diff --git a/test/lint/blocking_commands.rb b/test/lint/blocking_commands.rb
index 3500b53d05d507c5e6e7e62e8bf0f6556984cbd0..604400bf01092970eeeeffef2764f568ff17822c 100644
--- a/test/lint/blocking_commands.rb
+++ b/test/lint/blocking_commands.rb
@@ -27,7 +27,7 @@ module Lint
 
     def mock(options = {}, &blk)
       commands = build_mock_commands(options)
-      redis_mock(commands, { timeout: LOW_TIMEOUT }, &blk)
+      redis_mock(commands, { timeout: TIMEOUT }, &blk)
     end
 
     def build_mock_commands(options = {})
@@ -101,22 +101,8 @@ module Lint
     end
 
     def test_blpop_integer_like_timeout
-      mock do |r|
-        assert_equal ["{zap}foo", "1"], r.blpop("{zap}foo", FakeDuration.new(1))
-      end
-    end
-
-    def test_blpop_with_old_prototype
-      assert_equal ['{zap}foo', 's1'], r.blpop('{zap}foo', 0)
-      assert_equal ['{zap}foo', 's2'], r.blpop('{zap}foo', 0)
-      assert_equal ['{zap}bar', 's1'], r.blpop('{zap}bar', '{zap}foo', 0)
-      assert_equal ['{zap}bar', 's2'], r.blpop('{zap}foo', '{zap}bar', 0)
-    end
-
-    def test_blpop_timeout_with_old_prototype
-      mock do |r|
-        assert_equal ['{zap}foo', '0'], r.blpop('{zap}foo', 0)
-        assert_equal ['{zap}foo', '1'], r.blpop('{zap}foo', 1)
+      assert_raises ArgumentError do
+        assert_equal ["{zap}foo", "1"], r.blpop("{zap}foo", timeout: FakeDuration.new(1))
       end
     end
 
@@ -134,20 +120,6 @@ module Lint
       end
     end
 
-    def test_brpop_with_old_prototype
-      assert_equal ['{zap}foo', 's2'], r.brpop('{zap}foo', 0)
-      assert_equal ['{zap}foo', 's1'], r.brpop('{zap}foo', 0)
-      assert_equal ['{zap}bar', 's2'], r.brpop('{zap}bar', '{zap}foo', 0)
-      assert_equal ['{zap}bar', 's1'], r.brpop('{zap}foo', '{zap}bar', 0)
-    end
-
-    def test_brpop_timeout_with_old_prototype
-      mock do |r|
-        assert_equal ['{zap}foo', '0'], r.brpop('{zap}foo', 0)
-        assert_equal ['{zap}foo', '1'], r.brpop('{zap}foo', 1)
-      end
-    end
-
     def test_brpoplpush
       assert_equal 's2', r.brpoplpush('{zap}foo', '{zap}qux')
       assert_equal ['s2'], r.lrange('{zap}qux', 0, -1)
@@ -160,64 +132,56 @@ module Lint
       end
     end
 
-    def test_brpoplpush_with_old_prototype
-      assert_equal 's2', r.brpoplpush('{zap}foo', '{zap}qux', 0)
-      assert_equal ['s2'], r.lrange('{zap}qux', 0, -1)
+    def test_bzpopmin
+      assert_equal ['{szap}foo', 'a', 0.0], r.bzpopmin('{szap}foo', '{szap}bar', timeout: 1)
     end
 
-    def test_brpoplpush_timeout_with_old_prototype
-      mock do |r|
-        assert_equal '0', r.brpoplpush('{zap}foo', '{zap}bar', 0)
-        assert_equal '1', r.brpoplpush('{zap}foo', '{zap}bar', 1)
+    def test_bzpopmin_float_timeout
+      target_version "6.0" do
+        assert_nil r.bzpopmin('{szap}aaa', '{szap}bbb', timeout: LOW_TIMEOUT)
       end
     end
 
-    def test_bzpopmin
-      target_version('5.0.0') do
-        assert_equal ['{szap}foo', 'a', 0.0], r.bzpopmin('{szap}foo', '{szap}bar', 1)
-        assert_nil r.bzpopmin('{szap}aaa', '{szap}bbb', 2)
-      end
+    def test_bzpopmax
+      assert_equal ['{szap}foo', 'c', 2.0], r.bzpopmax('{szap}foo', '{szap}bar', timeout: 1)
     end
 
-    def test_bzpopmax
-      target_version('5.0.0') do
-        assert_equal ['{szap}foo', 'c', 2.0], r.bzpopmax('{szap}foo', '{szap}bar', 1)
-        assert_nil r.bzpopmax('{szap}aaa', '{szap}bbb', 1)
+    def test_bzpopmax_float_timeout
+      target_version "6.0" do
+        assert_nil r.bzpopmax('{szap}aaa', '{szap}bbb', timeout: LOW_TIMEOUT)
       end
     end
 
-    driver(:ruby, :hiredis) do
-      def test_blmove_socket_timeout
-        target_version "6.2" do
-          mock(delay: LOW_TIMEOUT * 5) do |r|
-            assert_raises(Redis::TimeoutError) do
-              r.blmove('{zap}foo', '{zap}bar', 'LEFT', 'RIGHT', timeout: LOW_TIMEOUT)
-            end
+    def test_blmove_socket_timeout
+      target_version "6.2" do
+        mock(delay: TIMEOUT * 5) do |r|
+          assert_raises(Redis::TimeoutError) do
+            r.blmove('{zap}foo', '{zap}bar', 'LEFT', 'RIGHT', timeout: LOW_TIMEOUT)
           end
         end
       end
+    end
 
-      def test_blpop_socket_timeout
-        mock(delay: LOW_TIMEOUT * 5) do |r|
-          assert_raises(Redis::TimeoutError) do
-            r.blpop('{zap}foo', timeout: LOW_TIMEOUT)
-          end
+    def test_blpop_socket_timeout
+      mock(delay: TIMEOUT * 5) do |r|
+        assert_raises(Redis::TimeoutError) do
+          r.blpop('{zap}foo', timeout: LOW_TIMEOUT)
         end
       end
+    end
 
-      def test_brpop_socket_timeout
-        mock(delay: LOW_TIMEOUT * 5) do |r|
-          assert_raises(Redis::TimeoutError) do
-            r.brpop('{zap}foo', timeout: LOW_TIMEOUT)
-          end
+    def test_brpop_socket_timeout
+      mock(delay: TIMEOUT * 5) do |r|
+        assert_raises(Redis::TimeoutError) do
+          r.brpop('{zap}foo', timeout: LOW_TIMEOUT)
         end
       end
+    end
 
-      def test_brpoplpush_socket_timeout
-        mock(delay: LOW_TIMEOUT * 5) do |r|
-          assert_raises(Redis::TimeoutError) do
-            r.brpoplpush('{zap}foo', '{zap}bar', timeout: LOW_TIMEOUT)
-          end
+    def test_brpoplpush_socket_timeout
+      mock(delay: TIMEOUT * 5) do |r|
+        assert_raises(Redis::TimeoutError) do
+          r.brpoplpush('{zap}foo', '{zap}bar', timeout: LOW_TIMEOUT)
         end
       end
     end
diff --git a/test/lint/hashes.rb b/test/lint/hashes.rb
index 4dc40e9f59d031eac9f562b34d9ad4fc3670803d..a6d470220a98b0ba790517947027357ab0280296 100644
--- a/test/lint/hashes.rb
+++ b/test/lint/hashes.rb
@@ -9,17 +9,15 @@ module Lint
     end
 
     def test_variadic_hset
-      target_version "4.0.0" do
-        assert_equal 2, r.hset("foo", "f1", "s1", "f2", "s2")
+      assert_equal 2, r.hset("foo", "f1", "s1", "f2", "s2")
 
-        assert_equal "s1", r.hget("foo", "f1")
-        assert_equal "s2", r.hget("foo", "f2")
+      assert_equal "s1", r.hget("foo", "f1")
+      assert_equal "s2", r.hget("foo", "f2")
 
-        assert_equal 2, r.hset("bar", { "f1" => "s1", "f2" => "s2" })
+      assert_equal 2, r.hset("bar", { "f1" => "s1", "f2" => "s2" })
 
-        assert_equal "s1", r.hget("bar", "f1")
-        assert_equal "s2", r.hget("bar", "f2")
-      end
+      assert_equal "s1", r.hget("bar", "f1")
+      assert_equal "s2", r.hget("bar", "f2")
     end
 
     def test_hsetnx
@@ -45,33 +43,29 @@ module Lint
     end
 
     def test_splat_hdel
-      target_version "2.3.9" do
-        r.hset("foo", "f1", "s1")
-        r.hset("foo", "f2", "s2")
+      r.hset("foo", "f1", "s1")
+      r.hset("foo", "f2", "s2")
 
-        assert_equal "s1", r.hget("foo", "f1")
-        assert_equal "s2", r.hget("foo", "f2")
+      assert_equal "s1", r.hget("foo", "f1")
+      assert_equal "s2", r.hget("foo", "f2")
 
-        assert_equal 2, r.hdel("foo", "f1", "f2")
+      assert_equal 2, r.hdel("foo", "f1", "f2")
 
-        assert_nil r.hget("foo", "f1")
-        assert_nil r.hget("foo", "f2")
-      end
+      assert_nil r.hget("foo", "f1")
+      assert_nil r.hget("foo", "f2")
     end
 
     def test_variadic_hdel
-      target_version "2.3.9" do
-        r.hset("foo", "f1", "s1")
-        r.hset("foo", "f2", "s2")
+      r.hset("foo", "f1", "s1")
+      r.hset("foo", "f2", "s2")
 
-        assert_equal "s1", r.hget("foo", "f1")
-        assert_equal "s2", r.hget("foo", "f2")
+      assert_equal "s1", r.hget("foo", "f1")
+      assert_equal "s2", r.hget("foo", "f2")
 
-        assert_equal 2, r.hdel("foo", ["f1", "f2"])
+      assert_equal 2, r.hdel("foo", ["f1", "f2"])
 
-        assert_nil r.hget("foo", "f1")
-        assert_nil r.hget("foo", "f2")
-      end
+      assert_nil r.hget("foo", "f1")
+      assert_nil r.hget("foo", "f2")
     end
 
     def test_hexists
@@ -137,12 +131,12 @@ module Lint
     end
 
     def test_hgetall
-      assert(r.hgetall("foo") == {})
+      assert_equal({}, r.hgetall("foo"))
 
       r.hset("foo", "f1", "s1")
       r.hset("foo", "f2", "s2")
 
-      assert(r.hgetall("foo") == { "f1" => "s1", "f2" => "s2" })
+      assert_equal({ "f1" => "s1", "f2" => "s2" }, r.hgetall("foo"))
     end
 
     def test_hmset
@@ -178,19 +172,19 @@ module Lint
       r.hset("foo", "f2", "s2")
       r.hset("foo", "f3", "s3")
 
-      assert(r.mapped_hmget("foo", "f1") == { "f1" => "s1" })
-      assert(r.mapped_hmget("foo", "f1", "f2") == { "f1" => "s1", "f2" => "s2" })
+      assert_equal({ "f1" => "s1" }, r.mapped_hmget("foo", "f1"))
+      assert_equal({ "f1" => "s1", "f2" => "s2" }, r.mapped_hmget("foo", "f1", "f2"))
     end
 
     def test_mapped_hmget_in_a_pipeline_returns_hash
       r.hset("foo", "f1", "s1")
       r.hset("foo", "f2", "s2")
 
-      result = r.pipelined do
-        r.mapped_hmget("foo", "f1", "f2")
+      result = r.pipelined do |pipeline|
+        pipeline.mapped_hmget("foo", "f1", "f2")
       end
 
-      assert_equal result[0], { "f1" => "s1", "f2" => "s2" }
+      assert_equal({ "f1" => "s1", "f2" => "s2" }, result[0])
     end
 
     def test_hincrby
@@ -208,28 +202,24 @@ module Lint
     end
 
     def test_hincrbyfloat
-      target_version "2.5.4" do
-        r.hincrbyfloat("foo", "f1", 1.23)
+      r.hincrbyfloat("foo", "f1", 1.23)
 
-        assert_equal 1.23, Float(r.hget("foo", "f1"))
+      assert_equal 1.23, Float(r.hget("foo", "f1"))
 
-        r.hincrbyfloat("foo", "f1", 0.77)
+      r.hincrbyfloat("foo", "f1", 0.77)
 
-        assert_equal "2", r.hget("foo", "f1")
+      assert_equal "2", r.hget("foo", "f1")
 
-        r.hincrbyfloat("foo", "f1", -0.1)
+      r.hincrbyfloat("foo", "f1", -0.1)
 
-        assert_equal 1.9, Float(r.hget("foo", "f1"))
-      end
+      assert_equal 1.9, Float(r.hget("foo", "f1"))
     end
 
     def test_hstrlen
-      target_version('3.2.0') do
-        redis.hmset('foo', 'f1', 'HelloWorld', 'f2', 99, 'f3', -256)
-        assert_equal 10, r.hstrlen('foo', 'f1')
-        assert_equal 2, r.hstrlen('foo', 'f2')
-        assert_equal 4, r.hstrlen('foo', 'f3')
-      end
+      redis.hmset('foo', 'f1', 'HelloWorld', 'f2', 99, 'f3', -256)
+      assert_equal 10, r.hstrlen('foo', 'f1')
+      assert_equal 2, r.hstrlen('foo', 'f2')
+      assert_equal 4, r.hstrlen('foo', 'f3')
     end
 
     def test_hscan
diff --git a/test/lint/hyper_log_log.rb b/test/lint/hyper_log_log.rb
index 7db1c14b4afb93c682a2642a295671ee43dcc473..ba2f3c596b53cd9a12ed1942a805d3a6d635dba3 100644
--- a/test/lint/hyper_log_log.rb
+++ b/test/lint/hyper_log_log.rb
@@ -3,66 +3,54 @@
 module Lint
   module HyperLogLog
     def test_pfadd
-      target_version "2.8.9" do
-        assert_equal true, r.pfadd("foo", "s1")
-        assert_equal true, r.pfadd("foo", "s2")
-        assert_equal false, r.pfadd("foo", "s1")
+      assert_equal true, r.pfadd("foo", "s1")
+      assert_equal true, r.pfadd("foo", "s2")
+      assert_equal false, r.pfadd("foo", "s1")
 
-        assert_equal 2, r.pfcount("foo")
-      end
+      assert_equal 2, r.pfcount("foo")
     end
 
     def test_variadic_pfadd
-      target_version "2.8.9" do
-        assert_equal true, r.pfadd("foo", ["s1", "s2"])
-        assert_equal true, r.pfadd("foo", ["s1", "s2", "s3"])
+      assert_equal true, r.pfadd("foo", ["s1", "s2"])
+      assert_equal true, r.pfadd("foo", ["s1", "s2", "s3"])
 
-        assert_equal 3, r.pfcount("foo")
-      end
+      assert_equal 3, r.pfcount("foo")
     end
 
     def test_pfcount
-      target_version "2.8.9" do
-        assert_equal 0, r.pfcount("foo")
+      assert_equal 0, r.pfcount("foo")
 
-        assert_equal true, r.pfadd("foo", "s1")
+      assert_equal true, r.pfadd("foo", "s1")
 
-        assert_equal 1, r.pfcount("foo")
-      end
+      assert_equal 1, r.pfcount("foo")
     end
 
     def test_variadic_pfcount
-      target_version "2.8.9" do
-        assert_equal 0, r.pfcount(["{1}foo", "{1}bar"])
+      assert_equal 0, r.pfcount(["{1}foo", "{1}bar"])
 
-        assert_equal true, r.pfadd("{1}foo", "s1")
-        assert_equal true, r.pfadd("{1}bar", "s1")
-        assert_equal true, r.pfadd("{1}bar", "s2")
+      assert_equal true, r.pfadd("{1}foo", "s1")
+      assert_equal true, r.pfadd("{1}bar", "s1")
+      assert_equal true, r.pfadd("{1}bar", "s2")
 
-        assert_equal 2, r.pfcount("{1}foo", "{1}bar")
-      end
+      assert_equal 2, r.pfcount("{1}foo", "{1}bar")
     end
 
     def test_variadic_pfcount_expanded
-      target_version "2.8.9" do
-        assert_equal 0, r.pfcount("{1}foo", "{1}bar")
+      assert_equal 0, r.pfcount("{1}foo", "{1}bar")
 
-        assert_equal true, r.pfadd("{1}foo", "s1")
-        assert_equal true, r.pfadd("{1}bar", "s1")
-        assert_equal true, r.pfadd("{1}bar", "s2")
+      assert_equal true, r.pfadd("{1}foo", "s1")
+      assert_equal true, r.pfadd("{1}bar", "s1")
+      assert_equal true, r.pfadd("{1}bar", "s2")
 
-        assert_equal 2, r.pfcount("{1}foo", "{1}bar")
-      end
+      assert_equal 2, r.pfcount("{1}foo", "{1}bar")
     end
 
     def test_pfmerge
-      target_version '2.8.9' do
-        r.pfadd 'foo', 's1'
-        r.pfadd 'bar', 's2'
+      r.pfadd 'foo', 's1'
+      r.pfadd 'bar', 's2'
 
-        assert_equal true, r.pfmerge('res', 'foo', 'bar')
-        assert_equal 2, r.pfcount('res')
-      end
+      assert_equal true, r.pfmerge('res', 'foo', 'bar')
+      assert_equal 2, r.pfcount('res')
     end
 
     def test_variadic_pfmerge_expanded
diff --git a/test/lint/lists.rb b/test/lint/lists.rb
index 534ab8b8e971b31d821f3ea775607552aa0bb8ad..ea18254dfd679b75f3efad4a21aee54a92a1049b 100644
--- a/test/lint/lists.rb
+++ b/test/lint/lists.rb
@@ -38,11 +38,9 @@ module Lint
     end
 
     def test_variadic_lpush
-      target_version "2.3.9" do # 2.4-rc6
-        assert_equal 3, r.lpush("foo", ["s1", "s2", "s3"])
-        assert_equal 3, r.llen("foo")
-        assert_equal "s3", r.lpop("foo")
-      end
+      assert_equal 3, r.lpush("foo", ["s1", "s2", "s3"])
+      assert_equal 3, r.llen("foo")
+      assert_equal "s3", r.lpop("foo")
     end
 
     def test_lpushx
@@ -63,11 +61,9 @@ module Lint
     end
 
     def test_variadic_rpush
-      target_version "2.3.9" do # 2.4-rc6
-        assert_equal 3, r.rpush("foo", ["s1", "s2", "s3"])
-        assert_equal 3, r.llen("foo")
-        assert_equal "s3", r.rpop("foo")
-      end
+      assert_equal 3, r.rpush("foo", ["s1", "s2", "s3"])
+      assert_equal 3, r.llen("foo")
+      assert_equal "s3", r.rpop("foo")
     end
 
     def test_rpushx
@@ -144,6 +140,7 @@ module Lint
       assert_equal 2, r.llen("foo")
       assert_equal "s1", r.lpop("foo")
       assert_equal 1, r.llen("foo")
+      assert_nil r.lpop("nonexistent")
     end
 
     def test_lpop_count
@@ -164,6 +161,7 @@ module Lint
       assert_equal 2, r.llen("foo")
       assert_equal "s2", r.rpop("foo")
       assert_equal 1, r.llen("foo")
+      assert_nil r.rpop("nonexistent")
     end
 
     def test_rpop_count
@@ -204,5 +202,31 @@ module Lint
       redis.rpush('{1}bar', %w[d e f])
       assert_equal 'c', redis.rpoplpush('{1}foo', '{1}bar')
     end
+
+    def test_blmpop
+      target_version('7.0') do
+        assert_nil r.blmpop(1.0, '{1}foo')
+
+        r.lpush('{1}foo', %w[a b c d e f g])
+        assert_equal ['{1}foo', ['g']], r.blmpop(1.0, '{1}foo')
+        assert_equal ['{1}foo', ['f', 'e']], r.blmpop(1.0, '{1}foo', count: 2)
+
+        r.lpush('{1}foo2', %w[a b])
+        assert_equal ['{1}foo', ['a']], r.blmpop(1.0, '{1}foo', '{1}foo2', modifier: "RIGHT")
+      end
+    end
+
+    def test_lmpop
+      target_version('7.0') do
+        assert_nil r.lmpop('{1}foo')
+
+        r.lpush('{1}foo', %w[a b c d e f g])
+        assert_equal ['{1}foo', ['g']], r.lmpop('{1}foo')
+        assert_equal ['{1}foo', ['f', 'e']], r.lmpop('{1}foo', count: 2)
+
+        r.lpush('{1}foo2', %w[a b])
+        assert_equal ['{1}foo', ['a']], r.lmpop('{1}foo', '{1}foo2', modifier: "RIGHT")
+      end
+    end
   end
 end
diff --git a/test/lint/sets.rb b/test/lint/sets.rb
index 3d5d25c04460d238711d6bf2a8a960b094d39405..bfaa5cf8451e476829d3e8f482c0fec3cdc2b294 100644
--- a/test/lint/sets.rb
+++ b/test/lint/sets.rb
@@ -3,9 +3,9 @@
 module Lint
   module Sets
     def test_sadd
-      assert_equal true, r.sadd("foo", "s1")
-      assert_equal true, r.sadd("foo", "s2")
-      assert_equal false, r.sadd("foo", "s1")
+      assert_equal 1, r.sadd("foo", "s1")
+      assert_equal 1, r.sadd("foo", "s2")
+      assert_equal 0, r.sadd("foo", "s1")
 
       assert_equal ["s1", "s2"], r.smembers("foo").sort
     end
@@ -13,26 +13,32 @@ module Lint
     def test_sadd?
       assert_equal true, r.sadd?("foo", "s1")
       assert_equal true, r.sadd?("foo", "s2")
-      assert_equal false, r.sadd?("foo", ["s1", "s2"])
+      assert_equal false, r.sadd?("foo", "s1")
 
       assert_equal ["s1", "s2"], r.smembers("foo").sort
     end
 
     def test_variadic_sadd
-      target_version "2.3.9" do # 2.4-rc6
-        assert_equal 2, r.sadd("foo", ["s1", "s2"])
-        assert_equal 1, r.sadd("foo", ["s1", "s2", "s3"])
+      assert_equal 2, r.sadd("foo", ["s1", "s2"])
+      assert_equal 1, r.sadd("foo", ["s1", "s2", "s3"])
 
-        assert_equal ["s1", "s2", "s3"], r.smembers("foo").sort
-      end
+      assert_equal ["s1", "s2", "s3"], r.smembers("foo").sort
+    end
+
+    def test_variadic_sadd?
+      assert_equal true, r.sadd?("foo", ["s1", "s2"])
+      assert_equal true, r.sadd?("foo", ["s1", "s2", "s3"])
+      assert_equal false, r.sadd?("foo", ["s1", "s2"])
+
+      assert_equal ["s1", "s2", "s3"], r.smembers("foo").sort
     end
 
     def test_srem
       r.sadd("foo", "s1")
       r.sadd("foo", "s2")
 
-      assert_equal true, r.srem("foo", "s1")
-      assert_equal false, r.srem("foo", "s3")
+      assert_equal 1, r.srem("foo", "s1")
+      assert_equal 0, r.srem("foo", "s3")
 
       assert_equal ["s2"], r.smembers("foo")
     end
@@ -41,24 +47,34 @@ module Lint
       r.sadd("foo", "s1")
       r.sadd("foo", "s2")
 
-      assert_equal true, r.srem?("foo", ["s1", "s5"])
-      assert_equal false, r.srem?("foo", ["s3", "s4"])
+      assert_equal true, r.srem?("foo", "s1")
+      assert_equal false, r.srem?("foo", "s3")
 
       assert_equal ["s2"], r.smembers("foo")
     end
 
     def test_variadic_srem
-      target_version "2.3.9" do # 2.4-rc6
-        r.sadd("foo", "s1")
-        r.sadd("foo", "s2")
-        r.sadd("foo", "s3")
+      r.sadd("foo", "s1")
+      r.sadd("foo", "s2")
+      r.sadd("foo", "s3")
 
-        assert_equal 1, r.srem("foo", ["s1", "aaa"])
-        assert_equal 0, r.srem("foo", ["bbb", "ccc", "ddd"])
-        assert_equal 1, r.srem("foo", ["eee", "s3"])
+      assert_equal 1, r.srem("foo", ["s1", "aaa"])
+      assert_equal 0, r.srem("foo", ["bbb", "ccc", "ddd"])
+      assert_equal 1, r.srem("foo", ["eee", "s3"])
 
-        assert_equal ["s2"], r.smembers("foo")
-      end
+      assert_equal ["s2"], r.smembers("foo")
+    end
+
+    def test_variadic_srem?
+      r.sadd("foo", "s1")
+      r.sadd("foo", "s2")
+      r.sadd("foo", "s3")
+
+      assert_equal true, r.srem?("foo", ["s1", "aaa"])
+      assert_equal false, r.srem?("foo", ["bbb", "ccc", "ddd"])
+      assert_equal true, r.srem?("foo", "eee", "s3")
+
+      assert_equal ["s2"], r.smembers("foo")
     end
 
     def test_spop
@@ -71,18 +87,16 @@ module Lint
     end
 
     def test_spop_with_positive_count
-      target_version "3.2.0" do
-        r.sadd "foo", "s1"
-        r.sadd "foo", "s2"
-        r.sadd "foo", "s3"
-        r.sadd "foo", "s4"
+      r.sadd "foo", "s1"
+      r.sadd "foo", "s2"
+      r.sadd "foo", "s3"
+      r.sadd "foo", "s4"
 
-        pops = r.spop("foo", 3)
+      pops = r.spop("foo", 3)
 
-        assert !(["s1", "s2", "s3", "s4"] & pops).empty?
-        assert_equal 3, pops.size
-        assert_equal 1, r.scard("foo")
-      end
+      assert !(["s1", "s2", "s3", "s4"] & pops).empty?
+      assert_equal 3, pops.size
+      assert_equal 1, r.scard("foo")
     end
 
     def test_scard
diff --git a/test/lint/sorted_sets.rb b/test/lint/sorted_sets.rb
index 4513e61bb7d5a5aaa5c2629a28a80692e6f9edeb..10f8ce5e98fcc6e47fca1709f445ab2e8021b663 100644
--- a/test/lint/sorted_sets.rb
+++ b/test/lint/sorted_sets.rb
@@ -9,43 +9,43 @@ module Lint
       assert_equal 1, r.zcard("foo")
       r.del "foo"
 
-      target_version "3.0.2" do
-        # XX option
-        assert_equal 0, r.zcard("foo")
-        assert_equal false, r.zadd("foo", 1, "s1", xx: true)
-        r.zadd("foo", 1, "s1")
-        assert_equal false, r.zadd("foo", 2, "s1", xx: true)
-        assert_equal 2, r.zscore("foo", "s1")
-        r.del "foo"
+      # XX option
+      assert_equal 0, r.zcard("foo")
+      assert_equal false, r.zadd("foo", 1, "s1", xx: true)
+      r.zadd("foo", 1, "s1")
+      assert_equal false, r.zadd("foo", 2, "s1", xx: true)
+      assert_equal 2, r.zscore("foo", "s1")
+      r.del "foo"
 
-        # NX option
-        assert_equal 0, r.zcard("foo")
-        assert_equal true, r.zadd("foo", 1, "s1", nx: true)
-        assert_equal false, r.zadd("foo", 2, "s1", nx: true)
-        assert_equal 1, r.zscore("foo", "s1")
-        assert_equal 1, r.zcard("foo")
-        r.del "foo"
+      # NX option
+      assert_equal 0, r.zcard("foo")
+      assert_equal true, r.zadd("foo", 1, "s1", nx: true)
+      assert_equal false, r.zadd("foo", 2, "s1", nx: true)
+      assert_equal 1, r.zscore("foo", "s1")
+      assert_equal 1, r.zcard("foo")
+      r.del "foo"
 
-        # CH option
-        assert_equal 0, r.zcard("foo")
-        assert_equal true, r.zadd("foo", 1, "s1", ch: true)
-        assert_equal false, r.zadd("foo", 1, "s1", ch: true)
-        assert_equal true, r.zadd("foo", 2, "s1", ch: true)
-        assert_equal 1, r.zcard("foo")
-        r.del "foo"
+      # CH option
+      assert_equal 0, r.zcard("foo")
+      assert_equal true, r.zadd("foo", 1, "s1", ch: true)
+      assert_equal false, r.zadd("foo", 1, "s1", ch: true)
+      assert_equal true, r.zadd("foo", 2, "s1", ch: true)
+      assert_equal 1, r.zcard("foo")
+      r.del "foo"
 
-        # INCR option
-        assert_equal 1.0, r.zadd("foo", 1, "s1", incr: true)
-        assert_equal 11.0, r.zadd("foo", 10, "s1", incr: true)
-        assert_equal(-Float::INFINITY, r.zadd("bar", "-inf", "s1", incr: true))
-        assert_equal(+Float::INFINITY, r.zadd("bar", "+inf", "s2", incr: true))
-        r.del 'foo'
-        r.del 'bar'
+      # INCR option
+      assert_equal 1.0, r.zadd("foo", 1, "s1", incr: true)
+      assert_equal 11.0, r.zadd("foo", 10, "s1", incr: true)
+      assert_equal(-Float::INFINITY, r.zadd("bar", "-inf", "s1", incr: true))
+      assert_equal(+Float::INFINITY, r.zadd("bar", "+inf", "s2", incr: true))
+      r.del 'foo'
+      r.del 'bar'
 
-        # Incompatible options combination
-        assert_raises(Redis::CommandError) { r.zadd("foo", 1, "s1", xx: true, nx: true) }
-      end
+      # Incompatible options combination
+      assert_raises(Redis::CommandError) { r.zadd("foo", 1, "s1", xx: true, nx: true) }
+    end
 
+    def test_zadd_keywords
       target_version "6.2" do
         # LT option
         r.zadd("foo", 2, "s1")
@@ -77,84 +77,82 @@ module Lint
     end
 
     def test_variadic_zadd
-      target_version "2.3.9" do # 2.4-rc6
-        # Non-nested array with pairs
-        assert_equal 0, r.zcard("foo")
+      # Non-nested array with pairs
+      assert_equal 0, r.zcard("foo")
 
-        assert_equal 2, r.zadd("foo", [1, "s1", 2, "s2"])
-        assert_equal 2, r.zcard("foo")
+      assert_equal 2, r.zadd("foo", [1, "s1", 2, "s2"])
+      assert_equal 2, r.zcard("foo")
 
-        assert_equal 1, r.zadd("foo", [4, "s1", 5, "s2", 6, "s3"])
-        assert_equal 3, r.zcard("foo")
+      assert_equal 1, r.zadd("foo", [4, "s1", 5, "s2", 6, "s3"])
+      assert_equal 3, r.zcard("foo")
 
-        r.del "foo"
+      r.del "foo"
 
-        # Nested array with pairs
-        assert_equal 0, r.zcard("foo")
+      # Nested array with pairs
+      assert_equal 0, r.zcard("foo")
 
-        assert_equal 2, r.zadd("foo", [[1, "s1"], [2, "s2"]])
-        assert_equal 2, r.zcard("foo")
+      assert_equal 2, r.zadd("foo", [[1, "s1"], [2, "s2"]])
+      assert_equal 2, r.zcard("foo")
 
-        assert_equal 1, r.zadd("foo", [[4, "s1"], [5, "s2"], [6, "s3"]])
-        assert_equal 3, r.zcard("foo")
+      assert_equal 1, r.zadd("foo", [[4, "s1"], [5, "s2"], [6, "s3"]])
+      assert_equal 3, r.zcard("foo")
 
-        r.del "foo"
+      r.del "foo"
 
-        # Empty array
-        assert_equal 0, r.zcard("foo")
+      # Empty array
+      assert_equal 0, r.zcard("foo")
 
-        assert_equal 0, r.zadd("foo", [])
-        assert_equal 0, r.zcard("foo")
+      assert_equal 0, r.zadd("foo", [])
+      assert_equal 0, r.zcard("foo")
 
-        r.del "foo"
+      r.del "foo"
 
-        # Wrong number of arguments
-        assert_raises(Redis::CommandError) { r.zadd("foo", ["bar"]) }
-        assert_raises(Redis::CommandError) { r.zadd("foo", ["bar", "qux", "zap"]) }
-      end
+      # Wrong number of arguments
+      assert_raises(Redis::CommandError) { r.zadd("foo", ["bar"]) }
+      assert_raises(Redis::CommandError) { r.zadd("foo", ["bar", "qux", "zap"]) }
 
-      target_version "3.0.2" do
-        # XX option
-        assert_equal 0, r.zcard("foo")
-        assert_equal 0, r.zadd("foo", [1, "s1", 2, "s2"], xx: true)
-        r.zadd("foo", [1, "s1", 2, "s2"])
-        assert_equal 0, r.zadd("foo", [2, "s1", 3, "s2", 4, "s3"], xx: true)
-        assert_equal 2, r.zscore("foo", "s1")
-        assert_equal 3, r.zscore("foo", "s2")
-        assert_nil r.zscore("foo", "s3")
-        assert_equal 2, r.zcard("foo")
-        r.del "foo"
+      # XX option
+      assert_equal 0, r.zcard("foo")
+      assert_equal 0, r.zadd("foo", [1, "s1", 2, "s2"], xx: true)
+      r.zadd("foo", [1, "s1", 2, "s2"])
+      assert_equal 0, r.zadd("foo", [2, "s1", 3, "s2", 4, "s3"], xx: true)
+      assert_equal 2, r.zscore("foo", "s1")
+      assert_equal 3, r.zscore("foo", "s2")
+      assert_nil r.zscore("foo", "s3")
+      assert_equal 2, r.zcard("foo")
+      r.del "foo"
 
-        # NX option
-        assert_equal 0, r.zcard("foo")
-        assert_equal 2, r.zadd("foo", [1, "s1", 2, "s2"], nx: true)
-        assert_equal 1, r.zadd("foo", [2, "s1", 3, "s2", 4, "s3"], nx: true)
-        assert_equal 1, r.zscore("foo", "s1")
-        assert_equal 2, r.zscore("foo", "s2")
-        assert_equal 4, r.zscore("foo", "s3")
-        assert_equal 3, r.zcard("foo")
-        r.del "foo"
+      # NX option
+      assert_equal 0, r.zcard("foo")
+      assert_equal 2, r.zadd("foo", [1, "s1", 2, "s2"], nx: true)
+      assert_equal 1, r.zadd("foo", [2, "s1", 3, "s2", 4, "s3"], nx: true)
+      assert_equal 1, r.zscore("foo", "s1")
+      assert_equal 2, r.zscore("foo", "s2")
+      assert_equal 4, r.zscore("foo", "s3")
+      assert_equal 3, r.zcard("foo")
+      r.del "foo"
 
-        # CH option
-        assert_equal 0, r.zcard("foo")
-        assert_equal 2, r.zadd("foo", [1, "s1", 2, "s2"], ch: true)
-        assert_equal 2, r.zadd("foo", [1, "s1", 3, "s2", 4, "s3"], ch: true)
-        assert_equal 3, r.zcard("foo")
-        r.del "foo"
+      # CH option
+      assert_equal 0, r.zcard("foo")
+      assert_equal 2, r.zadd("foo", [1, "s1", 2, "s2"], ch: true)
+      assert_equal 2, r.zadd("foo", [1, "s1", 3, "s2", 4, "s3"], ch: true)
+      assert_equal 3, r.zcard("foo")
+      r.del "foo"
 
-        # INCR option
-        assert_equal 1.0, r.zadd("foo", [1, "s1"], incr: true)
-        assert_equal 11.0, r.zadd("foo", [10, "s1"], incr: true)
-        assert_equal(-Float::INFINITY, r.zadd("bar", ["-inf", "s1"], incr: true))
-        assert_equal(+Float::INFINITY, r.zadd("bar", ["+inf", "s2"], incr: true))
-        assert_raises(Redis::CommandError) { r.zadd("foo", [1, "s1", 2, "s2"], incr: true) }
-        r.del 'foo'
-        r.del 'bar'
+      # INCR option
+      assert_equal 1.0, r.zadd("foo", [1, "s1"], incr: true)
+      assert_equal 11.0, r.zadd("foo", [10, "s1"], incr: true)
+      assert_equal(-Float::INFINITY, r.zadd("bar", ["-inf", "s1"], incr: true))
+      assert_equal(+Float::INFINITY, r.zadd("bar", ["+inf", "s2"], incr: true))
+      assert_raises(Redis::CommandError) { r.zadd("foo", [1, "s1", 2, "s2"], incr: true) }
+      r.del 'foo'
+      r.del 'bar'
 
-        # Incompatible options combination
-        assert_raises(Redis::CommandError) { r.zadd("foo", [1, "s1"], xx: true, nx: true) }
-      end
+      # Incompatible options combination
+      assert_raises(Redis::CommandError) { r.zadd("foo", [1, "s1"], xx: true, nx: true) }
+    end
 
+    def test_variadic_zadd_keywords
       target_version "6.2" do
         # LT option
         r.zadd("foo", 2, "s1")
@@ -189,25 +187,23 @@ module Lint
     end
 
     def test_variadic_zrem
-      target_version "2.3.9" do # 2.4-rc6
-        r.zadd("foo", 1, "s1")
-        r.zadd("foo", 2, "s2")
-        r.zadd("foo", 3, "s3")
+      r.zadd("foo", 1, "s1")
+      r.zadd("foo", 2, "s2")
+      r.zadd("foo", 3, "s3")
 
-        assert_equal 3, r.zcard("foo")
+      assert_equal 3, r.zcard("foo")
 
-        assert_equal 0, r.zrem("foo", [])
-        assert_equal 3, r.zcard("foo")
+      assert_equal 0, r.zrem("foo", [])
+      assert_equal 3, r.zcard("foo")
 
-        assert_equal 1, r.zrem("foo", ["s1", "aaa"])
-        assert_equal 2, r.zcard("foo")
+      assert_equal 1, r.zrem("foo", ["s1", "aaa"])
+      assert_equal 2, r.zcard("foo")
 
-        assert_equal 0, r.zrem("foo", ["bbb", "ccc", "ddd"])
-        assert_equal 2, r.zcard("foo")
+      assert_equal 0, r.zrem("foo", ["bbb", "ccc", "ddd"])
+      assert_equal 2, r.zcard("foo")
 
-        assert_equal 1, r.zrem("foo", ["eee", "s3"])
-        assert_equal 1, r.zcard("foo")
-      end
+      assert_equal 1, r.zrem("foo", ["eee", "s3"])
+      assert_equal 1, r.zcard("foo")
     end
 
     def test_zincrby
@@ -470,20 +466,44 @@ module Lint
     end
 
     def test_zpopmax
-      target_version('5.0.0') do
-        r.zadd('foo', %w[0 a 1 b 2 c 3 d])
-        assert_equal ['d', 3.0], r.zpopmax('foo')
-        assert_equal [['c', 2.0], ['b', 1.0]], r.zpopmax('foo', 2)
-        assert_equal [['a', 0.0]], r.zrange('foo', 0, -1, with_scores: true)
-      end
+      r.zadd('foo', %w[0 a 1 b 2 c 3 d])
+      assert_equal ['d', 3.0], r.zpopmax('foo')
+      assert_equal [['c', 2.0], ['b', 1.0]], r.zpopmax('foo', 2)
+      assert_equal [['a', 0.0]], r.zrange('foo', 0, -1, with_scores: true)
     end
 
     def test_zpopmin
-      target_version('5.0.0') do
-        r.zadd('foo', %w[0 a 1 b 2 c 3 d])
-        assert_equal ['a', 0.0], r.zpopmin('foo')
-        assert_equal [['b', 1.0], ['c', 2.0]], r.zpopmin('foo', 2)
-        assert_equal [['d', 3.0]], r.zrange('foo', 0, -1, with_scores: true)
+      r.zadd('foo', %w[0 a 1 b 2 c 3 d])
+      assert_equal ['a', 0.0], r.zpopmin('foo')
+      assert_equal [['b', 1.0], ['c', 2.0]], r.zpopmin('foo', 2)
+      assert_equal [['d', 3.0]], r.zrange('foo', 0, -1, with_scores: true)
+    end
+
+    def test_bzmpop
+      target_version('7.0') do
+        assert_nil r.bzmpop(1.0, '{1}foo')
+
+        r.zadd('{1}foo', %w[0 a 1 b 2 c 3 d])
+        assert_equal ['{1}foo', [['a', 0.0]]], r.bzmpop(1.0, '{1}foo')
+        assert_equal ['{1}foo', [['b', 1.0], ['c', 2.0], ['d', 3.0]]], r.bzmpop(1.0, '{1}foo', count: 4)
+
+        r.zadd('{1}foo', %w[0 a 1 b 2 c 3 d])
+        r.zadd('{1}foo2', %w[0 a 1 b 2 c 3 d])
+        assert_equal ['{1}foo', [['d', 3.0]]], r.bzmpop(1.0, '{1}foo', '{1}foo2', modifier: "MAX")
+      end
+    end
+
+    def test_zmpop
+      target_version('7.0') do
+        assert_nil r.zmpop('{1}foo')
+
+        r.zadd('{1}foo', %w[0 a 1 b 2 c 3 d])
+        assert_equal ['{1}foo', [['a', 0.0]]], r.zmpop('{1}foo')
+        assert_equal ['{1}foo', [['b', 1.0], ['c', 2.0], ['d', 3.0]]], r.zmpop('{1}foo', count: 4)
+
+        r.zadd('{1}foo', %w[0 a 1 b 2 c 3 d])
+        r.zadd('{1}foo2', %w[0 a 1 b 2 c 3 d])
+        assert_equal ['{1}foo', [['d', 3.0]]], r.zmpop('{1}foo', '{1}foo2', modifier: "MAX")
       end
     end
 
@@ -493,46 +513,40 @@ module Lint
     end
 
     def test_zlexcount
-      target_version '2.8.9' do
-        r.zadd 'foo', 0, 'aaren'
-        r.zadd 'foo', 0, 'abagael'
-        r.zadd 'foo', 0, 'abby'
-        r.zadd 'foo', 0, 'abbygail'
-
-        assert_equal 4, r.zlexcount('foo', '[a', "[a\xff")
-        assert_equal 4, r.zlexcount('foo', '[aa', "[ab\xff")
-        assert_equal 3, r.zlexcount('foo', '(aaren', "[ab\xff")
-        assert_equal 2, r.zlexcount('foo', '[aba', '(abbygail')
-        assert_equal 1, r.zlexcount('foo', '(aaren', '(abby')
-      end
+      r.zadd 'foo', 0, 'aaren'
+      r.zadd 'foo', 0, 'abagael'
+      r.zadd 'foo', 0, 'abby'
+      r.zadd 'foo', 0, 'abbygail'
+
+      assert_equal 4, r.zlexcount('foo', '[a', "[a\xff")
+      assert_equal 4, r.zlexcount('foo', '[aa', "[ab\xff")
+      assert_equal 3, r.zlexcount('foo', '(aaren', "[ab\xff")
+      assert_equal 2, r.zlexcount('foo', '[aba', '(abbygail')
+      assert_equal 1, r.zlexcount('foo', '(aaren', '(abby')
     end
 
     def test_zrangebylex
-      target_version '2.8.9' do
-        r.zadd 'foo', 0, 'aaren'
-        r.zadd 'foo', 0, 'abagael'
-        r.zadd 'foo', 0, 'abby'
-        r.zadd 'foo', 0, 'abbygail'
-
-        assert_equal %w[aaren abagael abby abbygail], r.zrangebylex('foo', '[a', "[a\xff")
-        assert_equal %w[aaren abagael], r.zrangebylex('foo', '[a', "[a\xff", limit: [0, 2])
-        assert_equal %w[abby abbygail], r.zrangebylex('foo', '(abb', "(abb\xff")
-        assert_equal %w[abbygail], r.zrangebylex('foo', '(abby', "(abby\xff")
-      end
+      r.zadd 'foo', 0, 'aaren'
+      r.zadd 'foo', 0, 'abagael'
+      r.zadd 'foo', 0, 'abby'
+      r.zadd 'foo', 0, 'abbygail'
+
+      assert_equal %w[aaren abagael abby abbygail], r.zrangebylex('foo', '[a', "[a\xff")
+      assert_equal %w[aaren abagael], r.zrangebylex('foo', '[a', "[a\xff", limit: [0, 2])
+      assert_equal %w[abby abbygail], r.zrangebylex('foo', '(abb', "(abb\xff")
+      assert_equal %w[abbygail], r.zrangebylex('foo', '(abby', "(abby\xff")
     end
 
     def test_zrevrangebylex
-      target_version '2.9.9' do
-        r.zadd 'foo', 0, 'aaren'
-        r.zadd 'foo', 0, 'abagael'
-        r.zadd 'foo', 0, 'abby'
-        r.zadd 'foo', 0, 'abbygail'
-
-        assert_equal %w[abbygail abby abagael aaren], r.zrevrangebylex('foo', "[a\xff", '[a')
-        assert_equal %w[abbygail abby], r.zrevrangebylex('foo', "[a\xff", '[a', limit: [0, 2])
-        assert_equal %w[abbygail abby], r.zrevrangebylex('foo', "(abb\xff", '(abb')
-        assert_equal %w[abbygail], r.zrevrangebylex('foo', "(abby\xff", '(abby')
-      end
+      r.zadd 'foo', 0, 'aaren'
+      r.zadd 'foo', 0, 'abagael'
+      r.zadd 'foo', 0, 'abby'
+      r.zadd 'foo', 0, 'abbygail'
+
+      assert_equal %w[abbygail abby abagael aaren], r.zrevrangebylex('foo', "[a\xff", '[a')
+      assert_equal %w[abbygail abby], r.zrevrangebylex('foo', "[a\xff", '[a', limit: [0, 2])
+      assert_equal %w[abbygail abby], r.zrevrangebylex('foo', "(abb\xff", '(abb')
+      assert_equal %w[abbygail], r.zrevrangebylex('foo', "(abby\xff", '(abby')
     end
 
     def test_zcount
diff --git a/test/lint/streams.rb b/test/lint/streams.rb
index d7060fbb0e2c0e81b5066c30a11d3f420b6ae784..f23a4ae8691331fef6cf3e569c9421d57a45500d 100644
--- a/test/lint/streams.rb
+++ b/test/lint/streams.rb
@@ -3,7 +3,6 @@
 module Lint
   module Streams
     MIN_REDIS_VERSION = '4.9.0'
-    MIN_REDIS_VERSION_XAUTOCLAIM = '6.2.0'
     ENTRY_ID_FORMAT = /\d+-\d+/.freeze
 
     def setup
@@ -90,8 +89,18 @@ module Lint
       assert_match ENTRY_ID_FORMAT, actual
     end
 
+    def test_xadd_with_nomkstream_option
+      omit_version('6.2.0')
+
+      actual = redis.xadd('s1', { f1: 'v1', f2: 'v2' }, nomkstream: true)
+      assert_nil actual
+
+      actual = redis.xadd('s1', { f1: 'v1', f2: 'v2' }, nomkstream: false)
+      assert_match ENTRY_ID_FORMAT, actual
+    end
+
     def test_xadd_with_invalid_arguments
-      assert_raises(Redis::CommandError) { redis.xadd(nil, {}) }
+      assert_raises(TypeError) { redis.xadd(nil, {}) }
       assert_raises(Redis::CommandError) { redis.xadd('', {}) }
       assert_raises(Redis::CommandError) { redis.xadd('s1', {}) }
     end
@@ -112,6 +121,62 @@ module Lint
       assert_equal 0, redis.xtrim('s1', 2, approximate: true)
     end
 
+    def test_xtrim_with_limit_option
+      omit_version('6.2.0')
+
+      begin
+        original = redis.config(:get, 'stream-node-max-entries')['stream-node-max-entries']
+        redis.config(:set, 'stream-node-max-entries', 1)
+
+        redis.xadd('s1', { f: 'v1' })
+        redis.xadd('s1', { f: 'v2' })
+        redis.xadd('s1', { f: 'v3' })
+        redis.xadd('s1', { f: 'v4' })
+
+        assert_equal 1, redis.xtrim('s1', 0, approximate: true, limit: 1)
+        error = assert_raises(Redis::CommandError) { redis.xtrim('s1', 0, limit: 1) }
+        assert_includes error.message, "ERR syntax error, LIMIT cannot be used without the special ~ option"
+      ensure
+        redis.config(:set, 'stream-node-max-entries', original)
+      end
+    end
+
+    def test_xtrim_with_maxlen_strategy
+      redis.xadd('s1', { f: 'v1' }, id: '0-1')
+      redis.xadd('s1', { f: 'v1' }, id: '0-2')
+      redis.xadd('s1', { f: 'v1' }, id: '1-0')
+      redis.xadd('s1', { f: 'v1' }, id: '1-1')
+      assert_equal(2, redis.xtrim('s1', 2, strategy: 'MAXLEN'))
+    end
+
+    def test_xtrim_with_minid_strategy
+      omit_version('6.2.0')
+
+      redis.xadd('s1', { f: 'v1' }, id: '0-1')
+      redis.xadd('s1', { f: 'v1' }, id: '0-2')
+      redis.xadd('s1', { f: 'v1' }, id: '1-0')
+      redis.xadd('s1', { f: 'v1' }, id: '1-1')
+      assert_equal(2, redis.xtrim('s1', '1-0', strategy: 'MINID'))
+    end
+
+    def test_xtrim_with_approximate_minid_strategy
+      omit_version('6.2.0')
+
+      redis.xadd('s1', { f: 'v1' }, id: '0-1')
+      redis.xadd('s1', { f: 'v1' }, id: '0-2')
+      redis.xadd('s1', { f: 'v1' }, id: '1-0')
+      redis.xadd('s1', { f: 'v1' }, id: '1-1')
+      assert_equal(0, redis.xtrim('s1', '1-0', strategy: 'MINID', approximate: true))
+    end
+
+    def test_xtrim_with_invalid_strategy
+      omit_version('6.2.0')
+
+      redis.xadd('s1', { f: 'v1' })
+      error = assert_raises(Redis::CommandError) { redis.xtrim('s1', '1-0', strategy: '') }
+      assert_includes error.message, "ERR syntax error"
+    end
+
     def test_xtrim_with_not_existed_stream
       assert_equal 0, redis.xtrim('not-existed-stream', 2)
     end
@@ -119,12 +184,10 @@ module Lint
     def test_xtrim_with_invalid_arguments
       if version >= '6.2'
         assert_raises(Redis::CommandError) { redis.xtrim('', '') }
-        assert_raises(Redis::CommandError) { redis.xtrim(nil, nil) }
         assert_equal 0, redis.xtrim('s1', 0)
         assert_raises(Redis::CommandError) { redis.xtrim('s1', -1, approximate: true) }
       else
         assert_equal 0, redis.xtrim('', '')
-        assert_equal 0, redis.xtrim(nil, nil)
         assert_equal 0, redis.xtrim('s1', 0)
         assert_equal 0, redis.xtrim('s1', -1, approximate: true)
       end
@@ -146,8 +209,8 @@ module Lint
     end
 
     def test_xdel_with_invalid_arguments
-      assert_equal 0, redis.xdel(nil, nil)
-      assert_equal 0, redis.xdel(nil, [nil])
+      assert_raises(TypeError) { redis.xdel(nil, nil) }
+      assert_raises(TypeError) { redis.xdel(nil, [nil]) }
       assert_equal 0, redis.xdel('', '')
       assert_equal 0, redis.xdel('', [''])
       assert_raises(Redis::CommandError) { redis.xdel('s1', []) }
@@ -222,7 +285,7 @@ module Lint
     end
 
     def test_xrange_with_invalid_arguments
-      assert_equal([], redis.xrange(nil))
+      assert_raises(TypeError) { redis.xrange(nil) }
       assert_equal([], redis.xrange(''))
     end
 
@@ -298,7 +361,7 @@ module Lint
     end
 
     def test_xrevrange_with_invalid_arguments
-      assert_equal([], redis.xrevrange(nil))
+      assert_raises(TypeError) { redis.xrevrange(nil) }
       assert_equal([], redis.xrevrange(''))
     end
 
@@ -313,7 +376,7 @@ module Lint
     end
 
     def test_xlen_with_invalid_key
-      assert_equal 0, redis.xlen(nil)
+      assert_raises(TypeError) { redis.xlen(nil) }
       assert_equal 0, redis.xlen('')
     end
 
@@ -357,19 +420,20 @@ module Lint
     def test_xread_does_not_raise_timeout_error_when_the_block_option_is_zero_msec
       prepared = false
       actual = nil
-      wire = Wire.new do
+      thread = Thread.new do
         prepared = true
         actual = redis.xread('s1', 0, block: 0)
       end
-      Wire.pass until prepared
-      redis.dup.xadd('s1', { f: 'v1' }, id: '0-1')
-      wire.join
+      Thread.pass until prepared
+      redis2 = init _new_client
+      redis2.xadd('s1', { f: 'v1' }, id: '0-1')
+      thread.join(3)
 
       assert_equal(['v1'], actual.fetch('s1').map { |i| i.last['f'] })
     end
 
     def test_xread_with_invalid_arguments
-      assert_raises(Redis::CommandError) { redis.xread(nil, nil) }
+      assert_raises(TypeError) { redis.xread(nil, nil) }
       assert_raises(Redis::CommandError) { redis.xread('', '') }
       assert_raises(Redis::CommandError) { redis.xread([], []) }
       assert_raises(Redis::CommandError) { redis.xread([''], ['']) }
@@ -481,7 +545,7 @@ module Lint
     end
 
     def test_xreadgroup_with_invalid_arguments
-      assert_raises(Redis::CommandError) { redis.xreadgroup(nil, nil, nil, nil) }
+      assert_raises(TypeError) { redis.xreadgroup(nil, nil, nil, nil) }
       assert_raises(Redis::CommandError) { redis.xreadgroup('', '', '', '') }
       assert_raises(Redis::CommandError) { redis.xreadgroup('', '', [], []) }
       assert_raises(Redis::CommandError) { redis.xreadgroup('', '', [''], ['']) }
@@ -534,7 +598,7 @@ module Lint
     end
 
     def test_xack_with_invalid_arguments
-      assert_equal 0, redis.xack(nil, nil, nil)
+      assert_raises(TypeError) { redis.xack(nil, nil, nil) }
       assert_equal 0, redis.xack('', '', '')
       assert_raises(Redis::CommandError) { redis.xack('', '', []) }
       assert_equal 0, redis.xack('', '', [''])
@@ -641,12 +705,12 @@ module Lint
     end
 
     def test_xclaim_with_invalid_arguments
-      assert_raises(Redis::CommandError) { redis.xclaim(nil, nil, nil, nil, nil) }
+      assert_raises(TypeError) { redis.xclaim(nil, nil, nil, nil, nil) }
       assert_raises(Redis::CommandError) { redis.xclaim('', '', '', '', '') }
     end
 
     def test_xautoclaim
-      omit_version(MIN_REDIS_VERSION_XAUTOCLAIM)
+      omit_version('6.2.0')
 
       redis.xadd('s1', { f: 'v1' }, id: '0-1')
       redis.xgroup(:create, 's1', 'g1', '$')
@@ -663,7 +727,7 @@ module Lint
     end
 
     def test_xautoclaim_with_justid_option
-      omit_version(MIN_REDIS_VERSION_XAUTOCLAIM)
+      omit_version('6.2.0')
 
       redis.xadd('s1', { f: 'v1' }, id: '0-1')
       redis.xgroup(:create, 's1', 'g1', '$')
@@ -679,7 +743,7 @@ module Lint
     end
 
     def test_xautoclaim_with_count_option
-      omit_version(MIN_REDIS_VERSION_XAUTOCLAIM)
+      omit_version('6.2.0')
 
       redis.xadd('s1', { f: 'v1' }, id: '0-1')
       redis.xgroup(:create, 's1', 'g1', '$')
@@ -696,7 +760,7 @@ module Lint
     end
 
     def test_xautoclaim_with_larger_interval
-      omit_version(MIN_REDIS_VERSION_XAUTOCLAIM)
+      omit_version('6.2.0')
 
       redis.xadd('s1', { f: 'v1' }, id: '0-1')
       redis.xgroup(:create, 's1', 'g1', '$')
@@ -711,6 +775,22 @@ module Lint
       assert_equal [], actual['entries']
     end
 
+    def test_xautoclaim_with_deleted_entry
+      omit_version('6.2.0')
+
+      redis.xadd('s1', { f: 'v1' }, id: '0-1')
+      redis.xgroup(:create, 's1', 'g1', '$')
+      redis.xadd('s1', { f: 'v2' }, id: '0-2')
+      redis.xreadgroup('g1', 'c1', 's1', '>')
+      redis.xdel('s1', '0-2')
+      sleep 0.01
+
+      actual = redis.xautoclaim('s1', 'g1', 'c2', 0, '0-0')
+
+      assert_equal '0-0', actual['next']
+      assert_equal [], actual['entries']
+    end
+
     def test_xpending
       redis.xadd('s1', { f: 'v1' }, id: '0-1')
       redis.xgroup(:create, 's1', 'g1', '$')
@@ -752,6 +832,51 @@ module Lint
       assert_equal 1, actual[2]['count']
     end
 
+    def test_xpending_with_range_and_idle_options
+      target_version "6.2" do
+        redis.xadd('s1', { f: 'v1' }, id: '0-1')
+        redis.xgroup(:create, 's1', 'g1', '$')
+        redis.xadd('s1', { f: 'v2' }, id: '0-2')
+        redis.xadd('s1', { f: 'v3' }, id: '0-3')
+        redis.xreadgroup('g1', 'c1', 's1', '>')
+
+        actual = redis.xpending('s1', 'g1', '-', '+', 10)
+        assert_equal 2, actual.size
+        actual = redis.xpending('s1', 'g1', '-', '+', 10, idle: 10)
+        assert_equal 0, actual.size
+        sleep 0.1
+        actual = redis.xpending('s1', 'g1', '-', '+', 10, idle: 10)
+        assert_equal 2, actual.size
+
+        redis.xadd('s1', { f: 'v4' }, id: '0-4')
+        redis.xreadgroup('g1', 'c2', 's1', '>')
+
+        actual = redis.xpending('s1', 'g1', '-', '+', 10, idle: 1000)
+        assert_equal 0, actual.size
+
+        actual = redis.xpending('s1', 'g1', '-', '+', 10)
+        assert_equal 3, actual.size
+        actual = redis.xpending('s1', 'g1', '-', '+', 10, idle: 10)
+        assert_equal 2, actual.size
+        sleep 0.01
+        actual = redis.xpending('s1', 'g1', '-', '+', 10, idle: 10)
+        assert_equal 3, actual.size
+
+        assert_equal '0-2', actual[0]['entry_id']
+        assert_equal 'c1', actual[0]['consumer']
+        assert_equal true, actual[0]['elapsed'] >= 0
+        assert_equal 1, actual[0]['count']
+        assert_equal '0-3', actual[1]['entry_id']
+        assert_equal 'c1', actual[1]['consumer']
+        assert_equal true, actual[1]['elapsed'] >= 0
+        assert_equal 1, actual[1]['count']
+        assert_equal '0-4', actual[2]['entry_id']
+        assert_equal 'c2', actual[2]['consumer']
+        assert_equal true, actual[2]['elapsed'] >= 0
+        assert_equal 1, actual[2]['count']
+      end
+    end
+
     def test_xpending_with_range_and_consumer_options
       redis.xadd('s1', { f: 'v1' }, id: '0-1')
       redis.xgroup(:create, 's1', 'g1', '$')
diff --git a/test/lint/strings.rb b/test/lint/strings.rb
index 78a483aa9424b85faffe5ae160781c740ea3cc84..ebf10a8f8f85d21b5a952657f87ee69993c83f7b 100644
--- a/test/lint/strings.rb
+++ b/test/lint/strings.rb
@@ -27,28 +27,22 @@ module Lint
     end
 
     def test_set_and_get_with_ascii_characters
-      with_external_encoding("ASCII-8BIT") do
-        (0..255).each do |i|
-          str = "#{i.chr}---#{i.chr}"
-          r.set("foo", str)
+      (0..255).each do |i|
+        str = "#{i.chr}---#{i.chr}"
+        r.set("foo", str)
 
-          assert_equal str, r.get("foo")
-        end
+        assert_equal str, r.get("foo")
       end
     end
 
     def test_set_with_ex
-      target_version "2.6.12" do
-        r.set("foo", "bar", ex: 2)
-        assert_in_range 0..2, r.ttl("foo")
-      end
+      r.set("foo", "bar", ex: 2)
+      assert_in_range 0..2, r.ttl("foo")
     end
 
     def test_set_with_px
-      target_version "2.6.12" do
-        r.set("foo", "bar", px: 2000)
-        assert_in_range 0..2, r.ttl("foo")
-      end
+      r.set("foo", "bar", px: 2000)
+      assert_in_range 0..2, r.ttl("foo")
     end
 
     def test_set_with_exat
@@ -66,26 +60,22 @@ module Lint
     end
 
     def test_set_with_nx
-      target_version "2.6.12" do
-        r.set("foo", "qux", nx: true)
-        assert !r.set("foo", "bar", nx: true)
-        assert_equal "qux", r.get("foo")
+      r.set("foo", "qux", nx: true)
+      assert !r.set("foo", "bar", nx: true)
+      assert_equal "qux", r.get("foo")
 
-        r.del("foo")
-        assert r.set("foo", "bar", nx: true)
-        assert_equal "bar", r.get("foo")
-      end
+      r.del("foo")
+      assert r.set("foo", "bar", nx: true)
+      assert_equal "bar", r.get("foo")
     end
 
     def test_set_with_xx
-      target_version "2.6.12" do
-        r.set("foo", "qux")
-        assert r.set("foo", "bar", xx: true)
-        assert_equal "bar", r.get("foo")
+      r.set("foo", "qux")
+      assert r.set("foo", "bar", xx: true)
+      assert_equal "bar", r.get("foo")
 
-        r.del("foo")
-        assert !r.set("foo", "bar", xx: true)
-      end
+      r.del("foo")
+      assert !r.set("foo", "bar", xx: true)
     end
 
     def test_set_with_keepttl
@@ -124,21 +114,17 @@ module Lint
     end
 
     def test_psetex
-      target_version "2.5.4" do
-        assert r.psetex("foo", 1000, "bar")
-        assert_equal "bar", r.get("foo")
-        assert [0, 1].include? r.ttl("foo")
-      end
+      assert r.psetex("foo", 1000, "bar")
+      assert_equal "bar", r.get("foo")
+      assert [0, 1].include? r.ttl("foo")
     end
 
     def test_psetex_with_non_string_value
-      target_version "2.5.4" do
-        value = ["b", "a", "r"]
+      value = ["b", "a", "r"]
 
-        assert r.psetex("foo", 1000, value)
-        assert_equal value.to_s, r.get("foo")
-        assert [0, 1].include? r.ttl("foo")
-      end
+      assert r.psetex("foo", 1000, value)
+      assert_equal value.to_s, r.get("foo")
+      assert [0, 1].include? r.ttl("foo")
     end
 
     def test_getex
@@ -208,11 +194,9 @@ module Lint
     end
 
     def test_incrbyfloat
-      target_version "2.5.4" do
-        assert_equal 1.23, r.incrbyfloat("foo", 1.23)
-        assert_equal 2, r.incrbyfloat("foo", 0.77)
-        assert_equal 1.9, r.incrbyfloat("foo", -0.1)
-      end
+      assert_equal 1.23, r.incrbyfloat("foo", 1.23)
+      assert_equal 2, r.incrbyfloat("foo", 0.77)
+      assert_equal 1.9, r.incrbyfloat("foo", -0.1)
     end
 
     def test_decr
@@ -259,11 +243,18 @@ module Lint
     end
 
     def test_bitcount
-      target_version "2.5.10" do
+      r.set("foo", "abcde")
+
+      assert_equal 10, r.bitcount("foo", 1, 3)
+      assert_equal 17, r.bitcount("foo", 0, -1)
+    end
+
+    def test_bitcount_bits_range
+      target_version "7.0" do
         r.set("foo", "abcde")
 
-        assert_equal 10, r.bitcount("foo", 1, 3)
-        assert_equal 17, r.bitcount("foo", 0, -1)
+        assert_equal 10, r.bitcount("foo", 8, 31, scale: :bit)
+        assert_equal 17, r.bitcount("foo", 0, -1, scale: :byte)
       end
     end
 
@@ -299,10 +290,8 @@ module Lint
     end
 
     def test_bitfield
-      target_version('3.2.0') do
-        mock(bitfield: ->(*_) { "*2\r\n:1\r\n:0\r\n" }) do |redis|
-          assert_equal [1, 0], redis.bitfield('foo', 'INCRBY', 'i5', 100, 1, 'GET', 'u4', 0)
-        end
+      mock(bitfield: ->(*_) { "*2\r\n:1\r\n:0\r\n" }) do |redis|
+        assert_equal [1, 0], redis.bitfield('foo', 'INCRBY', 'i5', 100, 1, 'GET', 'u4', 0)
       end
     end
 
@@ -312,6 +301,7 @@ module Lint
 
       assert_equal %w[s1 s2],         r.mget('{1}foo', '{1}bar')
       assert_equal ['s1', 's2', nil], r.mget('{1}foo', '{1}bar', '{1}baz')
+      assert_equal ['s1', 's2', nil], r.mget(['{1}foo', '{1}bar', '{1}baz'])
     end
 
     def test_mget_mapped
@@ -334,8 +324,8 @@ module Lint
       r.set('{1}foo', 's1')
       r.set('{1}bar', 's2')
 
-      result = r.pipelined do
-        r.mapped_mget('{1}foo', '{1}bar')
+      result = r.pipelined do |pipeline|
+        pipeline.mapped_mget('{1}foo', '{1}bar')
       end
 
       assert_equal({ '{1}foo' => 's1', '{1}bar' => 's2' }, result[0])
@@ -380,21 +370,21 @@ module Lint
     end
 
     def test_bitop
-      with_external_encoding('UTF-8') do
-        target_version '2.5.10' do
-          r.set('foo{1}', 'a')
-          r.set('bar{1}', 'b')
-
-          r.bitop(:and, 'foo&bar{1}', 'foo{1}', 'bar{1}')
-          assert_equal "\x60", r.get('foo&bar{1}')
-          r.bitop(:or, 'foo|bar{1}', 'foo{1}', 'bar{1}')
-          assert_equal "\x63", r.get('foo|bar{1}')
-          r.bitop(:xor, 'foo^bar{1}', 'foo{1}', 'bar{1}')
-          assert_equal "\x03", r.get('foo^bar{1}')
-          r.bitop(:not, '~foo{1}', 'foo{1}')
-          assert_equal "\x9E", r.get('~foo{1}')
-        end
-      end
+      r.set('foo{1}', 'a')
+      r.set('bar{1}', 'b')
+
+      r.bitop(:and, 'foo&bar{1}', 'foo{1}', 'bar{1}')
+      assert_equal "\x60", r.get('foo&bar{1}')
+
+      r.bitop(:and, 'foo&bar{1}', ['foo{1}', 'bar{1}'])
+      assert_equal "\x60", r.get('foo&bar{1}')
+
+      r.bitop(:or, 'foo|bar{1}', 'foo{1}', 'bar{1}')
+      assert_equal "\x63", r.get('foo|bar{1}')
+      r.bitop(:xor, 'foo^bar{1}', 'foo{1}', 'bar{1}')
+      assert_equal "\x03", r.get('foo^bar{1}')
+      r.bitop(:not, '~foo{1}', 'foo{1}')
+      assert_equal "\x9E".b, r.get('~foo{1}')
     end
   end
 end
diff --git a/test/lint/value_types.rb b/test/lint/value_types.rb
index 0b5d620d484b290a7d57a8a778a0124ffdd6957e..f8fce5b4451d1d1ae74868f4c6ed69e19a1bd914 100644
--- a/test/lint/value_types.rb
+++ b/test/lint/value_types.rb
@@ -8,18 +8,7 @@ module Lint
       r.set("foo", "s1")
 
       assert_equal 1, r.exists("foo")
-    end
-
-    def test_exists_integer
-      previous_exists_returns_integer = Redis.exists_returns_integer
-      Redis.exists_returns_integer = false
-      assert_equal false, r.exists("foo")
-
-      r.set("foo", "s1")
-
-      assert_equal true, r.exists("foo")
-    ensure
-      Redis.exists_returns_integer = previous_exists_returns_integer
+      assert_equal 1, r.exists(["foo"])
     end
 
     def test_variadic_exists
@@ -32,6 +21,7 @@ module Lint
       r.set("{1}bar", "s2")
 
       assert_equal 2, r.exists("{1}foo", "{1}bar")
+      assert_equal 2, r.exists(["{1}foo", "{1}bar"])
     end
 
     def test_exists?
@@ -40,10 +30,12 @@ module Lint
       r.set("{1}foo", "s1")
 
       assert_equal true, r.exists?("{1}foo")
+      assert_equal true, r.exists?(["{1}foo"])
 
       r.set("{1}bar", "s1")
 
       assert_equal true, r.exists?("{1}foo", "{1}bar")
+      assert_equal true, r.exists?(["{1}foo", "{1}bar"])
     end
 
     def test_type
@@ -83,12 +75,12 @@ module Lint
     end
 
     def test_pexpire
-      target_version "2.5.4" do
-        r.set("foo", "s1")
-        assert r.pexpire("foo", 2000)
-        assert_in_range 0..2, r.ttl("foo")
-      end
+      r.set("foo", "s1")
+      assert r.pexpire("foo", 2000)
+      assert_in_range 0..2, r.ttl("foo")
+    end
 
+    def test_pexpire_keywords
       target_version "7.0.0" do
         r.set("bar", "s2")
         refute r.pexpire("bar", 5_000, xx: true)
@@ -108,7 +100,9 @@ module Lint
       r.set("foo", "s1")
       assert r.expireat("foo", (Time.now + 2).to_i)
       assert_in_range 0..2, r.ttl("foo")
+    end
 
+    def test_expireat_keywords
       target_version "7.0.0" do
         r.set("bar", "s2")
         refute r.expireat("bar", (Time.now + 5).to_i, xx: true)
@@ -124,13 +118,26 @@ module Lint
       end
     end
 
-    def test_pexpireat
-      target_version "2.5.4" do
-        r.set("foo", "s1")
-        assert r.pexpireat("foo", (Time.now + 2).to_i * 1_000)
-        assert_in_range 0..2, r.ttl("foo")
+    def test_expiretime
+      target_version "7.0.0" do
+        r.set("foo", "blar")
+        assert_equal(-1, r.expiretime("foo"))
+
+        exp_time = (Time.now + 2).to_i
+        r.expireat("foo", exp_time)
+        assert_equal exp_time, r.expiretime("foo")
+
+        assert_equal(-2, r.expiretime("key-that-exists-not"))
       end
+    end
 
+    def test_pexpireat
+      r.set("foo", "s1")
+      assert r.pexpireat("foo", (Time.now + 2).to_i * 1_000)
+      assert_in_range 0..2, r.ttl("foo")
+    end
+
+    def test_pexpireat_keywords
       target_version "7.0.0" do
         r.set("bar", "s2")
         refute r.pexpireat("bar", (Time.now + 5).to_i * 1_000, xx: true)
@@ -146,6 +153,19 @@ module Lint
       end
     end
 
+    def test_pexpiretime
+      target_version "7.0.0" do
+        r.set("foo", "blar")
+        assert_equal(-1, r.pexpiretime("foo"))
+
+        exp_time = (Time.now + 2).to_i * 1_000
+        r.pexpireat("foo", exp_time)
+        assert_equal exp_time, r.pexpiretime("foo")
+
+        assert_equal(-2, r.pexpiretime("key-that-exists-not"))
+      end
+    end
+
     def test_persist
       r.set("foo", "s1")
       r.expire("foo", 1)
@@ -161,39 +181,35 @@ module Lint
     end
 
     def test_pttl
-      target_version "2.5.4" do
-        r.set("foo", "s1")
-        r.expire("foo", 2)
-        assert_in_range 1..2000, r.pttl("foo")
-      end
+      r.set("foo", "s1")
+      r.expire("foo", 2)
+      assert_in_range 1..2000, r.pttl("foo")
     end
 
     def test_dump_and_restore
-      target_version "2.5.7" do
-        r.set("foo", "a")
-        v = r.dump("foo")
-        r.del("foo")
-
-        assert r.restore("foo", 1000, v)
-        assert_equal "a", r.get("foo")
-        assert [0, 1].include? r.ttl("foo")
-
-        r.rpush("bar", ["b", "c", "d"])
-        w = r.dump("bar")
-        r.del("bar")
-
-        assert r.restore("bar", 1000, w)
-        assert_equal ["b", "c", "d"], r.lrange("bar", 0, -1)
-        assert [0, 1].include? r.ttl("bar")
-
-        r.set("bar", "somethingelse")
-        assert_raises(Redis::CommandError) { r.restore("bar", 1000, w) } # ensure by default replace is false
-        assert_raises(Redis::CommandError) { r.restore("bar", 1000, w, replace: false) }
-        assert_equal "somethingelse", r.get("bar")
-        assert r.restore("bar", 1000, w, replace: true)
-        assert_equal ["b", "c", "d"], r.lrange("bar", 0, -1)
-        assert [0, 1].include? r.ttl("bar")
-      end
+      r.set("foo", "a")
+      v = r.dump("foo")
+      r.del("foo")
+
+      assert r.restore("foo", 1000, v)
+      assert_equal "a", r.get("foo")
+      assert [0, 1].include? r.ttl("foo")
+
+      r.rpush("bar", ["b", "c", "d"])
+      w = r.dump("bar")
+      r.del("bar")
+
+      assert r.restore("bar", 1000, w)
+      assert_equal ["b", "c", "d"], r.lrange("bar", 0, -1)
+      assert [0, 1].include? r.ttl("bar")
+
+      r.set("bar", "somethingelse")
+      assert_raises(Redis::CommandError) { r.restore("bar", 1000, w) } # ensure by default replace is false
+      assert_raises(Redis::CommandError) { r.restore("bar", 1000, w, replace: false) }
+      assert_equal "somethingelse", r.get("bar")
+      assert r.restore("bar", 1000, w, replace: true)
+      assert_equal ["b", "c", "d"], r.lrange("bar", 0, -1)
+      assert [0, 1].include? r.ttl("bar")
     end
 
     def test_move
diff --git a/test/redis/bitpos_test.rb b/test/redis/bitpos_test.rb
index 48a816e9ce6e7ae9c53d5416e2415fd0f3301130..89304493b6e84948b255ffd300fa26d3e2a025a3 100644
--- a/test/redis/bitpos_test.rb
+++ b/test/redis/bitpos_test.rb
@@ -6,58 +6,52 @@ class TestBitpos < Minitest::Test
   include Helper::Client
 
   def test_bitpos_empty_zero
-    target_version "2.9.11" do
-      r.del "foo"
-      assert_equal(0, r.bitpos("foo", 0))
-    end
+    r.del "foo"
+    assert_equal(0, r.bitpos("foo", 0))
   end
 
   def test_bitpos_empty_one
-    target_version "2.9.11" do
-      r.del "foo"
-      assert_equal(-1, r.bitpos("foo", 1))
-    end
+    r.del "foo"
+    assert_equal(-1, r.bitpos("foo", 1))
   end
 
   def test_bitpos_zero
-    target_version "2.9.11" do
-      r.set "foo", "\xff\xf0\x00"
-      assert_equal(12, r.bitpos("foo", 0))
-    end
+    r.set "foo", "\xff\xf0\x00"
+    assert_equal(12, r.bitpos("foo", 0))
   end
 
   def test_bitpos_one
-    target_version "2.9.11" do
-      r.set "foo", "\x00\x0f\x00"
-      assert_equal(12, r.bitpos("foo", 1))
-    end
+    r.set "foo", "\x00\x0f\x00"
+    assert_equal(12, r.bitpos("foo", 1))
   end
 
   def test_bitpos_zero_end_is_given
-    target_version "2.9.11" do
-      r.set "foo", "\xff\xff\xff"
-      assert_equal(24, r.bitpos("foo", 0))
-      assert_equal(24, r.bitpos("foo", 0, 0))
-      assert_equal(-1, r.bitpos("foo", 0, 0, -1))
-    end
+    r.set "foo", "\xff\xff\xff"
+    assert_equal(24, r.bitpos("foo", 0))
+    assert_equal(24, r.bitpos("foo", 0, 0))
+    assert_equal(-1, r.bitpos("foo", 0, 0, -1))
   end
 
   def test_bitpos_one_intervals
-    target_version "2.9.11" do
+    r.set "foo", "\x00\xff\x00"
+    assert_equal(8,  r.bitpos("foo", 1, 0, -1))
+    assert_equal(8,  r.bitpos("foo", 1, 1, -1))
+    assert_equal(-1, r.bitpos("foo", 1, 2, -1))
+    assert_equal(-1, r.bitpos("foo", 1, 2, 200))
+    assert_equal(8,  r.bitpos("foo", 1, 1, 1))
+  end
+
+  def test_bitpos_one_intervals_bit_range
+    target_version "7.0" do
       r.set "foo", "\x00\xff\x00"
-      assert_equal(8,  r.bitpos("foo", 1, 0, -1))
-      assert_equal(8,  r.bitpos("foo", 1, 1, -1))
-      assert_equal(-1, r.bitpos("foo", 1, 2, -1))
-      assert_equal(-1, r.bitpos("foo", 1, 2, 200))
-      assert_equal(8,  r.bitpos("foo", 1, 1, 1))
+      assert_equal(8, r.bitpos("foo", 1, 8, -1, scale: 'bit'))
+      assert_equal(-1, r.bitpos("foo", 1, 8, -1, scale: 'byte'))
     end
   end
 
   def test_bitpos_raise_exception_if_stop_not_start
-    target_version "2.9.11" do
-      assert_raises(ArgumentError) do
-        r.bitpos("foo", 0, nil, 2)
-      end
+    assert_raises(ArgumentError) do
+      r.bitpos("foo", 0, nil, 2)
     end
   end
 end
diff --git a/test/redis/blocking_commands_test.rb b/test/redis/blocking_commands_test.rb
index f695765e982742d545d360e4f7f28c0c91cc4a95..bdd64e8ecf354532a73e231ed97effb429534260 100644
--- a/test/redis/blocking_commands_test.rb
+++ b/test/redis/blocking_commands_test.rb
@@ -15,8 +15,7 @@ class TestBlockingCommands < Minitest::Test
       yield(r)
       t2 = Time.now
 
-      assert timeout == r._client.timeout
-      assert delay <= (t2 - t1)
+      assert_operator delay, :<=, (t2 - t1)
     end
   end
 
@@ -47,18 +46,18 @@ class TestBlockingCommands < Minitest::Test
   end
 
   def test_brpoplpush_in_transaction
-    results = r.multi do
-      r.brpoplpush('foo', 'bar')
-      r.brpoplpush('foo', 'bar', timeout: 2)
+    results = r.multi do |transaction|
+      transaction.brpoplpush('foo', 'bar')
+      transaction.brpoplpush('foo', 'bar', timeout: 2)
     end
     assert_equal [nil, nil], results
   end
 
   def test_brpoplpush_in_pipeline
     mock do |r|
-      results = r.pipelined do
-        r.brpoplpush('foo', 'bar')
-        r.brpoplpush('foo', 'bar', timeout: 2)
+      results = r.pipelined do |transaction|
+        transaction.brpoplpush('foo', 'bar')
+        transaction.brpoplpush('foo', 'bar', timeout: 2)
       end
       assert_equal ['0', '2'], results
     end
diff --git a/test/redis/client_test.rb b/test/redis/client_test.rb
index af60040728ca559b947ffaacf96d3cc3857a52e0..bec97431ca21c85d22b1eda75c2fb3199f731d3e 100644
--- a/test/redis/client_test.rb
+++ b/test/redis/client_test.rb
@@ -26,57 +26,18 @@ class TestClient < Minitest::Test
     end
   end
 
-  def test_queue_commit
-    r.queue("SET", "foo", "bar")
-    r.queue("GET", "foo")
-    result = r.commit
+  def test_error_translate_subclasses
+    error = Class.new(RedisClient::CommandError)
+    assert_equal Redis::CommandError, Redis::Client.send(:translate_error_class, error)
 
-    assert_equal result, ["OK", "bar"]
-  end
-
-  def test_commit_raise
-    r.queue("SET", "foo", "bar")
-    r.queue("INCR")
-
-    assert_raises(Redis::CommandError) do
-      r.commit
-    end
-  end
-
-  def test_queue_after_error
-    r.queue("SET", "foo", "bar")
-    r.queue("INCR")
-
-    assert_raises(Redis::CommandError) do
-      r.commit
-    end
-
-    r.queue("SET",  "foo", "bar")
-    r.queue("INCR", "baz")
-    result = r.commit
-
-    assert_equal result, ["OK", 1]
-  end
-
-  def test_client_with_custom_connector
-    custom_connector = Class.new(Redis::Client::Connector) do
-      def resolve
-        @options[:host] = '127.0.0.5'
-        @options[:port] = '999'
-        @options
-      end
-    end
-
-    error = assert_raises do
-      new_redis = _new_client(connector: custom_connector)
-      new_redis.ping
+    assert_raises KeyError do
+      Redis::Client.send(:translate_error_class, StandardError)
     end
-    assert_match(/Error connecting to Redis on 127\.0\.0\.5:999 (.+)/, error.message)
   end
 
   def test_mixed_encoding
     r.call("MSET", "fée", "\x00\xFF".b, "じ案".encode(Encoding::SHIFT_JIS), "\t".encode(Encoding::ASCII))
-    assert_equal "\x00\xFF", r.call("GET", "fée")
+    assert_equal "\x00\xFF".b, r.call("GET", "fée")
     assert_equal "\t", r.call("GET", "じ案".encode(Encoding::SHIFT_JIS))
 
     r.call("SET", "\x00\xFF", "fée")
diff --git a/test/redis/command_map_test.rb b/test/redis/command_map_test.rb
deleted file mode 100644
index 42917b5f65738db27af0a933f2b0b270d3d57f93..0000000000000000000000000000000000000000
--- a/test/redis/command_map_test.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-require "helper"
-
-class TestCommandMap < Minitest::Test
-  include Helper::Client
-
-  def test_override_existing_commands
-    r.set("counter", 1)
-
-    assert_equal 2, r.incr("counter")
-
-    r._client.command_map[:incr] = :decr
-
-    assert_equal 1, r.incr("counter")
-  end
-
-  def test_override_non_existing_commands
-    r.set("key", "value")
-
-    assert_raises Redis::CommandError do
-      r.idontexist("key")
-    end
-
-    r._client.command_map[:idontexist] = :get
-
-    assert_equal "value", r.idontexist("key")
-  end
-end
diff --git a/test/redis/commands_on_geo_test.rb b/test/redis/commands_on_geo_test.rb
index 90171bf1615a1670bc4ca00d5c6ee417904aa744..62ba99c0a2e302056ade2a11c7fb5f07bfc44ef9 100644
--- a/test/redis/commands_on_geo_test.rb
+++ b/test/redis/commands_on_geo_test.rb
@@ -8,126 +8,96 @@ class TestCommandsGeo < Minitest::Test
   def setup
     super
 
-    target_version "3.2.0" do
-      added_items_count = r.geoadd("Sicily", 13.361389, 38.115556, "Palermo", 15.087269, 37.502669, "Catania")
-      assert_equal 2, added_items_count
-    end
+    added_items_count = r.geoadd("Sicily", 13.361389, 38.115556, "Palermo", 15.087269, 37.502669, "Catania")
+    assert_equal 2, added_items_count
   end
 
   def test_geoadd_with_array_params
-    target_version "3.2.0" do
-      added_items_count = r.geoadd("SicilyArray", [13.361389, 38.115556, "Palermo", 15.087269, 37.502669, "Catania"])
-      assert_equal 2, added_items_count
-    end
+    added_items_count = r.geoadd("SicilyArray", [13.361389, 38.115556, "Palermo", 15.087269, 37.502669, "Catania"])
+    assert_equal 2, added_items_count
   end
 
   def test_georadius_with_same_params
-    target_version "3.2.0" do
-      r.geoadd("Chad", 15, 15, "Kanem")
-      nearest_cities = r.georadius("Chad", 15, 15, 15, 'km', sort: 'asc')
-      assert_equal %w(Kanem), nearest_cities
-    end
+    r.geoadd("Chad", 15, 15, "Kanem")
+    nearest_cities = r.georadius("Chad", 15, 15, 15, 'km', sort: 'asc')
+    assert_equal %w(Kanem), nearest_cities
   end
 
   def test_georadius_with_sort
-    target_version "3.2.0" do
-      nearest_cities = r.georadius("Sicily", 15, 37, 200, 'km', sort: 'asc')
-      assert_equal %w(Catania Palermo), nearest_cities
+    nearest_cities = r.georadius("Sicily", 15, 37, 200, 'km', sort: 'asc')
+    assert_equal %w(Catania Palermo), nearest_cities
 
-      farthest_cities = r.georadius("Sicily", 15, 37, 200, 'km', sort: 'desc')
-      assert_equal %w(Palermo Catania), farthest_cities
-    end
+    farthest_cities = r.georadius("Sicily", 15, 37, 200, 'km', sort: 'desc')
+    assert_equal %w(Palermo Catania), farthest_cities
   end
 
   def test_georadius_with_count
-    target_version "3.2.0" do
-      city = r.georadius("Sicily", 15, 37, 200, 'km', count: 1)
-      assert_equal %w(Catania), city
-    end
+    city = r.georadius("Sicily", 15, 37, 200, 'km', count: 1)
+    assert_equal %w(Catania), city
   end
 
   def test_georadius_with_options_count_sort
-    target_version "3.2.0" do
-      city = r.georadius("Sicily", 15, 37, 200, 'km', sort: :desc, options: :WITHDIST, count: 1)
-      assert_equal [["Palermo", "190.4424"]], city
-    end
+    city = r.georadius("Sicily", 15, 37, 200, 'km', sort: :desc, options: :WITHDIST, count: 1)
+    assert_equal [["Palermo", "190.4424"]], city
   end
 
   def test_georadiusbymember_with_sort
-    target_version "3.2.0" do
-      nearest_cities = r.georadiusbymember("Sicily", "Catania", 200, 'km', sort: 'asc')
-      assert_equal %w(Catania Palermo), nearest_cities
+    nearest_cities = r.georadiusbymember("Sicily", "Catania", 200, 'km', sort: 'asc')
+    assert_equal %w(Catania Palermo), nearest_cities
 
-      farthest_cities = r.georadiusbymember("Sicily", "Catania", 200, 'km', sort: 'desc')
-      assert_equal %w(Palermo Catania), farthest_cities
-    end
+    farthest_cities = r.georadiusbymember("Sicily", "Catania", 200, 'km', sort: 'desc')
+    assert_equal %w(Palermo Catania), farthest_cities
   end
 
   def test_georadiusbymember_with_count
-    target_version "3.2.0" do
-      city = r.georadiusbymember("Sicily", "Catania", 200, 'km', count: 1)
-      assert_equal %w(Catania), city
-    end
+    city = r.georadiusbymember("Sicily", "Catania", 200, 'km', count: 1)
+    assert_equal %w(Catania), city
   end
 
   def test_georadiusbymember_with_options_count_sort
-    target_version "3.2.0" do
-      city = r.georadiusbymember("Sicily", "Catania", 200, 'km', sort: :desc, options: :WITHDIST, count: 1)
-      assert_equal [["Palermo", "166.2742"]], city
-    end
+    city = r.georadiusbymember("Sicily", "Catania", 200, 'km', sort: :desc, options: :WITHDIST, count: 1)
+    assert_equal [["Palermo", "166.2742"]], city
   end
 
   def test_geopos
-    target_version "3.2.0" do
-      location = r.geopos("Sicily", "Catania")
-      assert_equal [["15.08726745843887329", "37.50266842333162032"]], location
+    location = r.geopos("Sicily", "Catania")
+    assert_equal [["15.08726745843887329", "37.50266842333162032"]], location
 
-      locations = r.geopos("Sicily", ["Palermo", "Catania"])
-      assert_equal [["13.36138933897018433", "38.11555639549629859"], ["15.08726745843887329", "37.50266842333162032"]], locations
-    end
+    locations = r.geopos("Sicily", ["Palermo", "Catania"])
+    assert_equal [["13.36138933897018433", "38.11555639549629859"], ["15.08726745843887329", "37.50266842333162032"]], locations
   end
 
   def test_geopos_nonexistant_location
-    target_version "3.2.0" do
-      location = r.geopos("Sicily", "Rome")
-      assert_equal [nil], location
+    location = r.geopos("Sicily", "Rome")
+    assert_equal [nil], location
 
-      locations = r.geopos("Sicily", ["Rome", "Catania"])
-      assert_equal [nil, ["15.08726745843887329", "37.50266842333162032"]], locations
-    end
+    locations = r.geopos("Sicily", ["Rome", "Catania"])
+    assert_equal [nil, ["15.08726745843887329", "37.50266842333162032"]], locations
   end
 
   def test_geodist
-    target_version "3.2.0" do
-      distination_in_meters = r.geodist("Sicily", "Palermo", "Catania")
-      assert_equal "166274.1516", distination_in_meters
+    distination_in_meters = r.geodist("Sicily", "Palermo", "Catania")
+    assert_equal "166274.1516", distination_in_meters
 
-      distination_in_feet = r.geodist("Sicily", "Palermo", "Catania", 'ft')
-      assert_equal "545518.8700", distination_in_feet
-    end
+    distination_in_feet = r.geodist("Sicily", "Palermo", "Catania", 'ft')
+    assert_equal "545518.8700", distination_in_feet
   end
 
   def test_geodist_with_nonexistant_location
-    target_version "3.2.0" do
-      distination = r.geodist("Sicily", "Palermo", "Rome")
-      assert_nil distination
-    end
+    distination = r.geodist("Sicily", "Palermo", "Rome")
+    assert_nil distination
   end
 
   def test_geohash
-    target_version "3.2.0" do
-      geohash = r.geohash("Sicily", "Palermo")
-      assert_equal ["sqc8b49rny0"], geohash
+    geohash = r.geohash("Sicily", "Palermo")
+    assert_equal ["sqc8b49rny0"], geohash
 
-      geohashes = r.geohash("Sicily", ["Palermo", "Catania"])
-      assert_equal %w(sqc8b49rny0 sqdtr74hyu0), geohashes
-    end
+    geohashes = r.geohash("Sicily", ["Palermo", "Catania"])
+    assert_equal %w(sqc8b49rny0 sqdtr74hyu0), geohashes
   end
 
   def test_geohash_with_nonexistant_location
-    target_version "3.2.0" do
-      geohashes = r.geohash("Sicily", ["Palermo", "Rome"])
-      assert_equal ["sqc8b49rny0", nil], geohashes
-    end
+    geohashes = r.geohash("Sicily", ["Palermo", "Rome"])
+    assert_equal ["sqc8b49rny0", nil], geohashes
   end
 end
diff --git a/test/redis/commands_on_value_types_test.rb b/test/redis/commands_on_value_types_test.rb
index 56213af9eed79271bf24c77c835643f339fc5afe..b79d6898bd7d3203856e1c1693f11a9f34dc5ba5 100644
--- a/test/redis/commands_on_value_types_test.rb
+++ b/test/redis/commands_on_value_types_test.rb
@@ -43,39 +43,35 @@ class TestCommandsOnValueTypes < Minitest::Test
   end
 
   def test_unlink
-    target_version "4.0.0" do
-      r.set "foo", "s1"
-      r.set "bar", "s2"
-      r.set "baz", "s3"
+    r.set "foo", "s1"
+    r.set "bar", "s2"
+    r.set "baz", "s3"
 
-      assert_equal ["bar", "baz", "foo"], r.keys("*").sort
+    assert_equal ["bar", "baz", "foo"], r.keys("*").sort
 
-      assert_equal 1, r.unlink("foo")
+    assert_equal 1, r.unlink("foo")
 
-      assert_equal ["bar", "baz"], r.keys("*").sort
+    assert_equal ["bar", "baz"], r.keys("*").sort
 
-      assert_equal 2, r.unlink("bar", "baz")
+    assert_equal 2, r.unlink("bar", "baz")
 
-      assert_equal [], r.keys("*").sort
-    end
+    assert_equal [], r.keys("*").sort
   end
 
   def test_unlink_with_array_argument
-    target_version "4.0.0" do
-      r.set "foo", "s1"
-      r.set "bar", "s2"
-      r.set "baz", "s3"
+    r.set "foo", "s1"
+    r.set "bar", "s2"
+    r.set "baz", "s3"
 
-      assert_equal ["bar", "baz", "foo"], r.keys("*").sort
+    assert_equal ["bar", "baz", "foo"], r.keys("*").sort
 
-      assert_equal 1, r.unlink(["foo"])
+    assert_equal 1, r.unlink(["foo"])
 
-      assert_equal ["bar", "baz"], r.keys("*").sort
+    assert_equal ["bar", "baz"], r.keys("*").sort
 
-      assert_equal 2, r.unlink(["bar", "baz"])
+    assert_equal 2, r.unlink(["bar", "baz"])
 
-      assert_equal [], r.keys("*").sort
-    end
+    assert_equal [], r.keys("*").sort
   end
 
   def test_randomkey
@@ -140,19 +136,17 @@ class TestCommandsOnValueTypes < Minitest::Test
     assert_equal 0, r.dbsize
 
     # Test async
-    target_version "3.9.101" do
-      r.set("foo", "s1")
-      r.set("bar", "s2")
+    r.set("foo", "s1")
+    r.set("bar", "s2")
 
-      assert_equal 2, r.dbsize
+    assert_equal 2, r.dbsize
 
-      r.flushdb(async: true)
+    r.flushdb(async: true)
 
-      assert_equal 0, r.dbsize
+    assert_equal 0, r.dbsize
 
-      redis_mock(flushdb: ->(args) { "+FLUSHDB #{args.upcase}" }) do |redis|
-        assert_equal "FLUSHDB ASYNC", redis.flushdb(async: true)
-      end
+    redis_mock(flushdb: ->(args) { "+FLUSHDB #{args.upcase}" }) do |redis|
+      assert_equal "FLUSHDB ASYNC", redis.flushdb(async: true)
     end
   end
 
@@ -168,10 +162,8 @@ class TestCommandsOnValueTypes < Minitest::Test
     end
 
     # Test async
-    target_version "3.9.101" do
-      redis_mock(flushall: ->(args) { "+FLUSHALL #{args.upcase}" }) do |redis|
-        assert_equal "FLUSHALL ASYNC", redis.flushall(async: true)
-      end
+    redis_mock(flushall: ->(args) { "+FLUSHALL #{args.upcase}" }) do |redis|
+      assert_equal "FLUSHALL ASYNC", redis.flushall(async: true)
     end
   end
 
diff --git a/test/redis/connection_handling_test.rb b/test/redis/connection_handling_test.rb
index ac5ed73a9687adb4c779368aea21195b8bc33b58..04946c8ea061e2af33baa35cacce4ce91268393d 100644
--- a/test/redis/connection_handling_test.rb
+++ b/test/redis/connection_handling_test.rb
@@ -17,7 +17,7 @@ class TestConnectionHandling < Minitest::Test
       assert_equal "PONG", redis.ping
     end
 
-    assert_equal ["setname", "client-name"], @name
+    assert_equal ["SETNAME", "client-name"], @name
   end
 
   def test_ping
@@ -30,9 +30,9 @@ class TestConnectionHandling < Minitest::Test
     r.select 14
     assert_nil r.get("foo")
 
-    r._client.disconnect
+    r._client.close
 
-    assert_nil r.get("foo")
+    assert_equal "bar", r.get("foo")
   end
 
   def test_quit
@@ -127,91 +127,6 @@ class TestConnectionHandling < Minitest::Test
     end
   end
 
-  def test_shutdown_from_pipeline
-    commands = {
-      shutdown: -> { :exit }
-    }
-
-    redis_mock(commands) do |redis|
-      result = redis.pipelined do
-        redis.shutdown
-      end
-
-      assert_nil result
-      assert !redis._client.connected?
-    end
-  end
-
-  def test_shutdown_with_error_from_pipeline
-    connections = 0
-    commands = {
-      select: ->(*_) { connections += 1; "+OK\r\n" },
-      connections: -> { ":#{connections}\r\n" },
-      shutdown: -> { "-ERR could not shutdown\r\n" }
-    }
-
-    redis_mock(commands) do |redis|
-      connections = redis.connections
-
-      # SHUTDOWN replies with an error: test that it gets raised
-      assert_raises Redis::CommandError do
-        redis.pipelined do
-          redis.shutdown
-        end
-      end
-
-      # The connection should remain in tact
-      assert_equal connections, redis.connections
-    end
-  end
-
-  def test_shutdown_from_multi_exec
-    commands = {
-      multi: -> { "+OK\r\n" },
-      shutdown: -> { "+QUEUED\r\n" },
-      exec: -> { :exit }
-    }
-
-    redis_mock(commands) do |redis|
-      result = redis.multi do
-        redis.shutdown
-      end
-
-      assert_nil result
-      assert !redis._client.connected?
-    end
-  end
-
-  def test_shutdown_with_error_from_multi_exec
-    connections = 0
-    commands = {
-      select: ->(*_) { connections += 1; "+OK\r\n" },
-      connections: -> { ":#{connections}\r\n" },
-      multi: -> { "+OK\r\n" },
-      shutdown: -> { "+QUEUED\r\n" },
-      exec: -> { "*1\r\n-ERR could not shutdown\r\n" }
-    }
-
-    redis_mock(commands) do |redis|
-      connections = redis.connections
-
-      # SHUTDOWN replies with an error: test that it gets returned
-      # We should test for Redis::CommandError here, but hiredis doesn't yet do
-      # custom error classes.
-      err = nil
-
-      begin
-        redis.multi { redis.shutdown }
-      rescue => err
-      end
-
-      assert err.is_a?(StandardError)
-
-      # The connection should remain intact
-      assert_equal connections, redis.connections
-    end
-  end
-
   def test_slaveof
     redis_mock(slaveof: ->(host, port) { "+SLAVEOF #{host} #{port}" }) do |redis|
       assert_equal "SLAVEOF somehost 6381", redis.slaveof("somehost", 6381)
@@ -241,25 +156,4 @@ class TestConnectionHandling < Minitest::Test
   ensure
     r.config :set, "timeout", 300
   end
-
-  driver(:ruby, :hiredis) do
-    def test_consistency_on_multithreaded_env
-      t = nil
-
-      commands = {
-        set: ->(_key, _value) { t.kill; "+OK\r\n" },
-        incr: ->(_key) { ":1\r\n" }
-      }
-
-      redis_mock(commands) do |redis|
-        t = Thread.new do
-          redis.set("foo", "bar")
-        end
-
-        t.join
-
-        assert_equal 1, redis.incr("baz")
-      end
-    end
-  end
 end
diff --git a/test/redis/connection_test.rb b/test/redis/connection_test.rb
index d5172db720581ad35ee67789335f626b1fcfc47c..08a45e4ea43c1221f54b67d00f0e92ad73b22c84 100644
--- a/test/redis/connection_test.rb
+++ b/test/redis/connection_test.rb
@@ -6,7 +6,7 @@ class TestConnection < Minitest::Test
   include Helper::Client
 
   def test_provides_a_meaningful_inspect
-    assert_equal "#<Redis client v#{Redis::VERSION} for redis://127.0.0.1:#{PORT}/15>", r.inspect
+    assert_equal "#<Redis client v#{Redis::VERSION} for redis://localhost:#{PORT}/15>", r.inspect
   end
 
   def test_connection_with_user_and_password
@@ -27,46 +27,37 @@ class TestConnection < Minitest::Test
     end
   end
 
-  def test_connection_with_wrong_user_and_password
-    target_version "6.0" do
-      with_default_user_password do |_username, password|
-        redis = Redis.new(OPTIONS.merge(username: "does-not-exist", password: password))
-        assert_equal "PONG", redis.ping
-      end
-    end
-  end
-
   def test_connection_information
-    assert_equal "127.0.0.1",                 r.connection.fetch(:host)
+    assert_equal "localhost",                 r.connection.fetch(:host)
     assert_equal 6381,                        r.connection.fetch(:port)
     assert_equal 15,                          r.connection.fetch(:db)
-    assert_equal "127.0.0.1:6381",            r.connection.fetch(:location)
-    assert_equal "redis://127.0.0.1:6381/15", r.connection.fetch(:id)
+    assert_equal "localhost:6381",            r.connection.fetch(:location)
+    assert_equal "redis://localhost:6381/15", r.connection.fetch(:id)
   end
 
   def test_default_id_with_host_and_port
     redis = Redis.new(OPTIONS.merge(host: "host", port: "1234", db: 0))
-    assert_equal "redis://host:1234/0", redis.connection.fetch(:id)
+    assert_equal "redis://host:1234", redis.connection.fetch(:id)
   end
 
   def test_default_id_with_host_and_port_and_ssl
     redis = Redis.new(OPTIONS.merge(host: 'host', port: '1234', db: 0, ssl: true))
-    assert_equal "rediss://host:1234/0", redis.connection.fetch(:id)
+    assert_equal "rediss://host:1234", redis.connection.fetch(:id)
   end
 
   def test_default_id_with_host_and_port_and_explicit_scheme
-    redis = Redis.new(OPTIONS.merge(host: "host", port: "1234", db: 0, scheme: "foo"))
-    assert_equal "foo://host:1234/0", redis.connection.fetch(:id)
+    redis = Redis.new(OPTIONS.merge(host: "host", port: "1234", db: 0))
+    assert_equal "redis://host:1234", redis.connection.fetch(:id)
   end
 
   def test_default_id_with_path
     redis = Redis.new(OPTIONS.merge(path: "/tmp/redis.sock", db: 0))
-    assert_equal "unix:///tmp/redis.sock/0", redis.connection.fetch(:id)
+    assert_equal "unix:///tmp/redis.sock", redis.connection.fetch(:id)
   end
 
   def test_default_id_with_path_and_explicit_scheme
-    redis = Redis.new(OPTIONS.merge(path: "/tmp/redis.sock", db: 0, scheme: "foo"))
-    assert_equal "unix:///tmp/redis.sock/0", redis.connection.fetch(:id)
+    redis = Redis.new(OPTIONS.merge(path: "/tmp/redis.sock", db: 0))
+    assert_equal "unix:///tmp/redis.sock", redis.connection.fetch(:id)
   end
 
   def test_override_id
@@ -84,7 +75,7 @@ class TestConnection < Minitest::Test
       connection_id = redis.connection.fetch(:id)
     end
 
-    assert_equal "redis://127.0.0.1:6381/15", id
-    assert_equal "redis://127.0.0.1:6381/15", connection_id
+    assert_equal "redis://localhost:6381/15", id
+    assert_equal "redis://localhost:6381/15", connection_id
   end
 end
diff --git a/test/redis/encoding_test.rb b/test/redis/encoding_test.rb
index 71520b8bc62fab4aeac4515f35c2c6fabe592848..302bb69bf5ca63bfb25dc6635206c0b9be113880 100644
--- a/test/redis/encoding_test.rb
+++ b/test/redis/encoding_test.rb
@@ -6,10 +6,14 @@ class TestEncoding < Minitest::Test
   include Helper::Client
 
   def test_returns_properly_encoded_strings
-    with_external_encoding("UTF-8") do
-      r.set "foo", "שלום"
+    r.set "foo", "שלום"
 
-      assert_equal "Shalom שלום", "Shalom #{r.get('foo')}"
-    end
+    assert_equal "Shalom שלום", "Shalom #{r.get('foo')}"
+
+    refute_predicate "\xFF", :valid_encoding?
+    r.set("bar", "\xFF")
+    bytes = r.get("bar")
+    assert_equal "\xFF".b, bytes
+    assert_predicate bytes, :valid_encoding?
   end
 end
diff --git a/test/redis/error_replies_test.rb b/test/redis/error_replies_test.rb
index abf9c41896479974515dd72843c87e554c53fb61..d7df0e862ae79679a02872bd6a34aa38e875c9bc 100644
--- a/test/redis/error_replies_test.rb
+++ b/test/redis/error_replies_test.rb
@@ -19,40 +19,23 @@ class TestErrorReplies < Minitest::Test
 
   def test_error_reply_for_single_command
     with_reconnection_check do
-      begin
-        r.unknown_command
-      rescue => ex
-      ensure
-        assert ex.message =~ /unknown command/i
-      end
+      r.unknown_command
+    rescue => ex
+    ensure
+      assert ex.message =~ /unknown command/i
     end
   end
 
   def test_raise_first_error_reply_in_pipeline
     with_reconnection_check do
-      begin
-        r.pipelined do
-          r.set("foo", "s1")
-          r.incr("foo") # not an integer
-          r.lpush("foo", "value") # wrong kind of value
-        end
-      rescue => ex
-      ensure
-        assert ex.message =~ /not an integer/i
-      end
-    end
-  end
-
-  def test_recover_from_raise_in__call_loop
-    with_reconnection_check do
-      begin
-        r._client.call_loop([:invalid_monitor]) do
-          assert false # Should never be executed
-        end
-      rescue => ex
-      ensure
-        assert ex.message =~ /unknown command/i
+      r.pipelined do
+        r.set("foo", "s1")
+        r.incr("foo") # not an integer
+        r.lpush("foo", "value") # wrong kind of value
       end
+    rescue => ex
+    ensure
+      assert ex.message =~ /not an integer/i
     end
   end
 end
diff --git a/test/redis/fork_safety_test.rb b/test/redis/fork_safety_test.rb
index fdbf7ac71507ddb4a566dd0ed6126a4a779c9b19..6a7be285078aab67b7bc9213849273bd263ecbef 100644
--- a/test/redis/fork_safety_test.rb
+++ b/test/redis/fork_safety_test.rb
@@ -5,57 +5,21 @@ require "helper"
 class TestForkSafety < Minitest::Test
   include Helper::Client
 
-  driver(:ruby, :hiredis) do
-    def test_fork_safety
-      redis = Redis.new(OPTIONS)
-      redis.set "foo", 1
+  def setup
+    skip("Fork unavailable") unless Process.respond_to?(:fork)
+  end
 
-      child_pid = fork do
-        begin
-          # InheritedError triggers a reconnect,
-          # so we need to disable reconnects to force
-          # the exception bubble up
-          redis.without_reconnect do
-            redis.set "foo", 2
-          end
-          exit! 0
-        rescue Redis::InheritedError
-          exit! 127
-        end
+  def test_fork_safety
+    redis = Redis.new(OPTIONS)
+    pid = fork do
+      1000.times do
+        assert_equal "OK", redis.set("key", "foo")
       end
-
-      _, status = Process.wait2(child_pid)
-
-      assert_equal 127, status.exitstatus
-      assert_equal "1", redis.get("foo")
-    rescue NotImplementedError => error
-      raise unless error.message =~ /fork is not available/
     end
-
-    def test_fork_safety_with_enabled_inherited_socket
-      redis = Redis.new(OPTIONS.merge(inherit_socket: true))
-      redis.set "foo", 1
-
-      child_pid = fork do
-        begin
-          # InheritedError triggers a reconnect,
-          # so we need to disable reconnects to force
-          # the exception bubble up
-          redis.without_reconnect do
-            redis.set "foo", 2
-          end
-          exit! 0
-        rescue Redis::InheritedError
-          exit! 127
-        end
-      end
-
-      _, status = Process.wait2(child_pid)
-
-      assert_equal 0, status.exitstatus
-      assert_equal "2", redis.get("foo")
-    rescue NotImplementedError => error
-      raise unless error.message =~ /fork is not available/
+    1000.times do
+      assert_equal "PONG", redis.ping
     end
+    _, status = Process.wait2(pid)
+    assert_predicate(status, :success?)
   end
 end
diff --git a/test/redis/internals_test.rb b/test/redis/internals_test.rb
index 4a1fa0b169b1e90e2701675049e31034c374d205..1b718a1fcf18e5a86fed9a665f4a4b6932e4b90c 100644
--- a/test/redis/internals_test.rb
+++ b/test/redis/internals_test.rb
@@ -5,33 +5,20 @@ require "helper"
 class TestInternals < Minitest::Test
   include Helper::Client
 
-  def test_logger
-    r.ping
-
-    assert log.string["[Redis] command=PING"]
-    assert log.string =~ /\[Redis\] call_time=\d+\.\d+ ms/
-  end
-
   def test_large_payload
     # see: https://github.com/redis/redis-rb/issues/962
     # large payloads will trigger write_nonblock to write a portion
     # of the payload in connection/ruby.rb _write_to_socket
+
+    # We use a larger timeout for TruffleRuby
+    # https://github.com/redis/redis-rb/pull/1128#issuecomment-1218490684
+    r = init(_new_client(timeout: TIMEOUT * 5))
     large = "\u3042" * 4_000_000
     r.setex("foo", 10, large)
     result = r.get("foo")
     assert_equal result, large
   end
 
-  def test_logger_with_pipelining
-    r.pipelined do
-      r.set "foo", "bar"
-      r.get "foo"
-    end
-
-    assert log.string[" command=SET args=\"foo\" \"bar\""]
-    assert log.string[" command=GET args=\"foo\""]
-  end
-
   def test_recovers_from_failed_commands
     # See https://github.com/redis/redis-rb/issues#issue/28
 
@@ -50,26 +37,6 @@ class TestInternals < Minitest::Test
     end
   end
 
-  def test_redis_current
-    assert_equal "127.0.0.1", Redis.current._client.host
-    assert_equal 6379, Redis.current._client.port
-    assert_equal 0, Redis.current._client.db
-
-    Redis.current = Redis.new(OPTIONS.merge(port: 6380, db: 1))
-
-    t = Thread.new do
-      assert_equal "127.0.0.1", Redis.current._client.host
-      assert_equal 6380, Redis.current._client.port
-      assert_equal 1, Redis.current._client.db
-    end
-
-    t.join
-
-    assert_equal "127.0.0.1", Redis.current._client.host
-    assert_equal 6380, Redis.current._client.port
-    assert_equal 1, Redis.current._client.db
-  end
-
   def test_redis_connected?
     fresh_client = _new_client
     assert !fresh_client.connected?
@@ -85,33 +52,15 @@ class TestInternals < Minitest::Test
     Redis.new(OPTIONS.merge(timeout: 0))
   end
 
-  driver(:ruby) do
-    def test_tcp_keepalive
-      keepalive = { time: 20, intvl: 10, probes: 5 }
-
-      redis = Redis.new(OPTIONS.merge(tcp_keepalive: keepalive))
-      redis.ping
-
-      connection = redis._client.connection
-      actual_keepalive = connection.get_tcp_keepalive
-
-      %i[time intvl probes].each do |key|
-        assert_equal actual_keepalive[key], keepalive[key] if actual_keepalive.key?(key)
-      end
-    end
-  end
-
   def test_time
-    target_version "2.5.4" do
-      # Test that the difference between the time that Ruby reports and the time
-      # that Redis reports is minimal (prevents the test from being racy).
-      rv = r.time
+    # Test that the difference between the time that Ruby reports and the time
+    # that Redis reports is minimal (prevents the test from being racy).
+    rv = r.time
 
-      redis_usec = rv[0] * 1_000_000 + rv[1]
-      ruby_usec = Integer(Time.now.to_f * 1_000_000)
+    redis_usec = rv[0] * 1_000_000 + rv[1]
+    ruby_usec = Integer(Time.now.to_f * 1_000_000)
 
-      assert((ruby_usec - redis_usec).abs < 500_000)
-    end
+    assert((ruby_usec - redis_usec).abs < 500_000)
   end
 
   def test_connection_timeout
@@ -151,24 +100,6 @@ class TestInternals < Minitest::Test
     end
   end
 
-  def test_retry_when_wrapped_in_with_reconnect_true
-    close_on_ping([0]) do |redis|
-      redis.with_reconnect(true) do
-        assert_equal "1", redis.ping
-      end
-    end
-  end
-
-  def test_dont_retry_when_wrapped_in_with_reconnect_false
-    close_on_ping([0]) do |redis|
-      assert_raises Redis::ConnectionError do
-        redis.with_reconnect(false) do
-          redis.ping
-        end
-      end
-    end
-  end
-
   def test_dont_retry_when_wrapped_in_without_reconnect
     close_on_ping([0]) do |redis|
       assert_raises Redis::ConnectionError do
@@ -206,27 +137,21 @@ class TestInternals < Minitest::Test
   end
 
   def test_retry_with_custom_reconnect_attempts_and_exponential_backoff
-    close_on_ping([0, 1, 2], reconnect_attempts: 3,
-                             reconnect_delay_max: 0.5,
-                             reconnect_delay: 0.01) do |redis|
-      Kernel.expects(:sleep).with(0.01).returns(true)
-      Kernel.expects(:sleep).with(0.02).returns(true)
-      Kernel.expects(:sleep).with(0.04).returns(true)
+    close_on_ping([0, 1, 2], reconnect_attempts: [0.01, 0.02, 0.04]) do |redis|
+      redis._client.config.expects(:sleep).with(0.01).returns(true)
+      redis._client.config.expects(:sleep).with(0.02).returns(true)
+      redis._client.config.expects(:sleep).with(0.04).returns(true)
 
       assert_equal "3", redis.ping
     end
   end
 
-  def test_don_t_retry_when_second_read_in_pipeline_raises_econnreset
-    close_on_ping([1]) do |redis|
-      assert_raises Redis::ConnectionError do
-        redis.pipelined do
-          redis.ping
-          redis.ping # Second #read times out
-        end
+  def test_retry_pipeline_first_command
+    close_on_ping([0]) do |redis|
+      results = redis.pipelined do |pipeline|
+        pipeline.ping
       end
-
-      assert !redis._client.connected?
+      assert_equal ["1"], results
     end
   end
 
@@ -262,25 +187,7 @@ class TestInternals < Minitest::Test
 
   def test_retry_on_write_error_by_default
     close_on_connection([0]) do |redis|
-      assert_equal "1", redis._client.call(["x" * 128 * 1024])
-    end
-  end
-
-  def test_retry_on_write_error_when_wrapped_in_with_reconnect_true
-    close_on_connection([0]) do |redis|
-      redis.with_reconnect(true) do
-        assert_equal "1", redis._client.call(["x" * 128 * 1024])
-      end
-    end
-  end
-
-  def test_dont_retry_on_write_error_when_wrapped_in_with_reconnect_false
-    close_on_connection([0]) do |redis|
-      assert_raises Redis::ConnectionError do
-        redis.with_reconnect(false) do
-          redis._client.call(["x" * 128 * 1024])
-        end
-      end
+      assert_equal "1", redis._client.call_v(["x" * 128 * 1024])
     end
   end
 
@@ -288,7 +195,7 @@ class TestInternals < Minitest::Test
     close_on_connection([0]) do |redis|
       assert_raises Redis::ConnectionError do
         redis.without_reconnect do
-          redis._client.call(["x" * 128 * 1024])
+          redis._client.call_v(["x" * 128 * 1024])
         end
       end
     end
@@ -298,27 +205,24 @@ class TestInternals < Minitest::Test
     Redis.new(OPTIONS.merge(path: ENV.fetch("REDIS_SOCKET_PATH"))).ping
   end
 
-  driver(:ruby, :hiredis) do
-    def test_bubble_timeout_without_retrying
-      serv = TCPServer.new(6380)
+  def test_bubble_timeout_without_retrying
+    serv = TCPServer.new(6380)
 
-      redis = Redis.new(port: 6380, timeout: 0.1)
+    redis = Redis.new(port: 6380, timeout: 0.1)
 
-      assert_raises(Redis::TimeoutError) do
-        redis.ping
-      end
-    ensure
-      serv&.close
+    assert_raises(Redis::TimeoutError) do
+      redis.ping
     end
+  ensure
+    serv&.close
   end
 
   def test_client_options
-    redis = Redis.new(OPTIONS.merge(host: "host", port: 1234, db: 1, scheme: "foo"))
+    redis = Redis.new(OPTIONS.merge(host: "host", port: 1234, db: 1))
 
-    assert_equal "host", redis._client.options[:host]
-    assert_equal 1234, redis._client.options[:port]
-    assert_equal 1, redis._client.options[:db]
-    assert_equal "foo", redis._client.options[:scheme]
+    assert_equal "host", redis._client.host
+    assert_equal 1234, redis._client.port
+    assert_equal 1, redis._client.db
   end
 
   def test_resolves_localhost
@@ -367,19 +271,15 @@ class TestInternals < Minitest::Test
     redis_mock(commands, host: host, &:ping)
   end
 
-  driver(:ruby) do
-    af_family_supported(Socket::AF_INET) do
-      def test_connect_ipv4
-        af_test("127.0.0.1")
-      end
+  af_family_supported(Socket::AF_INET) do
+    def test_connect_ipv4
+      af_test("127.0.0.1")
     end
   end
 
-  driver(:ruby) do
-    af_family_supported(Socket::AF_INET6) do
-      def test_connect_ipv6
-        af_test("::1")
-      end
+  af_family_supported(Socket::AF_INET6) do
+    def test_connect_ipv6
+      af_test("::1")
     end
   end
 
@@ -391,4 +291,38 @@ class TestInternals < Minitest::Test
 
     assert_equal clients + 1, r.info["connected_clients"].to_i
   end
+
+  def test_reconnect_on_readonly_errors
+    tcp_server = TCPServer.new("127.0.0.1", 0)
+    tcp_server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
+    port = tcp_server.addr[1]
+
+    server_thread = Thread.new do
+      session = tcp_server.accept
+      io = RedisClient::RubyConnection::BufferedIO.new(session, read_timeout: 1, write_timeout: 1)
+      2.times do
+        command = RedisClient::RESP3.load(io)
+        case command.first.upcase
+        when "PING"
+          session.write("+PONG\r\n")
+        when "SET"
+          session.write("-READONLY You can't write against a read only replica.\r\n")
+        else
+          session.write("-ERR Unknown command #{command.first}\r\n")
+        end
+      end
+      session.close
+    end
+
+    redis = Redis.new(host: "127.0.0.1", port: port, timeout: 2, reconnect_attempts: 0)
+    assert_equal "PONG", redis.ping
+
+    assert_raises Redis::ReadOnlyError do
+      redis.set("foo", "bar")
+    end
+
+    refute_predicate redis, :connected?
+  ensure
+    server_thread&.kill
+  end
 end
diff --git a/test/redis/pipelining_commands_test.rb b/test/redis/pipelining_commands_test.rb
index 5b80103c29c2a24611ef41cabfecda0fe76773d4..6265921a04c5db342ef778d236ecbad9ac215e89 100644
--- a/test/redis/pipelining_commands_test.rb
+++ b/test/redis/pipelining_commands_test.rb
@@ -75,8 +75,8 @@ class TestPipeliningCommands < Minitest::Test
 
   def test_assignment_of_results_inside_the_block
     r.pipelined do |p|
-      @first = p.sadd("foo", 1)
-      @second = p.sadd("foo", 1)
+      @first = p.sadd?("foo", 1)
+      @second = p.sadd?("foo", 1)
     end
 
     assert_equal true, @first.value
@@ -89,8 +89,8 @@ class TestPipeliningCommands < Minitest::Test
     assert_raises(Redis::CommandError) do
       r.pipelined do |p|
         p.doesnt_exist
-        @first = p.sadd("foo", 1)
-        @second = p.sadd("foo", 1)
+        @first = p.sadd?("foo", 1)
+        @second = p.sadd?("foo", 1)
       end
     end
 
@@ -98,12 +98,24 @@ class TestPipeliningCommands < Minitest::Test
     assert_raises(Redis::FutureNotReady) { @second.value }
   end
 
+  def test_assignment_of_results_inside_the_block_without_raising_exception
+    r.pipelined(exception: false) do |p|
+      @first = p.doesnt_exist
+      @second = p.sadd?("foo", 1)
+      @third = p.sadd?("foo", 1)
+    end
+
+    assert_equal RedisClient::CommandError, @first.value.class
+    assert_equal true, @second.value
+    assert_equal false, @third.value
+  end
+
   def test_assignment_of_results_inside_a_nested_block
     r.pipelined do |p|
-      @first = p.sadd("foo", 1)
+      @first = p.sadd?("foo", 1)
 
-      r.pipelined do |p2|
-        @second = p2.sadd("foo", 1)
+      p.pipelined do |p2|
+        @second = p2.sadd?("foo", 1)
       end
     end
 
@@ -111,9 +123,33 @@ class TestPipeliningCommands < Minitest::Test
     assert_equal false, @second.value
   end
 
+  def test_nested_pipelining_returns_without_raising_exception
+    result = r.pipelined(exception: false) do |p1|
+      p1.doesnt_exist
+      p1.set("foo", "42")
+      p1.pipelined do |p2|
+        p2.doesnt_exist_again
+        p2.set("bar", "99")
+      end
+    end
+
+    assert result[0].is_a?(RedisClient::CommandError)
+    assert_equal ["doesnt_exist"], result[0].command
+
+    assert_equal "OK", result[1]
+
+    assert result[2].is_a?(RedisClient::CommandError)
+    assert_equal ["doesnt_exist_again"], result[2].command
+
+    assert_equal "OK", result[3]
+
+    assert_equal "42", r.get("foo")
+    assert_equal "99", r.get("bar")
+  end
+
   def test_futures_raise_when_confused_with_something_else
     r.pipelined do |p|
-      @result = p.sadd("foo", 1)
+      @result = p.sadd?("foo", 1)
     end
 
     assert_raises(NoMethodError) { @result.to_s }
@@ -130,20 +166,12 @@ class TestPipeliningCommands < Minitest::Test
   def test_futures_raise_when_command_errors_and_needs_transformation
     assert_raises(Redis::CommandError) do
       r.pipelined do |p|
-        @result = p.zrange("a", "b", 5, with_scores: true)
+        p.zadd("set", "1", "one")
+        @result = p.zincryby("set", "fail", "one")
       end
     end
   end
 
-  def test_futures_warn_when_tested_for_equality
-    r.pipelined do |p|
-      @result = p.sadd("foo", 1)
-    end
-
-    Redis.expects(:deprecate!).once
-    @result == 1
-  end
-
   def test_futures_can_be_identified
     r.pipelined do |p|
       @result = p.sadd("foo", 1)
@@ -191,24 +219,24 @@ class TestPipeliningCommands < Minitest::Test
 
   def test_hgetall_in_a_pipeline_returns_hash
     r.hmset("hash", "field", "value")
+    future = nil
     result = r.pipelined do |p|
-      p.hgetall("hash")
+      future = p.hgetall("hash")
     end
 
-    assert_equal result.first, { "field" => "value" }
+    assert_equal([{ "field" => "value" }], result)
+    assert_equal({ "field" => "value" }, future.value)
   end
 
   def test_zpopmax_in_a_pipeline_produces_future
-    target_version('5.0.0') do
-      r.zadd("sortedset", 1.0, "value")
-      future = nil
-      result = r.pipelined do
-        future = r.zpopmax("sortedset")
-      end
-
-      assert_equal [["value", 1.0]], result
-      assert_equal ["value", 1.0], future.value
+    r.zadd("sortedset", 1.0, "value")
+    future = nil
+    result = r.pipelined do |pipeline|
+      future = pipeline.zpopmax("sortedset")
     end
+
+    assert_equal [["value", 1.0]], result
+    assert_equal ["value", 1.0], future.value
   end
 
   def test_keys_in_a_pipeline
@@ -244,30 +272,9 @@ class TestPipeliningCommands < Minitest::Test
     assert_equal "2", r.get("db")
   end
 
-  def test_pipeline_select_client_db
-    r.select 1
-    r.pipelined do |p2|
-      p2.select 2
-    end
-
-    assert_equal 2, r._client.db
-  end
-
-  def test_nested_pipeline_select_client_db
-    r.select 1
-    r.pipelined do |p2|
-      p2.select 2
-      p2.pipelined do |p3|
-        p3.select 3
-      end
-    end
-
-    assert_equal 3, r._client.db
-  end
-
   def test_pipeline_interrupt_preserves_client
     original = r._client
-    Redis::Pipeline.stubs(:new).raises(Interrupt)
+    Redis::PipelinedConnection.stubs(:new).raises(Interrupt)
     assert_raises(Interrupt) { r.pipelined {} }
     assert_equal r._client, original
   end
diff --git a/test/redis/publish_subscribe_test.rb b/test/redis/publish_subscribe_test.rb
index e42a8b98d22b4715b3a0cef5403138231767e9d4..4deea52ccb9873096005b388567e0fb6cf4b4c77 100644
--- a/test/redis/publish_subscribe_test.rb
+++ b/test/redis/publish_subscribe_test.rb
@@ -5,6 +5,26 @@ require "helper"
 class TestPublishSubscribe < Minitest::Test
   include Helper::Client
 
+  def setup
+    @threads = {}
+    super
+  end
+
+  def teardown
+    super
+    @threads.each do |thread, redis|
+      if redis.subscribed?
+        redis.unsubscribe
+        redis.punsubscribe
+      end
+      redis.close
+      begin
+        thread.join(2) or warn("leaked thread")
+      rescue RedisClient::ConnectionError
+      end
+    end
+  end
+
   class TestError < StandardError
   end
 
@@ -12,8 +32,8 @@ class TestPublishSubscribe < Minitest::Test
     @subscribed = false
     @unsubscribed = false
 
-    wire = Wire.new do
-      r.subscribe("foo") do |on|
+    thread = new_thread do |r|
+      r.subscribe(channel_name) do |on|
         on.subscribe do |_channel, total|
           @subscribed = true
           @t1 = total
@@ -34,11 +54,10 @@ class TestPublishSubscribe < Minitest::Test
     end
 
     # Wait until the subscription is active before publishing
-    Wire.pass until @subscribed
-
-    Redis.new(OPTIONS).publish("foo", "s1")
+    Thread.pass until @subscribed
 
-    wire.join
+    redis.publish(channel_name, "s1")
+    thread.join
 
     assert @subscribed
     assert_equal 1, @t1
@@ -51,8 +70,8 @@ class TestPublishSubscribe < Minitest::Test
     @subscribed = false
     @unsubscribed = false
 
-    wire = Wire.new do
-      r.psubscribe("f*") do |on|
+    thread = new_thread do |r|
+      r.psubscribe("channel:*") do |on|
         on.psubscribe do |_pattern, total|
           @subscribed = true
           @t1 = total
@@ -73,11 +92,9 @@ class TestPublishSubscribe < Minitest::Test
     end
 
     # Wait until the subscription is active before publishing
-    Wire.pass until @subscribed
-
-    Redis.new(OPTIONS).publish("foo", "s1")
-
-    wire.join
+    Thread.pass until @subscribed
+    redis.publish(channel_name, "s1")
+    thread.join
 
     assert @subscribed
     assert_equal 1, @t1
@@ -86,74 +103,49 @@ class TestPublishSubscribe < Minitest::Test
     assert_equal "s1", @message
   end
 
-  def test_pubsub_with_numpat_subcommand
-    target_version("2.8.0") do
-      @subscribed = false
-      wire = Wire.new do
-        r.psubscribe("f*") do |on|
-          on.psubscribe { |_channel, _total| @subscribed = true }
-          on.pmessage   { |_pattern, _channel, _message| r.punsubscribe }
-        end
-      end
-      Wire.pass until @subscribed
-      redis = Redis.new(OPTIONS)
-      numpat_result = redis.pubsub(:numpat)
-
-      redis.publish("foo", "s1")
-      wire.join
-
-      assert_equal redis.pubsub(:numpat), 0
-      assert_equal numpat_result, 1
-    end
-  end
-
   def test_pubsub_with_channels_and_numsub_subcommnads
-    target_version("2.8.0") do
-      @subscribed = false
-      wire = Wire.new do
-        r.subscribe("foo") do |on|
-          on.subscribe { |_channel, _total| @subscribed = true }
-          on.message   { |_channel, _message| r.unsubscribe }
-        end
+    @subscribed = false
+    thread = new_thread do |r|
+      r.subscribe(channel_name) do |on|
+        on.subscribe { |_channel, _total| @subscribed = true }
+        on.message   { |_channel, _message| r.unsubscribe }
       end
-      Wire.pass until @subscribed
-      redis = Redis.new(OPTIONS)
-      channels_result = redis.pubsub(:channels)
-      channels_result.delete('__sentinel__:hello')
-      numsub_result = redis.pubsub(:numsub, 'foo', 'boo')
+    end
+    Thread.pass until @subscribed
+    channels_result = redis.pubsub(:channels)
+    channels_result.delete('__sentinel__:hello')
+    numsub_result = redis.pubsub(:numsub, channel_name, 'boo')
 
-      redis.publish("foo", "s1")
-      wire.join
+    redis.publish(channel_name, "s1")
+    thread.join
 
-      assert_equal channels_result, ['foo']
-      assert_equal numsub_result, ['foo', 1, 'boo', 0]
-    end
+    assert_includes channels_result, channel_name
+    assert_equal [channel_name, 1, 'boo', 0], numsub_result
   end
 
   def test_subscribe_connection_usable_after_raise
     @subscribed = false
 
-    wire = Wire.new do
-      begin
-        r.subscribe("foo") do |on|
-          on.subscribe do |_channel, _total|
-            @subscribed = true
-          end
+    thread = new_thread do |r|
+      r.subscribe(channel_name) do |on|
+        on.subscribe do |_channel, _total|
+          @subscribed = true
+        end
 
-          on.message do |_channel, _message|
-            raise TestError
-          end
+        on.message do |_channel, _message|
+          r.unsubscribe
+          raise TestError
         end
-      rescue TestError
       end
+    rescue TestError
     end
 
     # Wait until the subscription is active before publishing
-    Wire.pass until @subscribed
+    Thread.pass until @subscribed
 
-    Redis.new(OPTIONS).publish("foo", "s1")
+    redis.publish(channel_name, "s1")
 
-    wire.join
+    thread.join
 
     assert_equal "PONG", r.ping
   end
@@ -161,74 +153,75 @@ class TestPublishSubscribe < Minitest::Test
   def test_psubscribe_connection_usable_after_raise
     @subscribed = false
 
-    wire = Wire.new do
-      begin
-        r.psubscribe("f*") do |on|
-          on.psubscribe do |_pattern, _total|
-            @subscribed = true
-          end
+    thread = new_thread do |r|
+      r.psubscribe("channel:*") do |on|
+        on.psubscribe do |_pattern, _total|
+          @subscribed = true
+        end
 
-          on.pmessage do |_pattern, _channel, _message|
-            raise TestError
-          end
+        on.pmessage do |_pattern, _channel, _message|
+          raise TestError
         end
-      rescue TestError
       end
+    rescue TestError
     end
 
     # Wait until the subscription is active before publishing
-    Wire.pass until @subscribed
+    Thread.pass until @subscribed
 
-    Redis.new(OPTIONS).publish("foo", "s1")
+    redis.publish(channel_name, "s1")
 
-    wire.join
+    thread.join
 
     assert_equal "PONG", r.ping
   end
 
   def test_subscribe_within_subscribe
-    @channels = []
+    @channels = Queue.new
 
-    wire = Wire.new do
-      r.subscribe("foo") do |on|
+    thread = new_thread do |r|
+      r.subscribe(channel_name) do |on|
         on.subscribe do |channel, _total|
           @channels << channel
 
-          r.subscribe("bar") if channel == "foo"
+          r.subscribe("bar") if channel == channel_name
           r.unsubscribe if channel == "bar"
         end
       end
     end
 
-    wire.join
+    thread.join
 
-    assert_equal ["foo", "bar"], @channels
+    assert_equal [channel_name, "bar"], [@channels.pop, @channels.pop]
+    assert_empty @channels
   end
 
   def test_other_commands_within_a_subscribe
-    assert_raises Redis::CommandError do
-      r.subscribe("foo") do |on|
-        on.subscribe do |_channel, _total|
-          r.set("bar", "s2")
-        end
+    r.subscribe(channel_name) do |on|
+      on.subscribe do |_channel, _total|
+        r.set("bar", "s2")
+        r.unsubscribe(channel_name)
       end
     end
   end
 
   def test_subscribe_without_a_block
-    assert_raises LocalJumpError do
-      r.subscribe("foo")
+    error = assert_raises Redis::SubscriptionError do
+      r.subscribe(channel_name)
     end
+    assert_includes "This client is not subscribed", error.message
   end
 
   def test_unsubscribe_without_a_subscribe
-    assert_raises RuntimeError do
+    error = assert_raises Redis::SubscriptionError do
       r.unsubscribe
     end
+    assert_includes "This client is not subscribed", error.message
 
-    assert_raises RuntimeError do
+    error = assert_raises Redis::SubscriptionError do
       r.punsubscribe
     end
+    assert_includes "This client is not subscribed", error.message
   end
 
   def test_subscribe_past_a_timeout
@@ -254,28 +247,112 @@ class TestPublishSubscribe < Minitest::Test
   def test_subscribe_with_timeout
     received = false
 
-    assert_raises Redis::TimeoutError do
-      r.subscribe_with_timeout(LOW_TIMEOUT, "foo") do |on|
-        on.message do |_channel, _message|
-          received = true
-        end
+    r.subscribe_with_timeout(LOW_TIMEOUT, channel_name) do |on|
+      on.message do |_channel, _message|
+        received = true
       end
     end
 
-    assert !received
+    refute received
   end
 
   def test_psubscribe_with_timeout
     received = false
 
-    assert_raises Redis::TimeoutError do
-      r.psubscribe_with_timeout(LOW_TIMEOUT, "f*") do |on|
-        on.message do |_channel, _message|
-          received = true
+    r.psubscribe_with_timeout(LOW_TIMEOUT, "channel:*") do |on|
+      on.message do |_channel, _message|
+        received = true
+      end
+    end
+
+    refute received
+  end
+
+  def test_unsubscribe_from_another_thread
+    @unsubscribed = @subscribed = false
+    @subscribed_redis = nil
+    @messages = Queue.new
+    thread = new_thread do |r|
+      @subscribed_redis = r
+      r.subscribe(channel_name) do |on|
+        on.subscribe do |_channel, _total|
+          @subscribed = true
+        end
+
+        on.message do |channel, message|
+          @messages << [channel, message]
+        end
+
+        on.unsubscribe do |_channel, _total|
+          @unsubscribed = true
         end
       end
     end
 
-    assert !received
+    Thread.pass until @subscribed
+
+    redis.publish(channel_name, "test")
+    assert_equal [channel_name, "test"], @messages.pop
+    assert_empty @messages
+
+    @subscribed_redis.unsubscribe # this shouldn't block
+    refute_nil thread.join(2)
+    assert_equal true, @unsubscribed
+  end
+
+  def test_subscribe_from_another_thread
+    @events = Queue.new
+    @subscribed_redis = nil
+    thread = new_thread do |r|
+      r.subscribe(channel_name) do |on|
+        @subscribed_redis = r
+        on.subscribe do |channel, _total|
+          @events << ["subscribed", channel]
+        end
+
+        on.message do |channel, message|
+          @events << ["message", channel, message]
+        end
+
+        on.unsubscribe do |channel, _total|
+          @events << ["unsubscribed", channel]
+        end
+      end
+    end
+
+    Thread.pass until @subscribed_redis&.subscribed?
+
+    redis.publish(channel_name, "test")
+    @subscribed_redis.subscribe("#{channel_name}:2")
+    redis.publish("#{channel_name}:2", "test-2")
+
+    @subscribed_redis.unsubscribe(channel_name)
+    @subscribed_redis.unsubscribe # this shouldn't block
+
+    refute_nil thread.join(2)
+    expected = [
+      ["subscribed", channel_name],
+      ["message", channel_name, "test"],
+      ["subscribed", "#{channel_name}:2"],
+      ["message", "#{channel_name}:2", "test-2"],
+      ["unsubscribed", channel_name],
+      ["unsubscribed", "#{channel_name}:2"]
+    ]
+    assert_equal(expected, expected.map { @events.pop })
+    assert_empty @events
+  end
+
+  private
+
+  def new_thread(&block)
+    redis = Redis.new(OPTIONS)
+    thread = Thread.new(redis, &block)
+    thread.report_on_exception = true
+    @threads[thread] = redis
+    thread
+  end
+
+  def channel_name
+    @channel_name ||= "channel:#{rand}"
   end
 end
diff --git a/test/redis/remote_server_control_commands_test.rb b/test/redis/remote_server_control_commands_test.rb
index 785e5b50e5321a5c2a2d191a39e016825e1de189..e778054c299706825f6dd4752a5072627ca5a935 100644
--- a/test/redis/remote_server_control_commands_test.rb
+++ b/test/redis/remote_server_control_commands_test.rb
@@ -25,54 +25,29 @@ class TestRemoteServerControlCommands < Minitest::Test
   end
 
   def test_info_commandstats
-    target_version "2.5.7" do
-      r.config(:resetstat)
-      r.get("foo")
-      r.get("bar")
+    r.config(:resetstat)
+    r.get("foo")
+    r.get("bar")
 
-      result = r.info(:commandstats)
-      assert_equal '2', result['get']['calls']
-    end
+    result = r.info(:commandstats)
+    assert_equal '2', result['get']['calls']
   end
 
-  def test_monitor_redis_lt_2_5_0
-    return unless version < "2.5.0"
-
+  def test_monitor_redis
     log = []
 
-    wire = Wire.new do
-      Redis.new(OPTIONS).monitor do |line|
-        log << line
-        break if log.size == 3
-      end
-    end
-
-    Wire.pass while log.empty? # Faster than sleep
-
-    r.set "foo", "s1"
-
-    wire.join
-
-    assert log[-1]['(db 15) "set" "foo" "s1"']
-  end
-
-  def test_monitor_redis_gte_2_5_0
-    return unless version >= "2.5.0"
-
-    log = []
-
-    wire = Wire.new do
+    thread = Thread.new do
       Redis.new(OPTIONS).monitor do |line|
         log << line
         break if line =~ /set/
       end
     end
 
-    Wire.pass while log.empty? # Faster than sleep
+    Thread.pass while log.empty? # Faster than sleep
 
     r.set "foo", "s1"
 
-    wire.join
+    thread.join
 
     assert log[-1] =~ /\b15\b.* "set" "foo" "s1"/
   end
@@ -100,7 +75,7 @@ class TestRemoteServerControlCommands < Minitest::Test
 
     assert_equal 1, r.object(:refcount, "list")
     encoding = r.object(:encoding, "list")
-    assert encoding == "ziplist" || encoding == "quicklist", "Wrong encoding for list"
+    assert encoding == "ziplist" || encoding == "quicklist" || encoding == "listpack", "Wrong encoding for list"
     assert r.object(:idletime, "list").is_a?(Integer)
   end
 
@@ -121,8 +96,6 @@ class TestRemoteServerControlCommands < Minitest::Test
   end
 
   def test_client_list
-    return if version < "2.4.0"
-
     keys = [
       "addr",
       "fd",
@@ -153,8 +126,6 @@ class TestRemoteServerControlCommands < Minitest::Test
   end
 
   def test_client_kill
-    return if version < "2.6.9"
-
     r.client(:setname, 'redis-rb')
     clients = r.client(:list)
     i = clients.index { |client| client['name'] == 'redis-rb' }
@@ -166,8 +137,6 @@ class TestRemoteServerControlCommands < Minitest::Test
   end
 
   def test_client_getname_and_setname
-    return if version < "2.6.9"
-
     assert_nil r.client(:getname)
 
     r.client(:setname, 'redis-rb')
diff --git a/test/redis/scanning_test.rb b/test/redis/scanning_test.rb
index 77f97d2597e3121c08f3249f7f3c8c960c08c838..72eac1344d3c635f9610b854bfb011527a3a7425 100644
--- a/test/redis/scanning_test.rb
+++ b/test/redis/scanning_test.rb
@@ -6,51 +6,45 @@ class TestScanning < Minitest::Test
   include Helper::Client
 
   def test_scan_basic
-    target_version "2.7.105" do
-      r.debug :populate, 1000
-
-      cursor = 0
-      all_keys = []
-      loop do
-        cursor, keys = r.scan cursor
-        all_keys += keys
-        break if cursor == "0"
-      end
-
-      assert_equal 1000, all_keys.uniq.size
+    r.debug :populate, 1000
+
+    cursor = 0
+    all_keys = []
+    loop do
+      cursor, keys = r.scan cursor
+      all_keys += keys
+      break if cursor == "0"
     end
+
+    assert_equal 1000, all_keys.uniq.size
   end
 
   def test_scan_count
-    target_version "2.7.105" do
-      r.debug :populate, 1000
-
-      cursor = 0
-      all_keys = []
-      loop do
-        cursor, keys = r.scan cursor, count: 5
-        all_keys += keys
-        break if cursor == "0"
-      end
-
-      assert_equal 1000, all_keys.uniq.size
+    r.debug :populate, 1000
+
+    cursor = 0
+    all_keys = []
+    loop do
+      cursor, keys = r.scan cursor, count: 5
+      all_keys += keys
+      break if cursor == "0"
     end
+
+    assert_equal 1000, all_keys.uniq.size
   end
 
   def test_scan_match
-    target_version "2.7.105" do
-      r.debug :populate, 1000
-
-      cursor = 0
-      all_keys = []
-      loop do
-        cursor, keys = r.scan cursor, match: "key:1??"
-        all_keys += keys
-        break if cursor == "0"
-      end
-
-      assert_equal 100, all_keys.uniq.size
+    r.debug :populate, 1000
+
+    cursor = 0
+    all_keys = []
+    loop do
+      cursor, keys = r.scan cursor, match: "key:1??"
+      all_keys += keys
+      break if cursor == "0"
     end
+
+    assert_equal 100, all_keys.uniq.size
   end
 
   def test_scan_type
@@ -73,28 +67,24 @@ class TestScanning < Minitest::Test
   end
 
   def test_scan_each_enumerator
-    target_version "2.7.105" do
-      r.debug :populate, 1000
+    r.debug :populate, 1000
 
-      scan_enumerator = r.scan_each
-      assert_equal true, scan_enumerator.is_a?(::Enumerator)
+    scan_enumerator = r.scan_each
+    assert_equal true, scan_enumerator.is_a?(::Enumerator)
 
-      keys_from_scan = scan_enumerator.to_a.uniq
-      all_keys = r.keys "*"
+    keys_from_scan = scan_enumerator.to_a.uniq
+    all_keys = r.keys "*"
 
-      assert all_keys.sort == keys_from_scan.sort
-    end
+    assert all_keys.sort == keys_from_scan.sort
   end
 
   def test_scan_each_enumerator_match
-    target_version "2.7.105" do
-      r.debug :populate, 1000
+    r.debug :populate, 1000
 
-      keys_from_scan = r.scan_each(match: "key:1??").to_a.uniq
-      all_keys = r.keys "key:1??"
+    keys_from_scan = r.scan_each(match: "key:1??").to_a.uniq
+    all_keys = r.keys "key:1??"
 
-      assert all_keys.sort == keys_from_scan.sort
-    end
+    assert all_keys.sort == keys_from_scan.sort
   end
 
   def test_scan_each_enumerator_type
@@ -112,321 +102,261 @@ class TestScanning < Minitest::Test
   end
 
   def test_scan_each_block
-    target_version "2.7.105" do
-      r.debug :populate, 100
+    r.debug :populate, 100
 
-      keys_from_scan = []
-      r.scan_each do |key|
-        keys_from_scan << key
-      end
+    keys_from_scan = []
+    r.scan_each do |key|
+      keys_from_scan << key
+    end
 
-      all_keys = r.keys "*"
+    all_keys = r.keys "*"
 
-      assert all_keys.sort == keys_from_scan.uniq.sort
-    end
+    assert all_keys.sort == keys_from_scan.uniq.sort
   end
 
   def test_scan_each_block_match
-    target_version "2.7.105" do
-      r.debug :populate, 100
+    r.debug :populate, 100
 
-      keys_from_scan = []
-      r.scan_each(match: "key:1?") do |key|
-        keys_from_scan << key
-      end
-
-      all_keys = r.keys "key:1?"
-
-      assert all_keys.sort == keys_from_scan.uniq.sort
+    keys_from_scan = []
+    r.scan_each(match: "key:1?") do |key|
+      keys_from_scan << key
     end
-  end
-
-  def test_sscan_with_encoding
-    target_version "2.7.105" do
-      %i[intset hashtable].each do |enc|
-        r.del "set"
 
-        prefix = ""
-        prefix = "ele:" if enc == :hashtable
+    all_keys = r.keys "key:1?"
 
-        elements = []
-        100.times { |j| elements << "#{prefix}#{j}" }
-
-        r.sadd "set", elements
-
-        assert_equal enc.to_s, r.object("encoding", "set")
-
-        cursor = 0
-        all_keys = []
-        loop do
-          cursor, keys = r.sscan "set", cursor
-          all_keys += keys
-          break if cursor == "0"
-        end
-
-        assert_equal 100, all_keys.uniq.size
-      end
-    end
+    assert all_keys.sort == keys_from_scan.uniq.sort
   end
 
   def test_sscan_each_enumerator
-    target_version "2.7.105" do
-      elements = []
-      100.times { |j| elements << "ele:#{j}" }
-      r.sadd "set", elements
+    elements = []
+    100.times { |j| elements << "ele:#{j}" }
+    r.sadd "set", elements
 
-      scan_enumerator = r.sscan_each("set")
-      assert_equal true, scan_enumerator.is_a?(::Enumerator)
+    scan_enumerator = r.sscan_each("set")
+    assert_equal true, scan_enumerator.is_a?(::Enumerator)
 
-      keys_from_scan = scan_enumerator.to_a.uniq
-      all_keys = r.smembers("set")
+    keys_from_scan = scan_enumerator.to_a.uniq
+    all_keys = r.smembers("set")
 
-      assert all_keys.sort == keys_from_scan.sort
-    end
+    assert all_keys.sort == keys_from_scan.sort
   end
 
   def test_sscan_each_enumerator_match
-    target_version "2.7.105" do
-      elements = []
-      100.times { |j| elements << "ele:#{j}" }
-      r.sadd "set", elements
+    elements = []
+    100.times { |j| elements << "ele:#{j}" }
+    r.sadd "set", elements
 
-      keys_from_scan = r.sscan_each("set", match: "ele:1?").to_a.uniq
+    keys_from_scan = r.sscan_each("set", match: "ele:1?").to_a.uniq
 
-      all_keys = r.smembers("set").grep(/^ele:1.$/)
+    all_keys = r.smembers("set").grep(/^ele:1.$/)
 
-      assert all_keys.sort == keys_from_scan.sort
-    end
+    assert all_keys.sort == keys_from_scan.sort
   end
 
   def test_sscan_each_enumerator_block
-    target_version "2.7.105" do
-      elements = []
-      100.times { |j| elements << "ele:#{j}" }
-      r.sadd "set", elements
+    elements = []
+    100.times { |j| elements << "ele:#{j}" }
+    r.sadd "set", elements
 
-      keys_from_scan = []
-      r.sscan_each("set") do |key|
-        keys_from_scan << key
-      end
+    keys_from_scan = []
+    r.sscan_each("set") do |key|
+      keys_from_scan << key
+    end
 
-      all_keys = r.smembers("set")
+    all_keys = r.smembers("set")
 
-      assert all_keys.sort == keys_from_scan.uniq.sort
-    end
+    assert all_keys.sort == keys_from_scan.uniq.sort
   end
 
   def test_sscan_each_enumerator_block_match
-    target_version "2.7.105" do
-      elements = []
-      100.times { |j| elements << "ele:#{j}" }
-      r.sadd "set", elements
+    elements = []
+    100.times { |j| elements << "ele:#{j}" }
+    r.sadd "set", elements
 
-      keys_from_scan = []
-      r.sscan_each("set", match: "ele:1?") do |key|
-        keys_from_scan << key
-      end
+    keys_from_scan = []
+    r.sscan_each("set", match: "ele:1?") do |key|
+      keys_from_scan << key
+    end
 
-      all_keys = r.smembers("set").grep(/^ele:1.$/)
+    all_keys = r.smembers("set").grep(/^ele:1.$/)
 
-      assert all_keys.sort == keys_from_scan.uniq.sort
-    end
+    assert all_keys.sort == keys_from_scan.uniq.sort
   end
 
   def test_hscan_with_encoding
-    target_version "2.7.105" do
-      %i[ziplist hashtable].each do |enc|
-        r.del "set"
-
-        count = 1000
-        count = 30 if enc == :ziplist
+    %i[ziplist hashtable].each do |enc|
+      r.del "set"
 
-        elements = []
-        count.times { |j| elements << "key:#{j}" << j.to_s }
+      count = 1000
+      count = 30 if enc == :ziplist
 
-        r.hmset "hash", *elements
+      elements = []
+      count.times { |j| elements << "key:#{j}" << j.to_s }
 
-        cursor = 0
-        all_key_values = []
-        loop do
-          cursor, key_values = r.hscan "hash", cursor
-          all_key_values.concat key_values
-          break if cursor == "0"
-        end
+      r.hmset "hash", *elements
 
-        keys2 = []
-        all_key_values.each do |k, v|
-          assert_equal "key:#{v}", k
-          keys2 << k
-        end
+      cursor = 0
+      all_key_values = []
+      loop do
+        cursor, key_values = r.hscan "hash", cursor
+        all_key_values.concat key_values
+        break if cursor == "0"
+      end
 
-        assert_equal count, keys2.uniq.size
+      keys2 = []
+      all_key_values.each do |k, v|
+        assert_equal "key:#{v}", k
+        keys2 << k
       end
+
+      assert_equal count, keys2.uniq.size
     end
   end
 
   def test_hscan_each_enumerator
-    target_version "2.7.105" do
-      count = 1000
-      elements = []
-      count.times { |j| elements << "key:#{j}" << j.to_s }
-      r.hmset "hash", *elements
+    count = 1000
+    elements = []
+    count.times { |j| elements << "key:#{j}" << j.to_s }
+    r.hmset "hash", *elements
 
-      scan_enumerator = r.hscan_each("hash")
-      assert_equal true, scan_enumerator.is_a?(::Enumerator)
+    scan_enumerator = r.hscan_each("hash")
+    assert_equal true, scan_enumerator.is_a?(::Enumerator)
 
-      keys_from_scan = scan_enumerator.to_a.uniq
-      all_keys = r.hgetall("hash").to_a
+    keys_from_scan = scan_enumerator.to_a.uniq
+    all_keys = r.hgetall("hash").to_a
 
-      assert all_keys.sort == keys_from_scan.sort
-    end
+    assert all_keys.sort == keys_from_scan.sort
   end
 
   def test_hscan_each_enumerator_match
-    target_version "2.7.105" do
-      count = 100
-      elements = []
-      count.times { |j| elements << "key:#{j}" << j.to_s }
-      r.hmset "hash", *elements
+    count = 100
+    elements = []
+    count.times { |j| elements << "key:#{j}" << j.to_s }
+    r.hmset "hash", *elements
 
-      keys_from_scan = r.hscan_each("hash", match: "key:1?").to_a.uniq
-      all_keys = r.hgetall("hash").to_a.select { |k, _v| k =~ /^key:1.$/ }
+    keys_from_scan = r.hscan_each("hash", match: "key:1?").to_a.uniq
+    all_keys = r.hgetall("hash").to_a.select { |k, _v| k =~ /^key:1.$/ }
 
-      assert all_keys.sort == keys_from_scan.sort
-    end
+    assert all_keys.sort == keys_from_scan.sort
   end
 
   def test_hscan_each_block
-    target_version "2.7.105" do
-      count = 1000
-      elements = []
-      count.times { |j| elements << "key:#{j}" << j.to_s }
-      r.hmset "hash", *elements
+    count = 1000
+    elements = []
+    count.times { |j| elements << "key:#{j}" << j.to_s }
+    r.hmset "hash", *elements
+
+    keys_from_scan = []
+    r.hscan_each("hash") do |field, value|
+      keys_from_scan << [field, value]
+    end
+    all_keys = r.hgetall("hash").to_a
 
-      keys_from_scan = []
-      r.hscan_each("hash") do |field, value|
-        keys_from_scan << [field, value]
-      end
-      all_keys = r.hgetall("hash").to_a
+    assert all_keys.sort == keys_from_scan.uniq.sort
+  end
 
-      assert all_keys.sort == keys_from_scan.uniq.sort
+  def test_hscan_each_block_match
+    count = 1000
+    elements = []
+    count.times { |j| elements << "key:#{j}" << j.to_s }
+    r.hmset "hash", *elements
+
+    keys_from_scan = []
+    r.hscan_each("hash", match: "key:1?") do |field, value|
+      keys_from_scan << [field, value]
     end
+    all_keys = r.hgetall("hash").to_a.select { |k, _v| k =~ /^key:1.$/ }
+
+    assert all_keys.sort == keys_from_scan.uniq.sort
   end
 
-  def test_hscan_each_block_match
-    target_version "2.7.105" do
+  def test_zscan_with_encoding
+    %i[ziplist skiplist].each do |enc|
+      r.del "zset"
+
       count = 1000
+      count = 30 if enc == :ziplist
+
       elements = []
-      count.times { |j| elements << "key:#{j}" << j.to_s }
-      r.hmset "hash", *elements
+      count.times { |j| elements << j << "key:#{j}" }
 
-      keys_from_scan = []
-      r.hscan_each("hash", match: "key:1?") do |field, value|
-        keys_from_scan << [field, value]
-      end
-      all_keys = r.hgetall("hash").to_a.select { |k, _v| k =~ /^key:1.$/ }
+      r.zadd "zset", elements
 
-      assert all_keys.sort == keys_from_scan.uniq.sort
-    end
-  end
+      cursor = 0
+      all_key_scores = []
+      loop do
+        cursor, key_scores = r.zscan "zset", cursor
+        all_key_scores.concat key_scores
+        break if cursor == "0"
+      end
 
-  def test_zscan_with_encoding
-    target_version "2.7.105" do
-      %i[ziplist skiplist].each do |enc|
-        r.del "zset"
-
-        count = 1000
-        count = 30 if enc == :ziplist
-
-        elements = []
-        count.times { |j| elements << j << "key:#{j}" }
-
-        r.zadd "zset", elements
-
-        cursor = 0
-        all_key_scores = []
-        loop do
-          cursor, key_scores = r.zscan "zset", cursor
-          all_key_scores.concat key_scores
-          break if cursor == "0"
-        end
-
-        keys2 = []
-        all_key_scores.each do |k, v|
-          assert_equal true, v.is_a?(Float)
-          assert_equal "key:#{Integer(v)}", k
-          keys2 << k
-        end
-
-        assert_equal count, keys2.uniq.size
+      keys2 = []
+      all_key_scores.each do |k, v|
+        assert_equal true, v.is_a?(Float)
+        assert_equal "key:#{Integer(v)}", k
+        keys2 << k
       end
+
+      assert_equal count, keys2.uniq.size
     end
   end
 
   def test_zscan_each_enumerator
-    target_version "2.7.105" do
-      count = 1000
-      elements = []
-      count.times { |j| elements << j << "key:#{j}" }
-      r.zadd "zset", elements
+    count = 1000
+    elements = []
+    count.times { |j| elements << j << "key:#{j}" }
+    r.zadd "zset", elements
 
-      scan_enumerator = r.zscan_each "zset"
-      assert_equal true, scan_enumerator.is_a?(::Enumerator)
+    scan_enumerator = r.zscan_each "zset"
+    assert_equal true, scan_enumerator.is_a?(::Enumerator)
 
-      scores_from_scan = scan_enumerator.to_a.uniq
-      member_scores = r.zrange("zset", 0, -1, with_scores: true)
+    scores_from_scan = scan_enumerator.to_a.uniq
+    member_scores = r.zrange("zset", 0, -1, with_scores: true)
 
-      assert member_scores.sort == scores_from_scan.sort
-    end
+    assert member_scores.sort == scores_from_scan.sort
   end
 
   def test_zscan_each_enumerator_match
-    target_version "2.7.105" do
-      count = 1000
-      elements = []
-      count.times { |j| elements << j << "key:#{j}" }
-      r.zadd "zset", elements
+    count = 1000
+    elements = []
+    count.times { |j| elements << j << "key:#{j}" }
+    r.zadd "zset", elements
 
-      scores_from_scan = r.zscan_each("zset", match: "key:1??").to_a.uniq
-      member_scores = r.zrange("zset", 0, -1, with_scores: true)
-      filtered_members = member_scores.select { |k, _s| k =~ /^key:1..$/ }
+    scores_from_scan = r.zscan_each("zset", match: "key:1??").to_a.uniq
+    member_scores = r.zrange("zset", 0, -1, with_scores: true)
+    filtered_members = member_scores.select { |k, _s| k =~ /^key:1..$/ }
 
-      assert filtered_members.sort == scores_from_scan.sort
-    end
+    assert filtered_members.sort == scores_from_scan.sort
   end
 
   def test_zscan_each_block
-    target_version "2.7.105" do
-      count = 1000
-      elements = []
-      count.times { |j| elements << j << "key:#{j}" }
-      r.zadd "zset", elements
-
-      scores_from_scan = []
-      r.zscan_each("zset") do |member, score|
-        scores_from_scan << [member, score]
-      end
-      member_scores = r.zrange("zset", 0, -1, with_scores: true)
-
-      assert member_scores.sort == scores_from_scan.sort
+    count = 1000
+    elements = []
+    count.times { |j| elements << j << "key:#{j}" }
+    r.zadd "zset", elements
+
+    scores_from_scan = []
+    r.zscan_each("zset") do |member, score|
+      scores_from_scan << [member, score]
     end
+    member_scores = r.zrange("zset", 0, -1, with_scores: true)
+
+    assert member_scores.sort == scores_from_scan.sort
   end
 
   def test_zscan_each_block_match
-    target_version "2.7.105" do
-      count = 1000
-      elements = []
-      count.times { |j| elements << j << "key:#{j}" }
-      r.zadd "zset", elements
-
-      scores_from_scan = []
-      r.zscan_each("zset", match: "key:1??") do |member, score|
-        scores_from_scan << [member, score]
-      end
-      member_scores = r.zrange("zset", 0, -1, with_scores: true)
-      filtered_members = member_scores.select { |k, _s| k =~ /^key:1..$/ }
-
-      assert filtered_members.sort == scores_from_scan.sort
+    count = 1000
+    elements = []
+    count.times { |j| elements << j << "key:#{j}" }
+    r.zadd "zset", elements
+
+    scores_from_scan = []
+    r.zscan_each("zset", match: "key:1??") do |member, score|
+      scores_from_scan << [member, score]
     end
+    member_scores = r.zrange("zset", 0, -1, with_scores: true)
+    filtered_members = member_scores.select { |k, _s| k =~ /^key:1..$/ }
+
+    assert filtered_members.sort == scores_from_scan.sort
   end
 end
diff --git a/test/redis/scripting_test.rb b/test/redis/scripting_test.rb
index 56a8058057c0a047b1d13bede8443db55e6bb3da..f016501229fa5a55af490bf3f7572ce2447be8ef 100644
--- a/test/redis/scripting_test.rb
+++ b/test/redis/scripting_test.rb
@@ -10,68 +10,54 @@ class TestScripting < Minitest::Test
   end
 
   def test_script_exists
-    target_version "2.5.9" do # 2.6-rc1
-      a = to_sha("return 1")
-      b = a.succ
+    a = to_sha("return 1")
+    b = a.succ
 
-      assert_equal true, r.script(:exists, a)
-      assert_equal false, r.script(:exists, b)
-      assert_equal [true], r.script(:exists, [a])
-      assert_equal [false], r.script(:exists, [b])
-      assert_equal [true, false], r.script(:exists, [a, b])
-    end
+    assert_equal true, r.script(:exists, a)
+    assert_equal false, r.script(:exists, b)
+    assert_equal [true], r.script(:exists, [a])
+    assert_equal [false], r.script(:exists, [b])
+    assert_equal [true, false], r.script(:exists, [a, b])
   end
 
   def test_script_flush
-    target_version "2.5.9" do # 2.6-rc1
-      sha = to_sha("return 1")
-      assert r.script(:exists, sha)
-      assert_equal "OK", r.script(:flush)
-      assert !r.script(:exists, sha)
-    end
+    sha = to_sha("return 1")
+    assert r.script(:exists, sha)
+    assert_equal "OK", r.script(:flush)
+    assert !r.script(:exists, sha)
   end
 
   def test_script_kill
-    target_version "2.5.9" do # 2.6-rc1
-      redis_mock(script: ->(arg) { "+#{arg.upcase}" }) do |redis|
-        assert_equal "KILL", redis.script(:kill)
-      end
+    redis_mock(script: ->(arg) { "+#{arg.upcase}" }) do |redis|
+      assert_equal "KILL", redis.script(:kill)
     end
   end
 
   def test_eval
-    target_version "2.5.9" do # 2.6-rc1
-      assert_equal 0, r.eval("return #KEYS")
-      assert_equal 0, r.eval("return #ARGV")
-      assert_equal ["k1", "k2"], r.eval("return KEYS", ["k1", "k2"])
-      assert_equal ["a1", "a2"], r.eval("return ARGV", [], ["a1", "a2"])
-    end
+    assert_equal 0, r.eval("return #KEYS")
+    assert_equal 0, r.eval("return #ARGV")
+    assert_equal ["k1", "k2"], r.eval("return KEYS", ["k1", "k2"])
+    assert_equal ["a1", "a2"], r.eval("return ARGV", [], ["a1", "a2"])
   end
 
   def test_eval_with_options_hash
-    target_version "2.5.9" do # 2.6-rc1
-      assert_equal 0, r.eval("return #KEYS", {})
-      assert_equal 0, r.eval("return #ARGV", {})
-      assert_equal ["k1", "k2"], r.eval("return KEYS", { keys: ["k1", "k2"] })
-      assert_equal ["a1", "a2"], r.eval("return ARGV", { argv: ["a1", "a2"] })
-    end
+    assert_equal 0, r.eval("return #KEYS", {})
+    assert_equal 0, r.eval("return #ARGV", {})
+    assert_equal ["k1", "k2"], r.eval("return KEYS", { keys: ["k1", "k2"] })
+    assert_equal ["a1", "a2"], r.eval("return ARGV", { argv: ["a1", "a2"] })
   end
 
   def test_evalsha
-    target_version "2.5.9" do # 2.6-rc1
-      assert_equal 0, r.evalsha(to_sha("return #KEYS"))
-      assert_equal 0, r.evalsha(to_sha("return #ARGV"))
-      assert_equal ["k1", "k2"], r.evalsha(to_sha("return KEYS"), ["k1", "k2"])
-      assert_equal ["a1", "a2"], r.evalsha(to_sha("return ARGV"), [], ["a1", "a2"])
-    end
+    assert_equal 0, r.evalsha(to_sha("return #KEYS"))
+    assert_equal 0, r.evalsha(to_sha("return #ARGV"))
+    assert_equal ["k1", "k2"], r.evalsha(to_sha("return KEYS"), ["k1", "k2"])
+    assert_equal ["a1", "a2"], r.evalsha(to_sha("return ARGV"), [], ["a1", "a2"])
   end
 
   def test_evalsha_with_options_hash
-    target_version "2.5.9" do # 2.6-rc1
-      assert_equal 0, r.evalsha(to_sha("return #KEYS"), {})
-      assert_equal 0, r.evalsha(to_sha("return #ARGV"), {})
-      assert_equal ["k1", "k2"], r.evalsha(to_sha("return KEYS"), { keys: ["k1", "k2"] })
-      assert_equal ["a1", "a2"], r.evalsha(to_sha("return ARGV"), { argv: ["a1", "a2"] })
-    end
+    assert_equal 0, r.evalsha(to_sha("return #KEYS"), {})
+    assert_equal 0, r.evalsha(to_sha("return #ARGV"), {})
+    assert_equal ["k1", "k2"], r.evalsha(to_sha("return KEYS"), { keys: ["k1", "k2"] })
+    assert_equal ["a1", "a2"], r.evalsha(to_sha("return ARGV"), { argv: ["a1", "a2"] })
   end
 end
diff --git a/test/redis/ssl_test.rb b/test/redis/ssl_test.rb
index 769bf8a65c5d10d9b60ddb1e79e651584cd28674..0752973208500959225cba8d043b2ece2c2d99f2 100644
--- a/test/redis/ssl_test.rb
+++ b/test/redis/ssl_test.rb
@@ -5,55 +5,42 @@ require "helper"
 class SslTest < Minitest::Test
   include Helper::Client
 
-  driver(:ruby) do
-    def test_connection_to_non_ssl_server
-      assert_raises(Redis::CannotConnectError) do
-        redis = Redis.new(OPTIONS.merge(ssl: true, timeout: LOW_TIMEOUT))
-        redis.ping
-      end
-    end
-
-    def test_verified_ssl_connection
-      RedisMock.start({ ping: proc { "+PONG" } }, ssl_server_opts("trusted")) do |port|
-        redis = Redis.new(port: port, ssl: true, ssl_params: { ca_file: ssl_ca_file })
-        assert_equal redis.ping, "PONG"
-      end
+  def test_connection_to_non_ssl_server
+    assert_raises(Redis::CannotConnectError) do
+      redis = Redis.new(OPTIONS.merge(ssl: true, timeout: LOW_TIMEOUT))
+      redis.ping
     end
+  end
 
-    def test_unverified_ssl_connection
-      assert_raises(OpenSSL::SSL::SSLError) do
-        RedisMock.start({ ping: proc { "+PONG" } }, ssl_server_opts("untrusted")) do |port|
-          redis = Redis.new(port: port, ssl: true, ssl_params: { ca_file: ssl_ca_file })
-          redis.ping
-        end
-      end
+  def test_verified_ssl_connection
+    RedisMock.start({ ping: proc { "+PONG" } }, ssl_server_opts("trusted")) do |port|
+      redis = Redis.new(host: "127.0.0.1", port: port, ssl: true, ssl_params: { ca_file: ssl_ca_file })
+      assert_equal redis.ping, "PONG"
     end
+  end
 
-    def test_verify_certificates_by_default
-      assert_raises(OpenSSL::SSL::SSLError) do
-        RedisMock.start({ ping: proc { "+PONG" } }, ssl_server_opts("untrusted")) do |port|
-          redis = Redis.new(port: port, ssl: true)
-          redis.ping
-        end
+  def test_unverified_ssl_connection
+    assert_raises(Redis::CannotConnectError) do
+      RedisMock.start({ ping: proc { "+PONG" } }, ssl_server_opts("untrusted")) do |port|
+        redis = Redis.new(port: port, ssl: true, ssl_params: { ca_file: ssl_ca_file })
+        redis.ping
       end
     end
+  end
 
-    def test_ssl_blocking
-      RedisMock.start({}, ssl_server_opts("trusted")) do |port|
-        redis = Redis.new(port: port, ssl: true, ssl_params: { ca_file: ssl_ca_file })
-        assert_equal redis.set("boom", "a" * 10_000_000), "OK"
+  def test_verify_certificates_by_default
+    assert_raises(Redis::CannotConnectError) do
+      RedisMock.start({ ping: proc { "+PONG" } }, ssl_server_opts("untrusted")) do |port|
+        redis = Redis.new(port: port, ssl: true)
+        redis.ping
       end
     end
   end
 
-  driver(:hiredis, :synchrony) do
-    def test_ssl_not_implemented_exception
-      assert_raises(NotImplementedError) do
-        RedisMock.start({ ping: proc { "+PONG" } }, ssl_server_opts("trusted")) do |port|
-          redis = Redis.new(port: port, ssl: true, ssl_params: { ca_file: ssl_ca_file })
-          redis.ping
-        end
-      end
+  def test_ssl_blocking
+    RedisMock.start({}, ssl_server_opts("trusted")) do |port|
+      redis = Redis.new(host: "127.0.0.1", port: port, ssl: true, ssl_params: { ca_file: ssl_ca_file })
+      assert_equal redis.set("boom", "a" * 10_000_000), "OK"
     end
   end
 
diff --git a/test/redis/synchrony_driver.rb b/test/redis/synchrony_driver.rb
deleted file mode 100644
index 9734c77a33f6c92e358ef3d8ec19a381325b0928..0000000000000000000000000000000000000000
--- a/test/redis/synchrony_driver.rb
+++ /dev/null
@@ -1,86 +0,0 @@
-# frozen_string_literal: true
-
-require "em-synchrony"
-require "em-synchrony/connection_pool"
-
-require_relative "../lib/redis"
-require_relative "../lib/redis/connection/synchrony"
-
-require "helper"
-
-PORT    = 6381
-OPTIONS = { port: PORT, db: 15 }.freeze
-
-#
-# if running under Eventmachine + Synchrony (Ruby 1.9+), then
-# we can simulate the blocking API while performing the network
-# IO via the EM reactor.
-#
-
-EM.synchrony do
-  r = Redis.new OPTIONS
-  r.flushdb
-
-  r.rpush "foo", "s1"
-  r.rpush "foo", "s2"
-
-  assert_equal 2, r.llen("foo")
-  assert_equal "s2", r.rpop("foo")
-
-  r.set("foo", "bar")
-
-  assert_equal "bar", r.getset("foo", "baz")
-  assert_equal "baz", r.get("foo")
-
-  r.set("foo", "a")
-
-  assert_equal 1, r.getbit("foo", 1)
-  assert_equal 1, r.getbit("foo", 2)
-  assert_equal 0, r.getbit("foo", 3)
-  assert_equal 0, r.getbit("foo", 4)
-  assert_equal 0, r.getbit("foo", 5)
-  assert_equal 0, r.getbit("foo", 6)
-  assert_equal 1, r.getbit("foo", 7)
-
-  r.flushdb
-
-  # command pipelining
-  r.pipelined do
-    r.lpush "foo", "s1"
-    r.lpush "foo", "s2"
-  end
-
-  assert_equal 2, r.llen("foo")
-  assert_equal "s2", r.lpop("foo")
-  assert_equal "s1", r.lpop("foo")
-
-  assert_equal "OK", r._client.call(:quit)
-  assert_equal "PONG", r.ping
-
-  rpool = EM::Synchrony::ConnectionPool.new(size: 5) { Redis.new OPTIONS }
-
-  result = rpool.watch 'foo' do |rd|
-    assert_kind_of Redis, rd
-
-    rd.set "foo", "s1"
-    rd.multi do |multi|
-      multi.set "foo", "s2"
-    end
-  end
-
-  assert_nil result
-  assert_equal "s1", rpool.get("foo")
-
-  result = rpool.watch "foo" do |rd|
-    assert_kind_of Redis, rd
-
-    rd.multi do |multi|
-      multi.set "foo", "s3"
-    end
-  end
-
-  assert_equal ["OK"], result
-  assert_equal "s3", rpool.get("foo")
-
-  EM.stop
-end
diff --git a/test/redis/thread_safety_test.rb b/test/redis/thread_safety_test.rb
index ced338d270b5fc73640eb98f3305042ea626eeaa..57ec5e9c47a9ff2dd39ccc199a47361080993831 100644
--- a/test/redis/thread_safety_test.rb
+++ b/test/redis/thread_safety_test.rb
@@ -5,57 +5,25 @@ require "helper"
 class TestThreadSafety < Minitest::Test
   include Helper::Client
 
-  driver(:ruby, :hiredis) do
-    def test_thread_safety
-      redis = Redis.new(OPTIONS)
-      redis.set "foo", 1
-      redis.set "bar", 2
+  def test_thread_safety
+    redis = Redis.new(OPTIONS)
+    redis.set "foo", 1
+    redis.set "bar", 2
 
-      sample = 100
+    sample = 100
 
-      t1 = Thread.new do
-        @foos = Array.new(sample) { redis.get "foo" }
-      end
-
-      t2 = Thread.new do
-        @bars = Array.new(sample) { redis.get "bar" }
-      end
-
-      t1.join
-      t2.join
-
-      assert_equal ["1"], @foos.uniq
-      assert_equal ["2"], @bars.uniq
+    t1 = Thread.new do
+      @foos = Array.new(sample) { redis.get "foo" }
     end
 
-    def test_thread_safety_queue_commit
-      redis = Redis.new(OPTIONS)
-      redis.set "foo", 1
-      redis.set "bar", 2
-
-      sample = 100
-
-      t1 = Thread.new do
-        sample.times do
-          r.queue("get", "foo")
-        end
-
-        @foos = r.commit
-      end
-
-      t2 = Thread.new do
-        sample.times do
-          r.queue("get", "bar")
-        end
-
-        @bars = r.commit
-      end
+    t2 = Thread.new do
+      @bars = Array.new(sample) { redis.get "bar" }
+    end
 
-      t1.join
-      t2.join
+    t1.join
+    t2.join
 
-      assert_equal ["1"], @foos.uniq
-      assert_equal ["2"], @bars.uniq
-    end
+    assert_equal ["1"], @foos.uniq
+    assert_equal ["2"], @bars.uniq
   end
 end
diff --git a/test/redis/transactions_test.rb b/test/redis/transactions_test.rb
index 6699a9f9d4b53a044f32bbec497c45a78e3b9ec8..026d79cf3521a015b620ae18912e1e27353a0616 100644
--- a/test/redis/transactions_test.rb
+++ b/test/redis/transactions_test.rb
@@ -6,16 +6,9 @@ class TestTransactions < Minitest::Test
   include Helper::Client
 
   def test_multi_discard
-    r.multi
-
-    assert_equal "QUEUED", r.set("foo", "1")
-    assert_equal "QUEUED", r.get("foo")
-    assert_equal "QUEUED", r.zincrby("bar", 1,  "baz") # Floatify
-    assert_equal "QUEUED", r.hsetnx("plop", "foo", "bar") # Boolify
-
-    r.discard
-
-    assert_nil r.get("foo")
+    assert_raises(LocalJumpError) do
+      r.multi
+    end
   end
 
   def test_multi_exec_with_a_block
@@ -39,8 +32,9 @@ class TestTransactions < Minitest::Test
 
   def test_multi_in_pipeline
     foo_future = bar_future = nil
+    multi_future = nil
     response = r.pipelined do |pipeline|
-      pipeline.multi do |multi|
+      multi_future = pipeline.multi do |multi|
         multi.set("foo", "s1")
         foo_future = multi.get("foo")
       end
@@ -51,76 +45,34 @@ class TestTransactions < Minitest::Test
       end
     end
 
-    assert_equal "s1", foo_future.value
-    assert_equal "s2", bar_future.value
-
     assert_equal(["OK", "QUEUED", "QUEUED", ["OK", "s1"], "OK", "QUEUED", "QUEUED", ["OK", "s2"]], response)
-  end
 
-  def test_multi_in_pipeline_deprecated
-    foo_future = bar_future = nil
-    response = r.pipelined do
-      r.multi do |multi|
-        multi.set("foo", "s1")
-        foo_future = multi.get("foo")
-      end
-
-      r.multi do |multi|
-        multi.set("bar", "s2")
-        bar_future = multi.get("bar")
-      end
-    end
+    assert_equal ["OK", "s1"], multi_future.value
 
     assert_equal "s1", foo_future.value
     assert_equal "s2", bar_future.value
-
-    assert_equal(["OK", "QUEUED", "QUEUED", ["OK", "s1"], "OK", "QUEUED", "QUEUED", ["OK", "s2"]], response)
-  end
-
-  def test_multi_in_pipeline_double_deprecated
-    foo_future = bar_future = nil
-    response = r.pipelined do
-      r.multi do
-        r.set("foo", "s1")
-        foo_future = r.get("foo")
-      end
-
-      r.multi do
-        r.set("bar", "s2")
-        bar_future = r.get("bar")
-      end
-    end
-
-    assert_equal "s1", foo_future.value
-    assert_equal "s2", bar_future.value
-
-    assert_equal(["OK", "QUEUED", "QUEUED", ["OK", "s1"], "OK", "QUEUED", "QUEUED", ["OK", "s2"]], response)
   end
 
   def test_assignment_inside_multi_exec_block
     r.multi do |m|
-      @first = m.sadd("foo", 1)
-      @second = m.sadd("foo", 1)
+      @first = m.sadd?("foo", 1)
+      @second = m.sadd?("foo", 1)
     end
 
     assert_equal true, @first.value
     assert_equal false, @second.value
   end
 
-  # Although we could support accessing the values in these futures,
-  # it doesn't make a lot of sense.
   def test_assignment_inside_multi_exec_block_with_delayed_command_errors
     assert_raises(Redis::CommandError) do
       r.multi do |m|
         @first = m.set("foo", "s1")
         @second = m.incr("foo") # not an integer
-        @third = m.lpush("foo", "value") # wrong kind of value
       end
     end
 
     assert_equal "OK", @first.value
-    assert_raises(Redis::CommandError) { @second.value }
-    assert_raises(Redis::FutureNotReady) { @third.value }
+    assert_raises(Redis::FutureNotReady) { @second.value }
   end
 
   def test_assignment_inside_multi_exec_block_with_immediate_command_errors
@@ -149,16 +101,16 @@ class TestTransactions < Minitest::Test
   end
 
   def test_transformed_replies_as_return_values_for_multi_exec_block
-    info, = r.multi do |_m|
-      r.info
+    info, = r.multi do |transaction|
+      transaction.info
     end
 
-    assert info.is_a?(Hash)
+    assert_instance_of Hash, info
   end
 
   def test_transformed_replies_inside_multi_exec_block
-    r.multi do |_m|
-      @info = r.info
+    r.multi do |transaction|
+      @info = transaction.info
     end
 
     assert @info.value.is_a?(Hash)
@@ -260,7 +212,7 @@ class TestTransactions < Minitest::Test
 
   def test_multi_with_interrupt_preserves_client
     original = r._client
-    Redis::Pipeline.stubs(:new).raises(Interrupt)
+    Redis::MultiConnection.stubs(:new).raises(Interrupt)
     assert_raises(Interrupt) { r.multi {} }
     assert_equal r._client, original
   end
diff --git a/test/redis/url_param_test.rb b/test/redis/url_param_test.rb
index b2560fe70f68de663fb9dccf27a4a882c8f9127a..e14314ac2ea2941d9f4ea6bcd87a95edb4ba4172 100644
--- a/test/redis/url_param_test.rb
+++ b/test/redis/url_param_test.rb
@@ -5,10 +5,10 @@ require "helper"
 class TestUrlParam < Minitest::Test
   include Helper::Client
 
-  def test_url_defaults_to_______________
+  def test_url_defaults_to_localhost
     redis = Redis.new
 
-    assert_equal "127.0.0.1", redis._client.host
+    assert_equal "localhost", redis._client.host
     assert_equal 6379, redis._client.port
     assert_equal 0, redis._client.db
     assert_nil redis._client.password
@@ -23,49 +23,20 @@ class TestUrlParam < Minitest::Test
     assert_equal "secr3t", redis._client.password
   end
 
-  def test_allows_to_pass_in_a_url_with_string_key
-    redis = Redis.new "url" => "redis://:secr3t@foo.com:999/2"
-
-    assert_equal "foo.com", redis._client.host
-    assert_equal 999, redis._client.port
-    assert_equal 2, redis._client.db
-    assert_equal "secr3t", redis._client.password
-  end
-
   def test_unescape_password_from_url
     redis = Redis.new url: "redis://:secr3t%3A@foo.com:999/2"
 
     assert_equal "secr3t:", redis._client.password
   end
 
-  def test_unescape_password_from_url_with_string_key
-    redis = Redis.new "url" => "redis://:secr3t%3A@foo.com:999/2"
-
-    assert_equal "secr3t:", redis._client.password
-  end
-
   def test_does_not_unescape_password_when_explicitly_passed
     redis = Redis.new url: "redis://:secr3t%3A@foo.com:999/2", password: "secr3t%3A"
 
     assert_equal "secr3t%3A", redis._client.password
   end
 
-  def test_does_not_unescape_password_when_explicitly_passed_with_string_key
-    redis = Redis.new :url => "redis://:secr3t%3A@foo.com:999/2", "password" => "secr3t%3A"
-
-    assert_equal "secr3t%3A", redis._client.password
-  end
-
   def test_override_url_if_path_option_is_passed
-    redis = Redis.new url: "redis://:secr3t@foo.com/foo:999/2", path: "/tmp/redis.sock"
-
-    assert_equal "/tmp/redis.sock", redis._client.path
-    assert_nil redis._client.host
-    assert_nil redis._client.port
-  end
-
-  def test_override_url_if_path_option_is_passed_with_string_key
-    redis = Redis.new :url => "redis://:secr3t@foo.com/foo:999/2", "path" => "/tmp/redis.sock"
+    redis = Redis.new url: "redis://:secr3t@foo.com/2", path: "/tmp/redis.sock"
 
     assert_equal "/tmp/redis.sock", redis._client.path
     assert_nil redis._client.host
@@ -81,15 +52,6 @@ class TestUrlParam < Minitest::Test
     assert_equal "secr3t", redis._client.password
   end
 
-  def test_overrides_url_if_another_connection_option_is_passed_with_string_key
-    redis = Redis.new :url => "redis://:secr3t@foo.com:999/2", "port" => 1000
-
-    assert_equal "foo.com", redis._client.host
-    assert_equal 1000, redis._client.port
-    assert_equal 2, redis._client.db
-    assert_equal "secr3t", redis._client.password
-  end
-
   def test_does_not_overrides_url_if_a_nil_option_is_passed
     redis = Redis.new url: "redis://:secr3t@foo.com:999/2", port: nil
 
@@ -99,15 +61,6 @@ class TestUrlParam < Minitest::Test
     assert_equal "secr3t", redis._client.password
   end
 
-  def test_does_not_overrides_url_if_a_nil_option_is_passed_with_string_key
-    redis = Redis.new :url => "redis://:secr3t@foo.com:999/2", "port" => nil
-
-    assert_equal "foo.com", redis._client.host
-    assert_equal 999, redis._client.port
-    assert_equal 2, redis._client.db
-    assert_equal "secr3t", redis._client.password
-  end
-
   def test_does_not_modify_the_passed_options
     options = { url: "redis://:secr3t@foo.com:999/2" }
 
@@ -130,9 +83,9 @@ class TestUrlParam < Minitest::Test
   end
 
   def test_defaults_to_localhost
-    redis = Redis.new(url: "redis:///")
+    redis = Redis.new(url: "redis://")
 
-    assert_equal "127.0.0.1", redis._client.host
+    assert_equal "localhost", redis._client.host
   end
 
   def test_ipv6_url
@@ -144,7 +97,6 @@ class TestUrlParam < Minitest::Test
   def test_user_and_password
     redis = Redis.new(url: 'redis://johndoe:mysecret@foo.com:999/2')
 
-    assert_equal('redis', redis._client.scheme)
     assert_equal('johndoe', redis._client.username)
     assert_equal('mysecret', redis._client.password)
     assert_equal('foo.com', redis._client.host)
diff --git a/test/sentinel/sentinel_command_test.rb b/test/sentinel/sentinel_command_test.rb
index bf6372d89daf77929c27e00687021c8374782769..98124801f906f45c6e51eaa3fce67d750bb5e81a 100644
--- a/test/sentinel/sentinel_command_test.rb
+++ b/test/sentinel/sentinel_command_test.rb
@@ -7,6 +7,8 @@ class SentinelCommandsTest < Minitest::Test
   include Helper::Sentinel
 
   def test_sentinel_command_master
+    wait_for_quorum
+
     redis = build_sentinel_client
     result = redis.sentinel('master', MASTER_NAME)
 
@@ -15,6 +17,8 @@ class SentinelCommandsTest < Minitest::Test
   end
 
   def test_sentinel_command_masters
+    wait_for_quorum
+
     redis = build_sentinel_client
     result = redis.sentinel('masters')
 
@@ -24,6 +28,8 @@ class SentinelCommandsTest < Minitest::Test
   end
 
   def test_sentinel_command_slaves
+    wait_for_quorum
+
     redis = build_sentinel_client
     result = redis.sentinel('slaves', MASTER_NAME)
 
@@ -33,6 +39,8 @@ class SentinelCommandsTest < Minitest::Test
   end
 
   def test_sentinel_command_sentinels
+    wait_for_quorum
+
     redis = build_sentinel_client
     result = redis.sentinel('sentinels', MASTER_NAME)
 
@@ -51,9 +59,10 @@ class SentinelCommandsTest < Minitest::Test
   end
 
   def test_sentinel_command_ckquorum
+    wait_for_quorum
+
     redis = build_sentinel_client
     result = redis.sentinel('ckquorum', MASTER_NAME)
-
     assert_equal result, 'OK 3 usable Sentinels. Quorum and failover authorization can be reached'
   end
 end
diff --git a/test/sentinel/sentinel_test.rb b/test/sentinel/sentinel_test.rb
index c0d0e3ff8eae3be5f9e19add3abe033884f68014..541ae63b754135656332e4fdb16e0f439ca92ebf 100644
--- a/test/sentinel/sentinel_test.rb
+++ b/test/sentinel/sentinel_test.rb
@@ -6,13 +6,16 @@ class SentinelTest < Minitest::Test
   include Helper::Sentinel
 
   def test_sentinel_master_role_connection
+    wait_for_quorum
+
     actual = redis.role
 
     assert_equal 'master', actual[0]
-    assert_equal SLAVE_PORT, actual[2][0][1]
   end
 
   def test_sentinel_slave_role_connection
+    wait_for_quorum
+
     redis = build_slave_role_client
     actual = redis.role
 
@@ -20,6 +23,14 @@ class SentinelTest < Minitest::Test
     assert_equal MASTER_PORT.to_i, actual[2]
   end
 
+  def test_without_reconnect
+    wait_for_quorum
+
+    redis.without_reconnect do
+      redis.get("key")
+    end
+  end
+
   def test_the_client_can_connect_to_available_slaves
     commands = {
       sentinel: lambda do |*_|
@@ -69,7 +80,14 @@ class SentinelTest < Minitest::Test
     s2 = {
       sentinel: lambda do |command, *args|
         commands[:s2] << [command, *args]
-        ["127.0.0.1", "6381"]
+        case command
+        when "get-master-addr-by-name"
+          ["127.0.0.1", "6381"]
+        when "sentinels"
+          []
+        else
+          raise "Unexpected command #{[command, *args].inspect}"
+        end
       end
     }
 
@@ -84,7 +102,7 @@ class SentinelTest < Minitest::Test
     end
 
     assert_equal commands[:s1], [%w[get-master-addr-by-name master1]]
-    assert_equal commands[:s2], [%w[get-master-addr-by-name master1]]
+    assert_equal commands[:s2], [%w[get-master-addr-by-name master1], ["sentinels", "master1"]]
   end
 
   def test_sentinel_failover_prioritize_healthy_sentinel
@@ -106,7 +124,17 @@ class SentinelTest < Minitest::Test
     s2 = {
       sentinel: lambda do |command, *args|
         commands[:s2] << [command, *args]
-        ["127.0.0.1", "6381"]
+        case command
+        when "get-master-addr-by-name"
+          ["127.0.0.1", "6381"]
+        when "sentinels"
+          [
+            ["ip", "127.0.0.1", "port", "26381"],
+            ["ip", "127.0.0.1", "port", "26382"],
+          ]
+        else
+          raise "Unexpected command #{[command, *args].inspect}"
+        end
       end
     }
 
@@ -124,8 +152,8 @@ class SentinelTest < Minitest::Test
       end
     end
 
-    assert_equal commands[:s1], [%w[get-master-addr-by-name master1]]
-    assert_equal commands[:s2], [%w[get-master-addr-by-name master1], %w[get-master-addr-by-name master1]]
+    assert_equal [%w[get-master-addr-by-name master1]], commands[:s1]
+    assert_equal [%w[get-master-addr-by-name master1],  ["sentinels", "master1"]], commands[:s2]
   end
 
   def test_sentinel_with_non_sentinel_options
@@ -133,8 +161,8 @@ class SentinelTest < Minitest::Test
 
     sentinel = lambda do |port|
       {
-        auth: lambda do |pass|
-          commands[:s1] << ['auth', pass]
+        auth: lambda do |*args|
+          commands[:s1] << ['auth', *args]
           '+OK'
         end,
         select: lambda do |db|
@@ -143,14 +171,24 @@ class SentinelTest < Minitest::Test
         end,
         sentinel: lambda do |command, *args|
           commands[:s1] << [command, *args]
-          ['127.0.0.1', port.to_s]
+          case command
+          when "get-master-addr-by-name"
+            ["127.0.0.1", port.to_s]
+          when "sentinels"
+            [
+              ["ip", "127.0.0.1", "port", "26381"],
+              ["ip", "127.0.0.1", "port", "26382"],
+            ]
+          else
+            raise "Unexpected command #{[command, *args].inspect}"
+          end
         end
       }
     end
 
     master = {
-      auth: lambda do |pass|
-        commands[:m1] << ['auth', pass]
+      auth: lambda do |*args|
+        commands[:m1] << ['auth', *args]
         '+OK'
       end,
       role: lambda do
@@ -167,7 +205,7 @@ class SentinelTest < Minitest::Test
       end
     end
 
-    assert_equal [%w[get-master-addr-by-name master1]], commands[:s1]
+    assert_equal [%w[get-master-addr-by-name master1], ["sentinels", "master1"]], commands[:s1]
     assert_equal [%w[auth foo], %w[role]], commands[:m1]
   end
 
@@ -176,8 +214,8 @@ class SentinelTest < Minitest::Test
 
     sentinel = lambda do |port|
       {
-        auth: lambda do |pass|
-          commands[:s1] << ['auth', pass]
+        auth: lambda do |*args|
+          commands[:s1] << ['auth', *args]
           '+OK'
         end,
         select: lambda do |db|
@@ -186,14 +224,24 @@ class SentinelTest < Minitest::Test
         end,
         sentinel: lambda do |command, *args|
           commands[:s1] << [command, *args]
-          ['127.0.0.1', port.to_s]
+          case command
+          when "get-master-addr-by-name"
+            ["127.0.0.1", port.to_s]
+          when "sentinels"
+            [
+              ["ip", "127.0.0.1", "port", "26381"],
+              ["ip", "127.0.0.1", "port", "26382"],
+            ]
+          else
+            raise "Unexpected command #{[command, *args].inspect}"
+          end
         end
       }
     end
 
     master = {
-      auth: lambda do |pass|
-        commands[:m1] << ['auth', pass]
+      auth: lambda do |*args|
+        commands[:s1] << ['auth', *args]
         '-ERR Client sent AUTH, but no password is set'
       end,
       role: lambda do
@@ -204,13 +252,13 @@ class SentinelTest < Minitest::Test
 
     RedisMock.start(master) do |master_port|
       RedisMock.start(sentinel.call(master_port)) do |sen_port|
-        s = [{ host: '127.0.0.1', port: sen_port, password: 'foo' }]
-        r = Redis.new(host: 'master1', sentinels: s, role: :master)
+        s = [{ host: '127.0.0.1', port: sen_port }]
+        r = Redis.new(name: 'master1', sentinels: s, role: :master, sentinel_password: 'foo')
         assert r.ping
       end
     end
 
-    assert_equal [%w[auth foo], %w[get-master-addr-by-name master1]], commands[:s1]
+    assert_equal [%w[auth foo], %w[get-master-addr-by-name master1], ["sentinels", "master1"]], commands[:s1]
     assert_equal [%w[role]], commands[:m1]
   end
 
@@ -219,8 +267,8 @@ class SentinelTest < Minitest::Test
 
     sentinel = lambda do |port|
       {
-        auth: lambda do |pass|
-          commands[:s1] << ['auth', pass]
+        auth: lambda do |*args|
+          commands[:s1] << ['auth', *args]
           '+OK'
         end,
         select: lambda do |db|
@@ -229,31 +277,44 @@ class SentinelTest < Minitest::Test
         end,
         sentinel: lambda do |command, *args|
           commands[:s1] << [command, *args]
-          ['127.0.0.1', port.to_s]
-        end
+          case command
+          when "get-master-addr-by-name"
+            ["127.0.0.1", port.to_s]
+          when "sentinels"
+            [
+              ["ip", "127.0.0.1", "port", "26381"],
+              ["ip", "127.0.0.1", "port", "26382"],
+            ]
+          else
+            raise "Unexpected command #{[command, *args].inspect}"
+          end
+        end,
       }
     end
 
     master = {
-      auth: lambda do |pass|
-        commands[:m1] << ['auth', pass]
+      auth: lambda do |*args|
+        commands[:m1] << ['auth', *args]
         '+OK'
       end,
       role: lambda do
         commands[:m1] << ['role']
         ['master']
-      end
+      end,
+      sentinel: lambda do |command, *args|
+        commands[:s2] << [command, *args]
+      end,
     }
 
     RedisMock.start(master) do |master_port|
       RedisMock.start(sentinel.call(master_port)) do |sen_port|
-        s = [{ host: '127.0.0.1', port: sen_port, password: 'foo' }]
-        r = Redis.new(host: 'master1', sentinels: s, role: :master, password: 'bar')
+        s = [{ host: '127.0.0.1', port: sen_port }]
+        r = Redis.new(name: 'master1', sentinels: s, role: :master, password: 'bar', sentinel_password: 'foo')
         assert r.ping
       end
     end
 
-    assert_equal [%w[auth foo], %w[get-master-addr-by-name master1]], commands[:s1]
+    assert_equal [%w[auth foo], %w[get-master-addr-by-name master1], ["sentinels", "master1"]], commands[:s1]
     assert_equal [%w[auth bar], %w[role]], commands[:m1]
   end
 
@@ -272,7 +333,17 @@ class SentinelTest < Minitest::Test
         end,
         sentinel: lambda do |command, *args|
           commands[:s1] << [command, *args]
-          ['127.0.0.1', port.to_s]
+          case command
+          when "get-master-addr-by-name"
+            ["127.0.0.1", port.to_s]
+          when "sentinels"
+            [
+              ["ip", "127.0.0.1", "port", "26381"],
+              ["ip", "127.0.0.1", "port", "26382"],
+            ]
+          else
+            raise "Unexpected command #{[command, *args].inspect}"
+          end
         end
       }
     end
@@ -290,13 +361,13 @@ class SentinelTest < Minitest::Test
 
     RedisMock.start(master) do |master_port|
       RedisMock.start(sentinel.call(master_port)) do |sen_port|
-        s = [{ host: '127.0.0.1', port: sen_port, username: 'bob', password: 'foo' }]
-        r = Redis.new(host: 'master1', sentinels: s, role: :master, username: 'alice', password: 'bar')
+        s = [{ host: '127.0.0.1', port: sen_port }]
+        r = Redis.new(name: 'master1', sentinels: s, role: :master, username: 'alice', password: 'bar', sentinel_username: 'bob', sentinel_password: 'foo')
         assert r.ping
       end
     end
 
-    assert_equal [%w[auth bob foo], %w[get-master-addr-by-name master1]], commands[:s1]
+    assert_equal [%w[auth bob foo], %w[get-master-addr-by-name master1], ["sentinels", "master1"]], commands[:s1]
     assert_equal [%w[auth alice bar], %w[role]], commands[:m1]
   end
 
@@ -305,8 +376,17 @@ class SentinelTest < Minitest::Test
 
     sentinel = lambda do |port|
       {
-        sentinel: lambda do |_command, *_args|
-          ["127.0.0.1", port.to_s]
+        sentinel: lambda do |command, *_args|
+          case command
+          when "get-master-addr-by-name"
+            ["127.0.0.1", port.to_s]
+          when "sentinels"
+            [
+              ["ip", "127.0.0.1", "port", "26381"],
+            ]
+          else
+            raise "Unexpected command #{[command, *args].inspect}"
+          end
         end
       }
     end
@@ -317,18 +397,18 @@ class SentinelTest < Minitest::Test
       end
     }
 
-    ex = assert_raises(Redis::ConnectionError) do
+    ex = assert_raises(Redis::CannotConnectError) do
       RedisMock.start(master) do |master_port|
         RedisMock.start(sentinel.call(master_port)) do |sen_port|
           sentinels[0][:port] = sen_port
-          redis = Redis.new(url: "redis://master1", sentinels: sentinels, role: :master)
+          redis = Redis.new(url: "redis://master1", sentinels: sentinels, role: :master, reconnect_attempts: 0)
 
           assert redis.ping
         end
       end
     end
 
-    assert_match(/Instance role mismatch/, ex.message)
+    assert_match(/Expected to connect to a master, but the server is a replica/, ex.message)
   end
 
   def test_sentinel_retries
@@ -337,15 +417,28 @@ class SentinelTest < Minitest::Test
 
     connections = []
 
+    fails = Hash.new(0)
+
     handler = lambda do |id, port|
       {
-        sentinel: lambda do |_command, *_args|
+        sentinel: lambda do |command, *_args|
           connections << id
 
-          if connections.count(id) < 2
+          if fails[id] < 2
+            fails[id] += 1
             :close
           else
-            ["127.0.0.1", port.to_s]
+            case command
+            when "get-master-addr-by-name"
+              ["127.0.0.1", port.to_s]
+            when "sentinels"
+              [
+                ["ip", "127.0.0.1", "port", "26381"],
+                ["ip", "127.0.0.1", "port", "26382"],
+              ]
+            else
+              raise "Unexpected command #{[command, *args].inspect}"
+            end
           end
         end
       }
@@ -369,9 +462,10 @@ class SentinelTest < Minitest::Test
       end
     end
 
-    assert_equal %i[s1 s2 s1], connections
+    assert_equal %i[s1 s1 s2 s2 s1 s1], connections
 
     connections.clear
+    fails.clear
 
     ex = assert_raises(Redis::CannotConnectError) do
       RedisMock.start(master) do |master_port|
@@ -389,35 +483,4 @@ class SentinelTest < Minitest::Test
 
     assert_match(/No sentinels available/, ex.message)
   end
-
-  def test_sentinel_with_string_option_keys
-    commands = []
-
-    master = {
-      role: lambda do
-        ['master']
-      end
-    }
-
-    sentinel = lambda do |port|
-      {
-        sentinel: lambda do |command, *args|
-          commands << [command, *args]
-          ['127.0.0.1', port.to_s]
-        end
-      }
-    end
-
-    RedisMock.start(master) do |master_port|
-      RedisMock.start(sentinel.call(master_port)) do |sen_port|
-        sentinels = [{ 'host' => '127.0.0.1', 'port' => sen_port }]
-
-        redis = Redis.new(url: 'redis://master1', 'sentinels' => sentinels, 'role' => :master)
-
-        assert redis.ping
-      end
-    end
-
-    assert_equal [%w[get-master-addr-by-name master1]], commands
-  end
 end
diff --git a/test/support/conf/redis-7.2.conf b/test/support/conf/redis-7.2.conf
new file mode 100644
index 0000000000000000000000000000000000000000..261b18686ee750be1d9cacbd0841614f9dbe479e
--- /dev/null
+++ b/test/support/conf/redis-7.2.conf
@@ -0,0 +1,3 @@
+appendonly no
+save ""
+enable-debug-command yes
diff --git a/test/support/connection/hiredis.rb b/test/support/connection/hiredis.rb
deleted file mode 100644
index 2545fa7badf7686223d1f44cf124333f4a741d91..0000000000000000000000000000000000000000
--- a/test/support/connection/hiredis.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-# frozen_string_literal: true
-
-require_relative "../wire/thread"
diff --git a/test/support/connection/ruby.rb b/test/support/connection/ruby.rb
deleted file mode 100644
index 2545fa7badf7686223d1f44cf124333f4a741d91..0000000000000000000000000000000000000000
--- a/test/support/connection/ruby.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-# frozen_string_literal: true
-
-require_relative "../wire/thread"
diff --git a/test/support/connection/synchrony.rb b/test/support/connection/synchrony.rb
deleted file mode 100644
index 9228b499ef73bfc62054b9b03d18afca3956e577..0000000000000000000000000000000000000000
--- a/test/support/connection/synchrony.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-require_relative "../wire/synchrony"
-
-module Helper
-  def around
-    rv = nil
-
-    EM.synchrony do
-      begin
-        rv = yield
-      ensure
-        EM.stop
-      end
-    end
-
-    rv
-  end
-end
diff --git a/test/support/redis_mock.rb b/test/support/redis_mock.rb
index ab8bc059c115138353f1f1e178263d18c26ac658..ac10298332f5724ead6be52fb4416a4f8bfde54d 100644
--- a/test/support/redis_mock.rb
+++ b/test/support/redis_mock.rb
@@ -8,6 +8,8 @@ module RedisMock
       tcp_server = TCPServer.new(options[:host] || "127.0.0.1", 0)
       tcp_server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
 
+      @concurrent = options.delete(:concurrent)
+
       if options[:ssl]
         ctx = OpenSSL::SSL::SSLContext.new
 
@@ -32,14 +34,21 @@ module RedisMock
       @thread.kill
     end
 
-    def run
+    def run(&block)
       loop do
-        session = @server.accept
-
-        begin
-          return if yield(session) == :exit
-        ensure
-          session.close
+        if @concurrent
+          Thread.new(@server.accept) do |session|
+            block.call(session)
+          ensure
+            session.close
+          end
+        else
+          session = @server.accept
+          begin
+            return if yield(session) == :exit
+          ensure
+            session.close
+          end
         end
       end
     rescue => ex
@@ -93,7 +102,7 @@ module RedisMock
         end
 
         command = argv.shift
-        blk = commands[command.to_sym]
+        blk = commands[command.downcase.to_sym]
         blk ||= ->(*_) { "+OK" }
 
         response = blk.call(*argv)
diff --git a/test/support/wire/synchrony.rb b/test/support/wire/synchrony.rb
deleted file mode 100644
index b147aae13e61645dc716dbc3faf93a19b631ffbc..0000000000000000000000000000000000000000
--- a/test/support/wire/synchrony.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-class Wire < Fiber
-  # We cannot run this fiber explicitly because EM schedules it. Resuming the
-  # current fiber on the next tick to let the reactor do work.
-  def self.pass
-    f = Fiber.current
-    EM.next_tick { f.resume }
-    Fiber.yield
-  end
-
-  def self.sleep(sec)
-    EM::Synchrony.sleep(sec)
-  end
-
-  def initialize(&blk)
-    super
-
-    # Schedule run in next tick
-    EM.next_tick { resume }
-  end
-
-  def join
-    self.class.pass while alive?
-  end
-end
diff --git a/test/support/wire/thread.rb b/test/support/wire/thread.rb
deleted file mode 100644
index d91a8f67eab556eab15d6370c4ed2f703788fce2..0000000000000000000000000000000000000000
--- a/test/support/wire/thread.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-class Wire < Thread
-  def self.sleep(sec)
-    Kernel.sleep(sec)
-  end
-end