diff --git a/.ruby-version b/.ruby-version
new file mode 100644
index 0000000000000000000000000000000000000000..15a279981720791464e46ab21ae96b3c1c65c3b6
--- /dev/null
+++ b/.ruby-version
@@ -0,0 +1 @@
+3.3.0
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bbdc021bc137458dc3eb0aadd575cebeb9143e31..b38c4fc8f9dd329c90aab8da6371b59449364049 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Unreleased
 
+# 5.3.0
+
+- Fix the return type of `hgetall` when used inside a `multi` transaction which is itself inside a pipeline.
+
 # 5.2.0
 
 - Now require Ruby 2.6 because `redis-client` does.
diff --git a/README.md b/README.md
index d4ee0ab93c3c43809e7bf39ae91acc060956651d..d004a578d1beddb6e7b3c7498273a32f0369d6f0 100644
--- a/README.md
+++ b/README.md
@@ -273,6 +273,7 @@ See lib/redis/errors.rb for information about what exceptions are possible.
 ## Timeouts
 
 The client allows you to configure connect, read, and write timeouts.
+Starting in version 5.0, the default for each is 1. Before that, it was 5.
 Passing a single `timeout` option will set all three values:
 
 ```ruby
diff --git a/cluster/README.md b/cluster/README.md
index d4722207716eff7721bd90aea541f7cb2dc078b0..51c7e316c66359cff1fa9329976dc38d72eeb5b1 100644
--- a/cluster/README.md
+++ b/cluster/README.md
@@ -75,3 +75,27 @@ Redis::Cluster.new(nodes: %w[rediss://foo-endpoint.example.com:6379], fixed_host
 ```
 
 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.
+
+## Transaction with an optimistic locking
+Since Redis cluster is a distributed system, several behaviors are different from a standalone server.
+Client libraries can make them compatible up to a point, but a part of features needs some restrictions.
+Especially, some cautions are needed to use the transaction feature with an optimistic locking.
+
+```ruby
+# The client is an instance of the internal adapter for the optimistic locking
+redis.watch("{my}key") do |client|
+  if client.get("{my}key") == "some value"
+    # The tx is an instance of the internal adapter for the transaction
+    client.multi do |tx|
+      tx.set("{my}key", "other value")
+      tx.incr("{my}counter")
+    end
+  else
+    client.unwatch
+  end
+end
+```
+
+In a cluster mode client, you need to pass a block if you call the watch method and you need to specify an argument to the block.
+Also, you should use the block argument as a receiver to call commands in the block.
+Although the above restrictions are needed, this implementations is compatible with a standalone client.
diff --git a/cluster/bin/console b/cluster/bin/console
new file mode 100755
index 0000000000000000000000000000000000000000..22964251453ff0f7d14024d09fde483a95519497
--- /dev/null
+++ b/cluster/bin/console
@@ -0,0 +1,8 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require 'irb'
+require 'bundler/setup'
+require 'redis/cluster'
+
+IRB.start(File.expand_path('..', __dir__))
diff --git a/cluster/lib/redis/cluster.rb b/cluster/lib/redis/cluster.rb
index 5abbb628c72975672d64c7573245eb978ad1826c..8da108e19da002721c1ebc1f9c3bb5f05c00295e 100644
--- a/cluster/lib/redis/cluster.rb
+++ b/cluster/lib/redis/cluster.rb
@@ -96,8 +96,33 @@ class Redis
       send_command([:cluster, subcommand] + args, &block)
     end
 
+    # Watch the given keys to determine execution of the MULTI/EXEC block.
+    #
+    # Using a block is required for a cluster client. It's different from a standalone client.
+    # And you should use the block argument as a receiver if you call commands.
+    #
+    # An `#unwatch` is automatically issued if an exception is raised within the
+    # block that is a subclass of StandardError and is not a ConnectionError.
+    #
+    # @param keys [String, Array<String>] one or more keys to watch
+    # @return [Object] returns the return value of the block
+    #
+    # @example A typical use case.
+    #   # The client is an instance of the internal adapter for the optimistic locking
+    #   redis.watch("{my}key") do |client|
+    #     if client.get("{my}key") == "some value"
+    #       # The tx is an instance of the internal adapter for the transaction
+    #       client.multi do |tx|
+    #         tx.set("{my}key", "other value")
+    #         tx.incr("{my}counter")
+    #       end
+    #     else
+    #       client.unwatch
+    #     end
+    #   end
+    #   #=> ["OK", 6]
     def watch(*keys, &block)
-      synchronize { |c| c.call_v([:watch] + keys, &block) }
+      synchronize { |c| c.watch(*keys, &block) }
     end
 
     private
diff --git a/cluster/lib/redis/cluster/client.rb b/cluster/lib/redis/cluster/client.rb
index a0e889926496b181e64761bb76e9113f61b01595..b7ff6980178741683c51cd24e6b7dbf5ef622fa7 100644
--- a/cluster/lib/redis/cluster/client.rb
+++ b/cluster/lib/redis/cluster/client.rb
@@ -1,6 +1,7 @@
 # frozen_string_literal: true
 
 require 'redis-cluster-client'
+require 'redis/cluster/transaction_adapter'
 
 class Redis
   class Cluster
@@ -53,11 +54,11 @@ class Redis
       ruby2_keywords :initialize if respond_to?(:ruby2_keywords, true)
 
       def id
-        @router.node_keys.join(' ')
+        server_url.join(' ')
       end
 
       def server_url
-        @router.node_keys
+        @router.nil? ? @config.startup_nodes.keys : router.node_keys
       end
 
       def connected?
@@ -98,6 +99,34 @@ class Redis
         handle_errors { super(watch: watch, &block) }
       end
 
+      def watch(*keys, &block)
+        unless block_given?
+          raise(
+            Redis::Cluster::TransactionConsistencyError,
+            'A block is required if you use the cluster client.'
+          )
+        end
+
+        unless block.arity == 1
+          raise(
+            Redis::Cluster::TransactionConsistencyError,
+            'Given block needs an argument if you use the cluster client.'
+          )
+        end
+
+        handle_errors do
+          RedisClient::Cluster::OptimisticLocking.new(router).watch(keys) do |c, slot, asking|
+            transaction = Redis::Cluster::TransactionAdapter.new(
+              self, router, @command_builder, node: c, slot: slot, asking: asking
+            )
+
+            result = yield transaction
+            c.call('UNWATCH') unless transaction.lock_released?
+            result
+          end
+        end
+      end
+
       private
 
       def handle_errors
diff --git a/cluster/lib/redis/cluster/transaction_adapter.rb b/cluster/lib/redis/cluster/transaction_adapter.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4732824c1e13560f1e9fe21eb145e9c075a979b8
--- /dev/null
+++ b/cluster/lib/redis/cluster/transaction_adapter.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+require 'redis_client/cluster/transaction'
+
+class Redis
+  class Cluster
+    class TransactionAdapter
+      class Internal < RedisClient::Cluster::Transaction
+        def initialize(client, router, command_builder, node: nil, slot: nil, asking: false)
+          @client = client
+          super(router, command_builder, node: node, slot: slot, asking: asking)
+        end
+
+        def multi
+          raise(Redis::Cluster::TransactionConsistencyError, "Can't nest multi transaction")
+        end
+
+        def exec
+          # no need to do anything
+        end
+
+        def discard
+          # no need to do anything
+        end
+
+        def watch(*_)
+          raise(Redis::Cluster::TransactionConsistencyError, "Can't use watch in a transaction")
+        end
+
+        def unwatch
+          # no need to do anything
+        end
+
+        private
+
+        def method_missing(name, *args, **kwargs, &block)
+          return call(name, *args, **kwargs, &block) if @client.respond_to?(name)
+
+          super
+        end
+
+        def respond_to_missing?(name, include_private = false)
+          return true if @client.respond_to?(name)
+
+          super
+        end
+      end
+
+      def initialize(client, router, command_builder, node: nil, slot: nil, asking: false)
+        @client = client
+        @router = router
+        @command_builder = command_builder
+        @node = node
+        @slot = slot
+        @asking = asking
+        @lock_released = false
+      end
+
+      def lock_released?
+        @lock_released
+      end
+
+      def multi
+        @lock_released = true
+        transaction = Redis::Cluster::TransactionAdapter::Internal.new(
+          @client, @router, @command_builder, node: @node, slot: @slot, asking: @asking
+        )
+        yield transaction
+        transaction.execute
+      end
+
+      def exec
+        # no need to do anything
+      end
+
+      def discard
+        # no need to do anything
+      end
+
+      def watch(*_)
+        raise(Redis::Cluster::TransactionConsistencyError, "Can't nest watch command if you use the cluster client")
+      end
+
+      def unwatch
+        @lock_released = true
+        @node.call('UNWATCH')
+      end
+
+      private
+
+      def method_missing(name, *args, **kwargs, &block)
+        return @client.public_send(name, *args, **kwargs, &block) if @client.respond_to?(name)
+
+        super
+      end
+
+      def respond_to_missing?(name, include_private = false)
+        return true if @client.respond_to?(name)
+
+        super
+      end
+    end
+  end
+end
diff --git a/cluster/redis-clustering.gemspec b/cluster/redis-clustering.gemspec
index 05194217bd52874268cfaa93f5f14778af95cf47..5e45a7a6789f5cbc915b44b2efe732046edec460 100644
--- a/cluster/redis-clustering.gemspec
+++ b/cluster/redis-clustering.gemspec
@@ -47,5 +47,5 @@ Gem::Specification.new do |s|
   s.required_ruby_version = '>= 2.7.0'
 
   s.add_runtime_dependency('redis', s.version)
-  s.add_runtime_dependency('redis-cluster-client', '>= 0.7.11')
+  s.add_runtime_dependency('redis-cluster-client', '>= 0.10.0')
 end
diff --git a/cluster/test/client_internals_test.rb b/cluster/test/client_internals_test.rb
index 2f5d660d52b7222e28015afcfae86e7c7cdd0423..2cedbcbbec089ef123e2be0819a22bf3a95ad322 100644
--- a/cluster/test/client_internals_test.rb
+++ b/cluster/test/client_internals_test.rb
@@ -56,7 +56,8 @@ class TestClusterClientInternals < Minitest::Test
   def test_acl_auth_success
     target_version "6.0.0" do
       with_acl do |username, password|
-        r = _new_client(nodes: DEFAULT_PORTS.map { |port| "redis://#{username}:#{password}@#{DEFAULT_HOST}:#{port}" })
+        nodes = DEFAULT_PORTS.map { |port| "redis://#{username}:#{password}@#{DEFAULT_HOST}:#{port}" }
+        r = _new_client(nodes: nodes)
         assert_equal('PONG', r.ping)
       end
     end
@@ -66,7 +67,9 @@ class TestClusterClientInternals < Minitest::Test
     target_version "6.0.0" do
       with_acl do |username, _|
         assert_raises(Redis::Cluster::InitialSetupError) do
-          _new_client(nodes: DEFAULT_PORTS.map { |port| "redis://#{username}:wrongpassword@#{DEFAULT_HOST}:#{port}" })
+          nodes = DEFAULT_PORTS.map { |port| "redis://#{username}:wrongpassword@#{DEFAULT_HOST}:#{port}" }
+          r = _new_client(nodes: nodes)
+          r.ping
         end
       end
     end
diff --git a/cluster/test/client_transactions_test.rb b/cluster/test/client_transactions_test.rb
index 4258200e7f6de138f90340b5159c9cb40a20c27c..8db7ce9f6f7b935a59045362c2d27e9402a72710 100644
--- a/cluster/test/client_transactions_test.rb
+++ b/cluster/test/client_transactions_test.rb
@@ -7,10 +7,10 @@ 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')
+    actual = redis.multi do |tx|
+      tx.set('counter', '0')
+      tx.incr('counter')
+      tx.incr('counter')
     end
 
     assert_equal(['OK', 1, 2], actual)
@@ -18,9 +18,9 @@ class TestClusterClientTransactions < Minitest::Test
   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)
+    actual = redis.multi do |tx|
+      tx.mset('{key}1', 1, '{key}2', 2)
+      tx.mset('{key}3', 3, '{key}4', 4)
     end
 
     assert_equal(%w[OK OK], actual)
@@ -29,18 +29,18 @@ class TestClusterClientTransactions < Minitest::Test
 
   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)
+      redis.multi do |tx|
+        tx.set('key1', 1)
+        tx.set('key2', 2)
+        tx.set('key3', 3)
+        tx.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)
+      redis.multi do |tx|
+        tx.mset('key1', 1, 'key2', 2)
+        tx.mset('key3', 3, 'key4', 4)
       end
     end
 
@@ -59,14 +59,57 @@ class TestClusterClientTransactions < Minitest::Test
       Fiber.yield
     end
 
-    redis.watch('{key}1', '{key}2') do |tx|
+    redis.watch('{key}1', '{key}2') do |client|
       another.resume
-      v1 = redis.get('{key}1')
-      v2 = redis.get('{key}2')
-      tx.call('SET', '{key}1', v2)
-      tx.call('SET', '{key}2', v1)
+      v1 = client.get('{key}1')
+      v2 = client.get('{key}2')
+
+      client.multi do |tx|
+        tx.set('{key}1', v2)
+        tx.set('{key}2', v1)
+      end
     end
 
     assert_equal %w[3 4], redis.mget('{key}1', '{key}2')
   end
+
+  def test_cluster_client_can_be_used_compatible_with_standalone_client
+    redis.set('{my}key', 'value')
+    redis.set('{my}counter', '0')
+    redis.watch('{my}key', '{my}counter') do |client|
+      if client.get('{my}key') == 'value'
+        client.multi do |tx|
+          tx.set('{my}key', 'updated value')
+          tx.incr('{my}counter')
+        end
+      else
+        client.unwatch
+      end
+    end
+
+    assert_equal('updated value', redis.get('{my}key'))
+    assert_equal('1', redis.get('{my}counter'))
+
+    another = Fiber.new do
+      cli = build_another_client
+      cli.set('{my}key', 'another value')
+      cli.close
+      Fiber.yield
+    end
+
+    redis.watch('{my}key', '{my}counter') do |client|
+      another.resume
+      if client.get('{my}key') == 'value'
+        client.multi do |tx|
+          tx.set('{my}key', 'latest value')
+          tx.incr('{my}counter')
+        end
+      else
+        client.unwatch
+      end
+    end
+
+    assert_equal('another value', redis.get('{my}key'))
+    assert_equal('1', redis.get('{my}counter'))
+  end
 end
diff --git a/cluster/test/commands_on_keys_test.rb b/cluster/test/commands_on_keys_test.rb
index 6227e5b46e3a9ec75638494eca471366fcb6f59f..f9a7dfc97291df90429544928d40278eee458fae 100644
--- a/cluster/test/commands_on_keys_test.rb
+++ b/cluster/test/commands_on_keys_test.rb
@@ -18,10 +18,7 @@ class TestClusterCommandsOnKeys < Minitest::Test
   def test_del
     set_some_keys
 
-    assert_raises(Redis::CommandError, "CROSSSLOT Keys in request don't hash to the same slot") do
-      redis.del('key1', 'key2')
-    end
-
+    assert_equal 2, redis.del('key1', 'key2')
     assert_equal 2, redis.del('{key}1', '{key}2')
   end
 
diff --git a/cluster/test/commands_on_transactions_test.rb b/cluster/test/commands_on_transactions_test.rb
index 0877f4c7beea8951801b7607c4ad483369165bf2..663cdfbd3acfdff29c39c2f00e12cdd691a7b4a4 100644
--- a/cluster/test/commands_on_transactions_test.rb
+++ b/cluster/test/commands_on_transactions_test.rb
@@ -24,9 +24,7 @@ class TestClusterCommandsOnTransactions < Minitest::Test
       redis.multi
     end
 
-    assert_raises(ArgumentError) do
-      redis.multi {}
-    end
+    assert_empty(redis.multi {})
 
     assert_equal([1], redis.multi { |r| r.incr('counter') })
   end
@@ -43,22 +41,40 @@ class TestClusterCommandsOnTransactions < Minitest::Test
     end
 
     assert_raises(Redis::Cluster::TransactionConsistencyError) do
-      redis.watch('key1', 'key2') do |tx|
-        tx.call('SET', 'key1', '1')
-        tx.call('SET', 'key2', '2')
+      redis.watch('{key}1', '{key}2') {}
+    end
+
+    assert_raises(Redis::Cluster::TransactionConsistencyError) do
+      redis.watch('{key}1', '{key}2') do |cli|
+        cli.watch('{key}3')
+      end
+    end
+
+    assert_raises(Redis::Cluster::TransactionConsistencyError) do
+      redis.watch('key1', 'key2') do |cli|
+        cli.multi do |tx|
+          tx.set('key1', '1')
+          tx.set('key2', '2')
+        end
       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')
+      redis.watch('{hey}1', '{hey}2') do |cli|
+        cli.multi do |tx|
+          tx.set('{key}1', '1')
+          tx.set('{key}2', '2')
+        end
       end
     end
 
-    redis.watch('{key}1', '{key}2') do |tx|
-      tx.call('SET', '{key}1', '1')
-      tx.call('SET', '{key}2', '2')
+    assert_equal('hello', redis.watch('{key}1', '{key}2') { |_| 'hello' })
+
+    redis.watch('{key}1', '{key}2') do |cli|
+      cli.multi do |tx|
+        tx.set('{key}1', '1')
+        tx.set('{key}2', '2')
+      end
     end
 
     assert_equal %w[1 2], redis.mget('{key}1', '{key}2')
diff --git a/lib/redis/commands/hashes.rb b/lib/redis/commands/hashes.rb
index a9a505afe60931a87633eb4248e49043b8c6c0d8..a6e9c10c422f83419e58f2bc8543aeb8d084764c 100644
--- a/lib/redis/commands/hashes.rb
+++ b/lib/redis/commands/hashes.rb
@@ -222,6 +222,8 @@ class Redis
       #   - `:count => Integer`: return count keys at most per iteration
       #
       # @return [String, Array<[String, String]>] the next cursor and all found keys
+      #
+      # See the [Redis Server HSCAN documentation](https://redis.io/docs/latest/commands/hscan/) for further details
       def hscan(key, cursor, **options)
         _scan(:hscan, cursor, [key], **options) do |reply|
           [reply[0], reply[1].each_slice(2).to_a]
@@ -239,6 +241,8 @@ class Redis
       #   - `:count => Integer`: return count keys at most per iteration
       #
       # @return [Enumerator] an enumerator for all found keys
+      #
+      # See the [Redis Server HSCAN documentation](https://redis.io/docs/latest/commands/hscan/) for further details
       def hscan_each(key, **options, &block)
         return to_enum(:hscan_each, key, **options) unless block_given?
 
diff --git a/lib/redis/commands/keys.rb b/lib/redis/commands/keys.rb
index f507a924f3c4e82554f764515c5bf6a7bd648201..e7a39172f551b105c2f6264fb85494d2912aecb0 100644
--- a/lib/redis/commands/keys.rb
+++ b/lib/redis/commands/keys.rb
@@ -22,6 +22,8 @@ class Redis
       #   - `:type => String`: return keys only of the given type
       #
       # @return [String, Array<String>] the next cursor and all found keys
+      #
+      # See the [Redis Server SCAN documentation](https://redis.io/docs/latest/commands/scan/) for further details
       def scan(cursor, **options)
         _scan(:scan, cursor, [], **options)
       end
@@ -46,6 +48,8 @@ class Redis
       #   - `:type => String`: return keys only of the given type
       #
       # @return [Enumerator] an enumerator for all found keys
+      #
+      # See the [Redis Server SCAN documentation](https://redis.io/docs/latest/commands/scan/) for further details
       def scan_each(**options, &block)
         return to_enum(:scan_each, **options) unless block_given?
 
@@ -282,6 +286,8 @@ class Redis
       #
       # @param [String] pattern
       # @return [Array<String>]
+      #
+      # See the [Redis Server KEYS documentation](https://redis.io/docs/latest/commands/keys/) for further details
       def keys(pattern = "*")
         send_command([:keys, pattern]) do |reply|
           if reply.is_a?(String)
diff --git a/lib/redis/commands/pubsub.rb b/lib/redis/commands/pubsub.rb
index ccdababff71bb3c6328103c16146c07cf4ab3c66..d7292f6ae295e6bdbcbfb0f8d18c27e55ac31026 100644
--- a/lib/redis/commands/pubsub.rb
+++ b/lib/redis/commands/pubsub.rb
@@ -29,17 +29,23 @@ class Redis
       end
 
       # Listen for messages published to channels matching the given patterns.
+      # See the [Redis Server PSUBSCRIBE documentation](https://redis.io/docs/latest/commands/psubscribe/)
+      # for further details
       def psubscribe(*channels, &block)
         _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.
+      # See the [Redis Server PSUBSCRIBE documentation](https://redis.io/docs/latest/commands/psubscribe/)
+      # for further details
       def psubscribe_with_timeout(timeout, *channels, &block)
         _subscription(:psubscribe_with_timeout, timeout, channels, block)
       end
 
       # Stop listening for messages posted to channels matching the given patterns.
+      # See the [Redis Server PUNSUBSCRIBE documentation](https://redis.io/docs/latest/commands/punsubscribe/)
+      # for further details
       def punsubscribe(*channels)
         _subscription(:punsubscribe, 0, channels, nil)
       end
diff --git a/lib/redis/commands/sets.rb b/lib/redis/commands/sets.rb
index e0575139725c88fe3561620b708f23524669cd9c..b8b0b1cd8d512442bd1b99ca9487a29d724303d0 100644
--- a/lib/redis/commands/sets.rb
+++ b/lib/redis/commands/sets.rb
@@ -184,6 +184,8 @@ class Redis
       #   - `:count => Integer`: return count keys at most per iteration
       #
       # @return [String, Array<String>] the next cursor and all found members
+      #
+      # See the [Redis Server SSCAN documentation](https://redis.io/docs/latest/commands/sscan/) for further details
       def sscan(key, cursor, **options)
         _scan(:sscan, cursor, [key], **options)
       end
@@ -199,6 +201,8 @@ class Redis
       #   - `:count => Integer`: return count keys at most per iteration
       #
       # @return [Enumerator] an enumerator for all keys in the set
+      #
+      # See the [Redis Server SSCAN documentation](https://redis.io/docs/latest/commands/sscan/) for further details
       def sscan_each(key, **options, &block)
         return to_enum(:sscan_each, key, **options) unless block_given?
 
diff --git a/lib/redis/commands/sorted_sets.rb b/lib/redis/commands/sorted_sets.rb
index f07f952758425a3d2ba0be85fea30e88cc1f8767..4b60b4739a8124248f6aefcca750be4eb1e9d162 100644
--- a/lib/redis/commands/sorted_sets.rb
+++ b/lib/redis/commands/sorted_sets.rb
@@ -817,6 +817,8 @@ class Redis
       #
       # @return [String, Array<[String, Float]>] the next cursor and all found
       #   members and scores
+      #
+      # See the [Redis Server ZSCAN documentation](https://redis.io/docs/latest/commands/zscan/) for further details
       def zscan(key, cursor, **options)
         _scan(:zscan, cursor, [key], **options) do |reply|
           [reply[0], FloatifyPairs.call(reply[1])]
@@ -834,6 +836,8 @@ class Redis
       #   - `:count => Integer`: return count keys at most per iteration
       #
       # @return [Enumerator] an enumerator for all found scores and members
+      #
+      # See the [Redis Server ZSCAN documentation](https://redis.io/docs/latest/commands/zscan/) for further details
       def zscan_each(key, **options, &block)
         return to_enum(:zscan_each, key, **options) unless block_given?
 
diff --git a/lib/redis/distributed.rb b/lib/redis/distributed.rb
index 33185d7c35a2638e3dfc86d58e9c70e12895afaf..fb156fbaba2fd9020b0dc84153dea45dcada59c0 100644
--- a/lib/redis/distributed.rb
+++ b/lib/redis/distributed.rb
@@ -948,12 +948,16 @@ class Redis
     end
 
     # Listen for messages published to channels matching the given patterns.
+    # See the [Redis Server PSUBSCRIBE documentation](https://redis.io/docs/latest/commands/psubscribe/)
+    # for further details
     def psubscribe(*channels, &block)
       raise NotImplementedError
     end
 
     # Stop listening for messages posted to channels matching the given
     # patterns.
+    # See the [Redis Server PUNSUBSCRIBE documentation](https://redis.io/docs/latest/commands/punsubscribe/)
+    # for further details
     def punsubscribe(*channels)
       raise NotImplementedError
     end
diff --git a/lib/redis/pipeline.rb b/lib/redis/pipeline.rb
index 0e415aa984381be934619588523be1fd8d40a827..793b32dd9557b0160e9d9ec7abd1255f2e50831c 100644
--- a/lib/redis/pipeline.rb
+++ b/lib/redis/pipeline.rb
@@ -58,7 +58,7 @@ class Redis
 
   class MultiConnection < PipelinedConnection
     def multi
-      raise Redis::Error, "Can't nest multi transaction"
+      raise Redis::BaseError, "Can't nest multi transaction"
     end
 
     private
@@ -118,12 +118,14 @@ class Redis
     end
 
     def _set(replies)
-      if replies
-        @futures.each_with_index do |future, index|
+      @object = if replies
+        @futures.map.with_index do |future, index|
           future._set(replies[index])
+          future.value
         end
+      else
+        replies
       end
-      @object = replies
     end
   end
 end
diff --git a/lib/redis/version.rb b/lib/redis/version.rb
index 4d34fe45db19935e115ecb03a3a4bc580e959818..7049573a37f1af1124e95b49e04a0f3cb25082c4 100644
--- a/lib/redis/version.rb
+++ b/lib/redis/version.rb
@@ -1,5 +1,5 @@
 # frozen_string_literal: true
 
 class Redis
-  VERSION = '5.2.0'
+  VERSION = '5.3.0'
 end
diff --git a/test/redis/pipelining_commands_test.rb b/test/redis/pipelining_commands_test.rb
index 6265921a04c5db342ef778d236ecbad9ac215e89..92417ac7695eb309a266a307f129acfdbf12a7c2 100644
--- a/test/redis/pipelining_commands_test.rb
+++ b/test/redis/pipelining_commands_test.rb
@@ -239,6 +239,23 @@ class TestPipeliningCommands < Minitest::Test
     assert_equal ["value", 1.0], future.value
   end
 
+  def test_hgetall_in_a_multi_in_a_pipeline_returns_hash
+    future = nil
+    result = r.pipelined do |p|
+      p.multi do |m|
+        m.hmset("hash", "field", "value", "field2", "value2")
+        future = m.hgetall("hash")
+      end
+    end
+
+    if Gem::Version.new(Redis::VERSION) > Gem::Version.new("4.8")
+      result = result.last
+    end
+
+    assert_equal({ "field" => "value", "field2" => "value2" }, result.last)
+    assert_equal({ "field" => "value", "field2" => "value2" }, future.value)
+  end
+
   def test_keys_in_a_pipeline
     r.set("key", "value")
     result = r.pipelined do |p|