From bec52de1e67f7d77f4037268a04d611ca2e6492f Mon Sep 17 00:00:00 2001
From: Nived R S <nivedrsalini@gmail.com>
Date: Sat, 26 Oct 2024 18:41:04 +0530
Subject: [PATCH] New upstream version 5.3.0

---
 .ruby-version                                 |   1 +
 CHANGELOG.md                                  |   4 +
 README.md                                     |   1 +
 cluster/README.md                             |  24 ++++
 cluster/bin/console                           |   8 ++
 cluster/lib/redis/cluster.rb                  |  27 ++++-
 cluster/lib/redis/cluster/client.rb           |  33 +++++-
 .../lib/redis/cluster/transaction_adapter.rb  | 104 ++++++++++++++++++
 cluster/redis-clustering.gemspec              |   2 +-
 cluster/test/client_internals_test.rb         |   7 +-
 cluster/test/client_transactions_test.rb      |  83 ++++++++++----
 cluster/test/commands_on_keys_test.rb         |   5 +-
 cluster/test/commands_on_transactions_test.rb |  40 +++++--
 lib/redis/commands/hashes.rb                  |   4 +
 lib/redis/commands/keys.rb                    |   6 +
 lib/redis/commands/pubsub.rb                  |   6 +
 lib/redis/commands/sets.rb                    |   4 +
 lib/redis/commands/sorted_sets.rb             |   4 +
 lib/redis/distributed.rb                      |   4 +
 lib/redis/pipeline.rb                         |  10 +-
 lib/redis/version.rb                          |   2 +-
 test/redis/pipelining_commands_test.rb        |  17 +++
 22 files changed, 349 insertions(+), 47 deletions(-)
 create mode 100644 .ruby-version
 create mode 100755 cluster/bin/console
 create mode 100644 cluster/lib/redis/cluster/transaction_adapter.rb

diff --git a/.ruby-version b/.ruby-version
new file mode 100644
index 0000000..15a2799
--- /dev/null
+++ b/.ruby-version
@@ -0,0 +1 @@
+3.3.0
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bbdc021..b38c4fc 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 d4ee0ab..d004a57 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 d472220..51c7e31 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 0000000..2296425
--- /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 5abbb62..8da108e 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 a0e8899..b7ff698 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 0000000..4732824
--- /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 0519421..5e45a7a 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 2f5d660..2cedbcb 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 4258200..8db7ce9 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 6227e5b..f9a7dfc 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 0877f4c..663cdfb 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 a9a505a..a6e9c10 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 f507a92..e7a3917 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 ccdabab..d7292f6 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 e057513..b8b0b1c 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 f07f952..4b60b47 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 33185d7..fb156fb 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 0e415aa..793b32d 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 4d34fe4..7049573 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 6265921..92417ac 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|
-- 
GitLab