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|