├── test ├── db │ └── .gitkeep ├── support │ ├── conf │ │ ├── redis-5.0.conf │ │ ├── redis-6.0.conf │ │ ├── redis-6.2.conf │ │ ├── redis-7.0.conf │ │ └── redis-7.2.conf │ ├── ssl │ │ ├── gen_certs.sh │ │ ├── trusted-ca.crt │ │ ├── untrusted-ca.crt │ │ ├── untrusted-ca.key │ │ ├── trusted-ca.key │ │ ├── trusted-cert.key │ │ ├── untrusted-cert.key │ │ ├── trusted-cert.crt │ │ └── untrusted-cert.crt │ └── redis_mock.rb ├── redis │ ├── commands_on_sets_test.rb │ ├── commands_on_lists_test.rb │ ├── commands_on_hashes_test.rb │ ├── commands_on_strings_test.rb │ ├── commands_on_sorted_sets_test.rb │ ├── commands_on_hyper_log_log_test.rb │ ├── commands_on_streams_test.rb │ ├── unknown_commands_test.rb │ ├── helper_test.rb │ ├── encoding_test.rb │ ├── fork_safety_test.rb │ ├── persistence_control_commands_test.rb │ ├── thread_safety_test.rb │ ├── error_replies_test.rb │ ├── client_test.rb │ ├── bitpos_test.rb │ ├── sorting_test.rb │ ├── blocking_commands_test.rb │ ├── ssl_test.rb │ ├── scripting_test.rb │ ├── connection_test.rb │ ├── url_param_test.rb │ ├── remote_server_control_commands_test.rb │ ├── connection_handling_test.rb │ └── commands_on_geo_test.rb ├── test.conf.erb ├── distributed │ ├── connection_handling_test.rb │ ├── sorting_test.rb │ ├── commands_on_hashes_test.rb │ ├── commands_on_hyper_log_log_test.rb │ ├── persistence_control_commands_test.rb │ ├── commands_on_lists_test.rb │ ├── blocking_commands_test.rb │ ├── key_tags_test.rb │ ├── distributed_test.rb │ ├── remote_server_control_commands_test.rb │ ├── transactions_test.rb │ ├── commands_on_strings_test.rb │ ├── publish_subscribe_test.rb │ ├── commands_on_sorted_sets_test.rb │ ├── commands_on_sets_test.rb │ ├── internals_test.rb │ ├── scripting_test.rb │ ├── commands_on_value_types_test.rb │ └── commands_requiring_clustering_test.rb ├── lint │ ├── authentication.rb │ └── hyper_log_log.rb └── sentinel │ └── sentinel_command_test.rb ├── cluster ├── CHANGELOG.md ├── lib │ ├── redis-clustering.rb │ └── redis │ │ ├── cluster │ │ ├── version.rb │ │ └── client.rb │ │ └── cluster.rb ├── Gemfile ├── test │ ├── commands_on_hashes_test.rb │ ├── commands_on_strings_test.rb │ ├── commands_on_hyper_log_log_test.rb │ ├── blocking_commands_test.rb │ ├── commands_on_value_types_test.rb │ ├── commands_on_lists_test.rb │ ├── client_replicas_test.rb │ ├── commands_on_sets_test.rb │ ├── commands_on_connection_test.rb │ ├── commands_on_transactions_test.rb │ ├── client_transactions_test.rb │ ├── commands_on_scripting_test.rb │ ├── commands_on_sorted_sets_test.rb │ ├── commands_on_streams_test.rb │ ├── client_internals_test.rb │ ├── commands_on_geo_test.rb │ ├── client_pipelining_test.rb │ ├── commands_on_pub_sub_test.rb │ ├── helper.rb │ └── commands_on_keys_test.rb ├── LICENSE ├── redis-clustering.gemspec └── README.md ├── .yardopts ├── lib └── redis │ ├── version.rb │ ├── commands │ ├── cluster.rb │ ├── connection.rb │ ├── hyper_log_log.rb │ ├── pubsub.rb │ ├── bitmaps.rb │ ├── transactions.rb │ ├── geo.rb │ └── scripting.rb │ ├── errors.rb │ ├── hash_ring.rb │ ├── pipeline.rb │ ├── subscribe.rb │ └── client.rb ├── .github └── dependabot.yml ├── examples ├── unicorn │ ├── config.ru │ └── unicorn.rb ├── basic.rb ├── incr-decr.rb ├── sentinel │ ├── sentinel.conf │ └── start ├── list.rb ├── sets.rb ├── pubsub.rb ├── dist_redis.rb ├── sentinel.rb └── consistency.rb ├── bin ├── console ├── cluster_creator └── build ├── Gemfile ├── .gitignore ├── LICENSE ├── Rakefile ├── .rubocop_todo.yml ├── redis.gemspec ├── .rubocop.yml └── makefile /test/db/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cluster/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/support/conf/redis-5.0.conf: -------------------------------------------------------------------------------- 1 | appendonly no 2 | save "" 3 | -------------------------------------------------------------------------------- /test/support/conf/redis-6.0.conf: -------------------------------------------------------------------------------- 1 | appendonly no 2 | save "" 3 | -------------------------------------------------------------------------------- /test/support/conf/redis-6.2.conf: -------------------------------------------------------------------------------- 1 | appendonly no 2 | save "" 3 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --exclude redis/connection 2 | --exclude redis/compat 3 | --markup markdown 4 | -------------------------------------------------------------------------------- /test/support/conf/redis-7.0.conf: -------------------------------------------------------------------------------- 1 | appendonly no 2 | save "" 3 | enable-debug-command yes 4 | -------------------------------------------------------------------------------- /test/support/conf/redis-7.2.conf: -------------------------------------------------------------------------------- 1 | appendonly no 2 | save "" 3 | enable-debug-command yes 4 | -------------------------------------------------------------------------------- /cluster/lib/redis-clustering.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "redis/cluster" 4 | -------------------------------------------------------------------------------- /lib/redis/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Redis 4 | VERSION = '5.0.8' 5 | end 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /examples/unicorn/config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | run lambda { |_env| 4 | [200, { "Content-Type" => "text/plain" }, [MyApp.redis.randomkey]] 5 | } 6 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | $LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) 5 | require 'redis' 6 | 7 | require 'irb' 8 | IRB.start 9 | -------------------------------------------------------------------------------- /cluster/lib/redis/cluster/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "redis/version" 4 | 5 | class Redis 6 | class Cluster 7 | VERSION = Redis::VERSION 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/redis/commands_on_sets_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestCommandsOnSets < Minitest::Test 6 | include Helper::Client 7 | include Lint::Sets 8 | end 9 | -------------------------------------------------------------------------------- /test/redis/commands_on_lists_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestCommandsOnLists < Minitest::Test 6 | include Helper::Client 7 | include Lint::Lists 8 | end 9 | -------------------------------------------------------------------------------- /test/redis/commands_on_hashes_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestCommandsOnHashes < Minitest::Test 6 | include Helper::Client 7 | include Lint::Hashes 8 | end 9 | -------------------------------------------------------------------------------- /test/redis/commands_on_strings_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestCommandsOnStrings < Minitest::Test 6 | include Helper::Client 7 | include Lint::Strings 8 | end 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | 7 | gem 'minitest' 8 | gem 'rake' 9 | gem 'rubocop', '~> 1.25.1' 10 | gem 'mocha' 11 | 12 | gem 'hiredis-client' 13 | -------------------------------------------------------------------------------- /test/redis/commands_on_sorted_sets_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestCommandsOnSortedSets < Minitest::Test 6 | include Helper::Client 7 | include Lint::SortedSets 8 | end 9 | -------------------------------------------------------------------------------- /test/redis/commands_on_hyper_log_log_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestCommandsOnHyperLogLog < Minitest::Test 6 | include Helper::Client 7 | include Lint::HyperLogLog 8 | end 9 | -------------------------------------------------------------------------------- /test/test.conf.erb: -------------------------------------------------------------------------------- 1 | dir <%= REDIS_DIR %> 2 | pidfile <%= REDIS_PID %> 3 | port 6381 4 | unixsocket <%= REDIS_SOCKET %> 5 | timeout 300 6 | loglevel debug 7 | logfile <%= REDIS_LOG %> 8 | databases 16 9 | daemonize yes 10 | -------------------------------------------------------------------------------- /cluster/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | 7 | gem 'redis', path: File.expand_path("..", __dir__) 8 | gem 'minitest' 9 | gem 'rake' 10 | gem 'rubocop', '~> 1.25.1' 11 | gem 'mocha' 12 | -------------------------------------------------------------------------------- /examples/basic.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'redis' 4 | 5 | r = Redis.new 6 | 7 | r.del('foo') 8 | 9 | puts 10 | 11 | p 'set foo to "bar"' 12 | r['foo'] = 'bar' 13 | 14 | puts 15 | 16 | p 'value of foo' 17 | p r['foo'] 18 | -------------------------------------------------------------------------------- /test/redis/commands_on_streams_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | # ruby -w -Itest test/commands_on_streams_test.rb 6 | # @see https://redis.io/commands#stream 7 | class TestCommandsOnStreams < Minitest::Test 8 | include Helper::Client 9 | include Lint::Streams 10 | end 11 | -------------------------------------------------------------------------------- /cluster/test/commands_on_hashes_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | # ruby -w -Itest test/cluster_commands_on_hashes_test.rb 6 | # @see https://redis.io/commands#hash 7 | class TestClusterCommandsOnHashes < Minitest::Test 8 | include Helper::Cluster 9 | include Lint::Hashes 10 | end 11 | -------------------------------------------------------------------------------- /examples/incr-decr.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'redis' 4 | 5 | r = Redis.new 6 | 7 | puts 8 | p 'incr' 9 | r.del 'counter' 10 | 11 | p r.incr('counter') 12 | p r.incr('counter') 13 | p r.incr('counter') 14 | 15 | puts 16 | p 'decr' 17 | p r.decr('counter') 18 | p r.decr('counter') 19 | p r.decr('counter') 20 | -------------------------------------------------------------------------------- /test/redis/unknown_commands_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestUnknownCommands < Minitest::Test 6 | include Helper::Client 7 | 8 | def test_should_try_to_work 9 | assert_raises Redis::CommandError do 10 | r.not_yet_implemented_command 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.rdb 2 | *.swp 3 | Gemfile.lock 4 | *.gem 5 | /tmp/ 6 | /.idea 7 | /.yardoc 8 | /.bundle 9 | /cluster/.bundle 10 | /coverage/* 11 | /doc/ 12 | /examples/sentinel/sentinel.conf 13 | /nohup.out 14 | /pkg/* 15 | /rdsrv 16 | /redis/* 17 | /test/db 18 | /test/test.conf 19 | appendonly.aof 20 | appendonlydir 21 | temp-rewriteaof-*.aof 22 | .history 23 | 24 | -------------------------------------------------------------------------------- /examples/sentinel/sentinel.conf: -------------------------------------------------------------------------------- 1 | sentinel monitor master1 127.0.0.1 6380 2 2 | sentinel down-after-milliseconds master1 5000 3 | sentinel failover-timeout master1 15000 4 | sentinel parallel-syncs master1 1 5 | 6 | sentinel monitor master2 127.0.0.1 6381 2 7 | sentinel down-after-milliseconds master2 5000 8 | sentinel failover-timeout master2 15000 9 | sentinel parallel-syncs master2 1 10 | -------------------------------------------------------------------------------- /cluster/test/commands_on_strings_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | # ruby -w -Itest test/cluster_commands_on_strings_test.rb 6 | # @see https://redis.io/commands#string 7 | class TestClusterCommandsOnStrings < Minitest::Test 8 | include Helper::Cluster 9 | include Lint::Strings 10 | 11 | def mock(*args, &block) 12 | redis_cluster_mock(*args, &block) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /bin/cluster_creator: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | puts ARGV.join(" ") 5 | require 'bundler/setup' 6 | 7 | $LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) 8 | require_relative '../cluster/test/support/orchestrator' 9 | 10 | urls = ARGV.map { |host_port| "redis://#{host_port}" } 11 | orchestrator = ClusterOrchestrator.new(urls, timeout: 3.0) 12 | orchestrator.rebuild 13 | orchestrator.close 14 | -------------------------------------------------------------------------------- /cluster/test/commands_on_hyper_log_log_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | # ruby -w -Itest test/cluster_commands_on_hyper_log_log_test.rb 6 | # @see https://redis.io/commands#hyperloglog 7 | class TestClusterCommandsOnHyperLogLog < Minitest::Test 8 | include Helper::Cluster 9 | include Lint::HyperLogLog 10 | 11 | def test_pfmerge 12 | assert_raises Redis::CommandError do 13 | super 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /cluster/test/blocking_commands_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | # ruby -w -Itest test/cluster_blocking_commands_test.rb 6 | class TestClusterBlockingCommands < Minitest::Test 7 | include Helper::Cluster 8 | include Lint::BlockingCommands 9 | 10 | def mock(options = {}, &blk) 11 | commands = build_mock_commands(options) 12 | redis_cluster_mock(commands, { timeout: LOW_TIMEOUT, concurrent: true }, &blk) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /cluster/test/commands_on_value_types_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | # ruby -w -Itest test/cluster_commands_on_value_types_test.rb 6 | class TestClusterCommandsOnValueTypes < Minitest::Test 7 | include Helper::Cluster 8 | include Lint::ValueTypes 9 | 10 | def test_move 11 | assert_raises(Redis::CommandError) { super } 12 | end 13 | 14 | def test_copy 15 | assert_raises(Redis::CommandError) { super } 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/distributed/connection_handling_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestDistributedConnectionHandling < Minitest::Test 6 | include Helper::Distributed 7 | 8 | def test_ping 9 | assert_equal ["PONG"], r.ping 10 | end 11 | 12 | def test_select 13 | r.set "foo", "bar" 14 | 15 | r.select 14 16 | assert_nil r.get("foo") 17 | 18 | r.select 15 19 | 20 | assert_equal "bar", r.get("foo") 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/distributed/sorting_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestDistributedSorting < Minitest::Test 6 | include Helper::Distributed 7 | 8 | def test_sort 9 | assert_raises(Redis::Distributed::CannotDistribute) do 10 | r.set("foo:1", "s1") 11 | r.set("foo:2", "s2") 12 | 13 | r.rpush("bar", "1") 14 | r.rpush("bar", "2") 15 | 16 | r.sort("bar", get: "foo:*", limit: [0, 1]) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/redis/helper_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestHelper < Minitest::Test 6 | include Helper 7 | 8 | def test_version_comparison 9 | v = Version.new("2.0.1") 10 | 11 | assert v > "1" 12 | assert v > "2" 13 | assert v < "3" 14 | assert v < "10" 15 | 16 | assert v < "2.1" 17 | assert v < "2.0.2" 18 | assert v < "2.0.1.1" 19 | assert v < "2.0.10" 20 | 21 | assert v == "2.0.1" 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/redis/encoding_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestEncoding < Minitest::Test 6 | include Helper::Client 7 | 8 | def test_returns_properly_encoded_strings 9 | r.set "foo", "שלום" 10 | 11 | assert_equal "Shalom שלום", "Shalom #{r.get('foo')}" 12 | 13 | refute_predicate "\xFF", :valid_encoding? 14 | r.set("bar", "\xFF") 15 | bytes = r.get("bar") 16 | assert_equal "\xFF".b, bytes 17 | assert_predicate bytes, :valid_encoding? 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/distributed/commands_on_hashes_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestDistributedCommandsOnHashes < Minitest::Test 6 | include Helper::Distributed 7 | include Lint::Hashes 8 | 9 | def test_hscan 10 | # Not implemented yet 11 | end 12 | 13 | def test_hstrlen 14 | # Not implemented yet 15 | end 16 | 17 | def test_mapped_hmget_in_a_pipeline_returns_hash 18 | assert_raises(Redis::Distributed::CannotDistribute) do 19 | super 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /cluster/test/commands_on_lists_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | # ruby -w -Itest test/cluster_commands_on_lists_test.rb 6 | # @see https://redis.io/commands#list 7 | class TestClusterCommandsOnLists < Minitest::Test 8 | include Helper::Cluster 9 | include Lint::Lists 10 | 11 | def test_lmove 12 | target_version "6.2" do 13 | assert_raises(Redis::CommandError) { super } 14 | end 15 | end 16 | 17 | def test_rpoplpush 18 | assert_raises(Redis::CommandError) { super } 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /cluster/test/client_replicas_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | # ruby -w -Itest test/cluster_client_replicas_test.rb 6 | class TestClusterClientReplicas < Minitest::Test 7 | include Helper::Cluster 8 | 9 | def test_client_can_command_with_replica 10 | r = build_another_client(replica: true) 11 | 12 | 100.times do |i| 13 | assert_equal 'OK', r.set("key#{i}", i) 14 | end 15 | 16 | r.wait(1, TIMEOUT.to_i * 1000) 17 | 18 | 100.times do |i| 19 | assert_equal i.to_s, r.get("key#{i}") 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /examples/list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubygems' 4 | require 'redis' 5 | 6 | r = Redis.new 7 | 8 | r.del 'logs' 9 | 10 | puts 11 | 12 | p "pushing log messages into a LIST" 13 | r.rpush 'logs', 'some log message' 14 | r.rpush 'logs', 'another log message' 15 | r.rpush 'logs', 'yet another log message' 16 | r.rpush 'logs', 'also another log message' 17 | 18 | puts 19 | p 'contents of logs LIST' 20 | 21 | p r.lrange('logs', 0, -1) 22 | 23 | puts 24 | p 'Trim logs LIST to last 2 elements(easy circular buffer)' 25 | 26 | r.ltrim('logs', -2, -1) 27 | 28 | p r.lrange('logs', 0, -1) 29 | -------------------------------------------------------------------------------- /test/distributed/commands_on_hyper_log_log_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestDistributedCommandsOnHyperLogLog < Minitest::Test 6 | include Helper::Distributed 7 | include Lint::HyperLogLog 8 | 9 | def test_pfmerge 10 | assert_raises Redis::Distributed::CannotDistribute do 11 | super 12 | end 13 | end 14 | 15 | def test_pfcount_multiple_keys_diff_nodes 16 | assert_raises Redis::Distributed::CannotDistribute do 17 | r.pfadd 'foo', 's1' 18 | r.pfadd 'bar', 's2' 19 | 20 | assert r.pfcount('res', 'foo', 'bar') 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/redis/fork_safety_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestForkSafety < Minitest::Test 6 | include Helper::Client 7 | 8 | def setup 9 | skip("Fork unavailable") unless Process.respond_to?(:fork) 10 | end 11 | 12 | def test_fork_safety 13 | redis = Redis.new(OPTIONS) 14 | pid = fork do 15 | 1000.times do 16 | assert_equal "OK", redis.set("key", "foo") 17 | end 18 | end 19 | 1000.times do 20 | assert_equal "PONG", redis.ping 21 | end 22 | _, status = Process.wait2(pid) 23 | assert_predicate(status, :success?) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/redis/persistence_control_commands_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestPersistenceControlCommands < Minitest::Test 6 | include Helper::Client 7 | 8 | def test_save 9 | redis_mock(save: -> { "+SAVE" }) do |redis| 10 | assert_equal "SAVE", redis.save 11 | end 12 | end 13 | 14 | def test_bgsave 15 | redis_mock(bgsave: -> { "+BGSAVE" }) do |redis| 16 | assert_equal "BGSAVE", redis.bgsave 17 | end 18 | end 19 | 20 | def test_lastsave 21 | redis_mock(lastsave: -> { "+LASTSAVE" }) do |redis| 22 | assert_equal "LASTSAVE", redis.lastsave 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/redis/thread_safety_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestThreadSafety < Minitest::Test 6 | include Helper::Client 7 | 8 | def test_thread_safety 9 | redis = Redis.new(OPTIONS) 10 | redis.set "foo", 1 11 | redis.set "bar", 2 12 | 13 | sample = 100 14 | 15 | t1 = Thread.new do 16 | @foos = Array.new(sample) { redis.get "foo" } 17 | end 18 | 19 | t2 = Thread.new do 20 | @bars = Array.new(sample) { redis.get "bar" } 21 | end 22 | 23 | t1.join 24 | t2.join 25 | 26 | assert_equal ["1"], @foos.uniq 27 | assert_equal ["2"], @bars.uniq 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /examples/unicorn/unicorn.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "redis" 4 | 5 | worker_processes 3 6 | 7 | # If you set the connection to Redis *before* forking, 8 | # you will cause forks to share a file descriptor. 9 | # 10 | # This causes a concurrency problem by which one fork 11 | # can read or write to the socket while others are 12 | # performing other operations. 13 | # 14 | # Most likely you'll be getting ProtocolError exceptions 15 | # mentioning a wrong initial byte in the reply. 16 | # 17 | # Thus we need to connect to Redis after forking the 18 | # worker processes. 19 | 20 | after_fork do |_server, _worker| 21 | MyApp.redis.disconnect! 22 | end 23 | -------------------------------------------------------------------------------- /test/distributed/persistence_control_commands_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestDistributedPersistenceControlCommands < Minitest::Test 6 | include Helper::Distributed 7 | 8 | def test_save 9 | redis_mock(save: -> { "+SAVE" }) do |redis| 10 | assert_equal ["SAVE"], redis.save 11 | end 12 | end 13 | 14 | def test_bgsave 15 | redis_mock(bgsave: -> { "+BGSAVE" }) do |redis| 16 | assert_equal ["BGSAVE"], redis.bgsave 17 | end 18 | end 19 | 20 | def test_lastsave 21 | redis_mock(lastsave: -> { "+LASTSAVE" }) do |redis| 22 | assert_equal ["LASTSAVE"], redis.lastsave 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /examples/sets.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubygems' 4 | require 'redis' 5 | 6 | r = Redis.new 7 | 8 | r.del 'foo-tags' 9 | r.del 'bar-tags' 10 | 11 | puts 12 | p "create a set of tags on foo-tags" 13 | 14 | r.sadd 'foo-tags', 'one' 15 | r.sadd 'foo-tags', 'two' 16 | r.sadd 'foo-tags', 'three' 17 | 18 | puts 19 | p "create a set of tags on bar-tags" 20 | 21 | r.sadd 'bar-tags', 'three' 22 | r.sadd 'bar-tags', 'four' 23 | r.sadd 'bar-tags', 'five' 24 | 25 | puts 26 | p 'foo-tags' 27 | 28 | p r.smembers('foo-tags') 29 | 30 | puts 31 | p 'bar-tags' 32 | 33 | p r.smembers('bar-tags') 34 | 35 | puts 36 | p 'intersection of foo-tags and bar-tags' 37 | 38 | p r.sinter('foo-tags', 'bar-tags') 39 | -------------------------------------------------------------------------------- /test/distributed/commands_on_lists_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestDistributedCommandsOnLists < Minitest::Test 6 | include Helper::Distributed 7 | include Lint::Lists 8 | 9 | def test_lmove 10 | target_version "6.2" do 11 | assert_raises Redis::Distributed::CannotDistribute do 12 | r.lmove('foo', 'bar', 'LEFT', 'RIGHT') 13 | end 14 | end 15 | end 16 | 17 | def test_rpoplpush 18 | assert_raises Redis::Distributed::CannotDistribute do 19 | r.rpoplpush('foo', 'bar') 20 | end 21 | end 22 | 23 | def test_brpoplpush 24 | assert_raises Redis::Distributed::CannotDistribute do 25 | r.brpoplpush('foo', 'bar', timeout: 1) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/distributed/blocking_commands_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestDistributedBlockingCommands < Minitest::Test 6 | include Helper::Distributed 7 | include Lint::BlockingCommands 8 | 9 | def test_blmove_raises 10 | target_version "6.2" do 11 | assert_raises(Redis::Distributed::CannotDistribute) do 12 | r.blmove('foo', 'bar', 'LEFT', 'RIGHT') 13 | end 14 | end 15 | end 16 | 17 | def test_blpop_raises 18 | assert_raises(Redis::Distributed::CannotDistribute) do 19 | r.blpop(%w[foo bar]) 20 | end 21 | end 22 | 23 | def test_brpop_raises 24 | assert_raises(Redis::Distributed::CannotDistribute) do 25 | r.brpop(%w[foo bar]) 26 | end 27 | end 28 | 29 | def test_brpoplpush_raises 30 | assert_raises(Redis::Distributed::CannotDistribute) do 31 | r.brpoplpush('foo', 'bar') 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/redis/commands/cluster.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Redis 4 | module Commands 5 | module Cluster 6 | # Sends `CLUSTER *` command to random node and returns its reply. 7 | # 8 | # @see https://redis.io/commands#cluster Reference of cluster command 9 | # 10 | # @param subcommand [String, Symbol] the subcommand of cluster command 11 | # e.g. `:slots`, `:nodes`, `:slaves`, `:info` 12 | # 13 | # @return [Object] depends on the subcommand 14 | def cluster(subcommand, *args) 15 | send_command([:cluster, subcommand] + args) 16 | end 17 | 18 | # Sends `ASKING` command to random node and returns its reply. 19 | # 20 | # @see https://redis.io/topics/cluster-spec#ask-redirection ASK redirection 21 | # 22 | # @return [String] `'OK'` 23 | def asking 24 | send_command(%i[asking]) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/lint/authentication.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Lint 4 | module Authentication 5 | def test_auth_with_password 6 | mock(auth: ->(*_) { '+OK' }) do |r| 7 | assert_equal 'OK', r.auth('mysecret') 8 | end 9 | 10 | mock(auth: ->(*_) { '-ERR some error' }) do |r| 11 | assert_raises(Redis::BaseError) { r.auth('mysecret') } 12 | end 13 | end 14 | 15 | def test_auth_for_acl 16 | target_version "6.0.0" do 17 | with_acl do |username, password| 18 | assert_raises(Redis::CannotConnectError) { redis.auth(username, 'wrongpassword') } 19 | assert_equal 'OK', redis.auth(username, password) 20 | assert_equal 'PONG', redis.ping 21 | assert_raises(Redis::BaseError) { redis.echo('foo') } 22 | end 23 | end 24 | end 25 | 26 | def mock(*args, &block) 27 | redis_mock(*args, &block) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /cluster/test/commands_on_sets_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | # ruby -w -Itest test/cluster_commands_on_sets_test.rb 6 | # @see https://redis.io/commands#set 7 | class TestClusterCommandsOnSets < Minitest::Test 8 | include Helper::Cluster 9 | include Lint::Sets 10 | 11 | def test_sdiff 12 | assert_raises(Redis::CommandError) { super } 13 | end 14 | 15 | def test_sdiffstore 16 | assert_raises(Redis::CommandError) { super } 17 | end 18 | 19 | def test_sinter 20 | assert_raises(Redis::CommandError) { super } 21 | end 22 | 23 | def test_sinterstore 24 | assert_raises(Redis::CommandError) { super } 25 | end 26 | 27 | def test_smove 28 | assert_raises(Redis::CommandError) { super } 29 | end 30 | 31 | def test_sunion 32 | assert_raises(Redis::CommandError) { super } 33 | end 34 | 35 | def test_sunionstore 36 | assert_raises(Redis::CommandError) { super } 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /examples/pubsub.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "redis" 4 | 5 | puts <<~EOS 6 | To play with this example use redis-cli from another terminal, like this: 7 | 8 | $ redis-cli publish one hello 9 | 10 | Finally force the example to exit sending the 'exit' message with: 11 | 12 | $ redis-cli publish two exit 13 | 14 | EOS 15 | 16 | redis = Redis.new 17 | 18 | trap(:INT) { puts; exit } 19 | 20 | begin 21 | redis.subscribe(:one, :two) do |on| 22 | on.subscribe do |channel, subscriptions| 23 | puts "Subscribed to ##{channel} (#{subscriptions} subscriptions)" 24 | end 25 | 26 | on.message do |channel, message| 27 | puts "##{channel}: #{message}" 28 | redis.unsubscribe if message == "exit" 29 | end 30 | 31 | on.unsubscribe do |channel, subscriptions| 32 | puts "Unsubscribed from ##{channel} (#{subscriptions} subscriptions)" 33 | end 34 | end 35 | rescue Redis::BaseConnectionError => error 36 | puts "#{error}, retrying in 1s" 37 | sleep 1 38 | retry 39 | end 40 | -------------------------------------------------------------------------------- /examples/dist_redis.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "redis" 4 | require "redis/distributed" 5 | 6 | r = Redis::Distributed.new %w[ 7 | redis://localhost:6379 8 | redis://localhost:6380 9 | redis://localhost:6381 10 | redis://localhost:6382 11 | ] 12 | 13 | r.flushdb 14 | 15 | r['urmom'] = 'urmom' 16 | r['urdad'] = 'urdad' 17 | r['urmom1'] = 'urmom1' 18 | r['urdad1'] = 'urdad1' 19 | r['urmom2'] = 'urmom2' 20 | r['urdad2'] = 'urdad2' 21 | r['urmom3'] = 'urmom3' 22 | r['urdad3'] = 'urdad3' 23 | p r['urmom'] 24 | p r['urdad'] 25 | p r['urmom1'] 26 | p r['urdad1'] 27 | p r['urmom2'] 28 | p r['urdad2'] 29 | p r['urmom3'] 30 | p r['urdad3'] 31 | 32 | r.rpush 'listor', 'foo1' 33 | r.rpush 'listor', 'foo2' 34 | r.rpush 'listor', 'foo3' 35 | r.rpush 'listor', 'foo4' 36 | r.rpush 'listor', 'foo5' 37 | 38 | p r.rpop('listor') 39 | p r.rpop('listor') 40 | p r.rpop('listor') 41 | p r.rpop('listor') 42 | p r.rpop('listor') 43 | 44 | puts "key distribution:" 45 | 46 | r.ring.nodes.each do |node| 47 | p [node.client(:getname), node.keys("*")] 48 | end 49 | r.flushdb 50 | p r.keys('*') 51 | -------------------------------------------------------------------------------- /cluster/test/commands_on_connection_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | require 'lint/authentication' 5 | 6 | # ruby -w -Itest test/cluster_commands_on_connection_test.rb 7 | # @see https://redis.io/commands#connection 8 | class TestClusterCommandsOnConnection < Minitest::Test 9 | include Helper::Cluster 10 | include Lint::Authentication 11 | 12 | def test_echo 13 | assert_equal 'hogehoge', redis.echo('hogehoge') 14 | end 15 | 16 | def test_ping 17 | assert_equal 'hogehoge', redis.ping('hogehoge') 18 | end 19 | 20 | def test_quit 21 | redis2 = build_another_client 22 | assert_equal 'OK', redis2.quit 23 | end 24 | 25 | def test_select 26 | assert_raises(Redis::CommandError, 'ERR SELECT is not allowed in cluster mode') do 27 | redis.select(1) 28 | end 29 | end 30 | 31 | def test_swapdb 32 | assert_raises(Redis::CommandError, 'ERR SWAPDB is not allowed in cluster mode') do 33 | redis.swapdb(1, 2) 34 | end 35 | end 36 | 37 | def mock(*args, &block) 38 | redis_cluster_mock(*args, &block) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Ezra Zygmuntowicz 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /cluster/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Ezra Zygmuntowicz 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | Bundler::GemHelper.install_tasks(dir: "cluster", name: "redis-clustering") 5 | 6 | require 'rake/testtask' 7 | 8 | namespace :test do 9 | groups = %i(redis distributed sentinel) 10 | groups.each do |group| 11 | Rake::TestTask.new(group) do |t| 12 | t.libs << "test" 13 | t.libs << "lib" 14 | t.test_files = FileList["test/#{group}/**/*_test.rb"] 15 | t.options = '-v' if ENV['CI'] || ENV['VERBOSE'] 16 | end 17 | end 18 | 19 | lost_tests = Dir["test/**/*_test.rb"] - groups.map { |g| Dir["test/#{g}/**/*_test.rb"] }.flatten 20 | unless lost_tests.empty? 21 | abort "The following test files are in no group:\n#{lost_tests.join("\n")}" 22 | end 23 | 24 | Rake::TestTask.new(:cluster) do |t| 25 | t.libs << "cluster/test" << "test" 26 | t.libs << "cluster/lib" << "lib" 27 | t.test_files = FileList["cluster/test/**/*_test.rb"] 28 | t.options = '-v' if ENV['CI'] || ENV['VERBOSE'] 29 | end 30 | end 31 | 32 | task test: ["test:redis", "test:distributed", "test:sentinel", "test:cluster"] 33 | 34 | task default: :test 35 | -------------------------------------------------------------------------------- /test/support/ssl/gen_certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | get_subject() { 4 | if [ "$1" = "trusted" ] 5 | then 6 | echo "/C=IT/ST=Sicily/L=Catania/O=Redis/OU=Security/CN=127.0.0.1" 7 | else 8 | echo "/C=XX/ST=Untrusted/L=Evilville/O=Evil Hacker/OU=Attack Department/CN=127.0.0.1" 9 | fi 10 | } 11 | 12 | # Generate two CAs: one to be considered trusted, and one that's untrusted 13 | for type in trusted untrusted; do 14 | rm -rf ./demoCA 15 | mkdir -p ./demoCA 16 | mkdir -p ./demoCA/certs 17 | mkdir -p ./demoCA/crl 18 | mkdir -p ./demoCA/newcerts 19 | mkdir -p ./demoCA/private 20 | touch ./demoCA/index.txt 21 | 22 | openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out ${type}-ca.key 23 | openssl req -new -x509 -days 12500 -key ${type}-ca.key -sha256 -out ${type}-ca.crt -subj "$(get_subject $type)" 24 | openssl x509 -in ${type}-ca.crt -noout -next_serial -out ./demoCA/serial 25 | 26 | openssl req -newkey rsa:2048 -keyout ${type}-cert.key -nodes -out ${type}-cert.req -subj "$(get_subject $type)" 27 | openssl ca -days 12500 -cert ${type}-ca.crt -keyfile ${type}-ca.key -out ${type}-cert.crt -infiles ${type}-cert.req 28 | rm ${type}-cert.req 29 | done 30 | 31 | rm -rf ./demoCA 32 | -------------------------------------------------------------------------------- /examples/sentinel.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'redis' 4 | 5 | # This example creates a master-slave setup with a sentinel, then connects to 6 | # it and sends write commands in a loop. 7 | # 8 | # After 30 seconds, the master dies. You will be able to see how a new master 9 | # is elected and things continue to work as if nothing happened. 10 | # 11 | # To run this example: 12 | # 13 | # $ ruby -I./lib examples/sentinel.rb 14 | # 15 | 16 | at_exit do 17 | begin 18 | Process.kill(:INT, @redises) 19 | rescue Errno::ESRCH 20 | end 21 | 22 | Process.waitall 23 | end 24 | 25 | @redises = spawn("examples/sentinel/start") 26 | 27 | SENTINELS = [{ host: "127.0.0.1", port: 26_379 }, 28 | { host: "127.0.0.1", port: 26_380 }].freeze 29 | r = Redis.new(url: "redis://master1", sentinels: SENTINELS, role: :master) 30 | 31 | # Set keys into a loop. 32 | # 33 | # The example traps errors so that you can actually try to failover while 34 | # running the script to see redis-rb reconfiguring. 35 | (0..1_000_000).each do |i| 36 | begin 37 | r.set(i, i) 38 | $stdout.write("SET (#{i} times)\n") if i % 100 == 0 39 | rescue 40 | $stdout.write("E") 41 | end 42 | sleep(0.01) 43 | end 44 | -------------------------------------------------------------------------------- /cluster/test/commands_on_transactions_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | # ruby -w -Itest test/cluster_commands_on_transactions_test.rb 6 | # @see https://redis.io/commands#transactions 7 | class TestClusterCommandsOnTransactions < Minitest::Test 8 | include Helper::Cluster 9 | 10 | def test_discard 11 | assert_raises(Redis::Cluster::AmbiguousNodeError) do 12 | redis.discard 13 | end 14 | end 15 | 16 | def test_exec 17 | assert_raises(Redis::Cluster::AmbiguousNodeError) do 18 | redis.exec 19 | end 20 | end 21 | 22 | def test_multi 23 | assert_raises(LocalJumpError) do 24 | redis.multi 25 | end 26 | 27 | assert_raises(ArgumentError) do 28 | redis.multi {} 29 | end 30 | 31 | assert_equal([1], redis.multi { |r| r.incr('counter') }) 32 | end 33 | 34 | def test_unwatch 35 | assert_raises(Redis::Cluster::AmbiguousNodeError) do 36 | redis.unwatch 37 | end 38 | end 39 | 40 | def test_watch 41 | assert_raises(Redis::CommandError, "CROSSSLOT Keys in request don't hash to the same slot") do 42 | redis.watch('key1', 'key2') 43 | end 44 | 45 | assert_equal 'OK', redis.watch('{key}1', '{key}2') 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/redis/error_replies_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestErrorReplies < Minitest::Test 6 | include Helper::Client 7 | 8 | # Every test shouldn't disconnect from the server. Also, when error replies are 9 | # in play, the protocol should never get into an invalid state where there are 10 | # pending replies in the connection. Calling INFO after every test ensures that 11 | # the protocol is still in a valid state. 12 | def with_reconnection_check 13 | before = r.info["total_connections_received"] 14 | yield(r) 15 | after = r.info["total_connections_received"] 16 | ensure 17 | assert_equal before, after 18 | end 19 | 20 | def test_error_reply_for_single_command 21 | with_reconnection_check do 22 | r.unknown_command 23 | rescue => ex 24 | ensure 25 | assert ex.message =~ /unknown command/i 26 | end 27 | end 28 | 29 | def test_raise_first_error_reply_in_pipeline 30 | with_reconnection_check do 31 | r.pipelined do 32 | r.set("foo", "s1") 33 | r.incr("foo") # not an integer 34 | r.lpush("foo", "value") # wrong kind of value 35 | end 36 | rescue => ex 37 | ensure 38 | assert ex.message =~ /not an integer/i 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/redis/client_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestClient < Minitest::Test 6 | include Helper::Client 7 | 8 | def test_call 9 | result = r.call("PING") 10 | assert_equal result, "PONG" 11 | end 12 | 13 | def test_call_with_arguments 14 | result = r.call("SET", "foo", "bar") 15 | assert_equal result, "OK" 16 | end 17 | 18 | def test_call_integers 19 | result = r.call("INCR", "foo") 20 | assert_equal result, 1 21 | end 22 | 23 | def test_call_raise 24 | assert_raises(Redis::CommandError) do 25 | r.call("INCR") 26 | end 27 | end 28 | 29 | def test_error_translate_subclasses 30 | error = Class.new(RedisClient::CommandError) 31 | assert_equal Redis::CommandError, Redis::Client.send(:translate_error_class, error) 32 | 33 | assert_raises KeyError do 34 | Redis::Client.send(:translate_error_class, StandardError) 35 | end 36 | end 37 | 38 | def test_mixed_encoding 39 | r.call("MSET", "fée", "\x00\xFF".b, "じ案".encode(Encoding::SHIFT_JIS), "\t".encode(Encoding::ASCII)) 40 | assert_equal "\x00\xFF".b, r.call("GET", "fée") 41 | assert_equal "\t", r.call("GET", "じ案".encode(Encoding::SHIFT_JIS)) 42 | 43 | r.call("SET", "\x00\xFF", "fée") 44 | assert_equal "fée", r.call("GET", "\x00\xFF".b) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2022-01-08 23:15:30 UTC using RuboCop version 1.11.0. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 2 10 | Lint/HashCompareByIdentity: 11 | Exclude: 12 | - 'lib/redis.rb' 13 | 14 | # Offense count: 1 15 | # Cop supports --auto-correct. 16 | Lint/RedundantStringCoercion: 17 | Exclude: 18 | - 'examples/consistency.rb' 19 | 20 | # Offense count: 2 21 | # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers. 22 | # SupportedStyles: snake_case, normalcase, non_integer 23 | # AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339 24 | Naming/VariableNumber: 25 | Exclude: 26 | - 'test/remote_server_control_commands_test.rb' 27 | 28 | # Offense count: 6 29 | # Configuration parameters: AllowedMethods. 30 | # AllowedMethods: respond_to_missing? 31 | Style/OptionalBooleanParameter: 32 | Exclude: 33 | - 'lib/redis.rb' 34 | - 'lib/redis/client.rb' 35 | - 'lib/redis/cluster.rb' 36 | - 'lib/redis/cluster/node.rb' 37 | - 'lib/redis/cluster/slot.rb' 38 | - 'lib/redis/pipeline.rb' 39 | -------------------------------------------------------------------------------- /test/support/ssl/trusted-ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDsTCCApmgAwIBAgIUbNLy3vMeDQUSLVREVOSc8ElYPoMwDQYJKoZIhvcNAQEL 3 | BQAwZzELMAkGA1UEBhMCSVQxDzANBgNVBAgMBlNpY2lseTEQMA4GA1UEBwwHQ2F0 4 | YW5pYTEOMAwGA1UECgwFUmVkaXMxETAPBgNVBAsMCFNlY3VyaXR5MRIwEAYDVQQD 5 | DAkxMjcuMC4wLjEwIBcNMjAwODIxMDMxOTE1WhgPMjA1NDExMTEwMzE5MTVaMGcx 6 | CzAJBgNVBAYTAklUMQ8wDQYDVQQIDAZTaWNpbHkxEDAOBgNVBAcMB0NhdGFuaWEx 7 | DjAMBgNVBAoMBVJlZGlzMREwDwYDVQQLDAhTZWN1cml0eTESMBAGA1UEAwwJMTI3 8 | LjAuMC4xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3aqkNfQcXHfp 9 | YVZLE+EdMtR9cXlbAQAOX6LSAzh0ZC/LVTMXJvMrbjVqCE7Khm3A5lwXcnJ8EPJn 10 | Tj7B4Nc3aOjyn5U+WtAikJizN1exFEuHl0h16oERpVj17nxDycSfoZcMoPyEzqXK 11 | BQpiV4eNvWYu2NTeXQNbWPs84LUGJjb8WGhk1AhiGjeX5AN3yBnwGQN35+bWRCS0 12 | 66ITMPqrEGx47PemN+2ECL5wkMTFIQIRPbiKdsm39pxVKqEl5dzc57CoCN5kI/AA 13 | iiwjsDGApB6Xk9QzOpLRwdNEp96C3IjrWaJ//Obn4a4XkXUCLqtI8SGsQLujT9OH 14 | opns8/nYKwIDAQABo1MwUTAdBgNVHQ4EFgQURpoiXGek1Dk2H3dLqEF1YntLsOcw 15 | HwYDVR0jBBgwFoAURpoiXGek1Dk2H3dLqEF1YntLsOcwDwYDVR0TAQH/BAUwAwEB 16 | /zANBgkqhkiG9w0BAQsFAAOCAQEAB8cAFmMS/WrSCsedpyYG3s4bZSx3yxaXDBbE 17 | tseOQa1z3OUml1XH6DP0B3dioGRkL8O6C2wqqVdJyB4gqlG0kWD5nqFkYIh09pYM 18 | +SaUa1FzQVdDENNTMqB20MeOLLk8BAFX1kKRkC8Jm+6VFKtB+bW5nZ4qDDP4KMfr 19 | vZdL+Xo8+vYSsWztx0u4RCUKLlfUbcG8G7kTe4GoHXzwrvldmY9xARJgXXHMlLit 20 | gTORsdLj0jAlheTvfmW9/nc0H3edDly7DbueT0tFoeY02gkqayRXUVrnJ/Otmvj1 21 | pzEBSVA7Ri6cohiQVxOHmurwvwsxgZamPlou6ZZWY0tzkeLEbQ== 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /redis.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "./lib/redis/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "redis" 7 | 8 | s.version = Redis::VERSION 9 | 10 | s.homepage = "https://github.com/redis/redis-rb" 11 | 12 | s.summary = "A Ruby client library for Redis" 13 | 14 | s.description = <<-EOS 15 | A Ruby client that tries to match Redis' API one-to-one, while still 16 | providing an idiomatic interface. 17 | EOS 18 | 19 | s.license = "MIT" 20 | 21 | s.authors = [ 22 | "Ezra Zygmuntowicz", 23 | "Taylor Weibley", 24 | "Matthew Clark", 25 | "Brian McKinney", 26 | "Salvatore Sanfilippo", 27 | "Luca Guidi", 28 | "Michel Martens", 29 | "Damian Janowski", 30 | "Pieter Noordhuis" 31 | ] 32 | 33 | s.email = ["redis-db@googlegroups.com"] 34 | 35 | s.metadata = { 36 | "bug_tracker_uri" => "#{s.homepage}/issues", 37 | "changelog_uri" => "#{s.homepage}/blob/master/CHANGELOG.md", 38 | "documentation_uri" => "https://www.rubydoc.info/gems/redis/#{s.version}", 39 | "homepage_uri" => s.homepage, 40 | "source_code_uri" => "#{s.homepage}/tree/v#{s.version}" 41 | } 42 | 43 | s.files = Dir["CHANGELOG.md", "LICENSE", "README.md", "lib/**/*"] 44 | s.executables = `git ls-files -- exe/*`.split("\n").map { |f| File.basename(f) } 45 | 46 | s.required_ruby_version = '>= 2.5.0' 47 | 48 | s.add_runtime_dependency('redis-client', '>= 0.17.0') 49 | end 50 | -------------------------------------------------------------------------------- /lib/redis/commands/connection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Redis 4 | module Commands 5 | module Connection 6 | # Authenticate to the server. 7 | # 8 | # @param [Array] args includes both username and password 9 | # or only password 10 | # @return [String] `OK` 11 | # @see https://redis.io/commands/auth AUTH command 12 | def auth(*args) 13 | send_command([:auth, *args]) 14 | end 15 | 16 | # Ping the server. 17 | # 18 | # @param [optional, String] message 19 | # @return [String] `PONG` 20 | def ping(message = nil) 21 | send_command([:ping, message].compact) 22 | end 23 | 24 | # Echo the given string. 25 | # 26 | # @param [String] value 27 | # @return [String] 28 | def echo(value) 29 | send_command([:echo, value]) 30 | end 31 | 32 | # Change the selected database for the current connection. 33 | # 34 | # @param [Integer] db zero-based index of the DB to use (0 to 15) 35 | # @return [String] `OK` 36 | def select(db) 37 | send_command([:select, db]) 38 | end 39 | 40 | # Close the connection. 41 | # 42 | # @return [String] `OK` 43 | def quit 44 | synchronize do |client| 45 | client.call_v([:quit]) 46 | rescue ConnectionError 47 | ensure 48 | client.close 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/support/ssl/untrusted-ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID2TCCAsGgAwIBAgIUDRXLZuA0a5kneb7e8vKxFhCnawUwDQYJKoZIhvcNAQEL 3 | BQAwezELMAkGA1UEBhMCWFgxEjAQBgNVBAgMCVVudHJ1c3RlZDESMBAGA1UEBwwJ 4 | RXZpbHZpbGxlMRQwEgYDVQQKDAtFdmlsIEhhY2tlcjEaMBgGA1UECwwRQXR0YWNr 5 | IERlcGFydG1lbnQxEjAQBgNVBAMMCTEyNy4wLjAuMTAgFw0yMDA4MjEwMzE5MjRa 6 | GA8yMDU0MTExMTAzMTkyNFowezELMAkGA1UEBhMCWFgxEjAQBgNVBAgMCVVudHJ1 7 | c3RlZDESMBAGA1UEBwwJRXZpbHZpbGxlMRQwEgYDVQQKDAtFdmlsIEhhY2tlcjEa 8 | MBgGA1UECwwRQXR0YWNrIERlcGFydG1lbnQxEjAQBgNVBAMMCTEyNy4wLjAuMTCC 9 | ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL0fgnv0EeX0CAaiBw1CqnBA 10 | w6Z7jtu9siTEbUE6rUkTVkwnqFoPcIEu/zj/vGlmHK3+GjnFIK9y4TIsyPKPqneC 11 | SLlYaF5Y/0B1Kho5NLk0oJrZEuco6cUJ+Ip8FHhvFVmftkGCZo28gFOH8OvARVIP 12 | 6PdcY0oLT6V8LIMW8VzZj+WNqSOGGnJ4GJwE6euI79gUs21KSIFkq9hjvK8MPUQs 13 | 8CaebCR+Z4DkoOAqhQjKevCAss0nXQYxuWYgM/ZiCqUEFRP8wR3a10kuE2gdePK7 14 | AgE2QCR1FIUONTwEh5diiycWVTBC3Yp/gNys2de7AZ7K5tjAzqH1C6R8uHMGFXEC 15 | AwEAAaNTMFEwHQYDVR0OBBYEFC5GEu92pkUiyhhx2BDcBKeMbm72MB8GA1UdIwQY 16 | MBaAFC5GEu92pkUiyhhx2BDcBKeMbm72MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI 17 | hvcNAQELBQADggEBAAOpKzMjFjPC/j5YF7RpC5DguzoaCEV/IydsNWiObLAKU9u2 18 | 25eZzBIQFSQRUxCdWeI9jbXtF5ngy052Y5Ih9VKHIVWifUrZYO8s1xHG295Z2jaW 19 | hz8i9jdqK8U+1k6teLSjUo/87eL8hKptELv9net0T7aykx1e87rZy9sZm4G12uZc 20 | goW30H0F8M6nkyYLApSWjx/gibdWkDlCQXCbY8YXuZDuwhnB53/WGv5R9ym55plp 21 | MzmLu8xi0Ow3XbyvUzWNtSTpDMfcSrNc69+qr1DLDHW7ZZMsLZj7ONYrkqAbuKhi 22 | weYzff5/gaTxILtIRJx9Z7Vc0IUtA+lcZHjwRms= 23 | -----END CERTIFICATE----- 24 | -------------------------------------------------------------------------------- /lib/redis/commands/hyper_log_log.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Redis 4 | module Commands 5 | module HyperLogLog 6 | # Add one or more members to a HyperLogLog structure. 7 | # 8 | # @param [String] key 9 | # @param [String, Array] member one member, or array of members 10 | # @return [Boolean] true if at least 1 HyperLogLog internal register was altered. false otherwise. 11 | def pfadd(key, member) 12 | send_command([:pfadd, key, member], &Boolify) 13 | end 14 | 15 | # Get the approximate cardinality of members added to HyperLogLog structure. 16 | # 17 | # If called with multiple keys, returns the approximate cardinality of the 18 | # union of the HyperLogLogs contained in the keys. 19 | # 20 | # @param [String, Array] keys 21 | # @return [Integer] 22 | def pfcount(*keys) 23 | send_command([:pfcount] + keys.flatten(1)) 24 | end 25 | 26 | # Merge multiple HyperLogLog values into an unique value that will approximate the cardinality of the union of 27 | # the observed Sets of the source HyperLogLog structures. 28 | # 29 | # @param [String] dest_key destination key 30 | # @param [String, Array] source_key source key, or array of keys 31 | # @return [Boolean] 32 | def pfmerge(dest_key, *source_key) 33 | send_command([:pfmerge, dest_key, *source_key], &BoolifySet) 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /cluster/test/client_transactions_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | # ruby -w -Itest test/cluster_client_transactions_test.rb 6 | class TestClusterClientTransactions < Minitest::Test 7 | include Helper::Cluster 8 | 9 | def test_cluster_client_does_support_transaction_by_single_key 10 | actual = redis.multi do |r| 11 | r.set('counter', '0') 12 | r.incr('counter') 13 | r.incr('counter') 14 | end 15 | 16 | assert_equal(['OK', 1, 2], actual) 17 | assert_equal('2', redis.get('counter')) 18 | end 19 | 20 | def test_cluster_client_does_support_transaction_by_hashtag 21 | actual = redis.multi do |r| 22 | r.mset('{key}1', 1, '{key}2', 2) 23 | r.mset('{key}3', 3, '{key}4', 4) 24 | end 25 | 26 | assert_equal(%w[OK OK], actual) 27 | assert_equal(%w[1 2 3 4], redis.mget('{key}1', '{key}2', '{key}3', '{key}4')) 28 | end 29 | 30 | def test_cluster_client_does_not_support_transaction_by_multiple_keys 31 | assert_raises(Redis::Cluster::TransactionConsistencyError) do 32 | redis.multi do |r| 33 | r.set('key1', 1) 34 | r.set('key2', 2) 35 | r.set('key3', 3) 36 | r.set('key4', 4) 37 | end 38 | end 39 | 40 | assert_raises(Redis::Cluster::TransactionConsistencyError) do 41 | redis.multi do |r| 42 | r.mset('key1', 1, 'key2', 2) 43 | r.mset('key3', 3, 'key4', 4) 44 | end 45 | end 46 | 47 | (1..4).each do |i| 48 | assert_nil(redis.get("key#{i}")) 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/distributed/key_tags_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestDistributedKeyTags < Minitest::Test 6 | include Helper 7 | include Helper::Distributed 8 | 9 | def test_hashes_consistently 10 | r1 = Redis::Distributed.new ["redis://127.0.0.1:#{PORT}/15", *NODES] 11 | r2 = Redis::Distributed.new ["redis://127.0.0.1:#{PORT}/15", *NODES] 12 | r3 = Redis::Distributed.new ["redis://127.0.0.1:#{PORT}/15", *NODES] 13 | 14 | assert_equal r1.node_for("foo").id, r2.node_for("foo").id 15 | assert_equal r1.node_for("foo").id, r3.node_for("foo").id 16 | end 17 | 18 | def test_allows_clustering_of_keys 19 | r = Redis::Distributed.new(NODES) 20 | r.add_node("redis://127.0.0.1:#{PORT}/14") 21 | r.flushdb 22 | 23 | 100.times do |i| 24 | r.set "{foo}users:#{i}", i 25 | end 26 | 27 | assert_equal([0, 100], r.nodes.map { |node| node.keys.size }) 28 | end 29 | 30 | def test_distributes_keys_if_no_clustering_is_used 31 | r.add_node("redis://127.0.0.1:#{PORT}/13") 32 | r.flushdb 33 | 34 | r.set "users:1", 1 35 | r.set "users:4", 4 36 | 37 | assert_equal([1, 1], r.nodes.map { |node| node.keys.size }) 38 | end 39 | 40 | def test_allows_passing_a_custom_tag_extractor 41 | r = Redis::Distributed.new(NODES, tag: /^(.+?):/) 42 | r.add_node("redis://127.0.0.1:#{PORT}/14") 43 | r.flushdb 44 | 45 | 100.times do |i| 46 | r.set "foo:users:#{i}", i 47 | end 48 | 49 | assert_equal([0, 100], r.nodes.map { |node| node.keys.size }) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /examples/sentinel/start: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # This is a helper script used together with examples/sentinel.rb 5 | # It runs two Redis masters, two slaves for each of them, and two sentinels. 6 | # After 30 seconds, the first master dies. 7 | # 8 | # You don't need to run this script yourself. Rather, use examples/sentinel.rb. 9 | 10 | require "fileutils" 11 | 12 | pids = [] 13 | 14 | at_exit do 15 | pids.each do |pid| 16 | Process.kill(:INT, pid) 17 | rescue Errno::ESRCH 18 | end 19 | 20 | Process.waitall 21 | end 22 | 23 | base = __dir__ 24 | 25 | # Masters 26 | pids << spawn("redis-server --port 6380 --loglevel warning") 27 | pids << spawn("redis-server --port 6381 --loglevel warning") 28 | 29 | # Slaves of Master 1 30 | pids << spawn("redis-server --port 63800 --slaveof 127.0.0.1 6380 --loglevel warning") 31 | pids << spawn("redis-server --port 63801 --slaveof 127.0.0.1 6380 --loglevel warning") 32 | 33 | # Slaves of Master 2 34 | pids << spawn("redis-server --port 63810 --slaveof 127.0.0.1 6381 --loglevel warning") 35 | pids << spawn("redis-server --port 63811 --slaveof 127.0.0.1 6381 --loglevel warning") 36 | 37 | FileUtils.cp(File.join(base, "sentinel.conf"), "tmp/sentinel1.conf") 38 | FileUtils.cp(File.join(base, "sentinel.conf"), "tmp/sentinel2.conf") 39 | 40 | # Sentinels 41 | pids << spawn("redis-server tmp/sentinel1.conf --sentinel --port 26379") 42 | pids << spawn("redis-server tmp/sentinel2.conf --sentinel --port 26380") 43 | 44 | sleep 30 45 | 46 | Process.kill(:KILL, pids[0]) 47 | 48 | Process.waitall 49 | -------------------------------------------------------------------------------- /test/distributed/distributed_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestDistributed < Minitest::Test 6 | include Helper::Distributed 7 | 8 | def test_handle_multiple_servers 9 | @r = Redis::Distributed.new ["redis://127.0.0.1:#{PORT}/15", *NODES] 10 | 11 | 100.times do |idx| 12 | @r.set(idx.to_s, "foo#{idx}") 13 | end 14 | 15 | 100.times do |idx| 16 | assert_equal "foo#{idx}", @r.get(idx.to_s) 17 | end 18 | 19 | assert_equal "0", @r.keys("*").min 20 | assert_equal "string", @r.type("1") 21 | end 22 | 23 | def test_add_nodes 24 | @r = Redis::Distributed.new NODES, timeout: 10 25 | 26 | assert_equal "127.0.0.1", @r.nodes[0]._client.host 27 | assert_equal PORT, @r.nodes[0]._client.port 28 | assert_equal 15, @r.nodes[0]._client.db 29 | assert_equal 10, @r.nodes[0]._client.timeout 30 | 31 | @r.add_node("redis://127.0.0.1:6380/14") 32 | 33 | assert_equal "127.0.0.1", @r.nodes[1]._client.host 34 | assert_equal 6380, @r.nodes[1]._client.port 35 | assert_equal 14, @r.nodes[1]._client.db 36 | assert_equal 10, @r.nodes[1]._client.timeout 37 | end 38 | 39 | def test_pipelining_commands_cannot_be_distributed 40 | assert_raises Redis::Distributed::CannotDistribute do 41 | r.pipelined do 42 | r.lpush "foo", "s1" 43 | r.lpush "foo", "s2" 44 | end 45 | end 46 | end 47 | 48 | def test_unknown_commands_does_not_work_by_default 49 | assert_raises NoMethodError do 50 | r.not_yet_implemented_command 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /cluster/redis-clustering.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../lib/redis/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "redis-clustering" 7 | 8 | s.version = Redis::VERSION 9 | 10 | github_root = "https://github.com/redis/redis-rb" 11 | s.homepage = "#{github_root}/blob/master/cluster" 12 | 13 | s.summary = "A Ruby client library for Redis Cluster" 14 | 15 | s.description = <<-EOS 16 | A Ruby client that tries to match Redis' Cluster API one-to-one, while still 17 | providing an idiomatic interface. 18 | EOS 19 | 20 | s.license = "MIT" 21 | 22 | s.authors = [ 23 | "Ezra Zygmuntowicz", 24 | "Taylor Weibley", 25 | "Matthew Clark", 26 | "Brian McKinney", 27 | "Salvatore Sanfilippo", 28 | "Luca Guidi", 29 | "Michel Martens", 30 | "Damian Janowski", 31 | "Pieter Noordhuis" 32 | ] 33 | 34 | s.email = ["redis-db@googlegroups.com"] 35 | 36 | s.metadata = { 37 | "bug_tracker_uri" => "#{github_root}/issues", 38 | "changelog_uri" => "#{s.homepage}/CHANGELOG.md", 39 | "documentation_uri" => "https://www.rubydoc.info/gems/redis/#{s.version}", 40 | "homepage_uri" => s.homepage, 41 | "source_code_uri" => "#{github_root}/tree/v#{s.version}/cluster" 42 | } 43 | 44 | s.files = Dir["CHANGELOG.md", "LICENSE", "README.md", "lib/**/*"] 45 | s.executables = `git ls-files -- exe/*`.split("\n").map { |f| File.basename(f) } 46 | 47 | s.required_ruby_version = '>= 2.7.0' 48 | 49 | s.add_runtime_dependency('redis', s.version) 50 | s.add_runtime_dependency('redis-cluster-client', '>= 0.7.0') 51 | end 52 | -------------------------------------------------------------------------------- /test/distributed/remote_server_control_commands_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestDistributedRemoteServerControlCommands < Minitest::Test 6 | include Helper::Distributed 7 | 8 | def test_info 9 | keys = [ 10 | "redis_version", 11 | "uptime_in_seconds", 12 | "uptime_in_days", 13 | "connected_clients", 14 | "used_memory", 15 | "total_connections_received", 16 | "total_commands_processed" 17 | ] 18 | 19 | infos = r.info 20 | 21 | infos.each do |info| 22 | keys.each do |k| 23 | msg = "expected #info to include #{k}" 24 | assert info.keys.include?(k), msg 25 | end 26 | end 27 | end 28 | 29 | def test_info_commandstats 30 | r.nodes.each do |n| 31 | n.config(:resetstat) 32 | n.get("foo") 33 | n.get("bar") 34 | end 35 | 36 | r.info(:commandstats).each do |info| 37 | assert_equal '2', info['get']['calls'] 38 | end 39 | end 40 | 41 | def test_monitor 42 | r.monitor 43 | rescue Exception => ex 44 | ensure 45 | assert ex.is_a?(NotImplementedError) 46 | end 47 | 48 | def test_echo 49 | assert_equal ["foo bar baz\n"], r.echo("foo bar baz\n") 50 | end 51 | 52 | def test_time 53 | # Test that the difference between the time that Ruby reports and the time 54 | # that Redis reports is minimal (prevents the test from being racy). 55 | r.time.each do |rv| 56 | redis_usec = rv[0] * 1_000_000 + rv[1] 57 | ruby_usec = Integer(Time.now.to_f * 1_000_000) 58 | 59 | assert((ruby_usec - redis_usec).abs < 500_000) 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/redis/bitpos_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestBitpos < Minitest::Test 6 | include Helper::Client 7 | 8 | def test_bitpos_empty_zero 9 | r.del "foo" 10 | assert_equal(0, r.bitpos("foo", 0)) 11 | end 12 | 13 | def test_bitpos_empty_one 14 | r.del "foo" 15 | assert_equal(-1, r.bitpos("foo", 1)) 16 | end 17 | 18 | def test_bitpos_zero 19 | r.set "foo", "\xff\xf0\x00" 20 | assert_equal(12, r.bitpos("foo", 0)) 21 | end 22 | 23 | def test_bitpos_one 24 | r.set "foo", "\x00\x0f\x00" 25 | assert_equal(12, r.bitpos("foo", 1)) 26 | end 27 | 28 | def test_bitpos_zero_end_is_given 29 | r.set "foo", "\xff\xff\xff" 30 | assert_equal(24, r.bitpos("foo", 0)) 31 | assert_equal(24, r.bitpos("foo", 0, 0)) 32 | assert_equal(-1, r.bitpos("foo", 0, 0, -1)) 33 | end 34 | 35 | def test_bitpos_one_intervals 36 | r.set "foo", "\x00\xff\x00" 37 | assert_equal(8, r.bitpos("foo", 1, 0, -1)) 38 | assert_equal(8, r.bitpos("foo", 1, 1, -1)) 39 | assert_equal(-1, r.bitpos("foo", 1, 2, -1)) 40 | assert_equal(-1, r.bitpos("foo", 1, 2, 200)) 41 | assert_equal(8, r.bitpos("foo", 1, 1, 1)) 42 | end 43 | 44 | def test_bitpos_one_intervals_bit_range 45 | target_version "7.0" do 46 | r.set "foo", "\x00\xff\x00" 47 | assert_equal(8, r.bitpos("foo", 1, 8, -1, scale: 'bit')) 48 | assert_equal(-1, r.bitpos("foo", 1, 8, -1, scale: 'byte')) 49 | end 50 | end 51 | 52 | def test_bitpos_raise_exception_if_stop_not_start 53 | assert_raises(ArgumentError) do 54 | r.bitpos("foo", 0, nil, 2) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /test/redis/sorting_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestSorting < Minitest::Test 6 | include Helper::Client 7 | 8 | def test_sort 9 | r.set("foo:1", "s1") 10 | r.set("foo:2", "s2") 11 | 12 | r.rpush("bar", "1") 13 | r.rpush("bar", "2") 14 | 15 | assert_equal ["s1"], r.sort("bar", get: "foo:*", limit: [0, 1]) 16 | assert_equal ["s2"], r.sort("bar", get: "foo:*", limit: [0, 1], order: "desc alpha") 17 | end 18 | 19 | def test_sort_with_an_array_of_gets 20 | r.set("foo:1:a", "s1a") 21 | r.set("foo:1:b", "s1b") 22 | 23 | r.set("foo:2:a", "s2a") 24 | r.set("foo:2:b", "s2b") 25 | 26 | r.rpush("bar", "1") 27 | r.rpush("bar", "2") 28 | 29 | assert_equal [["s1a", "s1b"]], r.sort("bar", get: ["foo:*:a", "foo:*:b"], limit: [0, 1]) 30 | assert_equal [["s2a", "s2b"]], r.sort("bar", get: ["foo:*:a", "foo:*:b"], limit: [0, 1], order: "desc alpha") 31 | assert_equal [["s1a", "s1b"], ["s2a", "s2b"]], r.sort("bar", get: ["foo:*:a", "foo:*:b"]) 32 | end 33 | 34 | def test_sort_with_store 35 | r.set("foo:1", "s1") 36 | r.set("foo:2", "s2") 37 | 38 | r.rpush("bar", "1") 39 | r.rpush("bar", "2") 40 | 41 | r.sort("bar", get: "foo:*", store: "baz") 42 | assert_equal ["s1", "s2"], r.lrange("baz", 0, -1) 43 | end 44 | 45 | def test_sort_with_an_array_of_gets_and_with_store 46 | r.set("foo:1:a", "s1a") 47 | r.set("foo:1:b", "s1b") 48 | 49 | r.set("foo:2:a", "s2a") 50 | r.set("foo:2:b", "s2b") 51 | 52 | r.rpush("bar", "1") 53 | r.rpush("bar", "2") 54 | 55 | r.sort("bar", get: ["foo:*:a", "foo:*:b"], store: 'baz') 56 | assert_equal ["s1a", "s1b", "s2a", "s2b"], r.lrange("baz", 0, -1) 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/redis/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Redis 4 | # Base error for all redis-rb errors. 5 | class BaseError < StandardError 6 | end 7 | 8 | # Raised by the connection when a protocol error occurs. 9 | class ProtocolError < BaseError 10 | def initialize(reply_type) 11 | super(<<-EOS.gsub(/(?:^|\n)\s*/, " ")) 12 | Got '#{reply_type}' as initial reply byte. 13 | If you're in a forking environment, such as Unicorn, you need to 14 | connect to Redis after forking. 15 | EOS 16 | end 17 | end 18 | 19 | # Raised by the client when command execution returns an error reply. 20 | class CommandError < BaseError 21 | end 22 | 23 | class PermissionError < CommandError 24 | end 25 | 26 | class WrongTypeError < CommandError 27 | end 28 | 29 | class OutOfMemoryError < CommandError 30 | end 31 | 32 | # Base error for connection related errors. 33 | class BaseConnectionError < BaseError 34 | end 35 | 36 | # Raised when connection to a Redis server cannot be made. 37 | class CannotConnectError < BaseConnectionError 38 | end 39 | 40 | # Raised when connection to a Redis server is lost. 41 | class ConnectionError < BaseConnectionError 42 | end 43 | 44 | # Raised when performing I/O times out. 45 | class TimeoutError < BaseConnectionError 46 | end 47 | 48 | # Raised when the connection was inherited by a child process. 49 | class InheritedError < BaseConnectionError 50 | end 51 | 52 | # Generally raised during Redis failover scenarios 53 | class ReadOnlyError < BaseConnectionError 54 | end 55 | 56 | # Raised when client options are invalid. 57 | class InvalidClientOptionError < BaseError 58 | end 59 | 60 | class SubscriptionError < BaseError 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /cluster/test/commands_on_scripting_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | # ruby -w -Itest test/cluster_commands_on_scripting_test.rb 6 | # @see https://redis.io/commands#scripting 7 | class TestClusterCommandsOnScripting < Minitest::Test 8 | include Helper::Cluster 9 | 10 | def test_eval 11 | script = 'return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}' 12 | argv = %w[first second] 13 | 14 | keys = %w[key1 key2] 15 | assert_raises(Redis::CommandError, "CROSSSLOT Keys in request don't hash to the same slot") do 16 | redis.eval(script, keys: keys, argv: argv) 17 | end 18 | 19 | keys = %w[{key}1 {key}2] 20 | expected = %w[{key}1 {key}2 first second] 21 | assert_equal expected, redis.eval(script, keys: keys, argv: argv) 22 | end 23 | 24 | def test_evalsha 25 | sha = redis.script(:load, 'return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}') 26 | expected = %w[{key}1 {key}2 first second] 27 | assert_equal expected, redis.evalsha(sha, keys: %w[{key}1 {key}2], argv: %w[first second]) 28 | end 29 | 30 | def test_script_debug 31 | assert_equal 'OK', redis.script(:debug, 'yes') 32 | assert_equal 'OK', redis.script(:debug, 'no') 33 | end 34 | 35 | def test_script_exists 36 | sha = redis.script(:load, 'return 1') 37 | assert_equal true, redis.script(:exists, sha) 38 | assert_equal false, redis.script(:exists, 'unknownsha') 39 | end 40 | 41 | def test_script_flush 42 | assert_equal 'OK', redis.script(:flush) 43 | end 44 | 45 | def test_script_kill 46 | redis_cluster_mock(kill: -> { '+OK' }) do |redis| 47 | assert_equal 'OK', redis.script(:kill) 48 | end 49 | end 50 | 51 | def test_script_load 52 | assert_equal 'e0e1f9fabfc9d4800c877a703b823ac0578ff8db', redis.script(:load, 'return 1') 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/support/ssl/untrusted-ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC9H4J79BHl9AgG 3 | ogcNQqpwQMOme47bvbIkxG1BOq1JE1ZMJ6haD3CBLv84/7xpZhyt/ho5xSCvcuEy 4 | LMjyj6p3gki5WGheWP9AdSoaOTS5NKCa2RLnKOnFCfiKfBR4bxVZn7ZBgmaNvIBT 5 | h/DrwEVSD+j3XGNKC0+lfCyDFvFc2Y/ljakjhhpyeBicBOnriO/YFLNtSkiBZKvY 6 | Y7yvDD1ELPAmnmwkfmeA5KDgKoUIynrwgLLNJ10GMblmIDP2YgqlBBUT/MEd2tdJ 7 | LhNoHXjyuwIBNkAkdRSFDjU8BIeXYosnFlUwQt2Kf4DcrNnXuwGeyubYwM6h9Quk 8 | fLhzBhVxAgMBAAECggEBALaaq/RezsFHBFDTNRfanJJSFhazClalLFJPzmXC7/m0 9 | 0Agr6mM6sRgqdodkdVkXHO3qgQvyiAKfW0yE7Wk2yhMmGm3LLMqcB6kG96XmQj/o 10 | zoF0wsmrOTvkyrN75o/6QZUNnn5WGAsWTJlakoYuWUBI2FmuPLgLf9V6tcfE6TsJ 11 | s/ovMBxq/bDd+QEvgVXqNNClLKWhbN1vSEfGQxkrZQGbo5iQdoJjQI1dR6xRJR5n 12 | COrKw9AWRLpW/c8xsmuSEayKn+tJURKBAw0xhituUtKPJD+0uWwRQBCi72we8kv+ 13 | 0MYLGBvIiU98J16EEimHQXtt7GU/uaAG1CD4NTBAIyECgYEA67hFC232j0U9skf9 14 | WA7vHGuu9tOdQyO6t2VpWPuKpvXqDz+y/qB+v6JInGlqF+DmgEtdvThv2C2BxpDe 15 | 512szEzLL13BcIPJE2XYXWf79Y6zpY1rIJfcDC0smlSEd0SGv0/lvSNtNVewR9/j 16 | F1siw8+hp3l6zx88mZKEU35uSCUCgYEAzWTyax/HUQA98bhZ7cXdwd64GcjIcsny 17 | 6kQaZSCn02gw8YEnxrwWn4I/h6hS2TVnAQFpKjYUuBYHRvMAKGpPg9Jsc/1Af/oc 18 | z8Pjx7uUYENOyaYXzs2ZtCE0VpPHPbZBZTUSzzBLyxqq0QUXkA4s4+2zNF8SFsg7 19 | GEg2fonIYF0CgYEArlhPsSF3IQbMmEWIy43YK0Q2V9ey1IrjumvmnGsIZW8z3G13 20 | 3b8loGXOoOmTD/BHbJLR1Xedud4Gw7A5PhVaDo2qJvGIdsjye0dz3bpgcIJIu2U6 21 | 3BOWLOdouwlSJMjphSz6Noeyaabe+npNA+RjdULoRO+j9vgaoVfuSbcUqIUCgYBd 22 | Dis2lYM8E5v888TqkQbTWxCVvf3y48QGlyxOPOlMQpxKDnXy+CxXwC8ASyad+i/c 23 | qML4uN/SN0i8wEOGDARSePdh5Y9fa/W5u8prJ3Ul19jOS03mCAhnL9QClZljQDuI 24 | mu8Wp47vSfmyEViHj6SO75aNV7VeVQFREwZ9dfcukQKBgCOC7OVPF8lmOCFKDonp 25 | NWutEY5YFUnnDBi7CWLZxwesSWF7RBRvOQD+Pe0uedMeHZEMld9WZ2tO9fT+4HBu 26 | QtqJ3fCqxZkrkPOCrQK/A9orYw+9VAXuerqVyolYE7hjWuJtx43NIHgz3jJ/G7pK 27 | MS782OTQErMtMg/jN6HOryM6 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /test/support/ssl/trusted-ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDdqqQ19Bxcd+lh 3 | VksT4R0y1H1xeVsBAA5fotIDOHRkL8tVMxcm8ytuNWoITsqGbcDmXBdycnwQ8mdO 4 | PsHg1zdo6PKflT5a0CKQmLM3V7EUS4eXSHXqgRGlWPXufEPJxJ+hlwyg/ITOpcoF 5 | CmJXh429Zi7Y1N5dA1tY+zzgtQYmNvxYaGTUCGIaN5fkA3fIGfAZA3fn5tZEJLTr 6 | ohMw+qsQbHjs96Y37YQIvnCQxMUhAhE9uIp2ybf2nFUqoSXl3NznsKgI3mQj8ACK 7 | LCOwMYCkHpeT1DM6ktHB00Sn3oLciOtZon/85ufhrheRdQIuq0jxIaxAu6NP04ei 8 | mezz+dgrAgMBAAECggEARrXqkDOA4JZ34k8OwBato8tZANu/hgAolaVw7QoTRupg 9 | KJuVpR0pG4z6eA/6Vwun31Q9Por6vMU24yTt3/WHfXXh/7oyG/INNKchdGQK3viB 10 | FmdNBjOKF37bZOpLDZAlg/yVUL18+Ba27Qi0+ksJkgOIqi6tiGpLt4TdlKjqf0Gv 11 | EPslFgvxIAoAjUZFhkanDY06FHe+1Bpue+1O5Cg+cL1YzNZy5XSDprvL4o8EsAuM 12 | fOoWDxxq0Jt0Mq+asYmqkVTwvmsiQzJoaTh8gM28Owkp9PSk4L/uY1gXO015InQ4 13 | ZyK+ypETfTmtfVXrHrfWS96FQvXZmbyRP/fszVsFEQKBgQD0mBgiY5Bc6/0bdEOc 14 | dBTyhJvxdQ+PxOHQP5mmgJJlCfwby4VdwapFgpZ3OZBiVsb79C8sueIqsgxN2zIx 15 | fB5Q7iHqkslVCvRkE0LdWAce8sWZgHqSnKoUTSZTReU4BJis0AwaSR8Nrbxb1UWm 16 | GWX7ppgZYnabhkf8MHLtmPqRBwKBgQDoANf2A56//rPzvS22MZuqL9FzkfIy+t/S 17 | WUHek6p9He2QtJ6W+tyjwLhKFOwMyl/1nYH7il/mQQhdijaVxHo9w6KmZiPw30Zc 18 | eaSn01mpBj1ID2ZXDffWOYAeO32PhBcyw+85ucIMIrBJJ+CXqS6ceQj1t/PpJ5Y8 19 | KdE41/mKvQKBgQDTdrtG3/VroMtO9RGPLfz+Pw/jjWVK0ti4BoR8oyPuHtfL4AUJ 20 | renb9q7HnQjrPEMEiXRPotWaPBzPIvceOUSsi3TfLNDLqZDpBI4Gd5iQdSvJLn7K 21 | So/wxVKhJAisiazFm4kbIKSsWsxCSPzSQZseGkXdjHcmts19hxWVvXDD+QKBgQC7 22 | m5MHub2x/EGApEZGwq7iXHC/SBHW78/2xX7igf6n1n+5OJXV+V5afQmJvolzfmNC 23 | tu/ZfPg3tfcRzSZ+zbccIwtwC8Cck7DOLv/bRqmGaSk9EFbtprn3XeAgknLijypD 24 | PvZAc9pa/eIYBksz2Pd8SNPZ/7sZm419cUNi+CMu8QKBgQDh4xL3a9soUptVftlp 25 | Mjw0ww9mNVsIAOWKq7KRUyVPJhCcJwDKr3D6i1hONqzP9jQe+qpiCJ1lRcFop7b1 26 | SjXA38BdZ2YDAJzQHEmkJCg+ZJx08hfBbFt9XQpKZ/3zKw8hQpme7TsF0NaH7M0e 27 | HdX2uqhE7Go49EbvEMRu+jWEUw== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /test/support/ssl/trusted-cert.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDg5r+uWuf3AUpT 3 | 3UvY0kdbILQBMcK/CgfmKmCovx8BOj+wlosKGiyIubvcO+echr1D8YcNVlzPWDHs 4 | pJELqCx2V/DEmMf4vXSy1TD/EuMq8MPpGIGf0UNGwolhO2LLimshpYpZTK/IjtI9 5 | Snd/rPZp9uS3RzCiMKAsIWuj+MPe8WNiCXJxOG0CWzo9AyJnNk+XkVXgnMfoY78s 6 | 2Y1T/q7Q3hCH75l2hE67pv4iPgmYVC3no1SkV7JTqd9W2rUbvn/jrgj4+CAzTylL 7 | bSTREMTgBSUHy75tx/+J4Bd3dtvLTXXnE8FvH1+km0y4qTjpCjneQUWWcWvreidv 8 | kpOwqjVxAgMBAAECggEAULL7vLhp27vey9DwUlDBwfUuIe+VDa+vvese2+4YVfxs 9 | thSOt4VEzZq3ygLEzOmcKDEWYLbIfq4K2/sBAMnLintrrV+VAbAZm8Hb3usMEHBs 10 | G8vrV0ljdpR/byA8BwUYA+6+geR+ftygm9WIo4uQr90jnJAy5z/DeZJUaXXt8qTG 11 | pCnflCLrsBhsJFNQjqDvUnw08Cd34Nkx9gNlsGQYmWnRgqHERQuHN4bw/1Tx3Vnu 12 | memX77TlNoMttXca3cHjJ6UnKSjTxGBfco3VLlO0QpTdFaQVO6svvLjNFtodVQM3 13 | RrL5cyWk+2qOLHLY+YUE8zImZmvK7JClr7JomQQwQQKBgQD+NSMbhaTKoWfN/1NH 14 | Efrw1vTF62nfbC41jk5eenxhOhgqWT9vfeAi7lKXW3pY2ZA3dMjN/XCFr/xexVQ6 15 | 3R0p48lscmN5cslFB6vks4O/iK4J6t+xCLZzgi2XRCg2UT+nRPcbW8bPFTxpb2+x 16 | ++SFEHy0DC7pR1+XYDj5iqkHrQKBgQDifLY5rgVJxpVR42GCkUOUMC20GJisCqHv 17 | ABG9X8gjYKn/prSeEx5mCCKfwHkKo+aJrOWQ9TspjduG1Pelmxx4g5G3EBhobSIW 18 | vqsAfqVr3UWawlYNfA4+ek0m+xl55d1s/CNaXx+xmVjyzIBQwKqUAx67gtVULMCH 19 | 9PX36B5tVQKBgQDxB/kt014ZM0l1rS6NKKNDUM3uC/Tq/2whI7lzI7hjh+352X2o 20 | fTXUaRyunvI25LM1oen0RuY2HFOymG/xEE7itTT7OsrPEON+LHPz+bJmHXbHuIg5 21 | GAXHKBuKXfmy5v7v3xhePHsZRw1s+1hw7mITOTrEjPi+AArHQVlEYxE6UQKBgQCD 22 | +B0KEO895LtfArnvpYsWDtiipu5W2L8wjv7HNMdebdXAhDecIBHHbBgYs8MTwxry 23 | v87oHyyA8wqmTvOaCH6Xbjp6y6MdPfHuBN2JJUJoTn9fRLt1kgKOvx6zhv56O8lA 24 | 1s4Wu3SxPGRK3YQrCYibRBIlOn/pU0ZAMikccaFBHQKBgQCRsNhjKfHDRPCzhMnu 25 | SyOSIcB43JHOl9JeTvjmAnjE3m06pjppp+sHyR2FxghIz5IGepAVLKy6jbYyJATp 26 | ubb0br+1Jk8MdjprAd82dN9xqw/kt/rbCsJof0e38+aoRY06HjFRz4zXScOLG0ag 27 | Ym1C4aDdCQRZQmSmSHiVBRN70Q== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /test/support/ssl/untrusted-cert.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDEnDPtPf75DLdG 3 | VgSoVg47VTSl4iKrkOnx+SVEAXQAyyUn5FMhFJErmgBgMW/nZYiTmVwLt0SwsbZf 4 | XdLbq4RRMSrDc2egqgRHxWBbLzn6CTsJR5euqOyhftEifPEcbbX+PW6W+7RwJYGU 5 | UMmsb9zNXfke7RiKVzoFf/HdEq+Gt463XSzXwG9tmF9A5Pqj7SxDoKxqamxB6ITS 6 | HFlj7NClxx9QheOoVJW9BMuZXCpt7gSt15OJN3yi/fZOwnpMsvOCE8On78Nazvve 7 | CLdX+xjCV0CbGrEAhUlek8mcAh/pdnYPWS6EvjG9CXPIo5IjOsADmdl+mJqD6mk5 8 | adLgskgLAgMBAAECggEAenQXW2HLlm43EBWvHPFMN+QfwFmR4m2FZ/IXJb4J9ByS 9 | bcAljmry58cpCMCBxAtW/yb7T0i7/ZkRz1/uXmb7KF6JFeag2k5KEDF8jA5j+7kY 10 | DfWLIXuQthz4QJS0z1H9kfXNFTh774VMqYWPtliNm1M2P+7H5BHjz10a1Og4bpx4 11 | UzUpFQcPaao0Bu9Vvwj9kjzu8siZPWbqXexqt3S0sgpyCgvjozAUHMsLK8PJm08J 12 | 5QhOG0as5siEKnNYrRNxgbaebxDxanSuQAYnLsku2rlyZqnDtoCVzVgPfF3/hzD1 13 | Qs9W3bdiolRYxxo6rhjGrvv+KVG/wavJSYbBL6l4wQKBgQDz845M/XYlxYl27s5U 14 | i/BkB4yJge7DsfTWJpR7Zf3snlda3QEF/BsBRyFErA7stRfhOGEYDFrYaYgoVJGQ 15 | oZrVqVuwKgdmbsJoVOek0Ab4PguIEJYPBLy3KHCIAoeMZXBiUk3o/pww4kvTyFcB 16 | 8FiJRlLFd2298Lvowf2k6iBZOwKBgQDOUhRdD4Lkyi3N1Y+NGtFQsAPSiABbF/9f 17 | 0QF45Gkp53TCWnhZeF82+yGHnJ2y7xusC15nfv1HTkLL/UeibmVBX//v1SoAAPIq 18 | 9/+ftvOnEkLVQJ+WGmmtgazqcdg5/3lC1zfw4u2OjCZOVYArJXpi8bFQerf04BN6 19 | Jh2NcQpfcQKBgQDdFcPHHoXuoWF9edtgYBqSbQz+qdS7YhHj6r7yPnKr+KxuWpBM 20 | 3jeTJuWNmOlFuLFVmYTVCI1kR+/vrQTnMK5kKMJBmzVtrb9eUmREx4spewF0ZKO6 21 | JK7qxymE+dXidSQu1yxolibzXoMeAhhoV2vFrQfikePRGdUSkozO4qhCdQKBgQCl 22 | d459VANWGg/CFJScRfW5EHEAV7JxXD2jSqwzmHv+73HkrUn392HlZmLtr92Js9ot 23 | kLCVsHLQzSMlFmxtCLyMQcGxRvP4LMoLS/nmzYN7alnPTZSvfV9jl6xmGgef/BP0 24 | V0a2GkkLGbte95NjBxuwXsYmFUWTTmJQhGEPHqmDAQKBgDMNCGWVVceGB6UBeXfW 25 | kU7Egr8b3/wMJBy4wmilHIlCxtka6hLzx3+kTqLFIYlCq2sy7fvyLc8dX5bEQ7tZ 26 | v1Zd10mqvfWKBFm/8D691fxiwfBHAXNFRACmBtRb2NJVGL7CFCuuIAt/cyQTb+7l 27 | NsZKEc1x306JFtvKhdqIAWeY 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /test/distributed/transactions_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestDistributedTransactions < Minitest::Test 6 | include Helper::Distributed 7 | 8 | def test_multi_discard_without_watch 9 | @foo = nil 10 | 11 | assert_raises Redis::Distributed::CannotDistribute do 12 | r.multi { @foo = 1 } 13 | end 14 | 15 | assert_nil @foo 16 | 17 | assert_raises Redis::Distributed::CannotDistribute do 18 | r.discard 19 | end 20 | end 21 | 22 | def test_watch_unwatch_without_clustering 23 | assert_raises Redis::Distributed::CannotDistribute do 24 | r.watch("foo", "bar") 25 | end 26 | 27 | r.watch("{qux}foo", "{qux}bar") do 28 | assert_raises Redis::Distributed::CannotDistribute do 29 | r.get("{baz}foo") 30 | end 31 | 32 | r.unwatch 33 | end 34 | 35 | assert_raises Redis::Distributed::CannotDistribute do 36 | r.unwatch 37 | end 38 | end 39 | 40 | def test_watch_with_exception 41 | assert_raises StandardError do 42 | r.watch("{qux}foo", "{qux}bar") do 43 | raise StandardError, "woops" 44 | end 45 | end 46 | 47 | assert_equal "OK", r.set("{other}baz", 1) 48 | end 49 | 50 | def test_watch_unwatch 51 | assert_equal "OK", r.watch("{qux}foo", "{qux}bar") 52 | assert_equal "OK", r.unwatch 53 | end 54 | 55 | def test_watch_multi_with_block 56 | r.set("{qux}baz", 1) 57 | 58 | r.watch("{qux}foo", "{qux}bar", "{qux}baz") do 59 | assert_equal '1', r.get("{qux}baz") 60 | 61 | result = r.multi do |transaction| 62 | transaction.incrby("{qux}foo", 3) 63 | transaction.incrby("{qux}bar", 6) 64 | transaction.incrby("{qux}baz", 9) 65 | end 66 | 67 | assert_equal [3, 6, 10], result 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /cluster/test/commands_on_sorted_sets_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | # ruby -w -Itest test/cluster_commands_on_sorted_sets_test.rb 6 | # @see https://redis.io/commands#sorted_set 7 | class TestClusterCommandsOnSortedSets < Minitest::Test 8 | include Helper::Cluster 9 | include Lint::SortedSets 10 | 11 | def test_zrangestore 12 | assert_raises(Redis::CommandError) { super } 13 | end 14 | 15 | def test_zinter 16 | assert_raises(Redis::CommandError) { super } 17 | end 18 | 19 | def test_zinter_with_aggregate 20 | assert_raises(Redis::CommandError) { super } 21 | end 22 | 23 | def test_zinter_with_weights 24 | assert_raises(Redis::CommandError) { super } 25 | end 26 | 27 | def test_zinterstore 28 | assert_raises(Redis::CommandError) { super } 29 | end 30 | 31 | def test_zinterstore_with_aggregate 32 | assert_raises(Redis::CommandError) { super } 33 | end 34 | 35 | def test_zinterstore_with_weights 36 | assert_raises(Redis::CommandError) { super } 37 | end 38 | 39 | def test_zunion 40 | assert_raises(Redis::CommandError) { super } 41 | end 42 | 43 | def test_zunion_with_aggregate 44 | assert_raises(Redis::CommandError) { super } 45 | end 46 | 47 | def test_zunion_with_weights 48 | assert_raises(Redis::CommandError) { super } 49 | end 50 | 51 | def test_zunionstore 52 | assert_raises(Redis::CommandError) { super } 53 | end 54 | 55 | def test_zunionstore_with_aggregate 56 | assert_raises(Redis::CommandError) { super } 57 | end 58 | 59 | def test_zunionstore_with_weights 60 | assert_raises(Redis::CommandError) { super } 61 | end 62 | 63 | def test_zdiff 64 | assert_raises(Redis::CommandError) { super } 65 | end 66 | 67 | def test_zdiffstore 68 | assert_raises(Redis::CommandError) { super } 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /test/redis/blocking_commands_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestBlockingCommands < Minitest::Test 6 | include Helper::Client 7 | include Lint::BlockingCommands 8 | 9 | def assert_takes_longer_than_client_timeout 10 | timeout = LOW_TIMEOUT 11 | delay = timeout * 5 12 | 13 | mock(delay: delay) do |r| 14 | t1 = Time.now 15 | yield(r) 16 | t2 = Time.now 17 | 18 | assert_operator delay, :<=, (t2 - t1) 19 | end 20 | end 21 | 22 | def test_blmove_disable_client_timeout 23 | target_version "6.2" do 24 | assert_takes_longer_than_client_timeout do |r| 25 | assert_equal '0', r.blmove('foo', 'bar', 'LEFT', 'RIGHT') 26 | end 27 | end 28 | end 29 | 30 | def test_blpop_disable_client_timeout 31 | assert_takes_longer_than_client_timeout do |r| 32 | assert_equal %w[foo 0], r.blpop('foo') 33 | end 34 | end 35 | 36 | def test_brpop_disable_client_timeout 37 | assert_takes_longer_than_client_timeout do |r| 38 | assert_equal %w[foo 0], r.brpop('foo') 39 | end 40 | end 41 | 42 | def test_brpoplpush_disable_client_timeout 43 | assert_takes_longer_than_client_timeout do |r| 44 | assert_equal '0', r.brpoplpush('foo', 'bar') 45 | end 46 | end 47 | 48 | def test_brpoplpush_in_transaction 49 | results = r.multi do |transaction| 50 | transaction.brpoplpush('foo', 'bar') 51 | transaction.brpoplpush('foo', 'bar', timeout: 2) 52 | end 53 | assert_equal [nil, nil], results 54 | end 55 | 56 | def test_brpoplpush_in_pipeline 57 | mock do |r| 58 | results = r.pipelined do |transaction| 59 | transaction.brpoplpush('foo', 'bar') 60 | transaction.brpoplpush('foo', 'bar', timeout: 2) 61 | end 62 | assert_equal ['0', '2'], results 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/lint/hyper_log_log.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Lint 4 | module HyperLogLog 5 | def test_pfadd 6 | assert_equal true, r.pfadd("foo", "s1") 7 | assert_equal true, r.pfadd("foo", "s2") 8 | assert_equal false, r.pfadd("foo", "s1") 9 | 10 | assert_equal 2, r.pfcount("foo") 11 | end 12 | 13 | def test_variadic_pfadd 14 | assert_equal true, r.pfadd("foo", ["s1", "s2"]) 15 | assert_equal true, r.pfadd("foo", ["s1", "s2", "s3"]) 16 | 17 | assert_equal 3, r.pfcount("foo") 18 | end 19 | 20 | def test_pfcount 21 | assert_equal 0, r.pfcount("foo") 22 | 23 | assert_equal true, r.pfadd("foo", "s1") 24 | 25 | assert_equal 1, r.pfcount("foo") 26 | end 27 | 28 | def test_variadic_pfcount 29 | assert_equal 0, r.pfcount(["{1}foo", "{1}bar"]) 30 | 31 | assert_equal true, r.pfadd("{1}foo", "s1") 32 | assert_equal true, r.pfadd("{1}bar", "s1") 33 | assert_equal true, r.pfadd("{1}bar", "s2") 34 | 35 | assert_equal 2, r.pfcount("{1}foo", "{1}bar") 36 | end 37 | 38 | def test_variadic_pfcount_expanded 39 | assert_equal 0, r.pfcount("{1}foo", "{1}bar") 40 | 41 | assert_equal true, r.pfadd("{1}foo", "s1") 42 | assert_equal true, r.pfadd("{1}bar", "s1") 43 | assert_equal true, r.pfadd("{1}bar", "s2") 44 | 45 | assert_equal 2, r.pfcount("{1}foo", "{1}bar") 46 | end 47 | 48 | def test_pfmerge 49 | r.pfadd 'foo', 's1' 50 | r.pfadd 'bar', 's2' 51 | 52 | assert_equal true, r.pfmerge('res', 'foo', 'bar') 53 | assert_equal 2, r.pfcount('res') 54 | end 55 | 56 | def test_variadic_pfmerge_expanded 57 | redis.pfadd('{1}foo', %w[foo bar zap a]) 58 | redis.pfadd('{1}bar', %w[a b c foo]) 59 | assert_equal true, redis.pfmerge('{1}baz', '{1}foo', '{1}bar') 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /bin/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | TARBALL = ARGV[0] 5 | 6 | require 'digest/sha1' 7 | require 'English' 8 | require 'fileutils' 9 | 10 | class Builder 11 | TARBALL_CACHE_EXPIRATION = 60 * 10 12 | 13 | def initialize(redis_branch, tmp_dir) 14 | @redis_branch = redis_branch 15 | @tmp_dir = tmp_dir 16 | @build_dir = File.join(@tmp_dir, "cache", "redis-#{redis_branch}") 17 | end 18 | 19 | def run 20 | download_tarball_if_needed 21 | if old_checkum != checksum 22 | build 23 | update_checksum 24 | end 25 | 0 26 | end 27 | 28 | private 29 | 30 | def download_tarball_if_needed 31 | return if File.exist?(tarball_path) && File.mtime(tarball_path) > Time.now - TARBALL_CACHE_EXPIRATION 32 | 33 | command!('wget', '-q', tarball_url, '-O', tarball_path) 34 | end 35 | 36 | def tarball_path 37 | File.join(@tmp_dir, "redis-#{@redis_branch}.tar.gz") 38 | end 39 | 40 | def tarball_url 41 | "https://github.com/antirez/redis/archive/#{@redis_branch}.tar.gz" 42 | end 43 | 44 | def build 45 | FileUtils.rm_rf(@build_dir) 46 | FileUtils.mkdir_p(@build_dir) 47 | command!('tar', 'xf', tarball_path, '-C', File.expand_path('../', @build_dir)) 48 | Dir.chdir(@build_dir) do 49 | command!('make') 50 | end 51 | end 52 | 53 | def update_checksum 54 | File.write(checksum_path, checksum) 55 | end 56 | 57 | def old_checkum 58 | File.read(checksum_path) 59 | rescue Errno::ENOENT 60 | nil 61 | end 62 | 63 | def checksum_path 64 | File.join(@build_dir, 'build.checksum') 65 | end 66 | 67 | def checksum 68 | @checksum ||= Digest::SHA1.file(tarball_path).hexdigest 69 | end 70 | 71 | def command!(*args) 72 | puts "$ #{args.join(' ')}" 73 | raise "Command failed with status #{$CHILD_STATUS.exitstatus}" unless system(*args) 74 | end 75 | end 76 | 77 | exit Builder.new(ARGV[0], ARGV[1]).run 78 | -------------------------------------------------------------------------------- /cluster/test/commands_on_streams_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | # ruby -w -Itest test/cluster_commands_on_streams_test.rb 6 | # @see https://redis.io/commands#stream 7 | class TestClusterCommandsOnStreams < Minitest::Test 8 | include Helper::Cluster 9 | include Lint::Streams 10 | 11 | def test_xread_with_multiple_keys 12 | err_msg = "CROSSSLOT Keys in request don't hash to the same slot" 13 | assert_raises(Redis::CommandError, err_msg) { super } 14 | end 15 | 16 | def test_xread_with_multiple_keys_and_hash_tags 17 | redis.xadd('{s}1', { f: 'v01' }, id: '0-1') 18 | redis.xadd('{s}1', { f: 'v02' }, id: '0-2') 19 | redis.xadd('{s}2', { f: 'v11' }, id: '1-1') 20 | redis.xadd('{s}2', { f: 'v12' }, id: '1-2') 21 | 22 | actual = redis.xread(%w[{s}1 {s}2], %w[0-1 1-1]) 23 | 24 | assert_equal %w(0-2), actual['{s}1'].map(&:first) 25 | assert_equal(%w(v02), actual['{s}1'].map { |i| i.last['f'] }) 26 | 27 | assert_equal %w(1-2), actual['{s}2'].map(&:first) 28 | assert_equal(%w(v12), actual['{s}2'].map { |i| i.last['f'] }) 29 | end 30 | 31 | def test_xreadgroup_with_multiple_keys 32 | err_msg = "CROSSSLOT Keys in request don't hash to the same slot" 33 | assert_raises(Redis::CommandError, err_msg) { super } 34 | end 35 | 36 | def test_xreadgroup_with_multiple_keys_and_hash_tags 37 | redis.xadd('{s}1', { f: 'v01' }, id: '0-1') 38 | redis.xgroup(:create, '{s}1', 'g1', '$') 39 | redis.xadd('{s}2', { f: 'v11' }, id: '1-1') 40 | redis.xgroup(:create, '{s}2', 'g1', '$') 41 | redis.xadd('{s}1', { f: 'v02' }, id: '0-2') 42 | redis.xadd('{s}2', { f: 'v12' }, id: '1-2') 43 | 44 | actual = redis.xreadgroup('g1', 'c1', %w[{s}1 {s}2], %w[> >]) 45 | 46 | assert_equal %w(0-2), actual['{s}1'].map(&:first) 47 | assert_equal(%w(v02), actual['{s}1'].map { |i| i.last['f'] }) 48 | 49 | assert_equal %w(1-2), actual['{s}2'].map(&:first) 50 | assert_equal(%w(v12), actual['{s}2'].map { |i| i.last['f'] }) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/distributed/commands_on_strings_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestDistributedCommandsOnStrings < Minitest::Test 6 | include Helper::Distributed 7 | include Lint::Strings 8 | 9 | def test_mget 10 | r.set("foo", "s1") 11 | r.set("bar", "s2") 12 | 13 | assert_equal ["s1", "s2"], r.mget("foo", "bar") 14 | assert_equal ["s1", "s2", nil], r.mget("foo", "bar", "baz") 15 | assert_equal ["s1", "s2", nil], r.mget(["foo", "bar", "baz"]) 16 | end 17 | 18 | def test_mget_mapped 19 | r.set("foo", "s1") 20 | r.set("bar", "s2") 21 | 22 | response = r.mapped_mget("foo", "bar") 23 | 24 | assert_equal "s1", response["foo"] 25 | assert_equal "s2", response["bar"] 26 | 27 | response = r.mapped_mget("foo", "bar", "baz") 28 | 29 | assert_equal "s1", response["foo"] 30 | assert_equal "s2", response["bar"] 31 | assert_nil response["baz"] 32 | end 33 | 34 | def test_mset 35 | assert_raises Redis::Distributed::CannotDistribute do 36 | r.mset(:foo, "s1", :bar, "s2") 37 | end 38 | end 39 | 40 | def test_mset_mapped 41 | assert_raises Redis::Distributed::CannotDistribute do 42 | r.mapped_mset(foo: "s1", bar: "s2") 43 | end 44 | end 45 | 46 | def test_msetnx 47 | assert_raises Redis::Distributed::CannotDistribute do 48 | r.set("foo", "s1") 49 | r.msetnx(:foo, "s2", :bar, "s3") 50 | end 51 | end 52 | 53 | def test_msetnx_mapped 54 | assert_raises Redis::Distributed::CannotDistribute do 55 | r.set("foo", "s1") 56 | r.mapped_msetnx(foo: "s2", bar: "s3") 57 | end 58 | end 59 | 60 | def test_bitop 61 | assert_raises Redis::Distributed::CannotDistribute do 62 | r.set("foo", "a") 63 | r.set("bar", "b") 64 | 65 | r.bitop(:and, "foo&bar", "foo", "bar") 66 | end 67 | end 68 | 69 | def test_mapped_mget_in_a_pipeline_returns_hash 70 | assert_raises Redis::Distributed::CannotDistribute do 71 | super 72 | end 73 | end 74 | 75 | def test_bitfield 76 | # Not implemented yet 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /test/sentinel/sentinel_command_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | # @see https://redis.io/topics/sentinel#sentinel-commands Sentinel commands 6 | class SentinelCommandsTest < Minitest::Test 7 | include Helper::Sentinel 8 | 9 | def test_sentinel_command_master 10 | wait_for_quorum 11 | 12 | redis = build_sentinel_client 13 | result = redis.sentinel('master', MASTER_NAME) 14 | 15 | assert_equal result['name'], MASTER_NAME 16 | assert_equal result['ip'], LOCALHOST 17 | end 18 | 19 | def test_sentinel_command_masters 20 | wait_for_quorum 21 | 22 | redis = build_sentinel_client 23 | result = redis.sentinel('masters') 24 | 25 | assert_equal result[0]['name'], MASTER_NAME 26 | assert_equal result[0]['ip'], LOCALHOST 27 | assert_equal result[0]['port'], MASTER_PORT 28 | end 29 | 30 | def test_sentinel_command_slaves 31 | wait_for_quorum 32 | 33 | redis = build_sentinel_client 34 | result = redis.sentinel('slaves', MASTER_NAME) 35 | 36 | assert_equal result[0]['name'], "#{LOCALHOST}:#{SLAVE_PORT}" 37 | assert_equal result[0]['ip'], LOCALHOST 38 | assert_equal result[0]['port'], SLAVE_PORT 39 | end 40 | 41 | def test_sentinel_command_sentinels 42 | wait_for_quorum 43 | 44 | redis = build_sentinel_client 45 | result = redis.sentinel('sentinels', MASTER_NAME) 46 | 47 | assert_equal result[0]['ip'], LOCALHOST 48 | 49 | actual_ports = result.map { |r| r['port'] }.sort 50 | expected_ports = SENTINEL_PORTS[1..-1] 51 | assert_equal actual_ports, expected_ports 52 | end 53 | 54 | def test_sentinel_command_get_master_by_name 55 | redis = build_sentinel_client 56 | result = redis.sentinel('get-master-addr-by-name', MASTER_NAME) 57 | 58 | assert_equal result, [LOCALHOST, MASTER_PORT] 59 | end 60 | 61 | def test_sentinel_command_ckquorum 62 | wait_for_quorum 63 | 64 | redis = build_sentinel_client 65 | result = redis.sentinel('ckquorum', MASTER_NAME) 66 | assert_equal result, 'OK 3 usable Sentinels. Quorum and failover authorization can be reached' 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /cluster/test/client_internals_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | # ruby -w -Itest test/cluster_client_internals_test.rb 6 | class TestClusterClientInternals < Minitest::Test 7 | include Helper::Cluster 8 | 9 | def test_handle_multiple_servers 10 | 100.times { |i| redis.set(i.to_s, "hogehoge#{i}") } 11 | 100.times { |i| assert_equal "hogehoge#{i}", redis.get(i.to_s) } 12 | end 13 | 14 | def test_info_of_cluster_mode_is_enabled 15 | assert_equal '1', redis.info['cluster_enabled'] 16 | end 17 | 18 | def test_unknown_commands_does_not_work_by_default 19 | assert_raises(Redis::CommandError) do 20 | redis.not_yet_implemented_command('boo', 'foo') 21 | end 22 | end 23 | 24 | def test_connected? 25 | assert_equal true, redis.connected? 26 | end 27 | 28 | def test_close 29 | redis.close 30 | end 31 | 32 | def test_disconnect! 33 | redis.disconnect! 34 | end 35 | 36 | def test_asking 37 | assert_equal 'OK', redis.asking 38 | end 39 | 40 | def test_id 41 | expected = '127.0.0.1:16380 '\ 42 | '127.0.0.1:16381 '\ 43 | '127.0.0.1:16382' 44 | assert_equal expected, redis.id 45 | end 46 | 47 | def test_inspect 48 | expected = "#' 52 | 53 | assert_equal expected, redis.inspect 54 | end 55 | 56 | def test_acl_auth_success 57 | target_version "6.0.0" do 58 | with_acl do |username, password| 59 | r = _new_client(nodes: DEFAULT_PORTS.map { |port| "redis://#{username}:#{password}@#{DEFAULT_HOST}:#{port}" }) 60 | assert_equal('PONG', r.ping) 61 | end 62 | end 63 | end 64 | 65 | def test_acl_auth_failure 66 | target_version "6.0.0" do 67 | with_acl do |username, _| 68 | assert_raises(Redis::Cluster::InitialSetupError) do 69 | _new_client(nodes: DEFAULT_PORTS.map { |port| "redis://#{username}:wrongpassword@#{DEFAULT_HOST}:#{port}" }) 70 | end 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /test/redis/ssl_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class SslTest < Minitest::Test 6 | include Helper::Client 7 | 8 | def test_connection_to_non_ssl_server 9 | assert_raises(Redis::CannotConnectError) do 10 | redis = Redis.new(OPTIONS.merge(ssl: true, timeout: LOW_TIMEOUT)) 11 | redis.ping 12 | end 13 | end 14 | 15 | def test_verified_ssl_connection 16 | RedisMock.start({ ping: proc { "+PONG" } }, ssl_server_opts("trusted")) do |port| 17 | redis = Redis.new(host: "127.0.0.1", port: port, ssl: true, ssl_params: { ca_file: ssl_ca_file }) 18 | assert_equal redis.ping, "PONG" 19 | end 20 | end 21 | 22 | def test_unverified_ssl_connection 23 | assert_raises(Redis::CannotConnectError) do 24 | RedisMock.start({ ping: proc { "+PONG" } }, ssl_server_opts("untrusted")) do |port| 25 | redis = Redis.new(port: port, ssl: true, ssl_params: { ca_file: ssl_ca_file }) 26 | redis.ping 27 | end 28 | end 29 | end 30 | 31 | def test_verify_certificates_by_default 32 | assert_raises(Redis::CannotConnectError) do 33 | RedisMock.start({ ping: proc { "+PONG" } }, ssl_server_opts("untrusted")) do |port| 34 | redis = Redis.new(port: port, ssl: true) 35 | redis.ping 36 | end 37 | end 38 | end 39 | 40 | def test_ssl_blocking 41 | RedisMock.start({}, ssl_server_opts("trusted")) do |port| 42 | redis = Redis.new(host: "127.0.0.1", port: port, ssl: true, ssl_params: { ca_file: ssl_ca_file }) 43 | assert_equal redis.set("boom", "a" * 10_000_000), "OK" 44 | end 45 | end 46 | 47 | private 48 | 49 | def ssl_server_opts(prefix) 50 | ssl_cert = File.join(cert_path, "#{prefix}-cert.crt") 51 | ssl_key = File.join(cert_path, "#{prefix}-cert.key") 52 | 53 | { 54 | ssl: true, 55 | ssl_params: { 56 | cert: OpenSSL::X509::Certificate.new(File.read(ssl_cert)), 57 | key: OpenSSL::PKey::RSA.new(File.read(ssl_key)) 58 | } 59 | } 60 | end 61 | 62 | def ssl_ca_file 63 | File.join(cert_path, "trusted-ca.crt") 64 | end 65 | 66 | def cert_path 67 | File.expand_path('../support/ssl', __dir__) 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /test/redis/scripting_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestScripting < Minitest::Test 6 | include Helper::Client 7 | 8 | def to_sha(script) 9 | r.script(:load, script) 10 | end 11 | 12 | def test_script_exists 13 | a = to_sha("return 1") 14 | b = a.succ 15 | 16 | assert_equal true, r.script(:exists, a) 17 | assert_equal false, r.script(:exists, b) 18 | assert_equal [true], r.script(:exists, [a]) 19 | assert_equal [false], r.script(:exists, [b]) 20 | assert_equal [true, false], r.script(:exists, [a, b]) 21 | end 22 | 23 | def test_script_flush 24 | sha = to_sha("return 1") 25 | assert r.script(:exists, sha) 26 | assert_equal "OK", r.script(:flush) 27 | assert !r.script(:exists, sha) 28 | end 29 | 30 | def test_script_kill 31 | redis_mock(script: ->(arg) { "+#{arg.upcase}" }) do |redis| 32 | assert_equal "KILL", redis.script(:kill) 33 | end 34 | end 35 | 36 | def test_eval 37 | assert_equal 0, r.eval("return #KEYS") 38 | assert_equal 0, r.eval("return #ARGV") 39 | assert_equal ["k1", "k2"], r.eval("return KEYS", ["k1", "k2"]) 40 | assert_equal ["a1", "a2"], r.eval("return ARGV", [], ["a1", "a2"]) 41 | end 42 | 43 | def test_eval_with_options_hash 44 | assert_equal 0, r.eval("return #KEYS", {}) 45 | assert_equal 0, r.eval("return #ARGV", {}) 46 | assert_equal ["k1", "k2"], r.eval("return KEYS", { keys: ["k1", "k2"] }) 47 | assert_equal ["a1", "a2"], r.eval("return ARGV", { argv: ["a1", "a2"] }) 48 | end 49 | 50 | def test_evalsha 51 | assert_equal 0, r.evalsha(to_sha("return #KEYS")) 52 | assert_equal 0, r.evalsha(to_sha("return #ARGV")) 53 | assert_equal ["k1", "k2"], r.evalsha(to_sha("return KEYS"), ["k1", "k2"]) 54 | assert_equal ["a1", "a2"], r.evalsha(to_sha("return ARGV"), [], ["a1", "a2"]) 55 | end 56 | 57 | def test_evalsha_with_options_hash 58 | assert_equal 0, r.evalsha(to_sha("return #KEYS"), {}) 59 | assert_equal 0, r.evalsha(to_sha("return #ARGV"), {}) 60 | assert_equal ["k1", "k2"], r.evalsha(to_sha("return KEYS"), { keys: ["k1", "k2"] }) 61 | assert_equal ["a1", "a2"], r.evalsha(to_sha("return ARGV"), { argv: ["a1", "a2"] }) 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /cluster/test/commands_on_geo_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | # ruby -w -Itest test/cluster_commands_on_geo_test.rb 6 | # @see https://redis.io/commands#geo 7 | class TestClusterCommandsOnGeo < Minitest::Test 8 | include Helper::Cluster 9 | 10 | def add_sicily 11 | redis.geoadd('Sicily', 12 | 13.361389, 38.115556, 'Palermo', 13 | 15.087269, 37.502669, 'Catania') 14 | end 15 | 16 | def test_geoadd 17 | assert_equal 2, add_sicily 18 | end 19 | 20 | def test_geohash 21 | add_sicily 22 | assert_equal %w[sqc8b49rny0 sqdtr74hyu0], redis.geohash('Sicily', %w[Palermo Catania]) 23 | end 24 | 25 | def test_geopos 26 | add_sicily 27 | expected = [%w[13.36138933897018433 38.11555639549629859], 28 | %w[15.08726745843887329 37.50266842333162032], 29 | nil] 30 | assert_equal expected, redis.geopos('Sicily', %w[Palermo Catania NonExisting]) 31 | end 32 | 33 | def test_geodist 34 | add_sicily 35 | assert_equal '166274.1516', redis.geodist('Sicily', 'Palermo', 'Catania') 36 | assert_equal '166.2742', redis.geodist('Sicily', 'Palermo', 'Catania', 'km') 37 | assert_equal '103.3182', redis.geodist('Sicily', 'Palermo', 'Catania', 'mi') 38 | end 39 | 40 | def test_georadius 41 | add_sicily 42 | 43 | expected = [%w[Palermo 190.4424], %w[Catania 56.4413]] 44 | assert_equal expected, redis.georadius('Sicily', 15, 37, 200, 'km', 'WITHDIST') 45 | 46 | expected = [['Palermo', %w[13.36138933897018433 38.11555639549629859]], 47 | ['Catania', %w[15.08726745843887329 37.50266842333162032]]] 48 | assert_equal expected, redis.georadius('Sicily', 15, 37, 200, 'km', 'WITHCOORD') 49 | 50 | expected = [['Palermo', '190.4424', %w[13.36138933897018433 38.11555639549629859]], 51 | ['Catania', '56.4413', %w[15.08726745843887329 37.50266842333162032]]] 52 | assert_equal expected, redis.georadius('Sicily', 15, 37, 200, 'km', 'WITHDIST', 'WITHCOORD') 53 | end 54 | 55 | def test_georadiusbymember 56 | redis.geoadd('Sicily', 13.583333, 37.316667, 'Agrigento') 57 | add_sicily 58 | assert_equal %w[Agrigento Palermo], redis.georadiusbymember('Sicily', 'Agrigento', 100, 'km') 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test/distributed/publish_subscribe_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestDistributedPublishSubscribe < Minitest::Test 6 | include Helper::Distributed 7 | 8 | def test_subscribe_and_unsubscribe 9 | assert_raises Redis::Distributed::CannotDistribute do 10 | r.subscribe("foo", "bar") {} 11 | end 12 | 13 | assert_raises Redis::Distributed::CannotDistribute do 14 | r.subscribe("{qux}foo", "bar") {} 15 | end 16 | end 17 | 18 | def test_subscribe_and_unsubscribe_with_tags 19 | @subscribed = false 20 | @unsubscribed = false 21 | 22 | thread = Thread.new do 23 | r.subscribe("foo") do |on| 24 | on.subscribe do |_channel, total| 25 | @subscribed = true 26 | @t1 = total 27 | end 28 | 29 | on.message do |_channel, message| 30 | if message == "s1" 31 | r.unsubscribe 32 | @message = message 33 | end 34 | end 35 | 36 | on.unsubscribe do |_channel, total| 37 | @unsubscribed = true 38 | @t2 = total 39 | end 40 | end 41 | end 42 | 43 | # Wait until the subscription is active before publishing 44 | Thread.pass until @subscribed 45 | 46 | Redis::Distributed.new(NODES).publish("foo", "s1") 47 | 48 | thread.join 49 | 50 | assert @subscribed 51 | assert_equal 1, @t1 52 | assert @unsubscribed 53 | assert_equal 0, @t2 54 | assert_equal "s1", @message 55 | end 56 | 57 | def test_subscribe_within_subscribe 58 | @channels = [] 59 | 60 | thread = Thread.new do 61 | r.subscribe("foo") do |on| 62 | on.subscribe do |channel, _total| 63 | @channels << channel 64 | 65 | r.subscribe("bar") if channel == "foo" 66 | r.unsubscribe if channel == "bar" 67 | end 68 | end 69 | end 70 | 71 | thread.join 72 | 73 | assert_equal ["foo", "bar"], @channels 74 | end 75 | 76 | def test_other_commands_within_a_subscribe 77 | r.subscribe("foo") do |on| 78 | on.subscribe do |_channel, _total| 79 | r.set("bar", "s2") 80 | r.unsubscribe("foo") 81 | end 82 | end 83 | end 84 | 85 | def test_subscribe_without_a_block 86 | assert_raises Redis::SubscriptionError do 87 | r.subscribe("foo") 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/redis/hash_ring.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'zlib' 4 | require 'digest/md5' 5 | 6 | class Redis 7 | class HashRing 8 | POINTS_PER_SERVER = 160 # this is the default in libmemcached 9 | 10 | attr_reader :ring, :sorted_keys, :replicas, :nodes 11 | 12 | # nodes is a list of objects that have a proper to_s representation. 13 | # replicas indicates how many virtual points should be used pr. node, 14 | # replicas are required to improve the distribution. 15 | def initialize(nodes = [], replicas = POINTS_PER_SERVER) 16 | @replicas = replicas 17 | @ring = {} 18 | @nodes = [] 19 | @sorted_keys = [] 20 | nodes.each do |node| 21 | add_node(node) 22 | end 23 | end 24 | 25 | # Adds a `node` to the hash ring (including a number of replicas). 26 | def add_node(node) 27 | @nodes << node 28 | @replicas.times do |i| 29 | key = server_hash_for("#{node.id}:#{i}") 30 | @ring[key] = node 31 | @sorted_keys << key 32 | end 33 | @sorted_keys.sort! 34 | end 35 | 36 | def remove_node(node) 37 | @nodes.reject! { |n| n.id == node.id } 38 | @replicas.times do |i| 39 | key = server_hash_for("#{node.id}:#{i}") 40 | @ring.delete(key) 41 | @sorted_keys.reject! { |k| k == key } 42 | end 43 | end 44 | 45 | # get the node in the hash ring for this key 46 | def get_node(key) 47 | hash = hash_for(key) 48 | idx = binary_search(@sorted_keys, hash) 49 | @ring[@sorted_keys[idx]] 50 | end 51 | 52 | def iter_nodes(key) 53 | return [nil, nil] if @ring.empty? 54 | 55 | crc = hash_for(key) 56 | pos = binary_search(@sorted_keys, crc) 57 | @ring.size.times do |n| 58 | yield @ring[@sorted_keys[(pos + n) % @ring.size]] 59 | end 60 | end 61 | 62 | private 63 | 64 | def hash_for(key) 65 | Zlib.crc32(key) 66 | end 67 | 68 | def server_hash_for(key) 69 | Digest::MD5.digest(key).unpack1("L>") 70 | end 71 | 72 | # Find the closest index in HashRing with value <= the given value 73 | def binary_search(ary, value) 74 | upper = ary.size 75 | lower = 0 76 | 77 | while lower < upper 78 | mid = (lower + upper) / 2 79 | if ary[mid] > value 80 | upper = mid 81 | else 82 | lower = mid + 1 83 | end 84 | end 85 | 86 | upper - 1 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /test/distributed/commands_on_sorted_sets_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestDistributedCommandsOnSortedSets < Minitest::Test 6 | include Helper::Distributed 7 | include Lint::SortedSets 8 | 9 | def test_zrangestore 10 | assert_raises(Redis::Distributed::CannotDistribute) { super } 11 | end 12 | 13 | def test_zinter 14 | assert_raises(Redis::Distributed::CannotDistribute) { super } 15 | end 16 | 17 | def test_zinter_with_aggregate 18 | assert_raises(Redis::Distributed::CannotDistribute) { super } 19 | end 20 | 21 | def test_zinter_with_weights 22 | assert_raises(Redis::Distributed::CannotDistribute) { super } 23 | end 24 | 25 | def test_zinterstore 26 | assert_raises(Redis::Distributed::CannotDistribute) { super } 27 | end 28 | 29 | def test_zinterstore_with_aggregate 30 | assert_raises(Redis::Distributed::CannotDistribute) { super } 31 | end 32 | 33 | def test_zinterstore_with_weights 34 | assert_raises(Redis::Distributed::CannotDistribute) { super } 35 | end 36 | 37 | def test_zlexcount 38 | # Not implemented yet 39 | end 40 | 41 | def test_zpopmax 42 | # Not implemented yet 43 | end 44 | 45 | def test_zpopmin 46 | # Not implemented yet 47 | end 48 | 49 | def test_zrangebylex 50 | # Not implemented yet 51 | end 52 | 53 | def test_zremrangebylex 54 | # Not implemented yet 55 | end 56 | 57 | def test_zrevrangebylex 58 | # Not implemented yet 59 | end 60 | 61 | def test_zscan 62 | # Not implemented yet 63 | end 64 | 65 | def test_zunion 66 | assert_raises(Redis::Distributed::CannotDistribute) { super } 67 | end 68 | 69 | def test_zunion_with_aggregate 70 | assert_raises(Redis::Distributed::CannotDistribute) { super } 71 | end 72 | 73 | def test_zunion_with_weights 74 | assert_raises(Redis::Distributed::CannotDistribute) { super } 75 | end 76 | 77 | def test_zunionstore 78 | assert_raises(Redis::Distributed::CannotDistribute) { super } 79 | end 80 | 81 | def test_zunionstore_with_aggregate 82 | assert_raises(Redis::Distributed::CannotDistribute) { super } 83 | end 84 | 85 | def test_zunionstore_with_weights 86 | assert_raises(Redis::Distributed::CannotDistribute) { super } 87 | end 88 | 89 | def test_zdiff 90 | assert_raises(Redis::Distributed::CannotDistribute) { super } 91 | end 92 | 93 | def test_zdiffstore 94 | assert_raises(Redis::Distributed::CannotDistribute) { super } 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /cluster/test/client_pipelining_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | # ruby -w -Itest test/cluster_client_pipelining_test.rb 6 | class TestClusterClientPipelining < Minitest::Test 7 | include Helper::Cluster 8 | 9 | def test_pipelining_with_a_hash_tag 10 | p1 = p2 = p3 = p4 = p5 = p6 = nil 11 | 12 | redis.pipelined do |r| 13 | r.set('{Presidents.of.USA}:1', 'George Washington') 14 | r.set('{Presidents.of.USA}:2', 'John Adams') 15 | r.set('{Presidents.of.USA}:3', 'Thomas Jefferson') 16 | r.set('{Presidents.of.USA}:4', 'James Madison') 17 | r.set('{Presidents.of.USA}:5', 'James Monroe') 18 | r.set('{Presidents.of.USA}:6', 'John Quincy Adams') 19 | 20 | p1 = r.get('{Presidents.of.USA}:1') 21 | p2 = r.get('{Presidents.of.USA}:2') 22 | p3 = r.get('{Presidents.of.USA}:3') 23 | p4 = r.get('{Presidents.of.USA}:4') 24 | p5 = r.get('{Presidents.of.USA}:5') 25 | p6 = r.get('{Presidents.of.USA}:6') 26 | end 27 | 28 | [p1, p2, p3, p4, p5, p6].each do |actual| 29 | assert_equal true, actual.is_a?(Redis::Future) 30 | end 31 | 32 | assert_equal('George Washington', p1.value) 33 | assert_equal('John Adams', p2.value) 34 | assert_equal('Thomas Jefferson', p3.value) 35 | assert_equal('James Madison', p4.value) 36 | assert_equal('James Monroe', p5.value) 37 | assert_equal('John Quincy Adams', p6.value) 38 | end 39 | 40 | def test_pipelining_without_hash_tags 41 | result = redis.pipelined do |pipeline| 42 | pipeline.set(:a, 1) 43 | pipeline.set(:b, 2) 44 | pipeline.set(:c, 3) 45 | pipeline.set(:d, 4) 46 | pipeline.set(:e, 5) 47 | pipeline.set(:f, 6) 48 | end 49 | assert_equal ["OK"] * 6, result 50 | 51 | result = redis.pipelined do |pipeline| 52 | pipeline.get(:a) 53 | pipeline.get(:b) 54 | pipeline.get(:c) 55 | pipeline.get(:d) 56 | pipeline.get(:e) 57 | pipeline.get(:f) 58 | end 59 | assert_equal 1.upto(6).map(&:to_s), result 60 | end 61 | 62 | def test_pipeline_unmapped_errors_are_bubbled_up 63 | ex = Class.new(StandardError) 64 | assert_raises(ex) do 65 | redis.pipelined do |_pipe| 66 | raise ex, "boom" 67 | end 68 | end 69 | end 70 | 71 | def test_pipeline_error_subclasses_are_mapped 72 | ex = Class.new(RedisClient::ConnectionError) 73 | assert_raises(Redis::ConnectionError) do 74 | redis.pipelined do |_pipe| 75 | raise ex, "tick tock" 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /test/distributed/commands_on_sets_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestDistributedCommandsOnSets < Minitest::Test 6 | include Helper::Distributed 7 | include Lint::Sets 8 | 9 | def test_smove 10 | assert_raises Redis::Distributed::CannotDistribute do 11 | r.sadd 'foo', 's1' 12 | r.sadd 'bar', 's2' 13 | 14 | r.smove('foo', 'bar', 's1') 15 | end 16 | end 17 | 18 | def test_sinter 19 | assert_raises Redis::Distributed::CannotDistribute do 20 | r.sadd 'foo', 's1' 21 | r.sadd 'foo', 's2' 22 | r.sadd 'bar', 's2' 23 | 24 | r.sinter('foo', 'bar') 25 | end 26 | end 27 | 28 | def test_sinterstore 29 | assert_raises Redis::Distributed::CannotDistribute do 30 | r.sadd 'foo', 's1' 31 | r.sadd 'foo', 's2' 32 | r.sadd 'bar', 's2' 33 | 34 | r.sinterstore('baz', 'foo', 'bar') 35 | end 36 | end 37 | 38 | def test_sunion 39 | assert_raises Redis::Distributed::CannotDistribute do 40 | r.sadd 'foo', 's1' 41 | r.sadd 'foo', 's2' 42 | r.sadd 'bar', 's2' 43 | r.sadd 'bar', 's3' 44 | 45 | r.sunion('foo', 'bar') 46 | end 47 | end 48 | 49 | def test_sunionstore 50 | assert_raises Redis::Distributed::CannotDistribute do 51 | r.sadd 'foo', 's1' 52 | r.sadd 'foo', 's2' 53 | r.sadd 'bar', 's2' 54 | r.sadd 'bar', 's3' 55 | 56 | r.sunionstore('baz', 'foo', 'bar') 57 | end 58 | end 59 | 60 | def test_sdiff 61 | assert_raises Redis::Distributed::CannotDistribute do 62 | r.sadd 'foo', 's1' 63 | r.sadd 'foo', 's2' 64 | r.sadd 'bar', 's2' 65 | r.sadd 'bar', 's3' 66 | 67 | r.sdiff('foo', 'bar') 68 | end 69 | end 70 | 71 | def test_sdiffstore 72 | assert_raises Redis::Distributed::CannotDistribute do 73 | r.sadd 'foo', 's1' 74 | r.sadd 'foo', 's2' 75 | r.sadd 'bar', 's2' 76 | r.sadd 'bar', 's3' 77 | 78 | r.sdiffstore('baz', 'foo', 'bar') 79 | end 80 | end 81 | 82 | def test_sscan 83 | r.sadd 'foo', 's1' 84 | r.sadd 'foo', 's2' 85 | r.sadd 'bar', 's2' 86 | r.sadd 'bar', 's3' 87 | 88 | cursor, vals = r.sscan 'foo', 0 89 | assert_equal '0', cursor 90 | assert_equal %w[s1 s2], vals.sort 91 | end 92 | 93 | def test_sscan_each 94 | r.sadd 'foo', 's1' 95 | r.sadd 'foo', 's2' 96 | r.sadd 'bar', 's2' 97 | r.sadd 'bar', 's3' 98 | 99 | vals = r.sscan_each('foo').to_a 100 | assert_equal %w[s1 s2], vals.sort 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /test/distributed/internals_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestDistributedInternals < Minitest::Test 6 | include Helper::Distributed 7 | 8 | def test_provides_a_meaningful_inspect 9 | nodes = ["redis://127.0.0.1:#{PORT}/15", *NODES] 10 | redis = Redis::Distributed.new nodes 11 | 12 | assert_equal "#", redis.inspect 13 | end 14 | 15 | def test_default_as_urls 16 | nodes = ["redis://127.0.0.1:#{PORT}/15", *NODES] 17 | redis = Redis::Distributed.new nodes 18 | assert_equal(["redis://127.0.0.1:#{PORT}/15", *NODES], redis.nodes.map { |node| node._client.server_url }) 19 | end 20 | 21 | def test_default_as_config_hashes 22 | nodes = [OPTIONS.merge(host: '127.0.0.1'), OPTIONS.merge(host: 'somehost', port: PORT.next)] 23 | redis = Redis::Distributed.new nodes 24 | assert_equal(["redis://127.0.0.1:#{PORT}/15", "redis://somehost:#{PORT.next}/15"], redis.nodes.map { |node| node._client.server_url }) 25 | end 26 | 27 | def test_as_mix_and_match 28 | nodes = ["redis://127.0.0.1:7389/15", OPTIONS.merge(host: 'somehost'), OPTIONS.merge(host: 'somehost', port: PORT.next)] 29 | redis = Redis::Distributed.new nodes 30 | 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 }) 31 | end 32 | 33 | def test_override_id 34 | nodes = [OPTIONS.merge(host: '127.0.0.1', id: "test"), OPTIONS.merge(host: 'somehost', port: PORT.next, id: "test1")] 35 | redis = Redis::Distributed.new nodes 36 | assert_equal redis.nodes.first._client.id, "test" 37 | assert_equal redis.nodes.last._client.id, "test1" 38 | assert_equal "#", redis.inspect 39 | end 40 | 41 | def test_can_be_duped_to_create_a_new_connection 42 | redis = Redis::Distributed.new(NODES) 43 | 44 | clients = redis.info[0]["connected_clients"].to_i 45 | 46 | r2 = redis.dup 47 | r2.ping 48 | 49 | assert_equal clients + 1, redis.info[0]["connected_clients"].to_i 50 | end 51 | 52 | def test_keeps_options_after_dup 53 | r1 = Redis::Distributed.new(NODES, tag: /^(\w+):/) 54 | 55 | assert_raises(Redis::Distributed::CannotDistribute) do 56 | r1.sinter("foo", "bar") 57 | end 58 | 59 | assert_equal [], r1.sinter("baz:foo", "baz:bar") 60 | 61 | r2 = r1.dup 62 | 63 | assert_raises(Redis::Distributed::CannotDistribute) do 64 | r2.sinter("foo", "bar") 65 | end 66 | 67 | assert_equal [], r2.sinter("baz:foo", "baz:bar") 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /test/distributed/scripting_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestDistributedScripting < Minitest::Test 6 | include Helper::Distributed 7 | 8 | def to_sha(script) 9 | r.script(:load, script).first 10 | end 11 | 12 | def test_script_exists 13 | a = to_sha("return 1") 14 | b = a.succ 15 | 16 | assert_equal [true], r.script(:exists, a) 17 | assert_equal [false], r.script(:exists, b) 18 | assert_equal [[true]], r.script(:exists, [a]) 19 | assert_equal [[false]], r.script(:exists, [b]) 20 | assert_equal [[true, false]], r.script(:exists, [a, b]) 21 | end 22 | 23 | def test_script_flush 24 | sha = to_sha("return 1") 25 | assert r.script(:exists, sha).first 26 | assert_equal ["OK"], r.script(:flush) 27 | assert !r.script(:exists, sha).first 28 | end 29 | 30 | def test_script_kill 31 | redis_mock(script: ->(arg) { "+#{arg.upcase}" }) do |redis| 32 | assert_equal ["KILL"], redis.script(:kill) 33 | end 34 | end 35 | 36 | def test_eval 37 | assert_raises(Redis::Distributed::CannotDistribute) do 38 | r.eval("return #KEYS") 39 | end 40 | 41 | assert_raises(Redis::Distributed::CannotDistribute) do 42 | r.eval("return KEYS", ["k1", "k2"]) 43 | end 44 | 45 | assert_equal ["k1"], r.eval("return KEYS", ["k1"]) 46 | assert_equal ["a1", "a2"], r.eval("return ARGV", ["k1"], ["a1", "a2"]) 47 | end 48 | 49 | def test_eval_with_options_hash 50 | assert_raises(Redis::Distributed::CannotDistribute) do 51 | r.eval("return #KEYS", {}) 52 | end 53 | 54 | assert_raises(Redis::Distributed::CannotDistribute) do 55 | r.eval("return KEYS", { keys: ["k1", "k2"] }) 56 | end 57 | 58 | assert_equal ["k1"], r.eval("return KEYS", { keys: ["k1"] }) 59 | assert_equal ["a1", "a2"], r.eval("return ARGV", { keys: ["k1"], argv: ["a1", "a2"] }) 60 | end 61 | 62 | def test_evalsha 63 | assert_raises(Redis::Distributed::CannotDistribute) do 64 | r.evalsha(to_sha("return #KEYS")) 65 | end 66 | 67 | assert_raises(Redis::Distributed::CannotDistribute) do 68 | r.evalsha(to_sha("return KEYS"), ["k1", "k2"]) 69 | end 70 | 71 | assert_equal ["k1"], r.evalsha(to_sha("return KEYS"), ["k1"]) 72 | assert_equal ["a1", "a2"], r.evalsha(to_sha("return ARGV"), ["k1"], ["a1", "a2"]) 73 | end 74 | 75 | def test_evalsha_with_options_hash 76 | assert_raises(Redis::Distributed::CannotDistribute) do 77 | r.evalsha(to_sha("return #KEYS"), {}) 78 | end 79 | 80 | assert_raises(Redis::Distributed::CannotDistribute) do 81 | r.evalsha(to_sha("return KEYS"), { keys: ["k1", "k2"] }) 82 | end 83 | 84 | assert_equal ["k1"], r.evalsha(to_sha("return KEYS"), { keys: ["k1"] }) 85 | assert_equal ["a1", "a2"], r.evalsha(to_sha("return ARGV"), { keys: ["k1"], argv: ["a1", "a2"] }) 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/redis/commands/pubsub.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Redis 4 | module Commands 5 | module Pubsub 6 | # Post a message to a channel. 7 | def publish(channel, message) 8 | send_command([:publish, channel, message]) 9 | end 10 | 11 | def subscribed? 12 | !@subscription_client.nil? 13 | end 14 | 15 | # Listen for messages published to the given channels. 16 | def subscribe(*channels, &block) 17 | _subscription(:subscribe, 0, channels, block) 18 | end 19 | 20 | # Listen for messages published to the given channels. Throw a timeout error 21 | # if there is no messages for a timeout period. 22 | def subscribe_with_timeout(timeout, *channels, &block) 23 | _subscription(:subscribe_with_timeout, timeout, channels, block) 24 | end 25 | 26 | # Stop listening for messages posted to the given channels. 27 | def unsubscribe(*channels) 28 | _subscription(:unsubscribe, 0, channels, nil) 29 | end 30 | 31 | # Listen for messages published to channels matching the given patterns. 32 | def psubscribe(*channels, &block) 33 | _subscription(:psubscribe, 0, channels, block) 34 | end 35 | 36 | # Listen for messages published to channels matching the given patterns. 37 | # Throw a timeout error if there is no messages for a timeout period. 38 | def psubscribe_with_timeout(timeout, *channels, &block) 39 | _subscription(:psubscribe_with_timeout, timeout, channels, block) 40 | end 41 | 42 | # Stop listening for messages posted to channels matching the given patterns. 43 | def punsubscribe(*channels) 44 | _subscription(:punsubscribe, 0, channels, nil) 45 | end 46 | 47 | # Inspect the state of the Pub/Sub subsystem. 48 | # Possible subcommands: channels, numsub, numpat. 49 | def pubsub(subcommand, *args) 50 | send_command([:pubsub, subcommand] + args) 51 | end 52 | 53 | # Post a message to a channel in a shard. 54 | def spublish(channel, message) 55 | send_command([:spublish, channel, message]) 56 | end 57 | 58 | # Listen for messages published to the given channels in a shard. 59 | def ssubscribe(*channels, &block) 60 | _subscription(:ssubscribe, 0, channels, block) 61 | end 62 | 63 | # Listen for messages published to the given channels in a shard. 64 | # Throw a timeout error if there is no messages for a timeout period. 65 | def ssubscribe_with_timeout(timeout, *channels, &block) 66 | _subscription(:ssubscribe_with_timeout, timeout, channels, block) 67 | end 68 | 69 | # Stop listening for messages posted to the given channels in a shard. 70 | def sunsubscribe(*channels) 71 | _subscription(:sunsubscribe, 0, channels, nil) 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /test/redis/connection_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestConnection < Minitest::Test 6 | include Helper::Client 7 | 8 | def test_provides_a_meaningful_inspect 9 | assert_equal "#", r.inspect 10 | end 11 | 12 | def test_connection_with_user_and_password 13 | target_version "6.0" do 14 | with_acl do |username, password| 15 | redis = Redis.new(OPTIONS.merge(username: username, password: password)) 16 | assert_equal "PONG", redis.ping 17 | end 18 | end 19 | end 20 | 21 | def test_connection_with_default_user_and_password 22 | target_version "6.0" do 23 | with_default_user_password do |_username, password| 24 | redis = Redis.new(OPTIONS.merge(password: password)) 25 | assert_equal "PONG", redis.ping 26 | end 27 | end 28 | end 29 | 30 | def test_connection_information 31 | assert_equal "localhost", r.connection.fetch(:host) 32 | assert_equal 6381, r.connection.fetch(:port) 33 | assert_equal 15, r.connection.fetch(:db) 34 | assert_equal "localhost:6381", r.connection.fetch(:location) 35 | assert_equal "redis://localhost:6381/15", r.connection.fetch(:id) 36 | end 37 | 38 | def test_default_id_with_host_and_port 39 | redis = Redis.new(OPTIONS.merge(host: "host", port: "1234", db: 0)) 40 | assert_equal "redis://host:1234/0", redis.connection.fetch(:id) 41 | end 42 | 43 | def test_default_id_with_host_and_port_and_ssl 44 | redis = Redis.new(OPTIONS.merge(host: 'host', port: '1234', db: 0, ssl: true)) 45 | assert_equal "rediss://host:1234/0", redis.connection.fetch(:id) 46 | end 47 | 48 | def test_default_id_with_host_and_port_and_explicit_scheme 49 | redis = Redis.new(OPTIONS.merge(host: "host", port: "1234", db: 0)) 50 | assert_equal "redis://host:1234/0", redis.connection.fetch(:id) 51 | end 52 | 53 | def test_default_id_with_path 54 | redis = Redis.new(OPTIONS.merge(path: "/tmp/redis.sock", db: 0)) 55 | assert_equal "/tmp/redis.sock/0", redis.connection.fetch(:id) 56 | end 57 | 58 | def test_default_id_with_path_and_explicit_scheme 59 | redis = Redis.new(OPTIONS.merge(path: "/tmp/redis.sock", db: 0)) 60 | assert_equal "/tmp/redis.sock/0", redis.connection.fetch(:id) 61 | end 62 | 63 | def test_override_id 64 | redis = Redis.new(OPTIONS.merge(id: "test")) 65 | assert_equal "test", redis.connection.fetch(:id) 66 | end 67 | 68 | def test_id_inside_multi 69 | redis = Redis.new(OPTIONS) 70 | id = nil 71 | connection_id = nil 72 | 73 | redis.multi do 74 | id = redis.id 75 | connection_id = redis.connection.fetch(:id) 76 | end 77 | 78 | assert_equal "redis://localhost:6381/15", id 79 | assert_equal "redis://localhost:6381/15", connection_id 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/redis/commands/bitmaps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Redis 4 | module Commands 5 | module Bitmaps 6 | # Sets or clears the bit at offset in the string value stored at key. 7 | # 8 | # @param [String] key 9 | # @param [Integer] offset bit offset 10 | # @param [Integer] value bit value `0` or `1` 11 | # @return [Integer] the original bit value stored at `offset` 12 | def setbit(key, offset, value) 13 | send_command([:setbit, key, offset, value]) 14 | end 15 | 16 | # Returns the bit value at offset in the string value stored at key. 17 | # 18 | # @param [String] key 19 | # @param [Integer] offset bit offset 20 | # @return [Integer] `0` or `1` 21 | def getbit(key, offset) 22 | send_command([:getbit, key, offset]) 23 | end 24 | 25 | # Count the number of set bits in a range of the string value stored at key. 26 | # 27 | # @param [String] key 28 | # @param [Integer] start start index 29 | # @param [Integer] stop stop index 30 | # @param [String, Symbol] scale the scale of the offset range 31 | # e.g. 'BYTE' - interpreted as a range of bytes, 'BIT' - interpreted as a range of bits 32 | # @return [Integer] the number of bits set to 1 33 | def bitcount(key, start = 0, stop = -1, scale: nil) 34 | command = [:bitcount, key, start, stop] 35 | command << scale if scale 36 | send_command(command) 37 | end 38 | 39 | # Perform a bitwise operation between strings and store the resulting string in a key. 40 | # 41 | # @param [String] operation e.g. `and`, `or`, `xor`, `not` 42 | # @param [String] destkey destination key 43 | # @param [String, Array] keys one or more source keys to perform `operation` 44 | # @return [Integer] the length of the string stored in `destkey` 45 | def bitop(operation, destkey, *keys) 46 | keys.flatten!(1) 47 | command = [:bitop, operation, destkey] 48 | command.concat(keys) 49 | send_command(command) 50 | end 51 | 52 | # Return the position of the first bit set to 1 or 0 in a string. 53 | # 54 | # @param [String] key 55 | # @param [Integer] bit whether to look for the first 1 or 0 bit 56 | # @param [Integer] start start index 57 | # @param [Integer] stop stop index 58 | # @param [String, Symbol] scale the scale of the offset range 59 | # e.g. 'BYTE' - interpreted as a range of bytes, 'BIT' - interpreted as a range of bits 60 | # @return [Integer] the position of the first 1/0 bit. 61 | # -1 if looking for 1 and it is not found or start and stop are given. 62 | def bitpos(key, bit, start = nil, stop = nil, scale: nil) 63 | raise(ArgumentError, 'stop parameter specified without start parameter') if stop && !start 64 | 65 | command = [:bitpos, key, bit] 66 | command << start if start 67 | command << stop if stop 68 | command << scale if scale 69 | send_command(command) 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /cluster/README.md: -------------------------------------------------------------------------------- 1 | # Redis::Cluster 2 | 3 | ## Getting started 4 | 5 | Install with: 6 | 7 | ``` 8 | $ gem install redis-clustering 9 | ``` 10 | 11 | You can connect to Redis by instantiating the `Redis::Cluster` class: 12 | 13 | ```ruby 14 | require "redis-clustering" 15 | 16 | redis = Redis::Cluster.new(nodes: (7000..7005).map { |port| "redis://127.0.0.1:#{port}" }) 17 | ``` 18 | 19 | NB: Both `redis_cluster` and `redis-cluster` are unrelated and abandoned gems. 20 | 21 | ```ruby 22 | # Nodes can be passed to the client as an array of connection URLs. 23 | nodes = (7000..7005).map { |port| "redis://127.0.0.1:#{port}" } 24 | redis = Redis::Cluster.new(nodes: nodes) 25 | 26 | # You can also specify the options as a Hash. The options are the same as for a single server connection. 27 | (7000..7005).map { |port| { host: '127.0.0.1', port: port } } 28 | ``` 29 | 30 | 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. 31 | 32 | ```ruby 33 | Redis::Cluster.new(nodes: %w[redis://127.0.0.1:7000]) 34 | ``` 35 | 36 | 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. 37 | 38 | ```ruby 39 | Redis::Cluster.new(nodes: nodes, replica: true) 40 | ``` 41 | 42 | Also, you can specify the `:replica_affinity` option if you want to prevent accessing cross availability zones. 43 | 44 | ```ruby 45 | Redis::Cluster.new(nodes: nodes, replica: true, replica_affinity: :latency) 46 | ``` 47 | 48 | The calling code is responsible for [avoiding cross slot commands](https://redis.io/topics/cluster-spec#keys-distribution-model). 49 | 50 | ```ruby 51 | redis = Redis::Cluster.new(nodes: %w[redis://127.0.0.1:7000]) 52 | 53 | redis.mget('key1', 'key2') 54 | #=> Redis::CommandError (CROSSSLOT Keys in request don't hash to the same slot) 55 | 56 | redis.mget('{key}1', '{key}2') 57 | #=> [nil, nil] 58 | ``` 59 | 60 | * The client automatically reconnects after a failover occurred, but the caller is responsible for handling errors while it is happening. 61 | * The client support permanent node failures, and will reroute requests to promoted slaves. 62 | * The client supports `MOVED` and `ASK` redirections transparently. 63 | 64 | ## Cluster mode with SSL/TLS 65 | 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: 66 | 67 | ```ruby 68 | Redis::Cluster.new(nodes: %w[rediss://foo.example.com:6379]) 69 | ``` 70 | 71 | 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. 72 | 73 | ```ruby 74 | Redis::Cluster.new(nodes: %w[rediss://foo-endpoint.example.com:6379], fixed_hostname: 'foo-endpoint.example.com') 75 | ``` 76 | 77 | 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. 78 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | AllCops: 4 | TargetRubyVersion: 2.5 5 | 6 | Layout/LineLength: 7 | Max: 120 8 | Exclude: 9 | - 'test/**/*' 10 | 11 | Layout/CaseIndentation: 12 | EnforcedStyle: end 13 | 14 | Lint/RescueException: 15 | Enabled: false 16 | 17 | Lint/SuppressedException: 18 | Enabled: false 19 | 20 | Lint/AssignmentInCondition: 21 | Enabled: false 22 | 23 | Lint/UnifiedInteger: 24 | Enabled: false 25 | 26 | Lint/UnderscorePrefixedVariableName: 27 | Enabled: false 28 | 29 | Lint/MissingSuper: 30 | Enabled: false 31 | 32 | Metrics/ClassLength: 33 | Enabled: false 34 | 35 | Metrics/CyclomaticComplexity: 36 | Enabled: false 37 | 38 | Metrics/AbcSize: 39 | Enabled: false 40 | 41 | Metrics/BlockLength: 42 | Enabled: false 43 | 44 | Metrics/MethodLength: 45 | Enabled: false 46 | 47 | Metrics/ModuleLength: 48 | Enabled: false 49 | 50 | Metrics/ParameterLists: 51 | Enabled: false 52 | 53 | Metrics/PerceivedComplexity: 54 | Enabled: false 55 | 56 | Style/PercentLiteralDelimiters: 57 | Enabled: false 58 | 59 | Style/TrailingCommaInArrayLiteral: 60 | Enabled: false 61 | 62 | Style/TrailingCommaInArguments: 63 | Enabled: false 64 | 65 | Style/ParallelAssignment: 66 | Enabled: false 67 | 68 | Style/NumericPredicate: 69 | Enabled: false 70 | 71 | Style/IfUnlessModifier: 72 | Enabled: false 73 | 74 | Style/MutableConstant: 75 | Enabled: false # false positives 76 | 77 | Style/SignalException: 78 | Exclude: 79 | - 'lib/redis/connection/synchrony.rb' 80 | 81 | Style/StringLiterals: 82 | Enabled: false 83 | 84 | Style/DoubleNegation: 85 | Enabled: false 86 | 87 | Style/MultipleComparison: 88 | Enabled: false 89 | 90 | Style/GuardClause: 91 | Enabled: false 92 | 93 | Style/Semicolon: 94 | Enabled: false 95 | 96 | Style/Documentation: 97 | Enabled: false 98 | 99 | Style/FormatStringToken: 100 | Enabled: false 101 | 102 | Style/FormatString: 103 | Enabled: false 104 | 105 | Style/RescueStandardError: 106 | Enabled: false 107 | 108 | Style/WordArray: 109 | Enabled: false 110 | 111 | Lint/NonLocalExitFromIterator: 112 | Enabled: false 113 | 114 | Layout/EndAlignment: 115 | EnforcedStyleAlignWith: variable 116 | 117 | Layout/ElseAlignment: 118 | Enabled: false 119 | 120 | Layout/RescueEnsureAlignment: 121 | Enabled: false 122 | 123 | Naming/HeredocDelimiterNaming: 124 | Enabled: false 125 | 126 | Naming/VariableNumber: 127 | Enabled: false 128 | 129 | Naming/FileName: 130 | Enabled: false 131 | 132 | Naming/RescuedExceptionsVariableName: 133 | Enabled: false 134 | 135 | Naming/AccessorMethodName: 136 | Exclude: 137 | - lib/redis/connection/ruby.rb 138 | 139 | Naming/MethodParameterName: 140 | Enabled: false 141 | 142 | Metrics/BlockNesting: 143 | Enabled: false 144 | 145 | Style/HashTransformValues: 146 | Enabled: false 147 | 148 | Style/TrailingCommaInHashLiteral: 149 | Enabled: false 150 | 151 | Style/SymbolProc: 152 | Exclude: 153 | - 'test/**/*' 154 | 155 | Bundler/OrderedGems: 156 | Enabled: false 157 | 158 | Gemspec/RequiredRubyVersion: 159 | Exclude: 160 | - cluster/redis-clustering.gemspec 161 | -------------------------------------------------------------------------------- /test/distributed/commands_on_value_types_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestDistributedCommandsOnValueTypes < Minitest::Test 6 | include Helper::Distributed 7 | include Lint::ValueTypes 8 | 9 | def test_del 10 | r.set "foo", "s1" 11 | r.set "bar", "s2" 12 | r.set "baz", "s3" 13 | 14 | assert_equal ["bar", "baz", "foo"], r.keys("*").sort 15 | 16 | assert_equal 1, r.del("foo") 17 | 18 | assert_equal ["bar", "baz"], r.keys("*").sort 19 | 20 | assert_equal 2, r.del("bar", "baz") 21 | 22 | assert_equal [], r.keys("*").sort 23 | end 24 | 25 | def test_del_with_array_argument 26 | r.set "foo", "s1" 27 | r.set "bar", "s2" 28 | r.set "baz", "s3" 29 | 30 | assert_equal ["bar", "baz", "foo"], r.keys("*").sort 31 | 32 | assert_equal 1, r.del(["foo"]) 33 | 34 | assert_equal ["bar", "baz"], r.keys("*").sort 35 | 36 | assert_equal 2, r.del(["bar", "baz"]) 37 | 38 | assert_equal [], r.keys("*").sort 39 | end 40 | 41 | def test_unlink 42 | r.set "foo", "s1" 43 | r.set "bar", "s2" 44 | r.set "baz", "s3" 45 | 46 | assert_equal ["bar", "baz", "foo"], r.keys("*").sort 47 | 48 | assert_equal 1, r.unlink("foo") 49 | 50 | assert_equal ["bar", "baz"], r.keys("*").sort 51 | 52 | assert_equal 2, r.unlink("bar", "baz") 53 | 54 | assert_equal [], r.keys("*").sort 55 | end 56 | 57 | def test_unlink_with_array_argument 58 | r.set "foo", "s1" 59 | r.set "bar", "s2" 60 | r.set "baz", "s3" 61 | 62 | assert_equal ["bar", "baz", "foo"], r.keys("*").sort 63 | 64 | assert_equal 1, r.unlink(["foo"]) 65 | 66 | assert_equal ["bar", "baz"], r.keys("*").sort 67 | 68 | assert_equal 2, r.unlink(["bar", "baz"]) 69 | 70 | assert_equal [], r.keys("*").sort 71 | end 72 | 73 | def test_randomkey 74 | assert_raises Redis::Distributed::CannotDistribute do 75 | r.randomkey 76 | end 77 | end 78 | 79 | def test_rename 80 | assert_raises Redis::Distributed::CannotDistribute do 81 | r.set("foo", "s1") 82 | r.rename "foo", "bar" 83 | end 84 | 85 | assert_equal "s1", r.get("foo") 86 | assert_nil r.get("bar") 87 | end 88 | 89 | def test_renamenx 90 | assert_raises Redis::Distributed::CannotDistribute do 91 | r.set("foo", "s1") 92 | r.rename "foo", "bar" 93 | end 94 | 95 | assert_equal "s1", r.get("foo") 96 | assert_nil r.get("bar") 97 | end 98 | 99 | def test_dbsize 100 | assert_equal [0], r.dbsize 101 | 102 | r.set("foo", "s1") 103 | 104 | assert_equal [1], r.dbsize 105 | end 106 | 107 | def test_flushdb 108 | r.set("foo", "s1") 109 | r.set("bar", "s2") 110 | 111 | assert_equal [2], r.dbsize 112 | 113 | r.flushdb 114 | 115 | assert_equal [0], r.dbsize 116 | end 117 | 118 | def test_migrate 119 | r.set("foo", "s1") 120 | 121 | assert_raises Redis::Distributed::CannotDistribute do 122 | r.migrate("foo", {}) 123 | end 124 | end 125 | 126 | def test_copy 127 | r.set("foo", "s1") 128 | 129 | assert_raises Redis::Distributed::CannotDistribute do 130 | r.copy("foo", "bar") 131 | end 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /lib/redis/pipeline.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "delegate" 4 | 5 | class Redis 6 | class PipelinedConnection 7 | attr_accessor :db 8 | 9 | def initialize(pipeline, futures = []) 10 | @pipeline = pipeline 11 | @futures = futures 12 | end 13 | 14 | include Commands 15 | 16 | def pipelined 17 | yield self 18 | end 19 | 20 | def multi 21 | transaction = MultiConnection.new(@pipeline, @futures) 22 | send_command([:multi]) 23 | size = @futures.size 24 | yield transaction 25 | multi_future = MultiFuture.new(@futures[size..-1]) 26 | @pipeline.call_v([:exec]) do |result| 27 | multi_future._set(result) 28 | end 29 | @futures << multi_future 30 | multi_future 31 | end 32 | 33 | private 34 | 35 | def synchronize 36 | yield self 37 | end 38 | 39 | def send_command(command, &block) 40 | future = Future.new(command, block) 41 | @pipeline.call_v(command) do |result| 42 | future._set(result) 43 | end 44 | @futures << future 45 | future 46 | end 47 | 48 | def send_blocking_command(command, timeout, &block) 49 | future = Future.new(command, block) 50 | @pipeline.blocking_call_v(timeout, command) do |result| 51 | future._set(result) 52 | end 53 | @futures << future 54 | future 55 | end 56 | end 57 | 58 | class MultiConnection < PipelinedConnection 59 | def multi 60 | raise Redis::Error, "Can't nest multi transaction" 61 | end 62 | 63 | private 64 | 65 | # Blocking commands inside transaction behave like non-blocking. 66 | # It shouldn't be done though. 67 | # https://redis.io/commands/blpop/#blpop-inside-a-multi--exec-transaction 68 | def send_blocking_command(command, _timeout, &block) 69 | send_command(command, &block) 70 | end 71 | end 72 | 73 | class FutureNotReady < RuntimeError 74 | def initialize 75 | super("Value will be available once the pipeline executes.") 76 | end 77 | end 78 | 79 | class Future < BasicObject 80 | FutureNotReady = ::Redis::FutureNotReady.new 81 | 82 | def initialize(command, coerce) 83 | @command = command 84 | @object = FutureNotReady 85 | @coerce = coerce 86 | end 87 | 88 | def inspect 89 | "" 90 | end 91 | 92 | def _set(object) 93 | @object = @coerce ? @coerce.call(object) : object 94 | value 95 | end 96 | 97 | def value 98 | ::Kernel.raise(@object) if @object.is_a?(::StandardError) 99 | @object 100 | end 101 | 102 | def is_a?(other) 103 | self.class.ancestors.include?(other) 104 | end 105 | 106 | def class 107 | Future 108 | end 109 | end 110 | 111 | class MultiFuture < Future 112 | def initialize(futures) 113 | @futures = futures 114 | @command = [:exec] 115 | @object = FutureNotReady 116 | end 117 | 118 | def _set(replies) 119 | if replies 120 | @futures.each_with_index do |future, index| 121 | future._set(replies[index]) 122 | end 123 | end 124 | @object = replies 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /cluster/test/commands_on_pub_sub_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | # ruby -w -Itest test/cluster_commands_on_pub_sub_test.rb 6 | # @see https://redis.io/commands#pubsub 7 | class TestClusterCommandsOnPubSub < Minitest::Test 8 | include Helper::Cluster 9 | 10 | def test_publish_subscribe_unsubscribe_pubsub 11 | sub_cnt = 0 12 | messages = {} 13 | 14 | thread = Thread.new do 15 | redis.subscribe('channel1', 'channel2') do |on| 16 | on.subscribe { sub_cnt += 1 } 17 | on.message do |c, msg| 18 | messages[c] = msg 19 | redis.unsubscribe if messages.size == 2 20 | end 21 | end 22 | end 23 | 24 | Thread.pass until sub_cnt == 2 25 | 26 | publisher = build_another_client 27 | 28 | assert_equal %w[channel1 channel2], publisher.pubsub(:channels, 'channel*') 29 | assert_equal({ 'channel1' => 1, 'channel2' => 1, 'channel3' => 0 }, 30 | publisher.pubsub(:numsub, 'channel1', 'channel2', 'channel3')) 31 | 32 | publisher.publish('channel1', 'one') 33 | publisher.publish('channel2', 'two') 34 | publisher.publish('channel3', 'three') 35 | 36 | thread.join 37 | 38 | assert_equal(2, messages.size) 39 | assert_equal('one', messages['channel1']) 40 | assert_equal('two', messages['channel2']) 41 | end 42 | 43 | def test_publish_psubscribe_punsubscribe_pubsub 44 | sub_cnt = 0 45 | messages = {} 46 | 47 | thread = Thread.new do 48 | redis.psubscribe('guc*', 'her*') do |on| 49 | on.psubscribe { sub_cnt += 1 } 50 | on.pmessage do |_ptn, c, msg| 51 | messages[c] = msg 52 | redis.punsubscribe if messages.size == 2 53 | end 54 | end 55 | end 56 | 57 | Thread.pass until sub_cnt == 2 58 | 59 | publisher = build_another_client 60 | 61 | assert_equal 2, publisher.pubsub(:numpat) 62 | 63 | publisher.publish('burberry1', 'one') 64 | publisher.publish('gucci2', 'two') 65 | publisher.publish('hermes3', 'three') 66 | 67 | thread.join 68 | 69 | assert_equal(2, messages.size) 70 | assert_equal('two', messages['gucci2']) 71 | assert_equal('three', messages['hermes3']) 72 | end 73 | 74 | def test_spublish_ssubscribe_sunsubscribe_pubsub 75 | omit_version('7.0.0') 76 | 77 | sub_cnt = 0 78 | messages = {} 79 | 80 | thread = Thread.new do 81 | redis.ssubscribe('channel1', 'channel2') do |on| 82 | on.ssubscribe { sub_cnt += 1 } 83 | on.smessage do |c, msg| 84 | messages[c] = msg 85 | redis.sunsubscribe if messages.size == 2 86 | end 87 | end 88 | end 89 | 90 | Thread.pass until sub_cnt == 2 91 | 92 | publisher = build_another_client 93 | 94 | assert_equal %w[channel1 channel2], publisher.pubsub(:shardchannels, 'channel*') 95 | assert_equal({ 'channel1' => 1, 'channel2' => 1, 'channel3' => 0 }, 96 | publisher.pubsub(:shardnumsub, 'channel1', 'channel2', 'channel3')) 97 | 98 | publisher.spublish('channel1', 'one') 99 | publisher.spublish('channel2', 'two') 100 | publisher.spublish('channel3', 'three') 101 | 102 | thread.join 103 | 104 | assert_equal(2, messages.size) 105 | assert_equal('one', messages['channel1']) 106 | assert_equal('two', messages['channel2']) 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /test/redis/url_param_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestUrlParam < Minitest::Test 6 | include Helper::Client 7 | 8 | def test_url_defaults_to_localhost 9 | redis = Redis.new 10 | 11 | assert_equal "localhost", redis._client.host 12 | assert_equal 6379, redis._client.port 13 | assert_equal 0, redis._client.db 14 | assert_nil redis._client.password 15 | end 16 | 17 | def test_allows_to_pass_in_a_url 18 | redis = Redis.new url: "redis://:secr3t@foo.com:999/2" 19 | 20 | assert_equal "foo.com", redis._client.host 21 | assert_equal 999, redis._client.port 22 | assert_equal 2, redis._client.db 23 | assert_equal "secr3t", redis._client.password 24 | end 25 | 26 | def test_unescape_password_from_url 27 | redis = Redis.new url: "redis://:secr3t%3A@foo.com:999/2" 28 | 29 | assert_equal "secr3t:", redis._client.password 30 | end 31 | 32 | def test_does_not_unescape_password_when_explicitly_passed 33 | redis = Redis.new url: "redis://:secr3t%3A@foo.com:999/2", password: "secr3t%3A" 34 | 35 | assert_equal "secr3t%3A", redis._client.password 36 | end 37 | 38 | def test_override_url_if_path_option_is_passed 39 | redis = Redis.new url: "redis://:secr3t@foo.com/2", path: "/tmp/redis.sock" 40 | 41 | assert_equal "/tmp/redis.sock", redis._client.path 42 | assert_nil redis._client.host 43 | assert_nil redis._client.port 44 | end 45 | 46 | def test_overrides_url_if_another_connection_option_is_passed 47 | redis = Redis.new url: "redis://:secr3t@foo.com:999/2", port: 1000 48 | 49 | assert_equal "foo.com", redis._client.host 50 | assert_equal 1000, redis._client.port 51 | assert_equal 2, redis._client.db 52 | assert_equal "secr3t", redis._client.password 53 | end 54 | 55 | def test_does_not_overrides_url_if_a_nil_option_is_passed 56 | redis = Redis.new url: "redis://:secr3t@foo.com:999/2", port: nil 57 | 58 | assert_equal "foo.com", redis._client.host 59 | assert_equal 999, redis._client.port 60 | assert_equal 2, redis._client.db 61 | assert_equal "secr3t", redis._client.password 62 | end 63 | 64 | def test_does_not_modify_the_passed_options 65 | options = { url: "redis://:secr3t@foo.com:999/2" } 66 | 67 | Redis.new(options) 68 | 69 | assert(options == { url: "redis://:secr3t@foo.com:999/2" }) 70 | end 71 | 72 | def test_uses_redis_url_over_default_if_available 73 | ENV["REDIS_URL"] = "redis://:secr3t@foo.com:999/2" 74 | 75 | redis = Redis.new 76 | 77 | assert_equal "foo.com", redis._client.host 78 | assert_equal 999, redis._client.port 79 | assert_equal 2, redis._client.db 80 | assert_equal "secr3t", redis._client.password 81 | 82 | ENV.delete("REDIS_URL") 83 | end 84 | 85 | def test_defaults_to_localhost 86 | redis = Redis.new(url: "redis://") 87 | 88 | assert_equal "localhost", redis._client.host 89 | end 90 | 91 | def test_ipv6_url 92 | redis = Redis.new url: "redis://[::1]" 93 | 94 | assert_equal "::1", redis._client.host 95 | end 96 | 97 | def test_user_and_password 98 | redis = Redis.new(url: 'redis://johndoe:mysecret@foo.com:999/2') 99 | 100 | assert_equal('johndoe', redis._client.username) 101 | assert_equal('mysecret', redis._client.password) 102 | assert_equal('foo.com', redis._client.host) 103 | assert_equal(999, redis._client.port) 104 | assert_equal(2, redis._client.db) 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/redis/subscribe.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Redis 4 | class SubscribedClient 5 | def initialize(client) 6 | @client = client 7 | @write_monitor = Monitor.new 8 | end 9 | 10 | def call_v(command) 11 | @write_monitor.synchronize do 12 | @client.call_v(command) 13 | end 14 | end 15 | 16 | def subscribe(*channels, &block) 17 | subscription("subscribe", "unsubscribe", channels, block) 18 | end 19 | 20 | def subscribe_with_timeout(timeout, *channels, &block) 21 | subscription("subscribe", "unsubscribe", channels, block, timeout) 22 | end 23 | 24 | def psubscribe(*channels, &block) 25 | subscription("psubscribe", "punsubscribe", channels, block) 26 | end 27 | 28 | def psubscribe_with_timeout(timeout, *channels, &block) 29 | subscription("psubscribe", "punsubscribe", channels, block, timeout) 30 | end 31 | 32 | def ssubscribe(*channels, &block) 33 | subscription("ssubscribe", "sunsubscribe", channels, block) 34 | end 35 | 36 | def ssubscribe_with_timeout(timeout, *channels, &block) 37 | subscription("ssubscribe", "sunsubscribe", channels, block, timeout) 38 | end 39 | 40 | def unsubscribe(*channels) 41 | call_v([:unsubscribe, *channels]) 42 | end 43 | 44 | def punsubscribe(*channels) 45 | call_v([:punsubscribe, *channels]) 46 | end 47 | 48 | def sunsubscribe(*channels) 49 | call_v([:sunsubscribe, *channels]) 50 | end 51 | 52 | def close 53 | @client.close 54 | end 55 | 56 | protected 57 | 58 | def subscription(start, stop, channels, block, timeout = 0) 59 | sub = Subscription.new(&block) 60 | 61 | case start 62 | when "ssubscribe" then channels.each { |c| call_v([start, c]) } # avoid cross-slot keys 63 | else call_v([start, *channels]) 64 | end 65 | 66 | while event = @client.next_event(timeout) 67 | if event.is_a?(::RedisClient::CommandError) 68 | raise Client::ERROR_MAPPING.fetch(event.class), event.message 69 | end 70 | 71 | type, *rest = event 72 | if callback = sub.callbacks[type] 73 | callback.call(*rest) 74 | end 75 | break if type == stop && rest.last == 0 76 | end 77 | # No need to unsubscribe here. The real client closes the connection 78 | # whenever an exception is raised (see #ensure_connected). 79 | end 80 | end 81 | 82 | class Subscription 83 | attr :callbacks 84 | 85 | def initialize 86 | @callbacks = {} 87 | yield(self) 88 | end 89 | 90 | def subscribe(&block) 91 | @callbacks["subscribe"] = block 92 | end 93 | 94 | def unsubscribe(&block) 95 | @callbacks["unsubscribe"] = block 96 | end 97 | 98 | def message(&block) 99 | @callbacks["message"] = block 100 | end 101 | 102 | def psubscribe(&block) 103 | @callbacks["psubscribe"] = block 104 | end 105 | 106 | def punsubscribe(&block) 107 | @callbacks["punsubscribe"] = block 108 | end 109 | 110 | def pmessage(&block) 111 | @callbacks["pmessage"] = block 112 | end 113 | 114 | def ssubscribe(&block) 115 | @callbacks["ssubscribe"] = block 116 | end 117 | 118 | def sunsubscribe(&block) 119 | @callbacks["sunsubscribe"] = block 120 | end 121 | 122 | def smessage(&block) 123 | @callbacks["smessage"] = block 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lib/redis/commands/transactions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Redis 4 | module Commands 5 | module Transactions 6 | # Mark the start of a transaction block. 7 | # 8 | # @example With a block 9 | # redis.multi do |multi| 10 | # multi.set("key", "value") 11 | # multi.incr("counter") 12 | # end # => ["OK", 6] 13 | # 14 | # @yield [multi] the commands that are called inside this block are cached 15 | # and written to the server upon returning from it 16 | # @yieldparam [Redis] multi `self` 17 | # 18 | # @return [Array<...>] 19 | # - an array with replies 20 | # 21 | # @see #watch 22 | # @see #unwatch 23 | def multi 24 | synchronize do |client| 25 | client.multi do |raw_transaction| 26 | yield MultiConnection.new(raw_transaction) 27 | end 28 | end 29 | end 30 | 31 | # Watch the given keys to determine execution of the MULTI/EXEC block. 32 | # 33 | # Using a block is optional, but is necessary for thread-safety. 34 | # 35 | # An `#unwatch` is automatically issued if an exception is raised within the 36 | # block that is a subclass of StandardError and is not a ConnectionError. 37 | # 38 | # @example With a block 39 | # redis.watch("key") do 40 | # if redis.get("key") == "some value" 41 | # redis.multi do |multi| 42 | # multi.set("key", "other value") 43 | # multi.incr("counter") 44 | # end 45 | # else 46 | # redis.unwatch 47 | # end 48 | # end 49 | # # => ["OK", 6] 50 | # 51 | # @example Without a block 52 | # redis.watch("key") 53 | # # => "OK" 54 | # 55 | # @param [String, Array] keys one or more keys to watch 56 | # @return [Object] if using a block, returns the return value of the block 57 | # @return [String] if not using a block, returns `OK` 58 | # 59 | # @see #unwatch 60 | # @see #multi 61 | def watch(*keys) 62 | synchronize do |client| 63 | res = client.call_v([:watch] + keys) 64 | 65 | if block_given? 66 | begin 67 | yield(self) 68 | rescue ConnectionError 69 | raise 70 | rescue StandardError 71 | unwatch 72 | raise 73 | end 74 | else 75 | res 76 | end 77 | end 78 | end 79 | 80 | # Forget about all watched keys. 81 | # 82 | # @return [String] `OK` 83 | # 84 | # @see #watch 85 | # @see #multi 86 | def unwatch 87 | send_command([:unwatch]) 88 | end 89 | 90 | # Execute all commands issued after MULTI. 91 | # 92 | # Only call this method when `#multi` was called **without** a block. 93 | # 94 | # @return [nil, Array<...>] 95 | # - when commands were not executed, `nil` 96 | # - when commands were executed, an array with their replies 97 | # 98 | # @see #multi 99 | # @see #discard 100 | def exec 101 | send_command([:exec]) 102 | end 103 | 104 | # Discard all commands issued after MULTI. 105 | # 106 | # @return [String] `"OK"` 107 | # 108 | # @see #multi 109 | # @see #exec 110 | def discard 111 | send_command([:discard]) 112 | end 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /lib/redis/client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'redis-client' 4 | 5 | class Redis 6 | class Client < ::RedisClient 7 | ERROR_MAPPING = { 8 | RedisClient::ConnectionError => Redis::ConnectionError, 9 | RedisClient::CommandError => Redis::CommandError, 10 | RedisClient::ReadTimeoutError => Redis::TimeoutError, 11 | RedisClient::CannotConnectError => Redis::CannotConnectError, 12 | RedisClient::AuthenticationError => Redis::CannotConnectError, 13 | RedisClient::FailoverError => Redis::CannotConnectError, 14 | RedisClient::PermissionError => Redis::PermissionError, 15 | RedisClient::WrongTypeError => Redis::WrongTypeError, 16 | RedisClient::ReadOnlyError => Redis::ReadOnlyError, 17 | RedisClient::ProtocolError => Redis::ProtocolError, 18 | RedisClient::OutOfMemoryError => Redis::OutOfMemoryError, 19 | } 20 | 21 | class << self 22 | def config(**kwargs) 23 | super(protocol: 2, **kwargs) 24 | end 25 | 26 | def sentinel(**kwargs) 27 | super(protocol: 2, **kwargs, client_implementation: ::RedisClient) 28 | end 29 | 30 | def translate_error!(error, mapping: ERROR_MAPPING) 31 | redis_error = translate_error_class(error.class, mapping: mapping) 32 | raise redis_error, error.message, error.backtrace 33 | end 34 | 35 | private 36 | 37 | def translate_error_class(error_class, mapping: ERROR_MAPPING) 38 | mapping.fetch(error_class) 39 | rescue IndexError 40 | if (client_error = error_class.ancestors.find { |a| mapping[a] }) 41 | mapping[error_class] = mapping[client_error] 42 | else 43 | raise 44 | end 45 | end 46 | end 47 | 48 | def id 49 | config.id 50 | end 51 | 52 | def server_url 53 | config.server_url 54 | end 55 | 56 | def timeout 57 | config.read_timeout 58 | end 59 | 60 | def db 61 | config.db 62 | end 63 | 64 | def host 65 | config.host unless config.path 66 | end 67 | 68 | def port 69 | config.port unless config.path 70 | end 71 | 72 | def path 73 | config.path 74 | end 75 | 76 | def username 77 | config.username 78 | end 79 | 80 | def password 81 | config.password 82 | end 83 | 84 | undef_method :call 85 | undef_method :call_once 86 | undef_method :call_once_v 87 | undef_method :blocking_call 88 | 89 | def call_v(command, &block) 90 | super(command, &block) 91 | rescue ::RedisClient::Error => error 92 | Client.translate_error!(error) 93 | end 94 | 95 | def blocking_call_v(timeout, command, &block) 96 | if timeout && timeout > 0 97 | # Can't use the command timeout argument as the connection timeout 98 | # otherwise it would be very racy. So we add the regular read_timeout on top 99 | # to account for the network delay. 100 | timeout += config.read_timeout 101 | end 102 | 103 | super(timeout, command, &block) 104 | rescue ::RedisClient::Error => error 105 | Client.translate_error!(error) 106 | end 107 | 108 | def pipelined 109 | super 110 | rescue ::RedisClient::Error => error 111 | Client.translate_error!(error) 112 | end 113 | 114 | def multi(watch: nil) 115 | super 116 | rescue ::RedisClient::Error => error 117 | Client.translate_error!(error) 118 | end 119 | 120 | def inherit_socket! 121 | @inherit_socket = true 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /test/redis/remote_server_control_commands_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestRemoteServerControlCommands < Minitest::Test 6 | include Helper::Client 7 | 8 | def test_info 9 | keys = [ 10 | "redis_version", 11 | "uptime_in_seconds", 12 | "uptime_in_days", 13 | "connected_clients", 14 | "used_memory", 15 | "total_connections_received", 16 | "total_commands_processed" 17 | ] 18 | 19 | info = r.info 20 | 21 | keys.each do |k| 22 | msg = "expected #info to include #{k}" 23 | assert info.keys.include?(k), msg 24 | end 25 | end 26 | 27 | def test_info_commandstats 28 | r.config(:resetstat) 29 | r.get("foo") 30 | r.get("bar") 31 | 32 | result = r.info(:commandstats) 33 | assert_equal '2', result['get']['calls'] 34 | end 35 | 36 | def test_monitor_redis 37 | log = [] 38 | 39 | thread = Thread.new do 40 | Redis.new(OPTIONS).monitor do |line| 41 | log << line 42 | break if line =~ /set/ 43 | end 44 | end 45 | 46 | Thread.pass while log.empty? # Faster than sleep 47 | 48 | r.set "foo", "s1" 49 | 50 | thread.join 51 | 52 | assert log[-1] =~ /\b15\b.* "set" "foo" "s1"/ 53 | end 54 | 55 | def test_monitor_returns_value_for_break 56 | result = r.monitor do |line| 57 | break line 58 | end 59 | 60 | assert_equal "OK", result 61 | end 62 | 63 | def test_echo 64 | assert_equal "foo bar baz\n", r.echo("foo bar baz\n") 65 | end 66 | 67 | def test_debug 68 | r.set "foo", "s1" 69 | 70 | assert r.debug(:object, "foo").is_a?(String) 71 | end 72 | 73 | def test_object 74 | r.lpush "list", "value" 75 | 76 | assert_equal 1, r.object(:refcount, "list") 77 | encoding = r.object(:encoding, "list") 78 | assert encoding == "ziplist" || encoding == "quicklist" || encoding == "listpack", "Wrong encoding for list" 79 | assert r.object(:idletime, "list").is_a?(Integer) 80 | end 81 | 82 | def test_sync 83 | redis_mock(sync: -> { "+OK" }) do |redis| 84 | assert_equal "OK", redis.sync 85 | end 86 | end 87 | 88 | def test_slowlog 89 | r.slowlog(:reset) 90 | result = r.slowlog(:len) 91 | assert_equal 0, result 92 | end 93 | 94 | def test_client 95 | assert_equal r.instance_variable_get(:@client), r._client 96 | end 97 | 98 | def test_client_list 99 | keys = [ 100 | "addr", 101 | "fd", 102 | "name", 103 | "age", 104 | "idle", 105 | "flags", 106 | "db", 107 | "sub", 108 | "psub", 109 | "multi", 110 | "qbuf", 111 | "qbuf-free", 112 | "obl", 113 | "oll", 114 | "omem", 115 | "events", 116 | "cmd" 117 | ] 118 | 119 | clients = r.client(:list) 120 | clients.each do |client| 121 | keys.each do |k| 122 | msg = "expected #client(:list) to include #{k}" 123 | assert client.keys.include?(k), msg 124 | end 125 | end 126 | end 127 | 128 | def test_client_kill 129 | r.client(:setname, 'redis-rb') 130 | clients = r.client(:list) 131 | i = clients.index { |client| client['name'] == 'redis-rb' } 132 | assert_equal "OK", r.client(:kill, clients[i]["addr"]) 133 | 134 | clients = r.client(:list) 135 | i = clients.index { |client| client['name'] == 'redis-rb' } 136 | assert_nil i 137 | end 138 | 139 | def test_client_getname_and_setname 140 | assert_nil r.client(:getname) 141 | 142 | r.client(:setname, 'redis-rb') 143 | name = r.client(:getname) 144 | assert_equal 'redis-rb', name 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /cluster/lib/redis/cluster/client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'redis-cluster-client' 4 | 5 | class Redis 6 | class Cluster 7 | class Client < RedisClient::Cluster 8 | ERROR_MAPPING = ::Redis::Client::ERROR_MAPPING.merge( 9 | RedisClient::Cluster::InitialSetupError => Redis::Cluster::InitialSetupError, 10 | RedisClient::Cluster::OrchestrationCommandNotSupported => Redis::Cluster::OrchestrationCommandNotSupported, 11 | RedisClient::Cluster::AmbiguousNodeError => Redis::Cluster::AmbiguousNodeError, 12 | RedisClient::Cluster::ErrorCollection => Redis::Cluster::CommandErrorCollection, 13 | RedisClient::Cluster::Transaction::ConsistencyError => Redis::Cluster::TransactionConsistencyError, 14 | RedisClient::Cluster::NodeMightBeDown => Redis::Cluster::NodeMightBeDown, 15 | ) 16 | 17 | class << self 18 | def config(**kwargs) 19 | super(protocol: 2, **kwargs) 20 | end 21 | 22 | def sentinel(**kwargs) 23 | super(protocol: 2, **kwargs) 24 | end 25 | 26 | def translate_error!(error, mapping: ERROR_MAPPING) 27 | case error 28 | when RedisClient::Cluster::ErrorCollection 29 | error.errors.each do |_node, node_error| 30 | if node_error.is_a?(RedisClient::AuthenticationError) 31 | raise mapping.fetch(node_error.class), node_error.message, node_error.backtrace 32 | end 33 | end 34 | 35 | remapped_node_errors = error.errors.map do |node_key, node_error| 36 | remapped = mapping.fetch(node_error.class, node_error.class).new(node_error.message) 37 | remapped.set_backtrace node_error.backtrace 38 | [node_key, remapped] 39 | end.to_h 40 | 41 | raise(Redis::Cluster::CommandErrorCollection.new(remapped_node_errors, error.message).tap do |remapped| 42 | remapped.set_backtrace error.backtrace 43 | end) 44 | else 45 | Redis::Client.translate_error!(error, mapping: mapping) 46 | end 47 | end 48 | end 49 | 50 | def initialize(*) 51 | handle_errors { super } 52 | end 53 | ruby2_keywords :initialize if respond_to?(:ruby2_keywords, true) 54 | 55 | def id 56 | @router.node_keys.join(' ') 57 | end 58 | 59 | def server_url 60 | @router.node_keys 61 | end 62 | 63 | def connected? 64 | true 65 | end 66 | 67 | def disable_reconnection 68 | yield # TODO: do we need this, is it doable? 69 | end 70 | 71 | def timeout 72 | config.read_timeout 73 | end 74 | 75 | def db 76 | 0 77 | end 78 | 79 | undef_method :call 80 | undef_method :call_once 81 | undef_method :call_once_v 82 | undef_method :blocking_call 83 | 84 | def call_v(command, &block) 85 | handle_errors { super(command, &block) } 86 | end 87 | 88 | def blocking_call_v(timeout, command, &block) 89 | timeout += self.timeout if timeout && timeout > 0 90 | handle_errors { super(timeout, command, &block) } 91 | end 92 | 93 | def pipelined(&block) 94 | handle_errors { super(&block) } 95 | end 96 | 97 | def multi(watch: nil, &block) 98 | handle_errors { super(watch: watch, &block) } 99 | end 100 | 101 | private 102 | 103 | def handle_errors 104 | yield 105 | rescue ::RedisClient::Error => error 106 | Redis::Cluster::Client.translate_error!(error) 107 | end 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/redis/commands/geo.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Redis 4 | module Commands 5 | module Geo 6 | # Adds the specified geospatial items (latitude, longitude, name) to the specified key 7 | # 8 | # @param [String] key 9 | # @param [Array] member arguemnts for member or members: longitude, latitude, name 10 | # @return [Integer] number of elements added to the sorted set 11 | def geoadd(key, *member) 12 | send_command([:geoadd, key, *member]) 13 | end 14 | 15 | # Returns geohash string representing position for specified members of the specified key. 16 | # 17 | # @param [String] key 18 | # @param [String, Array] member one member or array of members 19 | # @return [Array] returns array containg geohash string if member is present, nil otherwise 20 | def geohash(key, member) 21 | send_command([:geohash, key, member]) 22 | end 23 | 24 | # Query a sorted set representing a geospatial index to fetch members matching a 25 | # given maximum distance from a point 26 | # 27 | # @param [Array] args key, longitude, latitude, radius, unit(m|km|ft|mi) 28 | # @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest 29 | # or the farthest to the nearest relative to the center 30 | # @param [Integer] count limit the results to the first N matching items 31 | # @param ['WITHDIST', 'WITHCOORD', 'WITHHASH'] options to return additional information 32 | # @return [Array] may be changed with `options` 33 | def georadius(*args, **geoptions) 34 | geoarguments = _geoarguments(*args, **geoptions) 35 | 36 | send_command([:georadius, *geoarguments]) 37 | end 38 | 39 | # Query a sorted set representing a geospatial index to fetch members matching a 40 | # given maximum distance from an already existing member 41 | # 42 | # @param [Array] args key, member, radius, unit(m|km|ft|mi) 43 | # @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest or the farthest 44 | # to the nearest relative to the center 45 | # @param [Integer] count limit the results to the first N matching items 46 | # @param ['WITHDIST', 'WITHCOORD', 'WITHHASH'] options to return additional information 47 | # @return [Array] may be changed with `options` 48 | def georadiusbymember(*args, **geoptions) 49 | geoarguments = _geoarguments(*args, **geoptions) 50 | 51 | send_command([:georadiusbymember, *geoarguments]) 52 | end 53 | 54 | # Returns longitude and latitude of members of a geospatial index 55 | # 56 | # @param [String] key 57 | # @param [String, Array] member one member or array of members 58 | # @return [Array, nil>] returns array of elements, where each 59 | # element is either array of longitude and latitude or nil 60 | def geopos(key, member) 61 | send_command([:geopos, key, member]) 62 | end 63 | 64 | # Returns the distance between two members of a geospatial index 65 | # 66 | # @param [String ]key 67 | # @param [Array] members 68 | # @param ['m', 'km', 'mi', 'ft'] unit 69 | # @return [String, nil] returns distance in spefied unit if both members present, nil otherwise. 70 | def geodist(key, member1, member2, unit = 'm') 71 | send_command([:geodist, key, member1, member2, unit]) 72 | end 73 | 74 | private 75 | 76 | def _geoarguments(*args, options: nil, sort: nil, count: nil) 77 | args << sort if sort 78 | args << 'COUNT' << Integer(count) if count 79 | args << options if options 80 | args 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test/redis/connection_handling_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | require 'lint/authentication' 5 | 6 | class TestConnectionHandling < Minitest::Test 7 | include Helper::Client 8 | include Lint::Authentication 9 | 10 | def test_id 11 | commands = { 12 | client: ->(cmd, name) { @name = [cmd, name]; "+OK" }, 13 | ping: -> { "+PONG" } 14 | } 15 | 16 | redis_mock(commands, id: "client-name") do |redis| 17 | assert_equal "PONG", redis.ping 18 | end 19 | 20 | assert_equal ["SETNAME", "client-name"], @name 21 | end 22 | 23 | def test_ping 24 | assert_equal "PONG", r.ping 25 | end 26 | 27 | def test_select 28 | r.set "foo", "bar" 29 | 30 | r.select 14 31 | assert_nil r.get("foo") 32 | 33 | r._client.close 34 | 35 | assert_equal "bar", r.get("foo") 36 | end 37 | 38 | def test_quit 39 | r.quit 40 | 41 | assert !r._client.connected? 42 | end 43 | 44 | def test_close 45 | quit = 0 46 | 47 | commands = { 48 | quit: lambda do 49 | quit += 1 50 | "+OK" 51 | end 52 | } 53 | 54 | redis_mock(commands) do |redis| 55 | assert_equal 0, quit 56 | 57 | redis.quit 58 | 59 | assert_equal 1, quit 60 | 61 | redis.ping 62 | 63 | redis.close 64 | 65 | assert_equal 1, quit 66 | 67 | assert !redis.connected? 68 | end 69 | end 70 | 71 | def test_disconnect 72 | quit = 0 73 | 74 | commands = { 75 | quit: lambda do 76 | quit += 1 77 | "+OK" 78 | end 79 | } 80 | 81 | redis_mock(commands) do |redis| 82 | assert_equal 0, quit 83 | 84 | redis.quit 85 | 86 | assert_equal 1, quit 87 | 88 | redis.ping 89 | 90 | redis.disconnect! 91 | 92 | assert_equal 1, quit 93 | 94 | assert !redis.connected? 95 | end 96 | end 97 | 98 | def test_shutdown 99 | commands = { 100 | shutdown: -> { :exit } 101 | } 102 | 103 | redis_mock(commands) do |redis| 104 | # SHUTDOWN does not reply: test that it does not raise here. 105 | assert_nil redis.shutdown 106 | end 107 | end 108 | 109 | def test_shutdown_with_error 110 | connections = 0 111 | commands = { 112 | select: ->(*_) { connections += 1; "+OK\r\n" }, 113 | connections: -> { ":#{connections}\r\n" }, 114 | shutdown: -> { "-ERR could not shutdown\r\n" } 115 | } 116 | 117 | redis_mock(commands) do |redis| 118 | connections = redis.connections 119 | 120 | # SHUTDOWN replies with an error: test that it gets raised 121 | assert_raises Redis::CommandError do 122 | redis.shutdown 123 | end 124 | 125 | # The connection should remain in tact 126 | assert_equal connections, redis.connections 127 | end 128 | end 129 | 130 | def test_slaveof 131 | redis_mock(slaveof: ->(host, port) { "+SLAVEOF #{host} #{port}" }) do |redis| 132 | assert_equal "SLAVEOF somehost 6381", redis.slaveof("somehost", 6381) 133 | end 134 | end 135 | 136 | def test_bgrewriteaof 137 | redis_mock(bgrewriteaof: -> { "+BGREWRITEAOF" }) do |redis| 138 | assert_equal "BGREWRITEAOF", redis.bgrewriteaof 139 | end 140 | end 141 | 142 | def test_config_get 143 | refute_nil r.config(:get, "*")["timeout"] 144 | 145 | config = r.config(:get, "timeout") 146 | assert_equal ["timeout"], config.keys 147 | assert !config.values.compact.empty? 148 | end 149 | 150 | def test_config_set 151 | assert_equal "OK", r.config(:set, "timeout", 200) 152 | assert_equal "200", r.config(:get, "*")["timeout"] 153 | 154 | assert_equal "OK", r.config(:set, "timeout", 100) 155 | assert_equal "100", r.config(:get, "*")["timeout"] 156 | ensure 157 | r.config :set, "timeout", 300 158 | end 159 | end 160 | -------------------------------------------------------------------------------- /test/redis/commands_on_geo_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestCommandsGeo < Minitest::Test 6 | include Helper::Client 7 | 8 | def setup 9 | super 10 | 11 | added_items_count = r.geoadd("Sicily", 13.361389, 38.115556, "Palermo", 15.087269, 37.502669, "Catania") 12 | assert_equal 2, added_items_count 13 | end 14 | 15 | def test_geoadd_with_array_params 16 | added_items_count = r.geoadd("SicilyArray", [13.361389, 38.115556, "Palermo", 15.087269, 37.502669, "Catania"]) 17 | assert_equal 2, added_items_count 18 | end 19 | 20 | def test_georadius_with_same_params 21 | r.geoadd("Chad", 15, 15, "Kanem") 22 | nearest_cities = r.georadius("Chad", 15, 15, 15, 'km', sort: 'asc') 23 | assert_equal %w(Kanem), nearest_cities 24 | end 25 | 26 | def test_georadius_with_sort 27 | nearest_cities = r.georadius("Sicily", 15, 37, 200, 'km', sort: 'asc') 28 | assert_equal %w(Catania Palermo), nearest_cities 29 | 30 | farthest_cities = r.georadius("Sicily", 15, 37, 200, 'km', sort: 'desc') 31 | assert_equal %w(Palermo Catania), farthest_cities 32 | end 33 | 34 | def test_georadius_with_count 35 | city = r.georadius("Sicily", 15, 37, 200, 'km', count: 1) 36 | assert_equal %w(Catania), city 37 | end 38 | 39 | def test_georadius_with_options_count_sort 40 | city = r.georadius("Sicily", 15, 37, 200, 'km', sort: :desc, options: :WITHDIST, count: 1) 41 | assert_equal [["Palermo", "190.4424"]], city 42 | end 43 | 44 | def test_georadiusbymember_with_sort 45 | nearest_cities = r.georadiusbymember("Sicily", "Catania", 200, 'km', sort: 'asc') 46 | assert_equal %w(Catania Palermo), nearest_cities 47 | 48 | farthest_cities = r.georadiusbymember("Sicily", "Catania", 200, 'km', sort: 'desc') 49 | assert_equal %w(Palermo Catania), farthest_cities 50 | end 51 | 52 | def test_georadiusbymember_with_count 53 | city = r.georadiusbymember("Sicily", "Catania", 200, 'km', count: 1) 54 | assert_equal %w(Catania), city 55 | end 56 | 57 | def test_georadiusbymember_with_options_count_sort 58 | city = r.georadiusbymember("Sicily", "Catania", 200, 'km', sort: :desc, options: :WITHDIST, count: 1) 59 | assert_equal [["Palermo", "166.2742"]], city 60 | end 61 | 62 | def test_geopos 63 | location = r.geopos("Sicily", "Catania") 64 | assert_equal [["15.08726745843887329", "37.50266842333162032"]], location 65 | 66 | locations = r.geopos("Sicily", ["Palermo", "Catania"]) 67 | assert_equal [["13.36138933897018433", "38.11555639549629859"], ["15.08726745843887329", "37.50266842333162032"]], locations 68 | end 69 | 70 | def test_geopos_nonexistant_location 71 | location = r.geopos("Sicily", "Rome") 72 | assert_equal [nil], location 73 | 74 | locations = r.geopos("Sicily", ["Rome", "Catania"]) 75 | assert_equal [nil, ["15.08726745843887329", "37.50266842333162032"]], locations 76 | end 77 | 78 | def test_geodist 79 | distination_in_meters = r.geodist("Sicily", "Palermo", "Catania") 80 | assert_equal "166274.1516", distination_in_meters 81 | 82 | distination_in_feet = r.geodist("Sicily", "Palermo", "Catania", 'ft') 83 | assert_equal "545518.8700", distination_in_feet 84 | end 85 | 86 | def test_geodist_with_nonexistant_location 87 | distination = r.geodist("Sicily", "Palermo", "Rome") 88 | assert_nil distination 89 | end 90 | 91 | def test_geohash 92 | geohash = r.geohash("Sicily", "Palermo") 93 | assert_equal ["sqc8b49rny0"], geohash 94 | 95 | geohashes = r.geohash("Sicily", ["Palermo", "Catania"]) 96 | assert_equal %w(sqc8b49rny0 sqdtr74hyu0), geohashes 97 | end 98 | 99 | def test_geohash_with_nonexistant_location 100 | geohashes = r.geohash("Sicily", ["Palermo", "Rome"]) 101 | assert_equal ["sqc8b49rny0", nil], geohashes 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /test/support/redis_mock.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "socket" 4 | 5 | module RedisMock 6 | class Server 7 | def initialize(options = {}) 8 | tcp_server = TCPServer.new(options[:host] || "127.0.0.1", 0) 9 | tcp_server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true) 10 | 11 | @concurrent = options.delete(:concurrent) 12 | 13 | if options[:ssl] 14 | ctx = OpenSSL::SSL::SSLContext.new 15 | 16 | ssl_params = options.fetch(:ssl_params, {}) 17 | ctx.set_params(ssl_params) unless ssl_params.empty? 18 | 19 | @server = OpenSSL::SSL::SSLServer.new(tcp_server, ctx) 20 | else 21 | @server = tcp_server 22 | end 23 | end 24 | 25 | def port 26 | @server.addr[1] 27 | end 28 | 29 | def start(&block) 30 | @thread = Thread.new { run(&block) } 31 | end 32 | 33 | def shutdown 34 | @thread.kill 35 | end 36 | 37 | def run(&block) 38 | loop do 39 | if @concurrent 40 | Thread.new(@server.accept) do |session| 41 | block.call(session) 42 | ensure 43 | session.close 44 | end 45 | else 46 | session = @server.accept 47 | begin 48 | return if yield(session) == :exit 49 | ensure 50 | session.close 51 | end 52 | end 53 | end 54 | rescue => ex 55 | warn "Error running mock server: #{ex.class}: #{ex.message}" 56 | warn ex.backtrace 57 | retry 58 | ensure 59 | @server.close 60 | end 61 | end 62 | 63 | # Starts a mock Redis server in a thread. 64 | # 65 | # The server will use the lambda handler passed as argument to handle 66 | # connections. For example: 67 | # 68 | # handler = lambda { |session| session.close } 69 | # RedisMock.start_with_handler(handler) do 70 | # # Every connection will be closed immediately 71 | # end 72 | # 73 | def self.start_with_handler(blk, options = {}) 74 | server = Server.new(options) 75 | port = server.port 76 | 77 | begin 78 | server.start(&blk) 79 | yield(port) 80 | ensure 81 | server.shutdown 82 | end 83 | end 84 | 85 | # Starts a mock Redis server in a thread. 86 | # 87 | # The server will reply with a `+OK` to all commands, but you can 88 | # customize it by providing a hash. For example: 89 | # 90 | # RedisMock.start(:ping => lambda { "+PONG" }) do |port| 91 | # assert_equal "PONG", Redis.new(:port => port).ping 92 | # end 93 | # 94 | def self.start(commands, options = {}, &blk) 95 | handler = lambda do |session| 96 | while line = session.gets 97 | argv = Array.new(line[1..-3].to_i) do 98 | bytes = session.gets[1..-3].to_i 99 | arg = session.read(bytes) 100 | session.read(2) # Discard \r\n 101 | arg 102 | end 103 | 104 | command = argv.shift 105 | blk = commands[command.downcase.to_sym] 106 | blk ||= ->(*_) { "+OK" } 107 | 108 | response = blk.call(*argv) 109 | 110 | # Convert a nil response to :close 111 | response ||= :close 112 | 113 | case response 114 | when :exit 115 | break :exit 116 | when :close 117 | break :close 118 | when Array 119 | session.write("*%d\r\n" % response.size) 120 | 121 | response.each do |resp| 122 | if resp.is_a?(Array) 123 | session.write("*%d\r\n" % resp.size) 124 | resp.each do |r| 125 | session.write("$%d\r\n%s\r\n" % [r.length, r]) 126 | end 127 | else 128 | session.write("$%d\r\n%s\r\n" % [resp.length, resp]) 129 | end 130 | end 131 | else 132 | session.write(response) 133 | session.write("\r\n") unless response.end_with?("\r\n") 134 | end 135 | end 136 | end 137 | 138 | start_with_handler(handler, options, &blk) 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /examples/consistency.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file implements a simple consistency test for Redis-rb (or any other 4 | # Redis environment if you pass a different client object) where a client 5 | # writes to the database using INCR in order to increment keys, but actively 6 | # remember the value the key should have. Before every write a read is performed 7 | # to check if the value in the database matches the value expected. 8 | # 9 | # In this way this program can check for lost writes, or acknowledged writes 10 | # that were executed. 11 | # 12 | # Copyright (C) 2013-2014 Salvatore Sanfilippo 13 | # 14 | # Permission is hereby granted, free of charge, to any person obtaining 15 | # a copy of this software and associated documentation files (the 16 | # "Software"), to deal in the Software without restriction, including 17 | # without limitation the rights to use, copy, modify, merge, publish, 18 | # distribute, sublicense, and/or sell copies of the Software, and to 19 | # permit persons to whom the Software is furnished to do so, subject to 20 | # the following conditions: 21 | # 22 | # The above copyright notice and this permission notice shall be 23 | # included in all copies or substantial portions of the Software. 24 | # 25 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 26 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 27 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 28 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 29 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 30 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 31 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 32 | 33 | require 'redis' 34 | 35 | class ConsistencyTester 36 | def initialize(redis) 37 | @r = redis 38 | @working_set = 10_000 39 | @keyspace = 100_000 40 | @writes = 0 41 | @reads = 0 42 | @failed_writes = 0 43 | @failed_reads = 0 44 | @lost_writes = 0 45 | @not_ack_writes = 0 46 | @delay = 0 47 | @cached = {} # We take our view of data stored in the DB. 48 | @prefix = [Process.pid.to_s, Time.now.usec, @r.object_id, ""].join("|") 49 | @errtime = {} 50 | end 51 | 52 | def genkey 53 | # Write more often to a small subset of keys 54 | ks = rand > 0.5 ? @keyspace : @working_set 55 | "#{@prefix}key_#{rand(ks).to_s}" 56 | end 57 | 58 | def check_consistency(key, value) 59 | expected = @cached[key] 60 | return unless expected # We lack info about previous state. 61 | 62 | if expected > value 63 | @lost_writes += expected - value 64 | elsif expected < value 65 | @not_ack_writes += value - expected 66 | end 67 | end 68 | 69 | def puterr(msg) 70 | puts msg if !@errtime[msg] || Time.now.to_i != @errtime[msg] 71 | @errtime[msg] = Time.now.to_i 72 | end 73 | 74 | def test 75 | last_report = Time.now.to_i 76 | loop do 77 | # Read 78 | key = genkey 79 | begin 80 | val = @r.get(key) 81 | check_consistency(key, val.to_i) 82 | @reads += 1 83 | rescue => e 84 | puterr "Reading: #{e.class}: #{e.message} (#{e.backtrace.first})" 85 | @failed_reads += 1 86 | end 87 | 88 | # Write 89 | begin 90 | @cached[key] = @r.incr(key).to_i 91 | @writes += 1 92 | rescue => e 93 | puterr "Writing: #{e.class}: #{e.message} (#{e.backtrace.first})" 94 | @failed_writes += 1 95 | end 96 | 97 | # Report 98 | sleep @delay 99 | next unless Time.now.to_i != last_report 100 | 101 | report = "#{@reads} R (#{@failed_reads} err) | " \ 102 | "#{@writes} W (#{@failed_writes} err) | " 103 | report += "#{@lost_writes} lost | " if @lost_writes > 0 104 | report += "#{@not_ack_writes} noack | " if @not_ack_writes > 0 105 | last_report = Time.now.to_i 106 | puts report 107 | end 108 | end 109 | end 110 | 111 | SENTINELS = [{ host: "127.0.0.1", port: 26_379 }, 112 | { host: "127.0.0.1", port: 26_380 }].freeze 113 | r = Redis.new(url: "redis://master1", sentinels: SENTINELS, role: :master) 114 | tester = ConsistencyTester.new(r) 115 | tester.test 116 | -------------------------------------------------------------------------------- /lib/redis/commands/scripting.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Redis 4 | module Commands 5 | module Scripting 6 | # Control remote script registry. 7 | # 8 | # @example Load a script 9 | # sha = redis.script(:load, "return 1") 10 | # # => 11 | # @example Check if a script exists 12 | # redis.script(:exists, sha) 13 | # # => true 14 | # @example Check if multiple scripts exist 15 | # redis.script(:exists, [sha, other_sha]) 16 | # # => [true, false] 17 | # @example Flush the script registry 18 | # redis.script(:flush) 19 | # # => "OK" 20 | # @example Kill a running script 21 | # redis.script(:kill) 22 | # # => "OK" 23 | # 24 | # @param [String] subcommand e.g. `exists`, `flush`, `load`, `kill` 25 | # @param [Array] args depends on subcommand 26 | # @return [String, Boolean, Array, ...] depends on subcommand 27 | # 28 | # @see #eval 29 | # @see #evalsha 30 | def script(subcommand, *args) 31 | subcommand = subcommand.to_s.downcase 32 | 33 | if subcommand == "exists" 34 | arg = args.first 35 | 36 | send_command([:script, :exists, arg]) do |reply| 37 | reply = reply.map { |r| Boolify.call(r) } 38 | 39 | if arg.is_a?(Array) 40 | reply 41 | else 42 | reply.first 43 | end 44 | end 45 | else 46 | send_command([:script, subcommand] + args) 47 | end 48 | end 49 | 50 | # Evaluate Lua script. 51 | # 52 | # @example EVAL without KEYS nor ARGV 53 | # redis.eval("return 1") 54 | # # => 1 55 | # @example EVAL with KEYS and ARGV as array arguments 56 | # redis.eval("return { KEYS, ARGV }", ["k1", "k2"], ["a1", "a2"]) 57 | # # => [["k1", "k2"], ["a1", "a2"]] 58 | # @example EVAL with KEYS and ARGV in a hash argument 59 | # redis.eval("return { KEYS, ARGV }", :keys => ["k1", "k2"], :argv => ["a1", "a2"]) 60 | # # => [["k1", "k2"], ["a1", "a2"]] 61 | # 62 | # @param [Array] keys optional array with keys to pass to the script 63 | # @param [Array] argv optional array with arguments to pass to the script 64 | # @param [Hash] options 65 | # - `:keys => Array`: optional array with keys to pass to the script 66 | # - `:argv => Array`: optional array with arguments to pass to the script 67 | # @return depends on the script 68 | # 69 | # @see #script 70 | # @see #evalsha 71 | def eval(*args) 72 | _eval(:eval, args) 73 | end 74 | 75 | # Evaluate Lua script by its SHA. 76 | # 77 | # @example EVALSHA without KEYS nor ARGV 78 | # redis.evalsha(sha) 79 | # # => 80 | # @example EVALSHA with KEYS and ARGV as array arguments 81 | # redis.evalsha(sha, ["k1", "k2"], ["a1", "a2"]) 82 | # # => 83 | # @example EVALSHA with KEYS and ARGV in a hash argument 84 | # redis.evalsha(sha, :keys => ["k1", "k2"], :argv => ["a1", "a2"]) 85 | # # => 86 | # 87 | # @param [Array] keys optional array with keys to pass to the script 88 | # @param [Array] argv optional array with arguments to pass to the script 89 | # @param [Hash] options 90 | # - `:keys => Array`: optional array with keys to pass to the script 91 | # - `:argv => Array`: optional array with arguments to pass to the script 92 | # @return depends on the script 93 | # 94 | # @see #script 95 | # @see #eval 96 | def evalsha(*args) 97 | _eval(:evalsha, args) 98 | end 99 | 100 | private 101 | 102 | def _eval(cmd, args) 103 | script = args.shift 104 | options = args.pop if args.last.is_a?(Hash) 105 | options ||= {} 106 | 107 | keys = args.shift || options[:keys] || [] 108 | argv = args.shift || options[:argv] || [] 109 | 110 | send_command([cmd, script, keys.length] + keys + argv) 111 | end 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /cluster/lib/redis/cluster.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "redis" 4 | 5 | class Redis 6 | class Cluster < ::Redis 7 | # Raised when client connected to redis as cluster mode 8 | # and failed to fetch cluster state information by commands. 9 | class InitialSetupError < BaseError 10 | end 11 | 12 | # Raised when client connected to redis as cluster mode 13 | # and some cluster subcommands were called. 14 | class OrchestrationCommandNotSupported < BaseError 15 | def initialize(command, subcommand = '') 16 | str = [command, subcommand].map(&:to_s).reject(&:empty?).join(' ').upcase 17 | msg = "#{str} command should be used with care "\ 18 | 'only by applications orchestrating Redis Cluster, like redis-trib, '\ 19 | 'and the command if used out of the right context can leave the cluster '\ 20 | 'in a wrong state or cause data loss.' 21 | super(msg) 22 | end 23 | end 24 | 25 | # Raised when error occurs on any node of cluster. 26 | class CommandErrorCollection < BaseError 27 | attr_reader :errors 28 | 29 | # @param errors [Hash{String => Redis::CommandError}] 30 | # @param error_message [String] 31 | def initialize(errors, error_message = 'Command errors were replied on any node') 32 | @errors = errors 33 | super(error_message) 34 | end 35 | end 36 | 37 | # Raised when cluster client can't select node. 38 | class AmbiguousNodeError < BaseError 39 | end 40 | 41 | class TransactionConsistencyError < BaseError 42 | end 43 | 44 | class NodeMightBeDown < BaseError 45 | end 46 | 47 | def connection 48 | raise NotImplementedError, "Redis::Cluster doesn't implement #connection" 49 | end 50 | 51 | # Create a new client instance 52 | # 53 | # @param [Hash] options 54 | # @option options [Float] :timeout (5.0) timeout in seconds 55 | # @option options [Float] :connect_timeout (same as timeout) timeout for initial connect in seconds 56 | # @option options [Symbol] :driver Driver to use, currently supported: `:ruby`, `:hiredis` 57 | # @option options [Integer, Array] :reconnect_attempts Number of attempts trying to connect, 58 | # or a list of sleep duration between attempts. 59 | # @option options [Boolean] :inherit_socket (false) Whether to use socket in forked process or not 60 | # @option options [Array String, Integer}>] :nodes List of cluster nodes to contact 61 | # @option options [Boolean] :replica Whether to use readonly replica nodes in Redis Cluster or not 62 | # @option options [Symbol] :replica_affinity scale reading strategy, currently supported: `:random`, `:latency` 63 | # @option options [String] :fixed_hostname Specify a FQDN if cluster mode enabled and 64 | # client has to connect nodes via single endpoint with SSL/TLS 65 | # @option options [Class] :connector Class of custom connector 66 | # 67 | # @return [Redis::Cluster] a new client instance 68 | def initialize(*) # rubocop:disable Lint/UselessMethodDefinition 69 | super 70 | end 71 | ruby2_keywords :initialize if respond_to?(:ruby2_keywords, true) 72 | 73 | # Sends `CLUSTER *` command to random node and returns its reply. 74 | # 75 | # @see https://redis.io/commands#cluster Reference of cluster command 76 | # 77 | # @param subcommand [String, Symbol] the subcommand of cluster command 78 | # e.g. `:slots`, `:nodes`, `:slaves`, `:info` 79 | # 80 | # @return [Object] depends on the subcommand 81 | def cluster(subcommand, *args) 82 | subcommand = subcommand.to_s.downcase 83 | block = case subcommand 84 | when 'slots' 85 | HashifyClusterSlots 86 | when 'nodes' 87 | HashifyClusterNodes 88 | when 'slaves' 89 | HashifyClusterSlaves 90 | when 'info' 91 | HashifyInfo 92 | else 93 | Noop 94 | end 95 | 96 | send_command([:cluster, subcommand] + args, &block) 97 | end 98 | 99 | private 100 | 101 | def initialize_client(options) 102 | cluster_config = RedisClient.cluster(**options, protocol: 2, client_implementation: ::Redis::Cluster::Client) 103 | cluster_config.new_client 104 | end 105 | end 106 | end 107 | 108 | require "redis/cluster/client" 109 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | REDIS_BRANCH ?= 7.2 2 | ROOT_DIR :=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 3 | TMP := tmp 4 | CONF := ${ROOT_DIR}/test/support/conf/redis-${REDIS_BRANCH}.conf 5 | BUILD_DIR := ${TMP}/cache/redis-${REDIS_BRANCH} 6 | TARBALL := ${TMP}/redis-${REDIS_BRANCH}.tar.gz 7 | BINARY := ${BUILD_DIR}/src/redis-server 8 | REDIS_CLIENT := ${BUILD_DIR}/src/redis-cli 9 | REDIS_TRIB := ${BUILD_DIR}/src/redis-trib.rb 10 | PID_PATH := ${BUILD_DIR}/redis.pid 11 | SOCKET_PATH := ${TMP}/redis.sock 12 | PORT := 6381 13 | SLAVE_PORT := 6382 14 | SLAVE_PID_PATH := ${BUILD_DIR}/redis_slave.pid 15 | SLAVE_SOCKET_PATH := ${BUILD_DIR}/redis_slave.sock 16 | HA_GROUP_NAME := master1 17 | SENTINEL_PORTS := 6400 6401 6402 18 | SENTINEL_PID_PATHS := $(addprefix ${TMP}/redis,$(addsuffix .pid,${SENTINEL_PORTS})) 19 | CLUSTER_PORTS := 16380 16381 16382 16383 16384 16385 20 | CLUSTER_PID_PATHS := $(addprefix ${TMP}/redis,$(addsuffix .pid,${CLUSTER_PORTS})) 21 | CLUSTER_CONF_PATHS := $(addprefix ${TMP}/nodes,$(addsuffix .conf,${CLUSTER_PORTS})) 22 | CLUSTER_ADDRS := $(addprefix 127.0.0.1:,${CLUSTER_PORTS}) 23 | 24 | define kill-redis 25 | (ls $1 > /dev/null 2>&1 && kill $$(cat $1) && rm -f $1) || true 26 | endef 27 | 28 | all: start_all test stop_all 29 | 30 | start_all: start start_slave start_sentinel wait_for_sentinel start_cluster create_cluster 31 | 32 | stop_all: stop_sentinel stop_slave stop stop_cluster 33 | 34 | ${TMP}: 35 | @mkdir -p $@ 36 | 37 | ${BINARY}: ${TMP} 38 | @bin/build ${REDIS_BRANCH} $< 39 | 40 | test: 41 | @env REDIS_SOCKET_PATH=${SOCKET_PATH} bundle exec rake test 42 | 43 | stop: 44 | @$(call kill-redis,${PID_PATH});\ 45 | 46 | start: ${BINARY} 47 | @cp ${CONF} ${TMP}/redis.conf; \ 48 | ${BINARY} ${TMP}/redis.conf \ 49 | --daemonize yes\ 50 | --pidfile ${PID_PATH}\ 51 | --port ${PORT}\ 52 | --unixsocket ${SOCKET_PATH} 53 | 54 | stop_slave: 55 | @$(call kill-redis,${SLAVE_PID_PATH}) 56 | 57 | start_slave: start 58 | @${BINARY}\ 59 | --daemonize yes\ 60 | --pidfile ${SLAVE_PID_PATH}\ 61 | --port ${SLAVE_PORT}\ 62 | --unixsocket ${SLAVE_SOCKET_PATH}\ 63 | --slaveof 127.0.0.1 ${PORT} 64 | 65 | stop_sentinel: stop_slave stop 66 | @$(call kill-redis,${SENTINEL_PID_PATHS}) 67 | @rm -f ${TMP}/sentinel*.conf || true 68 | 69 | start_sentinel: start start_slave 70 | @for port in ${SENTINEL_PORTS}; do\ 71 | conf=${TMP}/sentinel$$port.conf;\ 72 | touch $$conf;\ 73 | echo '' > $$conf;\ 74 | echo 'sentinel monitor ${HA_GROUP_NAME} 127.0.0.1 ${PORT} 2' >> $$conf;\ 75 | echo 'sentinel down-after-milliseconds ${HA_GROUP_NAME} 5000' >> $$conf;\ 76 | echo 'sentinel failover-timeout ${HA_GROUP_NAME} 30000' >> $$conf;\ 77 | echo 'sentinel parallel-syncs ${HA_GROUP_NAME} 1' >> $$conf;\ 78 | ${BINARY} $$conf\ 79 | --daemonize yes\ 80 | --pidfile ${TMP}/redis$$port.pid\ 81 | --port $$port\ 82 | --sentinel;\ 83 | done 84 | 85 | wait_for_sentinel: MAX_ATTEMPTS_FOR_WAIT ?= 60 86 | wait_for_sentinel: 87 | @for port in ${SENTINEL_PORTS}; do\ 88 | i=0;\ 89 | while : ; do\ 90 | if [ $${i} -ge ${MAX_ATTEMPTS_FOR_WAIT} ]; then\ 91 | echo "Max attempts exceeded: $${i} times";\ 92 | exit 1;\ 93 | fi;\ 94 | if [ $$(${REDIS_CLIENT} -p $${port} SENTINEL SLAVES ${HA_GROUP_NAME} | wc -l) -gt 1 ]; then\ 95 | break;\ 96 | fi;\ 97 | echo 'Waiting for Redis sentinel to be ready...';\ 98 | sleep 1;\ 99 | i=$$(( $${i}+1 ));\ 100 | done;\ 101 | done 102 | 103 | stop_cluster: 104 | @$(call kill-redis,${CLUSTER_PID_PATHS}) 105 | @rm -f appendonly.aof || true 106 | @rm -f ${CLUSTER_CONF_PATHS} || true 107 | 108 | start_cluster: ${BINARY} 109 | @for port in ${CLUSTER_PORTS}; do\ 110 | ${BINARY}\ 111 | --daemonize yes\ 112 | --appendonly no\ 113 | --cluster-enabled yes\ 114 | --cluster-config-file ${TMP}/nodes$$port.conf\ 115 | --cluster-node-timeout 5000\ 116 | --pidfile ${TMP}/redis$$port.pid\ 117 | --port $$port\ 118 | --unixsocket ${TMP}/redis$$port.sock;\ 119 | done 120 | 121 | create_cluster: 122 | @bin/cluster_creator ${CLUSTER_ADDRS} 123 | 124 | clean: 125 | @(test -d ${BUILD_DIR} && cd ${BUILD_DIR}/src && make clean distclean) || true 126 | 127 | .PHONY: all test stop start stop_slave start_slave stop_sentinel start_sentinel\ 128 | stop_cluster start_cluster create_cluster stop_all start_all clean 129 | -------------------------------------------------------------------------------- /cluster/test/helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../test/helper" 4 | $LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) 5 | 6 | require "redis-clustering" 7 | require_relative 'support/orchestrator' 8 | 9 | module Helper 10 | module Cluster 11 | include Generic 12 | 13 | DEFAULT_HOST = '127.0.0.1' 14 | DEFAULT_PORTS = (16_380..16_385).freeze 15 | 16 | ClusterSlotsRawReply = lambda { |host, port| 17 | # @see https://redis.io/topics/protocol 18 | <<-REPLY.delete(' ') 19 | *1\r 20 | *4\r 21 | :0\r 22 | :16383\r 23 | *3\r 24 | $#{host.size}\r 25 | #{host}\r 26 | :#{port}\r 27 | $40\r 28 | 649fa246273043021a05f547a79478597d3f1dc5\r 29 | *3\r 30 | $#{host.size}\r 31 | #{host}\r 32 | :#{port}\r 33 | $40\r 34 | 649fa246273043021a05f547a79478597d3f1dc5\r 35 | REPLY 36 | } 37 | 38 | ClusterNodesRawReply = lambda { |host, port| 39 | line = "649fa246273043021a05f547a79478597d3f1dc5 #{host}:#{port}@17000 "\ 40 | 'myself,master - 0 1530797742000 1 connected 0-16383' 41 | "$#{line.size}\r\n#{line}\r\n" 42 | } 43 | 44 | def init(redis) 45 | redis.flushall 46 | redis 47 | rescue Redis::CannotConnectError 48 | puts <<-MSG 49 | 50 | Cannot connect to Redis Cluster. 51 | 52 | Make sure Redis is running on localhost, port #{DEFAULT_PORTS}. 53 | 54 | Try this once: 55 | 56 | $ make stop_cluster 57 | 58 | Then run the build again: 59 | 60 | $ make 61 | 62 | MSG 63 | exit! 1 64 | end 65 | 66 | def build_another_client(options = {}) 67 | _new_client(options) 68 | end 69 | 70 | def redis_cluster_mock(commands, options = {}) 71 | host = DEFAULT_HOST 72 | port = nil 73 | 74 | cluster_subcommands = if commands.key?(:cluster) 75 | commands.delete(:cluster) 76 | .to_h { |k, v| [k.to_s.downcase, v] } 77 | else 78 | {} 79 | end 80 | 81 | commands[:cluster] = lambda { |subcommand, *args| 82 | subcommand = subcommand.downcase 83 | if cluster_subcommands.key?(subcommand) 84 | cluster_subcommands[subcommand].call(*args) 85 | else 86 | case subcommand.downcase 87 | when 'slots' then ClusterSlotsRawReply.call(host, port) 88 | when 'nodes' then ClusterNodesRawReply.call(host, port) 89 | else '+OK' 90 | end 91 | end 92 | } 93 | 94 | commands[:command] = ->(*_) { "*0\r\n" } 95 | 96 | RedisMock.start(commands, options) do |po| 97 | port = po 98 | scheme = options[:ssl] ? 'rediss' : 'redis' 99 | nodes = %W[#{scheme}://#{host}:#{port}] 100 | yield _new_client(options.merge(nodes: nodes)) 101 | end 102 | end 103 | 104 | def redis_cluster_down 105 | trib = ClusterOrchestrator.new(_default_nodes, timeout: TIMEOUT) 106 | trib.down 107 | yield 108 | ensure 109 | trib.rebuild 110 | trib.close 111 | end 112 | 113 | def redis_cluster_failover 114 | trib = ClusterOrchestrator.new(_default_nodes, timeout: TIMEOUT) 115 | trib.failover 116 | yield 117 | ensure 118 | trib.rebuild 119 | trib.close 120 | end 121 | 122 | def redis_cluster_fail_master 123 | trib = ClusterOrchestrator.new(_default_nodes, timeout: TIMEOUT) 124 | trib.fail_serving_master 125 | yield 126 | ensure 127 | trib.restart_cluster_nodes 128 | trib.rebuild 129 | trib.close 130 | end 131 | 132 | # @param slot [Integer] 133 | # @param src [String] : 134 | # @param dest [String] : 135 | def redis_cluster_resharding(slot, src:, dest:) 136 | trib = ClusterOrchestrator.new(_default_nodes, timeout: TIMEOUT) 137 | trib.start_resharding(slot, src, dest) 138 | yield 139 | trib.finish_resharding(slot, dest) 140 | ensure 141 | trib.rebuild 142 | trib.close 143 | end 144 | 145 | private 146 | 147 | def _default_nodes(host: DEFAULT_HOST, ports: DEFAULT_PORTS) 148 | ports.map { |port| "redis://#{host}:#{port}" } 149 | end 150 | 151 | def _format_options(options) 152 | { 153 | timeout: OPTIONS[:timeout], 154 | nodes: _default_nodes 155 | }.merge(options) 156 | end 157 | 158 | def _new_client(options = {}) 159 | Redis::Cluster.new(_format_options(options).merge(driver: ENV['DRIVER'])) 160 | end 161 | end 162 | end 163 | -------------------------------------------------------------------------------- /cluster/test/commands_on_keys_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | # ruby -w -Itest test/cluster_commands_on_keys_test.rb 6 | # @see https://redis.io/commands#generic 7 | class TestClusterCommandsOnKeys < Minitest::Test 8 | include Helper::Cluster 9 | 10 | def set_some_keys 11 | redis.set('key1', 'Hello') 12 | redis.set('key2', 'World') 13 | 14 | redis.set('{key}1', 'Hello') 15 | redis.set('{key}2', 'World') 16 | end 17 | 18 | def test_del 19 | set_some_keys 20 | 21 | assert_raises(Redis::CommandError, "CROSSSLOT Keys in request don't hash to the same slot") do 22 | redis.del('key1', 'key2') 23 | end 24 | 25 | assert_equal 2, redis.del('{key}1', '{key}2') 26 | end 27 | 28 | def test_migrate 29 | redis.set('mykey', 1) 30 | 31 | assert_raises(Redis::CommandError, 'ERR Target instance replied with error: MOVED 14687 127.0.0.1:7002') do 32 | # We cannot move between cluster nodes. 33 | redis.migrate('mykey', host: '127.0.0.1', port: 7000) 34 | end 35 | 36 | redis_cluster_mock(migrate: ->(*_) { '-IOERR error or timeout writing to target instance' }) do |redis| 37 | assert_raises(Redis::CommandError, 'IOERR error or timeout writing to target instance') do 38 | redis.migrate('mykey', host: '127.0.0.1', port: 11_211) 39 | end 40 | end 41 | 42 | redis_cluster_mock(migrate: ->(*_) { '+OK' }) do |redis| 43 | assert_equal 'OK', redis.migrate('mykey', host: '127.0.0.1', port: 6379) 44 | end 45 | end 46 | 47 | def test_object 48 | redis.lpush('mylist', 'Hello World') 49 | assert_equal 1, redis.object('refcount', 'mylist') 50 | assert(redis.object('idletime', 'mylist') >= 0) 51 | 52 | redis.set('foo', 1000) 53 | assert_equal 'int', redis.object('encoding', 'foo') 54 | 55 | redis.set('bar', '1000bar') 56 | assert_equal 'embstr', redis.object('encoding', 'bar') 57 | end 58 | 59 | def test_randomkey 60 | set_some_keys 61 | assert_equal true, redis.randomkey.is_a?(String) 62 | end 63 | 64 | def test_rename 65 | set_some_keys 66 | 67 | assert_raises(Redis::CommandError, "CROSSSLOT Keys in request don't hash to the same slot") do 68 | redis.rename('key1', 'key3') 69 | end 70 | 71 | assert_equal 'OK', redis.rename('{key}1', '{key}3') 72 | end 73 | 74 | def test_renamenx 75 | set_some_keys 76 | 77 | assert_raises(Redis::CommandError, "CROSSSLOT Keys in request don't hash to the same slot") do 78 | redis.renamenx('key1', 'key2') 79 | end 80 | 81 | assert_equal false, redis.renamenx('{key}1', '{key}2') 82 | end 83 | 84 | def test_sort 85 | redis.lpush('mylist', 3) 86 | redis.lpush('mylist', 1) 87 | redis.lpush('mylist', 5) 88 | redis.lpush('mylist', 2) 89 | redis.lpush('mylist', 4) 90 | assert_equal %w[1 2 3 4 5], redis.sort('mylist') 91 | end 92 | 93 | def test_touch 94 | set_some_keys 95 | assert_equal 1, redis.touch('key1') 96 | assert_equal 1, redis.touch('key2') 97 | if version < '6' 98 | assert_equal 1, redis.touch('key1', 'key2') 99 | else 100 | assert_raises(Redis::CommandError, "CROSSSLOT Keys in request don't hash to the same slot") do 101 | redis.touch('key1', 'key2') 102 | end 103 | end 104 | assert_equal 2, redis.touch('{key}1', '{key}2') 105 | end 106 | 107 | def test_unlink 108 | set_some_keys 109 | assert_raises(Redis::CommandError, "CROSSSLOT Keys in request don't hash to the same slot") do 110 | redis.unlink('key1', 'key2', 'key3') 111 | end 112 | assert_equal 2, redis.unlink('{key}1', '{key}2', '{key}3') 113 | end 114 | 115 | def test_wait 116 | set_some_keys 117 | assert_equal 3, redis.wait(1, TIMEOUT.to_i * 1000) 118 | end 119 | 120 | def test_scan 121 | set_some_keys 122 | 123 | cursor = 0 124 | all_keys = [] 125 | loop do 126 | cursor, keys = redis.scan(cursor, match: '{key}*') 127 | all_keys += keys 128 | break if cursor == '0' 129 | end 130 | 131 | assert_equal 2, all_keys.uniq.size 132 | end 133 | 134 | def test_scan_each 135 | require 'securerandom' 136 | 137 | 1000.times do |n| 138 | redis.set("test-#{::SecureRandom.uuid}", n) 139 | end 140 | 141 | 1000.times do |n| 142 | redis.set("random-#{::SecureRandom.uuid}", n) 143 | end 144 | 145 | keys_result = redis.keys('test-*') 146 | scan_result = redis.scan_each(match: 'test-*').to_a 147 | assert_equal(keys_result.size, 1000) 148 | assert_equal(scan_result.size, 1000) 149 | assert_equal(scan_result.sort, keys_result.sort) 150 | end 151 | end 152 | -------------------------------------------------------------------------------- /test/support/ssl/trusted-cert.crt: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 5 | 6c:d2:f2:de:f3:1e:0d:05:12:2d:54:44:54:e4:9c:f0:49:58:3e:84 6 | Signature Algorithm: sha256WithRSAEncryption 7 | Issuer: C=IT, ST=Sicily, L=Catania, O=Redis, OU=Security, CN=127.0.0.1 8 | Validity 9 | Not Before: Aug 21 03:19:15 2020 GMT 10 | Not After : Nov 11 03:19:15 2054 GMT 11 | Subject: C=IT, ST=Sicily, O=Redis, OU=Security, CN=127.0.0.1 12 | Subject Public Key Info: 13 | Public Key Algorithm: rsaEncryption 14 | RSA Public-Key: (2048 bit) 15 | Modulus: 16 | 00:e0:e6:bf:ae:5a:e7:f7:01:4a:53:dd:4b:d8:d2: 17 | 47:5b:20:b4:01:31:c2:bf:0a:07:e6:2a:60:a8:bf: 18 | 1f:01:3a:3f:b0:96:8b:0a:1a:2c:88:b9:bb:dc:3b: 19 | e7:9c:86:bd:43:f1:87:0d:56:5c:cf:58:31:ec:a4: 20 | 91:0b:a8:2c:76:57:f0:c4:98:c7:f8:bd:74:b2:d5: 21 | 30:ff:12:e3:2a:f0:c3:e9:18:81:9f:d1:43:46:c2: 22 | 89:61:3b:62:cb:8a:6b:21:a5:8a:59:4c:af:c8:8e: 23 | d2:3d:4a:77:7f:ac:f6:69:f6:e4:b7:47:30:a2:30: 24 | a0:2c:21:6b:a3:f8:c3:de:f1:63:62:09:72:71:38: 25 | 6d:02:5b:3a:3d:03:22:67:36:4f:97:91:55:e0:9c: 26 | c7:e8:63:bf:2c:d9:8d:53:fe:ae:d0:de:10:87:ef: 27 | 99:76:84:4e:bb:a6:fe:22:3e:09:98:54:2d:e7:a3: 28 | 54:a4:57:b2:53:a9:df:56:da:b5:1b:be:7f:e3:ae: 29 | 08:f8:f8:20:33:4f:29:4b:6d:24:d1:10:c4:e0:05: 30 | 25:07:cb:be:6d:c7:ff:89:e0:17:77:76:db:cb:4d: 31 | 75:e7:13:c1:6f:1f:5f:a4:9b:4c:b8:a9:38:e9:0a: 32 | 39:de:41:45:96:71:6b:eb:7a:27:6f:92:93:b0:aa: 33 | 35:71 34 | Exponent: 65537 (0x10001) 35 | X509v3 extensions: 36 | X509v3 Basic Constraints: 37 | CA:FALSE 38 | Netscape Comment: 39 | OpenSSL Generated Certificate 40 | X509v3 Subject Key Identifier: 41 | 56:AD:FB:49:9F:F6:B2:C0:07:21:57:D7:CE:6A:B2:ED:D7:30:74:57 42 | X509v3 Authority Key Identifier: 43 | keyid:46:9A:22:5C:67:A4:D4:39:36:1F:77:4B:A8:41:75:62:7B:4B:B0:E7 44 | 45 | Signature Algorithm: sha256WithRSAEncryption 46 | 5f:29:4e:fc:07:63:13:fd:84:91:90:cb:c5:f5:76:b2:b9:98: 47 | 15:42:d4:ef:44:fc:7a:60:35:9a:fb:ac:d4:c1:18:5b:c3:19: 48 | b3:4b:29:ee:e2:15:85:d5:1b:05:f5:62:86:87:aa:81:86:42: 49 | 12:25:ac:9e:f3:c6:51:c3:3d:0e:d2:00:db:74:bb:0f:d0:5f: 50 | bb:c5:8f:8f:79:45:0b:78:82:40:0c:fb:aa:fc:ef:5e:48:6c: 51 | e9:2b:4c:ac:a5:ab:e6:18:d7:8b:a6:4f:44:31:d3:81:d9:71: 52 | 2d:ed:76:9b:91:f5:ca:38:4e:ad:a9:66:00:a7:27:31:74:65: 53 | 11:a9:fa:11:91:03:d5:64:f5:43:98:6b:31:e5:f2:87:8c:4f: 54 | 52:8a:5c:a7:92:07:89:ab:44:8a:c4:87:07:1f:6a:f6:e4:7c: 55 | 42:31:bf:47:1a:5c:98:00:d2:aa:6c:75:ba:3d:24:9b:f3:e4: 56 | 04:c8:28:ea:97:4d:47:99:fc:69:f8:2c:62:44:a9:c8:82:0b: 57 | 26:85:bd:fb:e4:97:df:58:73:bf:09:1e:5d:73:05:16:42:47: 58 | 02:fb:b8:9d:81:1f:23:8b:b5:6c:5b:02:6d:3f:07:44:44:24: 59 | 19:ec:6e:57:10:7e:4a:cc:ac:18:79:e0:08:67:cd:6c:ee:61: 60 | fb:7d:46:22 61 | -----BEGIN CERTIFICATE----- 62 | MIIDxzCCAq+gAwIBAgIUbNLy3vMeDQUSLVREVOSc8ElYPoQwDQYJKoZIhvcNAQEL 63 | BQAwZzELMAkGA1UEBhMCSVQxDzANBgNVBAgMBlNpY2lseTEQMA4GA1UEBwwHQ2F0 64 | YW5pYTEOMAwGA1UECgwFUmVkaXMxETAPBgNVBAsMCFNlY3VyaXR5MRIwEAYDVQQD 65 | DAkxMjcuMC4wLjEwIBcNMjAwODIxMDMxOTE1WhgPMjA1NDExMTEwMzE5MTVaMFUx 66 | CzAJBgNVBAYTAklUMQ8wDQYDVQQIDAZTaWNpbHkxDjAMBgNVBAoMBVJlZGlzMREw 67 | DwYDVQQLDAhTZWN1cml0eTESMBAGA1UEAwwJMTI3LjAuMC4xMIIBIjANBgkqhkiG 68 | 9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4Oa/rlrn9wFKU91L2NJHWyC0ATHCvwoH5ipg 69 | qL8fATo/sJaLChosiLm73DvnnIa9Q/GHDVZcz1gx7KSRC6gsdlfwxJjH+L10stUw 70 | /xLjKvDD6RiBn9FDRsKJYTtiy4prIaWKWUyvyI7SPUp3f6z2afbkt0cwojCgLCFr 71 | o/jD3vFjYglycThtAls6PQMiZzZPl5FV4JzH6GO/LNmNU/6u0N4Qh++ZdoROu6b+ 72 | Ij4JmFQt56NUpFeyU6nfVtq1G75/464I+PggM08pS20k0RDE4AUlB8u+bcf/ieAX 73 | d3bby0115xPBbx9fpJtMuKk46Qo53kFFlnFr63onb5KTsKo1cQIDAQABo3sweTAJ 74 | BgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0 75 | aWZpY2F0ZTAdBgNVHQ4EFgQUVq37SZ/2ssAHIVfXzmqy7dcwdFcwHwYDVR0jBBgw 76 | FoAURpoiXGek1Dk2H3dLqEF1YntLsOcwDQYJKoZIhvcNAQELBQADggEBAF8pTvwH 77 | YxP9hJGQy8X1drK5mBVC1O9E/HpgNZr7rNTBGFvDGbNLKe7iFYXVGwX1YoaHqoGG 78 | QhIlrJ7zxlHDPQ7SANt0uw/QX7vFj495RQt4gkAM+6r8715IbOkrTKylq+YY14um 79 | T0Qx04HZcS3tdpuR9co4Tq2pZgCnJzF0ZRGp+hGRA9Vk9UOYazHl8oeMT1KKXKeS 80 | B4mrRIrEhwcfavbkfEIxv0caXJgA0qpsdbo9JJvz5ATIKOqXTUeZ/Gn4LGJEqciC 81 | CyaFvfvkl99Yc78JHl1zBRZCRwL7uJ2BHyOLtWxbAm0/B0REJBnsblcQfkrMrBh5 82 | 4AhnzWzuYft9RiI= 83 | -----END CERTIFICATE----- 84 | -------------------------------------------------------------------------------- /test/support/ssl/untrusted-cert.crt: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 5 | 0d:15:cb:66:e0:34:6b:99:27:79:be:de:f2:f2:b1:16:10:a7:6b:06 6 | Signature Algorithm: sha256WithRSAEncryption 7 | Issuer: C=XX, ST=Untrusted, L=Evilville, O=Evil Hacker, OU=Attack Department, CN=127.0.0.1 8 | Validity 9 | Not Before: Aug 21 03:19:25 2020 GMT 10 | Not After : Nov 11 03:19:25 2054 GMT 11 | Subject: C=XX, ST=Untrusted, O=Evil Hacker, OU=Attack Department, CN=127.0.0.1 12 | Subject Public Key Info: 13 | Public Key Algorithm: rsaEncryption 14 | RSA Public-Key: (2048 bit) 15 | Modulus: 16 | 00:c4:9c:33:ed:3d:fe:f9:0c:b7:46:56:04:a8:56: 17 | 0e:3b:55:34:a5:e2:22:ab:90:e9:f1:f9:25:44:01: 18 | 74:00:cb:25:27:e4:53:21:14:91:2b:9a:00:60:31: 19 | 6f:e7:65:88:93:99:5c:0b:b7:44:b0:b1:b6:5f:5d: 20 | d2:db:ab:84:51:31:2a:c3:73:67:a0:aa:04:47:c5: 21 | 60:5b:2f:39:fa:09:3b:09:47:97:ae:a8:ec:a1:7e: 22 | d1:22:7c:f1:1c:6d:b5:fe:3d:6e:96:fb:b4:70:25: 23 | 81:94:50:c9:ac:6f:dc:cd:5d:f9:1e:ed:18:8a:57: 24 | 3a:05:7f:f1:dd:12:af:86:b7:8e:b7:5d:2c:d7:c0: 25 | 6f:6d:98:5f:40:e4:fa:a3:ed:2c:43:a0:ac:6a:6a: 26 | 6c:41:e8:84:d2:1c:59:63:ec:d0:a5:c7:1f:50:85: 27 | e3:a8:54:95:bd:04:cb:99:5c:2a:6d:ee:04:ad:d7: 28 | 93:89:37:7c:a2:fd:f6:4e:c2:7a:4c:b2:f3:82:13: 29 | c3:a7:ef:c3:5a:ce:fb:de:08:b7:57:fb:18:c2:57: 30 | 40:9b:1a:b1:00:85:49:5e:93:c9:9c:02:1f:e9:76: 31 | 76:0f:59:2e:84:be:31:bd:09:73:c8:a3:92:23:3a: 32 | c0:03:99:d9:7e:98:9a:83:ea:69:39:69:d2:e0:b2: 33 | 48:0b 34 | Exponent: 65537 (0x10001) 35 | X509v3 extensions: 36 | X509v3 Basic Constraints: 37 | CA:FALSE 38 | Netscape Comment: 39 | OpenSSL Generated Certificate 40 | X509v3 Subject Key Identifier: 41 | E9:0A:98:2C:F0:CA:F7:5B:4B:D4:2C:64:62:44:65:17:5D:AE:71:0E 42 | X509v3 Authority Key Identifier: 43 | keyid:2E:46:12:EF:76:A6:45:22:CA:18:71:D8:10:DC:04:A7:8C:6E:6E:F6 44 | 45 | Signature Algorithm: sha256WithRSAEncryption 46 | a9:0a:60:a5:79:13:e8:ba:90:0e:49:73:59:bb:28:29:1c:36: 47 | 29:ff:dd:16:11:5c:8e:a3:dd:7c:9a:cc:26:df:f4:07:23:79: 48 | 5f:30:b9:e3:47:33:25:92:ce:ef:6d:37:a5:01:f5:a2:58:32: 49 | a9:24:7b:df:22:fb:c4:c5:e2:92:ac:94:ab:c5:38:ef:70:21: 50 | dd:a2:b4:9e:49:d9:32:23:87:ef:44:69:23:63:6f:96:73:73: 51 | b3:3d:ba:52:b9:94:dc:5d:50:13:d0:8d:af:6d:34:98:c0:ad: 52 | e1:b6:78:06:85:2a:e0:2c:a6:d0:f7:f4:79:79:04:72:ea:3b: 53 | 3c:43:0f:9e:5f:c5:11:64:9a:93:cb:df:0d:e6:3a:bc:5a:9c: 54 | 0e:6d:4b:2e:c3:5d:d9:8e:8d:93:8b:48:fa:85:87:ce:4b:88: 55 | 45:a7:c3:e2:eb:26:28:09:9f:58:cd:b0:a8:fb:4a:51:d8:13: 56 | 18:50:31:9e:20:0e:26:4c:be:10:54:62:34:2a:ca:23:88:0d: 57 | 81:6a:65:37:1c:14:b3:bf:63:11:cd:0b:1b:a2:fd:1e:f8:55: 58 | 82:e8:92:1f:59:f2:07:90:32:a4:c0:f9:cb:b8:9d:b2:f7:26: 59 | 73:0b:24:54:44:0a:96:20:f8:bd:4b:2b:ef:6b:79:00:c0:d8: 60 | 1f:24:50:b3 61 | -----BEGIN CERTIFICATE----- 62 | MIID7TCCAtWgAwIBAgIUDRXLZuA0a5kneb7e8vKxFhCnawYwDQYJKoZIhvcNAQEL 63 | BQAwezELMAkGA1UEBhMCWFgxEjAQBgNVBAgMCVVudHJ1c3RlZDESMBAGA1UEBwwJ 64 | RXZpbHZpbGxlMRQwEgYDVQQKDAtFdmlsIEhhY2tlcjEaMBgGA1UECwwRQXR0YWNr 65 | IERlcGFydG1lbnQxEjAQBgNVBAMMCTEyNy4wLjAuMTAgFw0yMDA4MjEwMzE5MjVa 66 | GA8yMDU0MTExMTAzMTkyNVowZzELMAkGA1UEBhMCWFgxEjAQBgNVBAgMCVVudHJ1 67 | c3RlZDEUMBIGA1UECgwLRXZpbCBIYWNrZXIxGjAYBgNVBAsMEUF0dGFjayBEZXBh 68 | cnRtZW50MRIwEAYDVQQDDAkxMjcuMC4wLjEwggEiMA0GCSqGSIb3DQEBAQUAA4IB 69 | DwAwggEKAoIBAQDEnDPtPf75DLdGVgSoVg47VTSl4iKrkOnx+SVEAXQAyyUn5FMh 70 | FJErmgBgMW/nZYiTmVwLt0SwsbZfXdLbq4RRMSrDc2egqgRHxWBbLzn6CTsJR5eu 71 | qOyhftEifPEcbbX+PW6W+7RwJYGUUMmsb9zNXfke7RiKVzoFf/HdEq+Gt463XSzX 72 | wG9tmF9A5Pqj7SxDoKxqamxB6ITSHFlj7NClxx9QheOoVJW9BMuZXCpt7gSt15OJ 73 | N3yi/fZOwnpMsvOCE8On78NazvveCLdX+xjCV0CbGrEAhUlek8mcAh/pdnYPWS6E 74 | vjG9CXPIo5IjOsADmdl+mJqD6mk5adLgskgLAgMBAAGjezB5MAkGA1UdEwQCMAAw 75 | LAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0G 76 | A1UdDgQWBBTpCpgs8Mr3W0vULGRiRGUXXa5xDjAfBgNVHSMEGDAWgBQuRhLvdqZF 77 | IsoYcdgQ3ASnjG5u9jANBgkqhkiG9w0BAQsFAAOCAQEAqQpgpXkT6LqQDklzWbso 78 | KRw2Kf/dFhFcjqPdfJrMJt/0ByN5XzC540czJZLO7203pQH1olgyqSR73yL7xMXi 79 | kqyUq8U473Ah3aK0nknZMiOH70RpI2NvlnNzsz26UrmU3F1QE9CNr200mMCt4bZ4 80 | BoUq4Cym0Pf0eXkEcuo7PEMPnl/FEWSak8vfDeY6vFqcDm1LLsNd2Y6Nk4tI+oWH 81 | zkuIRafD4usmKAmfWM2wqPtKUdgTGFAxniAOJky+EFRiNCrKI4gNgWplNxwUs79j 82 | Ec0LG6L9HvhVguiSH1nyB5AypMD5y7idsvcmcwskVEQKliD4vUsr72t5AMDYHyRQ 83 | sw== 84 | -----END CERTIFICATE----- 85 | -------------------------------------------------------------------------------- /test/distributed/commands_requiring_clustering_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "helper" 4 | 5 | class TestDistributedCommandsRequiringClustering < Minitest::Test 6 | include Helper::Distributed 7 | 8 | def test_rename 9 | r.set("{qux}foo", "s1") 10 | r.rename "{qux}foo", "{qux}bar" 11 | 12 | assert_equal "s1", r.get("{qux}bar") 13 | assert_nil r.get("{qux}foo") 14 | end 15 | 16 | def test_renamenx 17 | r.set("{qux}foo", "s1") 18 | r.set("{qux}bar", "s2") 19 | 20 | assert_equal false, r.renamenx("{qux}foo", "{qux}bar") 21 | 22 | assert_equal "s1", r.get("{qux}foo") 23 | assert_equal "s2", r.get("{qux}bar") 24 | end 25 | 26 | def test_lmove 27 | target_version "6.2" do 28 | r.rpush("{qux}foo", "s1") 29 | r.rpush("{qux}foo", "s2") 30 | r.rpush("{qux}bar", "s3") 31 | r.rpush("{qux}bar", "s4") 32 | 33 | assert_equal "s1", r.lmove("{qux}foo", "{qux}bar", "LEFT", "RIGHT") 34 | assert_equal ["s2"], r.lrange("{qux}foo", 0, -1) 35 | assert_equal ["s3", "s4", "s1"], r.lrange("{qux}bar", 0, -1) 36 | end 37 | end 38 | 39 | def test_brpoplpush 40 | r.rpush "{qux}foo", "s1" 41 | r.rpush "{qux}foo", "s2" 42 | 43 | assert_equal "s2", r.brpoplpush("{qux}foo", "{qux}bar", timeout: 1) 44 | assert_equal ["s2"], r.lrange("{qux}bar", 0, -1) 45 | end 46 | 47 | def test_rpoplpush 48 | r.rpush "{qux}foo", "s1" 49 | r.rpush "{qux}foo", "s2" 50 | 51 | assert_equal "s2", r.rpoplpush("{qux}foo", "{qux}bar") 52 | assert_equal ["s2"], r.lrange("{qux}bar", 0, -1) 53 | assert_equal "s1", r.rpoplpush("{qux}foo", "{qux}bar") 54 | assert_equal ["s1", "s2"], r.lrange("{qux}bar", 0, -1) 55 | end 56 | 57 | def test_smove 58 | r.sadd "{qux}foo", "s1" 59 | r.sadd "{qux}bar", "s2" 60 | 61 | assert r.smove("{qux}foo", "{qux}bar", "s1") 62 | assert r.sismember("{qux}bar", "s1") 63 | end 64 | 65 | def test_sinter 66 | r.sadd "{qux}foo", "s1" 67 | r.sadd "{qux}foo", "s2" 68 | r.sadd "{qux}bar", "s2" 69 | 70 | assert_equal ["s2"], r.sinter("{qux}foo", "{qux}bar") 71 | end 72 | 73 | def test_sinterstore 74 | r.sadd "{qux}foo", "s1" 75 | r.sadd "{qux}foo", "s2" 76 | r.sadd "{qux}bar", "s2" 77 | 78 | r.sinterstore("{qux}baz", "{qux}foo", "{qux}bar") 79 | 80 | assert_equal ["s2"], r.smembers("{qux}baz") 81 | end 82 | 83 | def test_sunion 84 | r.sadd "{qux}foo", "s1" 85 | r.sadd "{qux}foo", "s2" 86 | r.sadd "{qux}bar", "s2" 87 | r.sadd "{qux}bar", "s3" 88 | 89 | assert_equal ["s1", "s2", "s3"], r.sunion("{qux}foo", "{qux}bar").sort 90 | end 91 | 92 | def test_sunionstore 93 | r.sadd "{qux}foo", "s1" 94 | r.sadd "{qux}foo", "s2" 95 | r.sadd "{qux}bar", "s2" 96 | r.sadd "{qux}bar", "s3" 97 | 98 | r.sunionstore("{qux}baz", "{qux}foo", "{qux}bar") 99 | 100 | assert_equal ["s1", "s2", "s3"], r.smembers("{qux}baz").sort 101 | end 102 | 103 | def test_sdiff 104 | r.sadd "{qux}foo", "s1" 105 | r.sadd "{qux}foo", "s2" 106 | r.sadd "{qux}bar", "s2" 107 | r.sadd "{qux}bar", "s3" 108 | 109 | assert_equal ["s1"], r.sdiff("{qux}foo", "{qux}bar") 110 | assert_equal ["s3"], r.sdiff("{qux}bar", "{qux}foo") 111 | end 112 | 113 | def test_sdiffstore 114 | r.sadd "{qux}foo", "s1" 115 | r.sadd "{qux}foo", "s2" 116 | r.sadd "{qux}bar", "s2" 117 | r.sadd "{qux}bar", "s3" 118 | 119 | r.sdiffstore("{qux}baz", "{qux}foo", "{qux}bar") 120 | 121 | assert_equal ["s1"], r.smembers("{qux}baz") 122 | end 123 | 124 | def test_sort 125 | r.set("{qux}foo:1", "s1") 126 | r.set("{qux}foo:2", "s2") 127 | 128 | r.rpush("{qux}bar", "1") 129 | r.rpush("{qux}bar", "2") 130 | 131 | assert_equal ["s1"], r.sort("{qux}bar", get: "{qux}foo:*", limit: [0, 1]) 132 | assert_equal ["s2"], r.sort("{qux}bar", get: "{qux}foo:*", limit: [0, 1], order: "desc alpha") 133 | end 134 | 135 | def test_sort_with_an_array_of_gets 136 | r.set("{qux}foo:1:a", "s1a") 137 | r.set("{qux}foo:1:b", "s1b") 138 | 139 | r.set("{qux}foo:2:a", "s2a") 140 | r.set("{qux}foo:2:b", "s2b") 141 | 142 | r.rpush("{qux}bar", "1") 143 | r.rpush("{qux}bar", "2") 144 | 145 | assert_equal [["s1a", "s1b"]], r.sort("{qux}bar", get: ["{qux}foo:*:a", "{qux}foo:*:b"], limit: [0, 1]) 146 | assert_equal [["s2a", "s2b"]], r.sort("{qux}bar", get: ["{qux}foo:*:a", "{qux}foo:*:b"], limit: [0, 1], order: "desc alpha") 147 | assert_equal [["s1a", "s1b"], ["s2a", "s2b"]], r.sort("{qux}bar", get: ["{qux}foo:*:a", "{qux}foo:*:b"]) 148 | end 149 | 150 | def test_sort_with_store 151 | r.set("{qux}foo:1", "s1") 152 | r.set("{qux}foo:2", "s2") 153 | 154 | r.rpush("{qux}bar", "1") 155 | r.rpush("{qux}bar", "2") 156 | 157 | r.sort("{qux}bar", get: "{qux}foo:*", store: "{qux}baz") 158 | assert_equal ["s1", "s2"], r.lrange("{qux}baz", 0, -1) 159 | end 160 | 161 | def test_bitop 162 | r.set("{qux}foo", "a") 163 | r.set("{qux}bar", "b") 164 | 165 | r.bitop(:and, "{qux}foo&bar", "{qux}foo", "{qux}bar") 166 | assert_equal "\x60", r.get("{qux}foo&bar") 167 | r.bitop(:or, "{qux}foo|bar", "{qux}foo", "{qux}bar") 168 | assert_equal "\x63", r.get("{qux}foo|bar") 169 | r.bitop(:xor, "{qux}foo^bar", "{qux}foo", "{qux}bar") 170 | assert_equal "\x03", r.get("{qux}foo^bar") 171 | r.bitop(:not, "{qux}~foo", "{qux}foo") 172 | assert_equal "\x9E".b, r.get("{qux}~foo") 173 | end 174 | end 175 | --------------------------------------------------------------------------------