├── test ├── db │ └── .gitkeep ├── support │ ├── connection │ │ ├── hiredis.rb │ │ ├── ruby.rb │ │ └── synchrony.rb │ ├── wire │ │ ├── thread.rb │ │ └── synchrony.rb │ └── redis_mock.rb ├── test.conf ├── distributed_commands_on_hashes_test.rb ├── unknown_commands_test.rb ├── encoding_test.rb ├── distributed_commands_on_sorted_sets_test.rb ├── helper_test.rb ├── distributed_connection_handling_test.rb ├── distributed_sorting_test.rb ├── commands_on_hyper_log_log_test.rb ├── commands_on_hashes_test.rb ├── commands_on_lists_test.rb ├── distributed_commands_on_hyper_log_log_test.rb ├── distributed_commands_on_lists_test.rb ├── persistence_control_commands_test.rb ├── command_map_test.rb ├── distributed_persistence_control_commands_test.rb ├── thread_safety_test.rb ├── distributed_transactions_test.rb ├── blocking_commands_test.rb ├── lint │ ├── hyper_log_log.rb │ ├── value_types.rb │ ├── sets.rb │ ├── lists.rb │ ├── hashes.rb │ ├── blocking_commands.rb │ ├── strings.rb │ └── sorted_sets.rb ├── distributed_blocking_commands_test.rb ├── distributed_commands_on_strings_test.rb ├── distributed_key_tags_test.rb ├── error_replies_test.rb ├── distributed_test.rb ├── distributed_remote_server_control_commands_test.rb ├── sorting_test.rb ├── commands_on_sets_test.rb ├── bitpos_test.rb ├── fork_safety_test.rb ├── distributed_commands_on_sets_test.rb ├── synchrony_driver.rb ├── distributed_commands_on_value_types_test.rb ├── distributed_publish_subscribe_test.rb ├── distributed_internals_test.rb ├── scripting_test.rb ├── remote_server_control_commands_test.rb ├── commands_on_strings_test.rb ├── distributed_scripting_test.rb ├── commands_on_value_types_test.rb ├── commands_on_sorted_sets_test.rb ├── url_param_test.rb ├── distributed_commands_requiring_clustering_test.rb ├── publish_subscribe_test.rb ├── connection_handling_test.rb ├── helper.rb ├── pipelining_commands_test.rb ├── transactions_test.rb └── scanning_test.rb ├── lib └── redis │ ├── version.rb │ ├── connection │ ├── registry.rb │ ├── command_helper.rb │ ├── hiredis.rb │ ├── synchrony.rb │ └── ruby.rb │ ├── connection.rb │ ├── errors.rb │ ├── subscribe.rb │ ├── pipeline.rb │ └── hash_ring.rb ├── Gemfile ├── .yardopts ├── examples ├── unicorn │ ├── config.ru │ └── unicorn.rb ├── basic.rb ├── incr-decr.rb ├── list.rb ├── sets.rb ├── pubsub.rb └── dist_redis.rb ├── .gitignore ├── .travis └── Gemfile ├── benchmarking ├── speed.rb ├── suite.rb ├── pipeline.rb ├── logging.rb └── worker.rb ├── LICENSE ├── redis.gemspec ├── .travis.yml ├── Rakefile ├── .order ├── README.md └── CHANGELOG.md /test/db/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/redis/version.rb: -------------------------------------------------------------------------------- 1 | class Redis 2 | VERSION = "3.1.0" 3 | end 4 | -------------------------------------------------------------------------------- /test/support/connection/hiredis.rb: -------------------------------------------------------------------------------- 1 | require "support/wire/thread" 2 | -------------------------------------------------------------------------------- /test/support/connection/ruby.rb: -------------------------------------------------------------------------------- 1 | require "support/wire/thread" 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | source 'https://rubygems.org' 3 | 4 | gemspec 5 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --exclude redis/connection 2 | --exclude redis/compat 3 | --markup markdown 4 | -------------------------------------------------------------------------------- /test/support/wire/thread.rb: -------------------------------------------------------------------------------- 1 | class Wire < Thread 2 | def self.sleep(sec) 3 | Kernel.sleep(sec) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /examples/unicorn/config.ru: -------------------------------------------------------------------------------- 1 | run lambda { |env| 2 | [200, {"Content-Type" => "text/plain"}, [Redis.current.randomkey]] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.rdb 2 | *.swp 3 | Gemfile.lock 4 | *.gem 5 | /tmp/ 6 | /.idea 7 | /.yardoc 8 | /coverage/* 9 | /doc/ 10 | /nohup.out 11 | /pkg/* 12 | /rdsrv 13 | /redis/* 14 | /test/db 15 | -------------------------------------------------------------------------------- /test/test.conf: -------------------------------------------------------------------------------- 1 | dir ./test/db 2 | pidfile ./redis.pid 3 | port 6381 4 | unixsocket ./redis.sock 5 | timeout 300 6 | loglevel debug 7 | logfile stdout 8 | databases 16 9 | daemonize yes 10 | -------------------------------------------------------------------------------- /examples/basic.rb: -------------------------------------------------------------------------------- 1 | require 'redis' 2 | 3 | r = Redis.new 4 | 5 | r.del('foo') 6 | 7 | puts 8 | 9 | p'set foo to "bar"' 10 | r['foo'] = 'bar' 11 | 12 | puts 13 | 14 | p 'value of foo' 15 | p r['foo'] 16 | -------------------------------------------------------------------------------- /.travis/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec :path => "../" 4 | 5 | case ENV["conn"] 6 | when "hiredis" 7 | gem "hiredis" 8 | when "synchrony" 9 | gem "hiredis" 10 | gem "em-synchrony" 11 | end 12 | -------------------------------------------------------------------------------- /examples/incr-decr.rb: -------------------------------------------------------------------------------- 1 | require 'redis' 2 | 3 | r = Redis.new 4 | 5 | puts 6 | p 'incr' 7 | r.del 'counter' 8 | 9 | p r.incr('counter') 10 | p r.incr('counter') 11 | p r.incr('counter') 12 | 13 | puts 14 | p 'decr' 15 | p r.decr('counter') 16 | p r.decr('counter') 17 | p r.decr('counter') 18 | -------------------------------------------------------------------------------- /test/support/connection/synchrony.rb: -------------------------------------------------------------------------------- 1 | require "support/wire/synchrony" 2 | 3 | module Helper 4 | def around 5 | rv = nil 6 | 7 | EM.synchrony do 8 | begin 9 | rv = yield 10 | ensure 11 | EM.stop 12 | end 13 | end 14 | 15 | rv 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/distributed_commands_on_hashes_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | require "lint/hashes" 5 | 6 | class TestDistributedCommandsOnHashes < Test::Unit::TestCase 7 | 8 | include Helper::Distributed 9 | include Lint::Hashes 10 | end 11 | -------------------------------------------------------------------------------- /test/unknown_commands_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | 5 | class TestUnknownCommands < Test::Unit::TestCase 6 | 7 | include Helper::Client 8 | 9 | def test_should_try_to_work 10 | assert_raise Redis::CommandError do 11 | r.not_yet_implemented_command 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /benchmarking/speed.rb: -------------------------------------------------------------------------------- 1 | # Run with 2 | # 3 | # $ ruby -Ilib benchmarking/speed.rb 4 | # 5 | 6 | require "benchmark" 7 | require "redis" 8 | 9 | r = Redis.new 10 | n = (ARGV.shift || 20000).to_i 11 | 12 | elapsed = Benchmark.realtime do 13 | # n sets, n gets 14 | n.times do |i| 15 | key = "foo#{i}" 16 | r[key] = key * 10 17 | r[key] 18 | end 19 | end 20 | 21 | puts '%.2f Kops' % (2 * n / 1000 / elapsed) 22 | -------------------------------------------------------------------------------- /lib/redis/connection/registry.rb: -------------------------------------------------------------------------------- 1 | class Redis 2 | module Connection 3 | 4 | # Store a list of loaded connection drivers in the Connection module. 5 | # Redis::Client uses the last required driver by default, and will be aware 6 | # of the loaded connection drivers if the user chooses to override the 7 | # default connection driver. 8 | def self.drivers 9 | @drivers ||= [] 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/encoding_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | 5 | class TestEncoding < Test::Unit::TestCase 6 | 7 | include Helper::Client 8 | 9 | def test_returns_properly_encoded_strings 10 | if defined?(Encoding) 11 | with_external_encoding("UTF-8") do 12 | r.set "foo", "שלום" 13 | 14 | assert_equal "Shalom שלום", "Shalom " + r.get("foo") 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/distributed_commands_on_sorted_sets_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | require "lint/sorted_sets" 5 | 6 | class TestDistributedCommandsOnSortedSets < Test::Unit::TestCase 7 | 8 | include Helper::Distributed 9 | include Lint::SortedSets 10 | 11 | def test_zcount 12 | r.zadd "foo", 1, "s1" 13 | r.zadd "foo", 2, "s2" 14 | r.zadd "foo", 3, "s3" 15 | 16 | assert_equal 2, r.zcount("foo", 2, 3) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/helper_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | 5 | class TestHelper < Test::Unit::TestCase 6 | 7 | include Helper 8 | 9 | def test_version_comparison 10 | v = Version.new("2.0.1") 11 | 12 | assert v > "1" 13 | assert v > "2" 14 | assert v < "3" 15 | assert v < "10" 16 | 17 | assert v < "2.1" 18 | assert v < "2.0.2" 19 | assert v < "2.0.1.1" 20 | assert v < "2.0.10" 21 | 22 | assert v == "2.0.1" 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/distributed_connection_handling_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | 5 | class TestDistributedConnectionHandling < Test::Unit::TestCase 6 | 7 | include Helper::Distributed 8 | 9 | def test_ping 10 | assert_equal ["PONG"], r.ping 11 | end 12 | 13 | def test_select 14 | r.set "foo", "bar" 15 | 16 | r.select 14 17 | assert_equal nil, r.get("foo") 18 | 19 | r.select 15 20 | 21 | assert_equal "bar", r.get("foo") 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/distributed_sorting_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | 5 | class TestDistributedSorting < Test::Unit::TestCase 6 | 7 | include Helper::Distributed 8 | 9 | def test_sort 10 | assert_raise(Redis::Distributed::CannotDistribute) do 11 | r.set("foo:1", "s1") 12 | r.set("foo:2", "s2") 13 | 14 | r.rpush("bar", "1") 15 | r.rpush("bar", "2") 16 | 17 | r.sort("bar", :get => "foo:*", :limit => [0, 1]) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/commands_on_hyper_log_log_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | require "lint/hyper_log_log" 5 | 6 | class TestCommandsOnHyperLogLog < Test::Unit::TestCase 7 | 8 | include Helper::Client 9 | include Lint::HyperLogLog 10 | 11 | def test_pfmerge 12 | target_version "2.8.9" do 13 | r.pfadd "foo", "s1" 14 | r.pfadd "bar", "s2" 15 | 16 | assert_equal true, r.pfmerge("res", "foo", "bar") 17 | assert_equal 2, r.pfcount("res") 18 | end 19 | end 20 | 21 | end -------------------------------------------------------------------------------- /lib/redis/connection.rb: -------------------------------------------------------------------------------- 1 | require "redis/connection/registry" 2 | 3 | # If a connection driver was required before this file, the array 4 | # Redis::Connection.drivers will contain one or more classes. The last driver 5 | # in this array will be used as default driver. If this array is empty, we load 6 | # the plain Ruby driver as our default. Another driver can be required at a 7 | # later point in time, causing it to be the last element of the #drivers array 8 | # and therefore be chosen by default. 9 | require "redis/connection/ruby" if Redis::Connection.drivers.empty? -------------------------------------------------------------------------------- /examples/list.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'redis' 3 | 4 | r = Redis.new 5 | 6 | r.del 'logs' 7 | 8 | puts 9 | 10 | p "pushing log messages into a LIST" 11 | r.rpush 'logs', 'some log message' 12 | r.rpush 'logs', 'another log message' 13 | r.rpush 'logs', 'yet another log message' 14 | r.rpush 'logs', 'also another log message' 15 | 16 | puts 17 | p 'contents of logs LIST' 18 | 19 | p r.lrange('logs', 0, -1) 20 | 21 | puts 22 | p 'Trim logs LIST to last 2 elements(easy circular buffer)' 23 | 24 | r.ltrim('logs', -2, -1) 25 | 26 | p r.lrange('logs', 0, -1) 27 | -------------------------------------------------------------------------------- /test/commands_on_hashes_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | require "lint/hashes" 5 | 6 | class TestCommandsOnHashes < Test::Unit::TestCase 7 | 8 | include Helper::Client 9 | include Lint::Hashes 10 | 11 | def test_mapped_hmget_in_a_pipeline_returns_hash 12 | r.hset("foo", "f1", "s1") 13 | r.hset("foo", "f2", "s2") 14 | 15 | result = r.pipelined do 16 | r.mapped_hmget("foo", "f1", "f2") 17 | end 18 | 19 | assert_equal result[0], { "f1" => "s1", "f2" => "s2" } 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/commands_on_lists_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | require "lint/lists" 5 | 6 | class TestCommandsOnLists < Test::Unit::TestCase 7 | 8 | include Helper::Client 9 | include Lint::Lists 10 | 11 | def test_rpoplpush 12 | r.rpush "foo", "s1" 13 | r.rpush "foo", "s2" 14 | 15 | assert_equal "s2", r.rpoplpush("foo", "bar") 16 | assert_equal ["s2"], r.lrange("bar", 0, -1) 17 | assert_equal "s1", r.rpoplpush("foo", "bar") 18 | assert_equal ["s1", "s2"], r.lrange("bar", 0, -1) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/support/wire/synchrony.rb: -------------------------------------------------------------------------------- 1 | class Wire < Fiber 2 | # We cannot run this fiber explicitly because EM schedules it. Resuming the 3 | # current fiber on the next tick to let the reactor do work. 4 | def self.pass 5 | f = Fiber.current 6 | EM.next_tick { f.resume } 7 | Fiber.yield 8 | end 9 | 10 | def self.sleep(sec) 11 | EM::Synchrony.sleep(sec) 12 | end 13 | 14 | def initialize(&blk) 15 | super 16 | 17 | # Schedule run in next tick 18 | EM.next_tick { resume } 19 | end 20 | 21 | def join 22 | self.class.pass while alive? 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/distributed_commands_on_hyper_log_log_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | require "lint/hyper_log_log" 5 | 6 | class TestDistributedCommandsOnHyperLogLog < Test::Unit::TestCase 7 | 8 | include Helper::Distributed 9 | include Lint::HyperLogLog 10 | 11 | def test_pfmerge 12 | target_version "2.8.9" do 13 | assert_raise Redis::Distributed::CannotDistribute do 14 | r.pfadd "foo", "s1" 15 | r.pfadd "bar", "s2" 16 | 17 | assert r.pfmerge("res", "foo", "bar") 18 | end 19 | end 20 | end 21 | 22 | end -------------------------------------------------------------------------------- /test/distributed_commands_on_lists_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | require "lint/lists" 5 | 6 | class TestDistributedCommandsOnLists < Test::Unit::TestCase 7 | 8 | include Helper::Distributed 9 | include Lint::Lists 10 | 11 | def test_rpoplpush 12 | assert_raise Redis::Distributed::CannotDistribute do 13 | r.rpoplpush("foo", "bar") 14 | end 15 | end 16 | 17 | def test_brpoplpush 18 | assert_raise Redis::Distributed::CannotDistribute do 19 | r.brpoplpush("foo", "bar", :timeout => 1) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /examples/unicorn/unicorn.rb: -------------------------------------------------------------------------------- 1 | require "redis" 2 | 3 | worker_processes 3 4 | 5 | # If you set the connection to Redis *before* forking, 6 | # you will cause forks to share a file descriptor. 7 | # 8 | # This causes a concurrency problem by which one fork 9 | # can read or write to the socket while others are 10 | # performing other operations. 11 | # 12 | # Most likely you'll be getting ProtocolError exceptions 13 | # mentioning a wrong initial byte in the reply. 14 | # 15 | # Thus we need to connect to Redis after forking the 16 | # worker processes. 17 | 18 | after_fork do |server, worker| 19 | Redis.current.quit 20 | end 21 | -------------------------------------------------------------------------------- /examples/sets.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'redis' 3 | 4 | r = Redis.new 5 | 6 | r.del 'foo-tags' 7 | r.del 'bar-tags' 8 | 9 | puts 10 | p "create a set of tags on foo-tags" 11 | 12 | r.sadd 'foo-tags', 'one' 13 | r.sadd 'foo-tags', 'two' 14 | r.sadd 'foo-tags', 'three' 15 | 16 | puts 17 | p "create a set of tags on bar-tags" 18 | 19 | r.sadd 'bar-tags', 'three' 20 | r.sadd 'bar-tags', 'four' 21 | r.sadd 'bar-tags', 'five' 22 | 23 | puts 24 | p 'foo-tags' 25 | 26 | p r.smembers('foo-tags') 27 | 28 | puts 29 | p 'bar-tags' 30 | 31 | p r.smembers('bar-tags') 32 | 33 | puts 34 | p 'intersection of foo-tags and bar-tags' 35 | 36 | p r.sinter('foo-tags', 'bar-tags') 37 | -------------------------------------------------------------------------------- /test/persistence_control_commands_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | 5 | class TestPersistenceControlCommands < Test::Unit::TestCase 6 | 7 | include Helper::Client 8 | 9 | def test_save 10 | redis_mock(:save => lambda { "+SAVE" }) do |redis| 11 | assert_equal "SAVE", redis.save 12 | end 13 | end 14 | 15 | def test_bgsave 16 | redis_mock(:bgsave => lambda { "+BGSAVE" }) do |redis| 17 | assert_equal "BGSAVE", redis.bgsave 18 | end 19 | end 20 | 21 | def test_lastsave 22 | redis_mock(:lastsave => lambda { "+LASTSAVE" }) do |redis| 23 | assert_equal "LASTSAVE", redis.lastsave 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/command_map_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | 5 | class TestCommandMap < Test::Unit::TestCase 6 | 7 | include Helper::Client 8 | 9 | def test_override_existing_commands 10 | r.set("counter", 1) 11 | 12 | assert_equal 2, r.incr("counter") 13 | 14 | r.client.command_map[:incr] = :decr 15 | 16 | assert_equal 1, r.incr("counter") 17 | end 18 | 19 | def test_override_non_existing_commands 20 | r.set("key", "value") 21 | 22 | assert_raise Redis::CommandError do 23 | r.idontexist("key") 24 | end 25 | 26 | r.client.command_map[:idontexist] = :get 27 | 28 | assert_equal "value", r.idontexist("key") 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/distributed_persistence_control_commands_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | 5 | class TestDistributedPersistenceControlCommands < Test::Unit::TestCase 6 | 7 | include Helper::Distributed 8 | 9 | def test_save 10 | redis_mock(:save => lambda { "+SAVE" }) do |redis| 11 | assert_equal ["SAVE"], redis.save 12 | end 13 | end 14 | 15 | def test_bgsave 16 | redis_mock(:bgsave => lambda { "+BGSAVE" }) do |redis| 17 | assert_equal ["BGSAVE"], redis.bgsave 18 | end 19 | end 20 | 21 | def test_lastsave 22 | redis_mock(:lastsave => lambda { "+LASTSAVE" }) do |redis| 23 | assert_equal ["LASTSAVE"], redis.lastsave 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/thread_safety_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | 5 | class TestThreadSafety < Test::Unit::TestCase 6 | 7 | include Helper::Client 8 | 9 | driver(:ruby, :hiredis) do 10 | def test_thread_safety 11 | redis = Redis.new(OPTIONS) 12 | redis.set "foo", 1 13 | redis.set "bar", 2 14 | 15 | sample = 100 16 | 17 | t1 = Thread.new do 18 | $foos = Array.new(sample) { redis.get "foo" } 19 | end 20 | 21 | t2 = Thread.new do 22 | $bars = Array.new(sample) { redis.get "bar" } 23 | end 24 | 25 | t1.join 26 | t2.join 27 | 28 | assert_equal ["1"], $foos.uniq 29 | assert_equal ["2"], $bars.uniq 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/distributed_transactions_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | 5 | class TestDistributedTransactions < Test::Unit::TestCase 6 | 7 | include Helper::Distributed 8 | 9 | def test_multi_discard 10 | @foo = nil 11 | 12 | assert_raise Redis::Distributed::CannotDistribute do 13 | r.multi { @foo = 1 } 14 | end 15 | 16 | assert_equal nil, @foo 17 | 18 | assert_raise Redis::Distributed::CannotDistribute do 19 | r.discard 20 | end 21 | end 22 | 23 | def test_watch_unwatch 24 | assert_raise Redis::Distributed::CannotDistribute do 25 | r.watch("foo") 26 | end 27 | 28 | assert_raise Redis::Distributed::CannotDistribute do 29 | r.unwatch 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /benchmarking/suite.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | def run_in_background(command) 4 | fork { system command } 5 | end 6 | 7 | def with_all_segments(&block) 8 | 0.upto(9) do |segment_number| 9 | block_size = 100000 10 | start_index = segment_number * block_size 11 | end_index = start_index + block_size - 1 12 | block.call(start_index, end_index) 13 | end 14 | end 15 | 16 | #with_all_segments do |start_index, end_index| 17 | # puts "Initializing keys from #{start_index} to #{end_index}" 18 | # system "ruby worker.rb initialize #{start_index} #{end_index} 0" 19 | #end 20 | 21 | with_all_segments do |start_index, end_index| 22 | run_in_background "ruby worker.rb write #{start_index} #{end_index} 10" 23 | run_in_background "ruby worker.rb read #{start_index} #{end_index} 1" 24 | end 25 | -------------------------------------------------------------------------------- /examples/pubsub.rb: -------------------------------------------------------------------------------- 1 | require "redis" 2 | 3 | puts <<-EOS 4 | To play with this example use redis-cli from another terminal, like this: 5 | 6 | $ redis-cli publish one hello 7 | 8 | Finally force the example to exit sending the 'exit' message with: 9 | 10 | $ redis-cli publish two exit 11 | 12 | EOS 13 | 14 | redis = Redis.new 15 | 16 | trap(:INT) { puts; exit } 17 | 18 | begin 19 | redis.subscribe(:one, :two) do |on| 20 | on.subscribe do |channel, subscriptions| 21 | puts "Subscribed to ##{channel} (#{subscriptions} subscriptions)" 22 | end 23 | 24 | on.message do |channel, message| 25 | puts "##{channel}: #{message}" 26 | redis.unsubscribe if message == "exit" 27 | end 28 | 29 | on.unsubscribe do |channel, subscriptions| 30 | puts "Unsubscribed from ##{channel} (#{subscriptions} subscriptions)" 31 | end 32 | end 33 | rescue Redis::BaseConnectionError => error 34 | puts "#{error}, retrying in 1s" 35 | sleep 1 36 | retry 37 | end 38 | -------------------------------------------------------------------------------- /examples/dist_redis.rb: -------------------------------------------------------------------------------- 1 | require "redis" 2 | require "redis/distributed" 3 | 4 | r = Redis::Distributed.new %w[redis://localhost:6379 redis://localhost:6380 redis://localhost:6381 redis://localhost:6382] 5 | 6 | r.flushdb 7 | 8 | r['urmom'] = 'urmom' 9 | r['urdad'] = 'urdad' 10 | r['urmom1'] = 'urmom1' 11 | r['urdad1'] = 'urdad1' 12 | r['urmom2'] = 'urmom2' 13 | r['urdad2'] = 'urdad2' 14 | r['urmom3'] = 'urmom3' 15 | r['urdad3'] = 'urdad3' 16 | p r['urmom'] 17 | p r['urdad'] 18 | p r['urmom1'] 19 | p r['urdad1'] 20 | p r['urmom2'] 21 | p r['urdad2'] 22 | p r['urmom3'] 23 | p r['urdad3'] 24 | 25 | r.rpush 'listor', 'foo1' 26 | r.rpush 'listor', 'foo2' 27 | r.rpush 'listor', 'foo3' 28 | r.rpush 'listor', 'foo4' 29 | r.rpush 'listor', 'foo5' 30 | 31 | p r.rpop('listor') 32 | p r.rpop('listor') 33 | p r.rpop('listor') 34 | p r.rpop('listor') 35 | p r.rpop('listor') 36 | 37 | puts "key distribution:" 38 | 39 | r.ring.nodes.each do |node| 40 | p [node.client, node.keys("*")] 41 | end 42 | r.flushdb 43 | p r.keys('*') 44 | -------------------------------------------------------------------------------- /lib/redis/connection/command_helper.rb: -------------------------------------------------------------------------------- 1 | class Redis 2 | module Connection 3 | module CommandHelper 4 | 5 | COMMAND_DELIMITER = "\r\n" 6 | 7 | def build_command(args) 8 | command = [nil] 9 | 10 | args.each do |i| 11 | if i.is_a? Array 12 | i.each do |j| 13 | j = j.to_s 14 | command << "$#{j.bytesize}" 15 | command << j 16 | end 17 | else 18 | i = i.to_s 19 | command << "$#{i.bytesize}" 20 | command << i 21 | end 22 | end 23 | 24 | command[0] = "*#{(command.length - 1) / 2}" 25 | 26 | # Trailing delimiter 27 | command << "" 28 | command.join(COMMAND_DELIMITER) 29 | end 30 | 31 | protected 32 | 33 | if defined?(Encoding::default_external) 34 | def encode(string) 35 | string.force_encoding(Encoding::default_external) 36 | end 37 | else 38 | def encode(string) 39 | string 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /benchmarking/pipeline.rb: -------------------------------------------------------------------------------- 1 | require "benchmark" 2 | 3 | $:.push File.join(File.dirname(__FILE__), 'lib') 4 | 5 | require 'redis' 6 | 7 | ITERATIONS = 10000 8 | 9 | @r = Redis.new 10 | 11 | Benchmark.bmbm do |benchmark| 12 | benchmark.report("set") do 13 | @r.flushdb 14 | 15 | ITERATIONS.times do |i| 16 | @r.set("foo#{i}", "Hello world!") 17 | @r.get("foo#{i}") 18 | end 19 | end 20 | 21 | benchmark.report("set (pipelined)") do 22 | @r.flushdb 23 | 24 | @r.pipelined do 25 | ITERATIONS.times do |i| 26 | @r.set("foo#{i}", "Hello world!") 27 | @r.get("foo#{i}") 28 | end 29 | end 30 | end 31 | 32 | benchmark.report("lpush+ltrim") do 33 | @r.flushdb 34 | 35 | ITERATIONS.times do |i| 36 | @r.lpush "lpush#{i}", i 37 | @r.ltrim "ltrim#{i}", 0, 30 38 | end 39 | end 40 | 41 | benchmark.report("lpush+ltrim (pipelined)") do 42 | @r.flushdb 43 | 44 | @r.pipelined do 45 | ITERATIONS.times do |i| 46 | @r.lpush "lpush#{i}", i 47 | @r.ltrim "ltrim#{i}", 0, 30 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /test/blocking_commands_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | require "lint/blocking_commands" 5 | 6 | class TestBlockingCommands < Test::Unit::TestCase 7 | 8 | include Helper::Client 9 | include Lint::BlockingCommands 10 | 11 | def assert_takes_longer_than_client_timeout 12 | timeout = OPTIONS[:timeout] 13 | delay = timeout * 2 14 | 15 | mock(:delay => delay) do |r| 16 | t1 = Time.now 17 | yield(r) 18 | t2 = Time.now 19 | 20 | assert timeout == r.client.timeout 21 | assert delay <= (t2 - t1) 22 | end 23 | end 24 | 25 | def test_blpop_disable_client_timeout 26 | assert_takes_longer_than_client_timeout do |r| 27 | assert_equal ["foo", "0"], r.blpop("foo") 28 | end 29 | end 30 | 31 | def test_brpop_disable_client_timeout 32 | assert_takes_longer_than_client_timeout do |r| 33 | assert_equal ["foo", "0"], r.brpop("foo") 34 | end 35 | end 36 | 37 | def test_brpoplpush_disable_client_timeout 38 | assert_takes_longer_than_client_timeout do |r| 39 | assert_equal "0", r.brpoplpush("foo", "bar") 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /redis.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | $:.unshift File.expand_path("../lib", __FILE__) 4 | 5 | require "redis/version" 6 | 7 | Gem::Specification.new do |s| 8 | s.name = "redis" 9 | 10 | s.version = Redis::VERSION 11 | 12 | s.homepage = "https://github.com/redis/redis-rb" 13 | 14 | s.summary = "A Ruby client library for Redis" 15 | 16 | s.description = <<-EOS 17 | A Ruby client that tries to match Redis' API one-to-one, while still 18 | providing an idiomatic interface. It features thread-safety, 19 | client-side sharding, pipelining, and an obsession for performance. 20 | EOS 21 | 22 | s.license = "MIT" 23 | 24 | s.authors = [ 25 | "Ezra Zygmuntowicz", 26 | "Taylor Weibley", 27 | "Matthew Clark", 28 | "Brian McKinney", 29 | "Salvatore Sanfilippo", 30 | "Luca Guidi", 31 | "Michel Martens", 32 | "Damian Janowski", 33 | "Pieter Noordhuis" 34 | ] 35 | 36 | s.email = ["redis-db@googlegroups.com"] 37 | 38 | s.files = `git ls-files`.split("\n") 39 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 40 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 41 | 42 | s.add_development_dependency("rake") 43 | end 44 | -------------------------------------------------------------------------------- /lib/redis/errors.rb: -------------------------------------------------------------------------------- 1 | class Redis 2 | # Base error for all redis-rb errors. 3 | class BaseError < RuntimeError 4 | end 5 | 6 | # Raised by the connection when a protocol error occurs. 7 | class ProtocolError < BaseError 8 | def initialize(reply_type) 9 | super(<<-EOS.gsub(/(?:^|\n)\s*/, " ")) 10 | Got '#{reply_type}' as initial reply byte. 11 | If you're in a forking environment, such as Unicorn, you need to 12 | connect to Redis after forking. 13 | EOS 14 | end 15 | end 16 | 17 | # Raised by the client when command execution returns an error reply. 18 | class CommandError < BaseError 19 | end 20 | 21 | # Base error for connection related errors. 22 | class BaseConnectionError < BaseError 23 | end 24 | 25 | # Raised when connection to a Redis server cannot be made. 26 | class CannotConnectError < BaseConnectionError 27 | end 28 | 29 | # Raised when connection to a Redis server is lost. 30 | class ConnectionError < BaseConnectionError 31 | end 32 | 33 | # Raised when performing I/O times out. 34 | class TimeoutError < BaseConnectionError 35 | end 36 | 37 | # Raised when the connection was inherited by a child process. 38 | class InheritedError < BaseConnectionError 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/lint/hyper_log_log.rb: -------------------------------------------------------------------------------- 1 | module Lint 2 | 3 | module HyperLogLog 4 | 5 | def test_pfadd 6 | target_version "2.8.9" do 7 | assert_equal true, r.pfadd("foo", "s1") 8 | assert_equal true, r.pfadd("foo", "s2") 9 | assert_equal false, r.pfadd("foo", "s1") 10 | 11 | assert_equal 2, r.pfcount("foo") 12 | end 13 | end 14 | 15 | def test_variadic_pfadd 16 | target_version "2.8.9" do 17 | assert_equal true, r.pfadd("foo", ["s1", "s2"]) 18 | assert_equal true, r.pfadd("foo", ["s1", "s2", "s3"]) 19 | 20 | assert_equal 3, r.pfcount("foo") 21 | end 22 | end 23 | 24 | def test_pfcount 25 | target_version "2.8.9" do 26 | assert_equal 0, r.pfcount("foo") 27 | 28 | assert_equal true, r.pfadd("foo", "s1") 29 | 30 | assert_equal 1, r.pfcount("foo") 31 | end 32 | end 33 | 34 | def test_variadic_pfcount 35 | target_version "2.8.9" do 36 | assert_equal 0, r.pfcount(["foo", "bar"]) 37 | 38 | assert_equal true, r.pfadd("foo", "s1") 39 | assert_equal true, r.pfadd("bar", "s1") 40 | assert_equal true, r.pfadd("bar", "s2") 41 | 42 | assert_equal 2, r.pfcount(["foo", "bar"]) 43 | end 44 | end 45 | 46 | end 47 | 48 | end -------------------------------------------------------------------------------- /test/distributed_blocking_commands_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | require "lint/blocking_commands" 5 | 6 | class TestDistributedBlockingCommands < Test::Unit::TestCase 7 | 8 | include Helper::Distributed 9 | include Lint::BlockingCommands 10 | 11 | def test_blpop_raises 12 | assert_raises(Redis::Distributed::CannotDistribute) do 13 | r.blpop(["foo", "bar"]) 14 | end 15 | end 16 | 17 | def test_blpop_raises_with_old_prototype 18 | assert_raises(Redis::Distributed::CannotDistribute) do 19 | r.blpop("foo", "bar", 0) 20 | end 21 | end 22 | 23 | def test_brpop_raises 24 | assert_raises(Redis::Distributed::CannotDistribute) do 25 | r.brpop(["foo", "bar"]) 26 | end 27 | end 28 | 29 | def test_brpop_raises_with_old_prototype 30 | assert_raises(Redis::Distributed::CannotDistribute) do 31 | r.brpop("foo", "bar", 0) 32 | end 33 | end 34 | 35 | def test_brpoplpush_raises 36 | assert_raises(Redis::Distributed::CannotDistribute) do 37 | r.brpoplpush("foo", "bar") 38 | end 39 | end 40 | 41 | def test_brpoplpush_raises_with_old_prototype 42 | assert_raises(Redis::Distributed::CannotDistribute) do 43 | r.brpoplpush("foo", "bar", 0) 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | branches: 4 | only: 5 | - master 6 | 7 | rvm: 8 | - 1.8.7 9 | - 1.9.2 10 | - 1.9.3 11 | - 2.0.0 12 | - 2.1.0 13 | - jruby-18mode 14 | - jruby-19mode 15 | 16 | gemfile: 17 | - .travis/Gemfile 18 | 19 | env: 20 | global: 21 | - VERBOSE=true 22 | - TIMEOUT=1 23 | matrix: 24 | - conn=ruby REDIS_BRANCH=2.6 25 | - conn=hiredis REDIS_BRANCH=2.6 26 | - conn=synchrony REDIS_BRANCH=2.6 27 | - conn=ruby REDIS_BRANCH=2.8 28 | - conn=ruby REDIS_BRANCH=unstable 29 | 30 | matrix: 31 | exclude: 32 | # hiredis 33 | - rvm: jruby-18mode 34 | gemfile: .travis/Gemfile 35 | env: conn=hiredis REDIS_BRANCH=2.6 36 | - rvm: jruby-19mode 37 | gemfile: .travis/Gemfile 38 | env: conn=hiredis REDIS_BRANCH=2.6 39 | 40 | # synchrony 41 | - rvm: 1.8.7 42 | gemfile: .travis/Gemfile 43 | env: conn=synchrony REDIS_BRANCH=2.6 44 | - rvm: jruby-18mode 45 | gemfile: .travis/Gemfile 46 | env: conn=synchrony REDIS_BRANCH=2.6 47 | - rvm: jruby-19mode 48 | gemfile: .travis/Gemfile 49 | env: conn=synchrony REDIS_BRANCH=2.6 50 | 51 | notifications: 52 | irc: 53 | - irc.freenode.net#redis-rb 54 | email: 55 | - damian.janowski@gmail.com 56 | - pcnoordhuis@gmail.com 57 | -------------------------------------------------------------------------------- /test/distributed_commands_on_strings_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | require "lint/strings" 5 | 6 | class TestDistributedCommandsOnStrings < Test::Unit::TestCase 7 | 8 | include Helper::Distributed 9 | include Lint::Strings 10 | 11 | def test_mget 12 | assert_raise Redis::Distributed::CannotDistribute do 13 | r.mget("foo", "bar") 14 | end 15 | end 16 | 17 | def test_mget_mapped 18 | assert_raise Redis::Distributed::CannotDistribute do 19 | r.mapped_mget("foo", "bar") 20 | end 21 | end 22 | 23 | def test_mset 24 | assert_raise Redis::Distributed::CannotDistribute do 25 | r.mset(:foo, "s1", :bar, "s2") 26 | end 27 | end 28 | 29 | def test_mset_mapped 30 | assert_raise Redis::Distributed::CannotDistribute do 31 | r.mapped_mset(:foo => "s1", :bar => "s2") 32 | end 33 | end 34 | 35 | def test_msetnx 36 | assert_raise Redis::Distributed::CannotDistribute do 37 | r.set("foo", "s1") 38 | r.msetnx(:foo, "s2", :bar, "s3") 39 | end 40 | end 41 | 42 | def test_msetnx_mapped 43 | assert_raise Redis::Distributed::CannotDistribute do 44 | r.set("foo", "s1") 45 | r.mapped_msetnx(:foo => "s2", :bar => "s3") 46 | end 47 | end 48 | 49 | def test_bitop 50 | target_version "2.5.10" do 51 | assert_raise Redis::Distributed::CannotDistribute do 52 | r.set("foo", "a") 53 | r.set("bar", "b") 54 | 55 | r.bitop(:and, "foo&bar", "foo", "bar") 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/distributed_key_tags_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | 5 | class TestDistributedKeyTags < Test::Unit::TestCase 6 | 7 | include Helper 8 | include Helper::Distributed 9 | 10 | def test_hashes_consistently 11 | r1 = Redis::Distributed.new ["redis://127.0.0.1:#{PORT}/15", *NODES] 12 | r2 = Redis::Distributed.new ["redis://127.0.0.1:#{PORT}/15", *NODES] 13 | r3 = Redis::Distributed.new ["redis://127.0.0.1:#{PORT}/15", *NODES] 14 | 15 | assert_equal r1.node_for("foo").id, r2.node_for("foo").id 16 | assert_equal r1.node_for("foo").id, r3.node_for("foo").id 17 | end 18 | 19 | def test_allows_clustering_of_keys 20 | r = Redis::Distributed.new(NODES) 21 | r.add_node("redis://127.0.0.1:#{PORT}/14") 22 | r.flushdb 23 | 24 | 100.times do |i| 25 | r.set "{foo}users:#{i}", i 26 | end 27 | 28 | assert_equal [0, 100], r.nodes.map { |node| node.keys.size } 29 | end 30 | 31 | def test_distributes_keys_if_no_clustering_is_used 32 | r.add_node("redis://127.0.0.1:#{PORT}/14") 33 | r.flushdb 34 | 35 | r.set "users:1", 1 36 | r.set "users:4", 4 37 | 38 | assert_equal [1, 1], r.nodes.map { |node| node.keys.size } 39 | end 40 | 41 | def test_allows_passing_a_custom_tag_extractor 42 | r = Redis::Distributed.new(NODES, :tag => /^(.+?):/) 43 | r.add_node("redis://127.0.0.1:#{PORT}/14") 44 | r.flushdb 45 | 46 | 100.times do |i| 47 | r.set "foo:users:#{i}", i 48 | end 49 | 50 | assert_equal [0, 100], r.nodes.map { |node| node.keys.size } 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /benchmarking/logging.rb: -------------------------------------------------------------------------------- 1 | # Run with 2 | # 3 | # $ ruby -Ilib benchmarking/logging.rb 4 | # 5 | 6 | begin 7 | require "bench" 8 | rescue LoadError 9 | $stderr.puts "`gem install bench` and try again." 10 | exit 1 11 | end 12 | 13 | require "redis" 14 | require "logger" 15 | 16 | def log(level, namespace = nil) 17 | logger = (namespace || Kernel).const_get(:Logger).new("/dev/null") 18 | logger.level = (namespace || Logger).const_get(level) 19 | logger 20 | end 21 | 22 | def stress(redis) 23 | redis.flushdb 24 | 25 | n = (ARGV.shift || 2000).to_i 26 | 27 | n.times do |i| 28 | key = "foo:#{i}" 29 | redis.set key, i 30 | redis.get key 31 | end 32 | end 33 | 34 | default = Redis.new 35 | 36 | logging_redises = [ 37 | Redis.new(:logger => log(:DEBUG)), 38 | Redis.new(:logger => log(:INFO)), 39 | ] 40 | 41 | begin 42 | require "log4r" 43 | 44 | logging_redises += [ 45 | Redis.new(:logger => log(:DEBUG, Log4r)), 46 | Redis.new(:logger => log(:INFO, Log4r)), 47 | ] 48 | rescue LoadError 49 | $stderr.puts "Log4r not installed. `gem install log4r` if you want to compare it against Ruby's Logger (spoiler: it's much faster)." 50 | end 51 | 52 | benchmark "Default options (no logger)" do 53 | stress(default) 54 | end 55 | 56 | logging_redises.each do |redis| 57 | logger = redis.client.logger 58 | 59 | case logger 60 | when Logger 61 | level = Logger::SEV_LABEL[logger.level] 62 | when Log4r::Logger 63 | level = logger.levels[logger.level] 64 | end 65 | 66 | benchmark "#{logger.class} on #{level}" do 67 | stress(redis) 68 | end 69 | end 70 | 71 | run 10 72 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rake/testtask" 2 | 3 | ENV["REDIS_BRANCH"] ||= "unstable" 4 | 5 | REDIS_DIR = File.expand_path(File.join("..", "test"), __FILE__) 6 | REDIS_CNF = File.join(REDIS_DIR, "test.conf") 7 | REDIS_PID = File.join(REDIS_DIR, "db", "redis.pid") 8 | BINARY = "tmp/redis-#{ENV["REDIS_BRANCH"]}/src/redis-server" 9 | 10 | task :default => :run 11 | 12 | desc "Run tests and manage server start/stop" 13 | task :run => [:start, :test, :stop] 14 | 15 | desc "Start the Redis server" 16 | task :start => BINARY do 17 | sh "#{BINARY} --version" 18 | 19 | redis_running = \ 20 | begin 21 | File.exists?(REDIS_PID) && Process.kill(0, File.read(REDIS_PID).to_i) 22 | rescue Errno::ESRCH 23 | FileUtils.rm REDIS_PID 24 | false 25 | end 26 | 27 | unless redis_running 28 | unless system("#{BINARY} #{REDIS_CNF}") 29 | abort "could not start redis-server" 30 | end 31 | end 32 | 33 | at_exit do 34 | Rake::Task["stop"].invoke 35 | end 36 | end 37 | 38 | desc "Stop the Redis server" 39 | task :stop do 40 | if File.exists?(REDIS_PID) 41 | Process.kill "INT", File.read(REDIS_PID).to_i 42 | FileUtils.rm REDIS_PID 43 | end 44 | end 45 | 46 | desc "Clean up testing artifacts" 47 | task :clean do 48 | FileUtils.rm_f(BINARY) 49 | end 50 | 51 | file BINARY do 52 | branch = ENV.fetch("REDIS_BRANCH") 53 | 54 | sh <<-SH 55 | mkdir -p tmp; 56 | cd tmp; 57 | rm -rf redis-#{branch}; 58 | wget https://github.com/antirez/redis/archive/#{branch}.tar.gz -O #{branch}.tar.gz; 59 | tar xf #{branch}.tar.gz; 60 | cd redis-#{branch}; 61 | make 62 | SH 63 | end 64 | 65 | Rake::TestTask.new do |t| 66 | t.options = "-v" if $VERBOSE 67 | t.test_files = FileList["test/*_test.rb"] 68 | end 69 | -------------------------------------------------------------------------------- /test/error_replies_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | 5 | class TestErrorReplies < Test::Unit::TestCase 6 | 7 | include Helper::Client 8 | 9 | # Every test shouldn't disconnect from the server. Also, when error replies are 10 | # in play, the protocol should never get into an invalid state where there are 11 | # pending replies in the connection. Calling INFO after every test ensures that 12 | # the protocol is still in a valid state. 13 | def with_reconnection_check 14 | before = r.info["total_connections_received"] 15 | yield(r) 16 | after = r.info["total_connections_received"] 17 | ensure 18 | assert_equal before, after 19 | end 20 | 21 | def test_error_reply_for_single_command 22 | with_reconnection_check do 23 | begin 24 | r.unknown_command 25 | rescue => ex 26 | ensure 27 | assert ex.message =~ /unknown command/i 28 | end 29 | end 30 | end 31 | 32 | def test_raise_first_error_reply_in_pipeline 33 | with_reconnection_check do 34 | begin 35 | r.pipelined do 36 | r.set("foo", "s1") 37 | r.incr("foo") # not an integer 38 | r.lpush("foo", "value") # wrong kind of value 39 | end 40 | rescue => ex 41 | ensure 42 | assert ex.message =~ /not an integer/i 43 | end 44 | end 45 | end 46 | 47 | def test_recover_from_raise_in__call_loop 48 | with_reconnection_check do 49 | begin 50 | r.client.call_loop([:invalid_monitor]) do 51 | assert false # Should never be executed 52 | end 53 | rescue => ex 54 | ensure 55 | assert ex.message =~ /unknown command/i 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /benchmarking/worker.rb: -------------------------------------------------------------------------------- 1 | BENCHMARK_ROOT = File.dirname(__FILE__) 2 | REDIS_ROOT = File.join(BENCHMARK_ROOT, "..", "lib") 3 | 4 | $: << REDIS_ROOT 5 | require 'redis' 6 | require 'benchmark' 7 | 8 | def show_usage 9 | puts <<-EOL 10 | Usage: worker.rb [read:write] 11 | EOL 12 | end 13 | 14 | def shift_from_argv 15 | value = ARGV.shift 16 | unless value 17 | show_usage 18 | exit -1 19 | end 20 | value 21 | end 22 | 23 | operation = shift_from_argv.to_sym 24 | start_index = shift_from_argv.to_i 25 | end_index = shift_from_argv.to_i 26 | sleep_msec = shift_from_argv.to_i 27 | sleep_duration = sleep_msec/1000.0 28 | 29 | redis = Redis.new 30 | 31 | case operation 32 | when :initialize 33 | 34 | start_index.upto(end_index) do |i| 35 | redis[i] = 0 36 | end 37 | 38 | when :clear 39 | 40 | start_index.upto(end_index) do |i| 41 | redis.delete(i) 42 | end 43 | 44 | when :read, :write 45 | 46 | puts "Starting to #{operation} at segment #{end_index + 1}" 47 | 48 | loop do 49 | t1 = Time.now 50 | start_index.upto(end_index) do |i| 51 | case operation 52 | when :read 53 | redis.get(i) 54 | when :write 55 | redis.incr(i) 56 | else 57 | raise "Unknown operation: #{operation}" 58 | end 59 | sleep sleep_duration 60 | end 61 | t2 = Time.now 62 | 63 | requests_processed = end_index - start_index 64 | time = t2 - t1 65 | puts "#{t2.strftime("%H:%M")} [segment #{end_index + 1}] : Processed #{requests_processed} requests in #{time} seconds - #{(requests_processed/time).round} requests/sec" 66 | end 67 | 68 | else 69 | raise "Unknown operation: #{operation}" 70 | end 71 | 72 | -------------------------------------------------------------------------------- /test/distributed_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | 5 | class TestDistributed < Test::Unit::TestCase 6 | 7 | include Helper::Distributed 8 | 9 | def test_handle_multiple_servers 10 | @r = Redis::Distributed.new ["redis://127.0.0.1:#{PORT}/15", *NODES] 11 | 12 | 100.times do |idx| 13 | @r.set(idx.to_s, "foo#{idx}") 14 | end 15 | 16 | 100.times do |idx| 17 | assert_equal "foo#{idx}", @r.get(idx.to_s) 18 | end 19 | 20 | assert_equal "0", @r.keys("*").sort.first 21 | assert_equal "string", @r.type("1") 22 | end 23 | 24 | def test_add_nodes 25 | logger = Logger.new("/dev/null") 26 | 27 | @r = Redis::Distributed.new NODES, :logger => logger, :timeout => 10 28 | 29 | assert_equal "127.0.0.1", @r.nodes[0].client.host 30 | assert_equal PORT, @r.nodes[0].client.port 31 | assert_equal 15, @r.nodes[0].client.db 32 | assert_equal 10, @r.nodes[0].client.timeout 33 | assert_equal logger, @r.nodes[0].client.logger 34 | 35 | @r.add_node("redis://127.0.0.1:6380/14") 36 | 37 | assert_equal "127.0.0.1", @r.nodes[1].client.host 38 | assert_equal 6380, @r.nodes[1].client.port 39 | assert_equal 14, @r.nodes[1].client.db 40 | assert_equal 10, @r.nodes[1].client.timeout 41 | assert_equal logger, @r.nodes[1].client.logger 42 | end 43 | 44 | def test_pipelining_commands_cannot_be_distributed 45 | assert_raise Redis::Distributed::CannotDistribute do 46 | r.pipelined do 47 | r.lpush "foo", "s1" 48 | r.lpush "foo", "s2" 49 | end 50 | end 51 | end 52 | 53 | def test_unknown_commands_does_not_work_by_default 54 | assert_raise NoMethodError do 55 | r.not_yet_implemented_command 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /test/distributed_remote_server_control_commands_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | 5 | class TestDistributedRemoteServerControlCommands < Test::Unit::TestCase 6 | 7 | include Helper::Distributed 8 | 9 | def test_info 10 | keys = [ 11 | "redis_version", 12 | "uptime_in_seconds", 13 | "uptime_in_days", 14 | "connected_clients", 15 | "used_memory", 16 | "total_connections_received", 17 | "total_commands_processed", 18 | ] 19 | 20 | infos = r.info 21 | 22 | infos.each do |info| 23 | keys.each do |k| 24 | msg = "expected #info to include #{k}" 25 | assert info.keys.include?(k), msg 26 | end 27 | end 28 | end 29 | 30 | def test_info_commandstats 31 | target_version "2.5.7" do 32 | r.nodes.each { |n| n.config(:resetstat) } 33 | r.ping # Executed on every node 34 | 35 | r.info(:commandstats).each do |info| 36 | assert_equal "1", info["ping"]["calls"] 37 | end 38 | end 39 | end 40 | 41 | def test_monitor 42 | begin 43 | r.monitor 44 | rescue Exception => ex 45 | ensure 46 | assert ex.kind_of?(NotImplementedError) 47 | end 48 | end 49 | 50 | def test_echo 51 | assert_equal ["foo bar baz\n"], r.echo("foo bar baz\n") 52 | end 53 | 54 | def test_time 55 | target_version "2.5.4" do 56 | # Test that the difference between the time that Ruby reports and the time 57 | # that Redis reports is minimal (prevents the test from being racy). 58 | r.time.each do |rv| 59 | redis_usec = rv[0] * 1_000_000 + rv[1] 60 | ruby_usec = Integer(Time.now.to_f * 1_000_000) 61 | 62 | assert 500_000 > (ruby_usec - redis_usec).abs 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/redis/connection/hiredis.rb: -------------------------------------------------------------------------------- 1 | require "redis/connection/registry" 2 | require "redis/errors" 3 | require "hiredis/connection" 4 | require "timeout" 5 | 6 | class Redis 7 | module Connection 8 | class Hiredis 9 | 10 | def self.connect(config) 11 | connection = ::Hiredis::Connection.new 12 | 13 | if config[:scheme] == "unix" 14 | connection.connect_unix(config[:path], Integer(config[:timeout] * 1_000_000)) 15 | else 16 | connection.connect(config[:host], config[:port], Integer(config[:timeout] * 1_000_000)) 17 | end 18 | 19 | instance = new(connection) 20 | instance.timeout = config[:timeout] 21 | instance 22 | rescue Errno::ETIMEDOUT 23 | raise TimeoutError 24 | end 25 | 26 | def initialize(connection) 27 | @connection = connection 28 | end 29 | 30 | def connected? 31 | @connection && @connection.connected? 32 | end 33 | 34 | def timeout=(timeout) 35 | # Hiredis works with microsecond timeouts 36 | @connection.timeout = Integer(timeout * 1_000_000) 37 | end 38 | 39 | def disconnect 40 | @connection.disconnect 41 | @connection = nil 42 | end 43 | 44 | def write(command) 45 | @connection.write(command.flatten(1)) 46 | rescue Errno::EAGAIN 47 | raise TimeoutError 48 | end 49 | 50 | def read 51 | reply = @connection.read 52 | reply = CommandError.new(reply.message) if reply.is_a?(RuntimeError) 53 | reply 54 | rescue Errno::EAGAIN 55 | raise TimeoutError 56 | rescue RuntimeError => err 57 | raise ProtocolError.new(err.message) 58 | end 59 | end 60 | end 61 | end 62 | 63 | Redis::Connection.drivers << Redis::Connection::Hiredis 64 | -------------------------------------------------------------------------------- /test/sorting_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | 5 | class TestSorting < Test::Unit::TestCase 6 | 7 | include Helper::Client 8 | 9 | def test_sort 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 | assert_equal ["s1"], r.sort("bar", :get => "foo:*", :limit => [0, 1]) 17 | assert_equal ["s2"], r.sort("bar", :get => "foo:*", :limit => [0, 1], :order => "desc alpha") 18 | end 19 | 20 | def test_sort_with_an_array_of_gets 21 | r.set("foo:1:a", "s1a") 22 | r.set("foo:1:b", "s1b") 23 | 24 | r.set("foo:2:a", "s2a") 25 | r.set("foo:2:b", "s2b") 26 | 27 | r.rpush("bar", "1") 28 | r.rpush("bar", "2") 29 | 30 | assert_equal [["s1a", "s1b"]], r.sort("bar", :get => ["foo:*:a", "foo:*:b"], :limit => [0, 1]) 31 | assert_equal [["s2a", "s2b"]], r.sort("bar", :get => ["foo:*:a", "foo:*:b"], :limit => [0, 1], :order => "desc alpha") 32 | assert_equal [["s1a", "s1b"], ["s2a", "s2b"]], r.sort("bar", :get => ["foo:*:a", "foo:*:b"]) 33 | end 34 | 35 | def test_sort_with_store 36 | r.set("foo:1", "s1") 37 | r.set("foo:2", "s2") 38 | 39 | r.rpush("bar", "1") 40 | r.rpush("bar", "2") 41 | 42 | r.sort("bar", :get => "foo:*", :store => "baz") 43 | assert_equal ["s1", "s2"], r.lrange("baz", 0, -1) 44 | end 45 | 46 | def test_sort_with_an_array_of_gets_and_with_store 47 | r.set("foo:1:a", "s1a") 48 | r.set("foo:1:b", "s1b") 49 | 50 | r.set("foo:2:a", "s2a") 51 | r.set("foo:2:b", "s2b") 52 | 53 | r.rpush("bar", "1") 54 | r.rpush("bar", "2") 55 | 56 | r.sort("bar", :get => ["foo:*:a", "foo:*:b"], :store => 'baz') 57 | assert_equal ["s1a", "s1b", "s2a", "s2b"], r.lrange("baz", 0, -1) 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/commands_on_sets_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | require "lint/sets" 5 | 6 | class TestCommandsOnSets < Test::Unit::TestCase 7 | 8 | include Helper::Client 9 | include Lint::Sets 10 | 11 | def test_smove 12 | r.sadd "foo", "s1" 13 | r.sadd "bar", "s2" 14 | 15 | assert r.smove("foo", "bar", "s1") 16 | assert r.sismember("bar", "s1") 17 | end 18 | 19 | def test_sinter 20 | r.sadd "foo", "s1" 21 | r.sadd "foo", "s2" 22 | r.sadd "bar", "s2" 23 | 24 | assert_equal ["s2"], r.sinter("foo", "bar") 25 | end 26 | 27 | def test_sinterstore 28 | r.sadd "foo", "s1" 29 | r.sadd "foo", "s2" 30 | r.sadd "bar", "s2" 31 | 32 | r.sinterstore("baz", "foo", "bar") 33 | 34 | assert_equal ["s2"], r.smembers("baz") 35 | end 36 | 37 | def test_sunion 38 | r.sadd "foo", "s1" 39 | r.sadd "foo", "s2" 40 | r.sadd "bar", "s2" 41 | r.sadd "bar", "s3" 42 | 43 | assert_equal ["s1", "s2", "s3"], r.sunion("foo", "bar").sort 44 | end 45 | 46 | def test_sunionstore 47 | r.sadd "foo", "s1" 48 | r.sadd "foo", "s2" 49 | r.sadd "bar", "s2" 50 | r.sadd "bar", "s3" 51 | 52 | r.sunionstore("baz", "foo", "bar") 53 | 54 | assert_equal ["s1", "s2", "s3"], r.smembers("baz").sort 55 | end 56 | 57 | def test_sdiff 58 | r.sadd "foo", "s1" 59 | r.sadd "foo", "s2" 60 | r.sadd "bar", "s2" 61 | r.sadd "bar", "s3" 62 | 63 | assert_equal ["s1"], r.sdiff("foo", "bar") 64 | assert_equal ["s3"], r.sdiff("bar", "foo") 65 | end 66 | 67 | def test_sdiffstore 68 | r.sadd "foo", "s1" 69 | r.sadd "foo", "s2" 70 | r.sadd "bar", "s2" 71 | r.sadd "bar", "s3" 72 | 73 | r.sdiffstore("baz", "foo", "bar") 74 | 75 | assert_equal ["s1"], r.smembers("baz") 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /test/bitpos_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | 5 | unless defined?(Enumerator) 6 | Enumerator = Enumerable::Enumerator 7 | end 8 | 9 | class TestBitpos < Test::Unit::TestCase 10 | 11 | include Helper::Client 12 | 13 | def test_bitpos_empty_zero 14 | target_version "2.9.11" do 15 | r.del "foo" 16 | assert_equal 0, r.bitpos("foo", 0) 17 | end 18 | end 19 | 20 | def test_bitpos_empty_one 21 | target_version "2.9.11" do 22 | r.del "foo" 23 | assert_equal -1, r.bitpos("foo", 1) 24 | end 25 | end 26 | 27 | def test_bitpos_zero 28 | target_version "2.9.11" do 29 | r.set "foo", "\xff\xf0\x00" 30 | assert_equal 12, r.bitpos("foo", 0) 31 | end 32 | end 33 | 34 | def test_bitpos_one 35 | target_version "2.9.11" do 36 | r.set "foo", "\x00\x0f\x00" 37 | assert_equal 12, r.bitpos("foo", 1) 38 | end 39 | end 40 | 41 | def test_bitpos_zero_end_is_given 42 | target_version "2.9.11" do 43 | r.set "foo", "\xff\xff\xff" 44 | assert_equal 24, r.bitpos("foo", 0) 45 | assert_equal 24, r.bitpos("foo", 0, 0) 46 | assert_equal -1, r.bitpos("foo", 0, 0, -1) 47 | end 48 | end 49 | 50 | def test_bitpos_one_intervals 51 | target_version "2.9.11" do 52 | r.set "foo", "\x00\xff\x00" 53 | assert_equal 8, r.bitpos("foo", 1, 0, -1) 54 | assert_equal 8, r.bitpos("foo", 1, 1, -1) 55 | assert_equal -1, r.bitpos("foo", 1, 2, -1) 56 | assert_equal -1, r.bitpos("foo", 1, 2, 200) 57 | assert_equal 8, r.bitpos("foo", 1, 1, 1) 58 | end 59 | end 60 | 61 | def test_bitpos_raise_exception_if_stop_not_start 62 | target_version "2.9.11" do 63 | assert_raises(ArgumentError) do 64 | r.bitpos("foo", 0, nil, 2) 65 | end 66 | end 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /test/fork_safety_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | 5 | class TestForkSafety < Test::Unit::TestCase 6 | 7 | include Helper::Client 8 | include Helper::Skipable 9 | 10 | driver(:ruby, :hiredis) do 11 | def test_fork_safety 12 | redis = Redis.new(OPTIONS) 13 | redis.set "foo", 1 14 | 15 | child_pid = fork do 16 | begin 17 | # InheritedError triggers a reconnect, 18 | # so we need to disable reconnects to force 19 | # the exception bubble up 20 | redis.without_reconnect do 21 | redis.set "foo", 2 22 | end 23 | rescue Redis::InheritedError 24 | exit 127 25 | end 26 | end 27 | 28 | _, status = Process.wait2(child_pid) 29 | 30 | assert_equal 127, status.exitstatus 31 | assert_equal "1", redis.get("foo") 32 | 33 | rescue NotImplementedError => error 34 | raise unless error.message =~ /fork is not available/ 35 | return skip(error.message) 36 | end 37 | 38 | def test_fork_safety_with_enabled_inherited_socket 39 | redis = Redis.new(OPTIONS.merge(:inherit_socket => true)) 40 | redis.set "foo", 1 41 | 42 | child_pid = fork do 43 | begin 44 | # InheritedError triggers a reconnect, 45 | # so we need to disable reconnects to force 46 | # the exception bubble up 47 | redis.without_reconnect do 48 | redis.set "foo", 2 49 | end 50 | rescue Redis::InheritedError 51 | exit 127 52 | end 53 | end 54 | 55 | _, status = Process.wait2(child_pid) 56 | 57 | assert_equal 0, status.exitstatus 58 | assert_equal "2", redis.get("foo") 59 | 60 | rescue NotImplementedError => error 61 | raise unless error.message =~ /fork is not available/ 62 | return skip(error.message) 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/redis/subscribe.rb: -------------------------------------------------------------------------------- 1 | class Redis 2 | class SubscribedClient 3 | def initialize(client) 4 | @client = client 5 | end 6 | 7 | def call(command) 8 | @client.process([command]) 9 | end 10 | 11 | def subscribe(*channels, &block) 12 | subscription("subscribe", "unsubscribe", channels, block) 13 | end 14 | 15 | def psubscribe(*channels, &block) 16 | subscription("psubscribe", "punsubscribe", channels, block) 17 | end 18 | 19 | def unsubscribe(*channels) 20 | call([:unsubscribe, *channels]) 21 | end 22 | 23 | def punsubscribe(*channels) 24 | call([:punsubscribe, *channels]) 25 | end 26 | 27 | protected 28 | 29 | def subscription(start, stop, channels, block) 30 | sub = Subscription.new(&block) 31 | 32 | unsubscribed = false 33 | 34 | begin 35 | @client.call_loop([start, *channels]) do |line| 36 | type, *rest = line 37 | sub.callbacks[type].call(*rest) 38 | unsubscribed = type == stop && rest.last == 0 39 | break if unsubscribed 40 | end 41 | ensure 42 | # No need to unsubscribe here. The real client closes the connection 43 | # whenever an exception is raised (see #ensure_connected). 44 | end 45 | end 46 | end 47 | 48 | class Subscription 49 | attr :callbacks 50 | 51 | def initialize 52 | @callbacks = Hash.new do |hash, key| 53 | hash[key] = lambda { |*_| } 54 | end 55 | 56 | yield(self) 57 | end 58 | 59 | def subscribe(&block) 60 | @callbacks["subscribe"] = block 61 | end 62 | 63 | def unsubscribe(&block) 64 | @callbacks["unsubscribe"] = block 65 | end 66 | 67 | def message(&block) 68 | @callbacks["message"] = block 69 | end 70 | 71 | def psubscribe(&block) 72 | @callbacks["psubscribe"] = block 73 | end 74 | 75 | def punsubscribe(&block) 76 | @callbacks["punsubscribe"] = block 77 | end 78 | 79 | def pmessage(&block) 80 | @callbacks["pmessage"] = block 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /test/distributed_commands_on_sets_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | require "lint/sets" 5 | 6 | class TestDistributedCommandsOnSets < Test::Unit::TestCase 7 | 8 | include Helper::Distributed 9 | include Lint::Sets 10 | 11 | def test_smove 12 | assert_raise Redis::Distributed::CannotDistribute do 13 | r.sadd "foo", "s1" 14 | r.sadd "bar", "s2" 15 | 16 | r.smove("foo", "bar", "s1") 17 | end 18 | end 19 | 20 | def test_sinter 21 | assert_raise Redis::Distributed::CannotDistribute do 22 | r.sadd "foo", "s1" 23 | r.sadd "foo", "s2" 24 | r.sadd "bar", "s2" 25 | 26 | r.sinter("foo", "bar") 27 | end 28 | end 29 | 30 | def test_sinterstore 31 | assert_raise Redis::Distributed::CannotDistribute do 32 | r.sadd "foo", "s1" 33 | r.sadd "foo", "s2" 34 | r.sadd "bar", "s2" 35 | 36 | r.sinterstore("baz", "foo", "bar") 37 | end 38 | end 39 | 40 | def test_sunion 41 | assert_raise Redis::Distributed::CannotDistribute do 42 | r.sadd "foo", "s1" 43 | r.sadd "foo", "s2" 44 | r.sadd "bar", "s2" 45 | r.sadd "bar", "s3" 46 | 47 | r.sunion("foo", "bar") 48 | end 49 | end 50 | 51 | def test_sunionstore 52 | assert_raise Redis::Distributed::CannotDistribute do 53 | r.sadd "foo", "s1" 54 | r.sadd "foo", "s2" 55 | r.sadd "bar", "s2" 56 | r.sadd "bar", "s3" 57 | 58 | r.sunionstore("baz", "foo", "bar") 59 | end 60 | end 61 | 62 | def test_sdiff 63 | assert_raise Redis::Distributed::CannotDistribute do 64 | r.sadd "foo", "s1" 65 | r.sadd "foo", "s2" 66 | r.sadd "bar", "s2" 67 | r.sadd "bar", "s3" 68 | 69 | r.sdiff("foo", "bar") 70 | end 71 | end 72 | 73 | def test_sdiffstore 74 | assert_raise Redis::Distributed::CannotDistribute do 75 | r.sadd "foo", "s1" 76 | r.sadd "foo", "s2" 77 | r.sadd "bar", "s2" 78 | r.sadd "bar", "s3" 79 | 80 | r.sdiffstore("baz", "foo", "bar") 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /test/synchrony_driver.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'em-synchrony' 4 | require 'em-synchrony/connection_pool' 5 | 6 | require 'redis' 7 | require 'redis/connection/synchrony' 8 | 9 | 10 | require File.expand_path("./helper", File.dirname(__FILE__)) 11 | 12 | PORT = 6381 13 | OPTIONS = {:port => PORT, :db => 15} 14 | 15 | # 16 | # if running under Eventmachine + Synchrony (Ruby 1.9+), then 17 | # we can simulate the blocking API while performing the network 18 | # IO via the EM reactor. 19 | # 20 | 21 | EM.synchrony do 22 | r = Redis.new OPTIONS 23 | r.flushdb 24 | 25 | r.rpush "foo", "s1" 26 | r.rpush "foo", "s2" 27 | 28 | assert_equal 2, r.llen("foo") 29 | assert_equal "s2", r.rpop("foo") 30 | 31 | r.set("foo", "bar") 32 | 33 | assert_equal "bar", r.getset("foo", "baz") 34 | assert_equal "baz", r.get("foo") 35 | 36 | r.set("foo", "a") 37 | 38 | assert_equal 1, r.getbit("foo", 1) 39 | assert_equal 1, r.getbit("foo", 2) 40 | assert_equal 0, r.getbit("foo", 3) 41 | assert_equal 0, r.getbit("foo", 4) 42 | assert_equal 0, r.getbit("foo", 5) 43 | assert_equal 0, r.getbit("foo", 6) 44 | assert_equal 1, r.getbit("foo", 7) 45 | 46 | r.flushdb 47 | 48 | # command pipelining 49 | r.pipelined do 50 | r.lpush "foo", "s1" 51 | r.lpush "foo", "s2" 52 | end 53 | 54 | assert_equal 2, r.llen("foo") 55 | assert_equal "s2", r.lpop("foo") 56 | assert_equal "s1", r.lpop("foo") 57 | 58 | assert_equal "OK", r.client.call(:quit) 59 | assert_equal "PONG", r.ping 60 | 61 | 62 | rpool = EM::Synchrony::ConnectionPool.new(size: 5) { Redis.new OPTIONS } 63 | 64 | result = rpool.watch 'foo' do |rd| 65 | assert_kind_of Redis, rd 66 | 67 | rd.set "foo", "s1" 68 | rd.multi do |multi| 69 | multi.set "foo", "s2" 70 | end 71 | end 72 | 73 | assert_equal nil, result 74 | assert_equal "s1", rpool.get("foo") 75 | 76 | result = rpool.watch "foo" do |rd| 77 | assert_kind_of Redis, rd 78 | 79 | rd.multi do |multi| 80 | multi.set "foo", "s3" 81 | end 82 | end 83 | 84 | assert_equal ["OK"], result 85 | assert_equal "s3", rpool.get("foo") 86 | 87 | EM.stop 88 | end 89 | -------------------------------------------------------------------------------- /test/distributed_commands_on_value_types_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | require "lint/value_types" 5 | 6 | class TestDistributedCommandsOnValueTypes < Test::Unit::TestCase 7 | 8 | include Helper::Distributed 9 | include Lint::ValueTypes 10 | 11 | def test_del 12 | r.set "foo", "s1" 13 | r.set "bar", "s2" 14 | r.set "baz", "s3" 15 | 16 | assert_equal ["bar", "baz", "foo"], r.keys("*").sort 17 | 18 | assert_equal 1, r.del("foo") 19 | 20 | assert_equal ["bar", "baz"], r.keys("*").sort 21 | 22 | assert_equal 2, r.del("bar", "baz") 23 | 24 | assert_equal [], r.keys("*").sort 25 | end 26 | 27 | def test_del_with_array_argument 28 | r.set "foo", "s1" 29 | r.set "bar", "s2" 30 | r.set "baz", "s3" 31 | 32 | assert_equal ["bar", "baz", "foo"], r.keys("*").sort 33 | 34 | assert_equal 1, r.del(["foo"]) 35 | 36 | assert_equal ["bar", "baz"], r.keys("*").sort 37 | 38 | assert_equal 2, r.del(["bar", "baz"]) 39 | 40 | assert_equal [], r.keys("*").sort 41 | end 42 | 43 | def test_randomkey 44 | assert_raise Redis::Distributed::CannotDistribute do 45 | r.randomkey 46 | end 47 | end 48 | 49 | def test_rename 50 | assert_raise Redis::Distributed::CannotDistribute do 51 | r.set("foo", "s1") 52 | r.rename "foo", "bar" 53 | end 54 | 55 | assert_equal "s1", r.get("foo") 56 | assert_equal nil, r.get("bar") 57 | end 58 | 59 | def test_renamenx 60 | assert_raise Redis::Distributed::CannotDistribute do 61 | r.set("foo", "s1") 62 | r.rename "foo", "bar" 63 | end 64 | 65 | assert_equal "s1", r.get("foo") 66 | assert_equal nil , r.get("bar") 67 | end 68 | 69 | def test_dbsize 70 | assert_equal [0], r.dbsize 71 | 72 | r.set("foo", "s1") 73 | 74 | assert_equal [1], r.dbsize 75 | end 76 | 77 | def test_flushdb 78 | r.set("foo", "s1") 79 | r.set("bar", "s2") 80 | 81 | assert_equal [2], r.dbsize 82 | 83 | r.flushdb 84 | 85 | assert_equal [0], r.dbsize 86 | end 87 | 88 | def test_migrate 89 | r.set("foo", "s1") 90 | 91 | assert_raise Redis::Distributed::CannotDistribute do 92 | r.migrate("foo", {}) 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /test/distributed_publish_subscribe_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | 5 | class TestDistributedPublishSubscribe < Test::Unit::TestCase 6 | 7 | include Helper::Distributed 8 | 9 | def test_subscribe_and_unsubscribe 10 | assert_raise Redis::Distributed::CannotDistribute do 11 | r.subscribe("foo", "bar") { } 12 | end 13 | 14 | assert_raise Redis::Distributed::CannotDistribute do 15 | r.subscribe("{qux}foo", "bar") { } 16 | end 17 | end 18 | 19 | def test_subscribe_and_unsubscribe_with_tags 20 | @subscribed = false 21 | @unsubscribed = false 22 | 23 | wire = Wire.new do 24 | r.subscribe("foo") do |on| 25 | on.subscribe do |channel, total| 26 | @subscribed = true 27 | @t1 = total 28 | end 29 | 30 | on.message do |channel, message| 31 | if message == "s1" 32 | r.unsubscribe 33 | @message = message 34 | end 35 | end 36 | 37 | on.unsubscribe do |channel, total| 38 | @unsubscribed = true 39 | @t2 = total 40 | end 41 | end 42 | end 43 | 44 | # Wait until the subscription is active before publishing 45 | Wire.pass while !@subscribed 46 | 47 | Redis::Distributed.new(NODES).publish("foo", "s1") 48 | 49 | wire.join 50 | 51 | assert @subscribed 52 | assert_equal 1, @t1 53 | assert @unsubscribed 54 | assert_equal 0, @t2 55 | assert_equal "s1", @message 56 | end 57 | 58 | def test_subscribe_within_subscribe 59 | @channels = [] 60 | 61 | wire = Wire.new do 62 | r.subscribe("foo") do |on| 63 | on.subscribe do |channel, total| 64 | @channels << channel 65 | 66 | r.subscribe("bar") if channel == "foo" 67 | r.unsubscribe if channel == "bar" 68 | end 69 | end 70 | end 71 | 72 | wire.join 73 | 74 | assert_equal ["foo", "bar"], @channels 75 | end 76 | 77 | def test_other_commands_within_a_subscribe 78 | assert_raise Redis::CommandError do 79 | r.subscribe("foo") do |on| 80 | on.subscribe do |channel, total| 81 | r.set("bar", "s2") 82 | end 83 | end 84 | end 85 | end 86 | 87 | def test_subscribe_without_a_block 88 | assert_raise LocalJumpError do 89 | r.subscribe("foo") 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /test/distributed_internals_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | 5 | class TestDistributedInternals < Test::Unit::TestCase 6 | 7 | include Helper::Distributed 8 | 9 | def test_provides_a_meaningful_inspect 10 | nodes = ["redis://127.0.0.1:#{PORT}/15", *NODES] 11 | redis = Redis::Distributed.new nodes 12 | 13 | assert_equal "#", redis.inspect 14 | end 15 | 16 | def test_default_as_urls 17 | nodes = ["redis://127.0.0.1:#{PORT}/15", *NODES] 18 | redis = Redis::Distributed.new nodes 19 | assert_equal ["redis://127.0.0.1:#{PORT}/15", *NODES], redis.nodes.map { |node| node.client.id} 20 | end 21 | 22 | def test_default_as_config_hashes 23 | nodes = [OPTIONS.merge(:host => '127.0.0.1'), OPTIONS.merge(:host => 'somehost', :port => PORT.next)] 24 | redis = Redis::Distributed.new nodes 25 | assert_equal ["redis://127.0.0.1:#{PORT}/15","redis://somehost:#{PORT.next}/15"], redis.nodes.map { |node| node.client.id } 26 | end 27 | 28 | def test_as_mix_and_match 29 | nodes = ["redis://127.0.0.1:7389/15", OPTIONS.merge(:host => 'somehost'), OPTIONS.merge(:host => 'somehost', :port => PORT.next)] 30 | redis = Redis::Distributed.new nodes 31 | assert_equal ["redis://127.0.0.1:7389/15", "redis://somehost:#{PORT}/15", "redis://somehost:#{PORT.next}/15"], redis.nodes.map { |node| node.client.id } 32 | end 33 | 34 | def test_override_id 35 | nodes = [OPTIONS.merge(:host => '127.0.0.1', :id => "test"), OPTIONS.merge( :host => 'somehost', :port => PORT.next, :id => "test1")] 36 | redis = Redis::Distributed.new nodes 37 | assert_equal redis.nodes.first.client.id, "test" 38 | assert_equal redis.nodes.last.client.id, "test1" 39 | assert_equal "#", redis.inspect 40 | end 41 | 42 | def test_can_be_duped_to_create_a_new_connection 43 | redis = Redis::Distributed.new(NODES) 44 | 45 | clients = redis.info[0]["connected_clients"].to_i 46 | 47 | r2 = redis.dup 48 | r2.ping 49 | 50 | assert_equal clients + 1, redis.info[0]["connected_clients"].to_i 51 | end 52 | 53 | def test_keeps_options_after_dup 54 | r1 = Redis::Distributed.new(NODES, :tag => /^(\w+):/) 55 | 56 | assert_raise(Redis::Distributed::CannotDistribute) do 57 | r1.sinter("foo", "bar") 58 | end 59 | 60 | assert_equal [], r1.sinter("baz:foo", "baz:bar") 61 | 62 | r2 = r1.dup 63 | 64 | assert_raise(Redis::Distributed::CannotDistribute) do 65 | r2.sinter("foo", "bar") 66 | end 67 | 68 | assert_equal [], r2.sinter("baz:foo", "baz:bar") 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /test/scripting_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | 5 | class TestScripting < Test::Unit::TestCase 6 | 7 | include Helper::Client 8 | 9 | def to_sha(script) 10 | r.script(:load, script) 11 | end 12 | 13 | def test_script_exists 14 | target_version "2.5.9" do # 2.6-rc1 15 | a = to_sha("return 1") 16 | b = a.succ 17 | 18 | assert_equal true, r.script(:exists, a) 19 | assert_equal false, r.script(:exists, b) 20 | assert_equal [true], r.script(:exists, [a]) 21 | assert_equal [false], r.script(:exists, [b]) 22 | assert_equal [true, false], r.script(:exists, [a, b]) 23 | end 24 | end 25 | 26 | def test_script_flush 27 | target_version "2.5.9" do # 2.6-rc1 28 | sha = to_sha("return 1") 29 | assert r.script(:exists, sha) 30 | assert_equal "OK", r.script(:flush) 31 | assert !r.script(:exists, sha) 32 | end 33 | end 34 | 35 | def test_script_kill 36 | target_version "2.5.9" do # 2.6-rc1 37 | redis_mock(:script => lambda { |arg| "+#{arg.upcase}" }) do |redis| 38 | assert_equal "KILL", redis.script(:kill) 39 | end 40 | end 41 | end 42 | 43 | def test_eval 44 | target_version "2.5.9" do # 2.6-rc1 45 | assert_equal 0, r.eval("return #KEYS") 46 | assert_equal 0, r.eval("return #ARGV") 47 | assert_equal ["k1", "k2"], r.eval("return KEYS", ["k1", "k2"]) 48 | assert_equal ["a1", "a2"], r.eval("return ARGV", [], ["a1", "a2"]) 49 | end 50 | end 51 | 52 | def test_eval_with_options_hash 53 | target_version "2.5.9" do # 2.6-rc1 54 | assert_equal 0, r.eval("return #KEYS", {}) 55 | assert_equal 0, r.eval("return #ARGV", {}) 56 | assert_equal ["k1", "k2"], r.eval("return KEYS", { :keys => ["k1", "k2"] }) 57 | assert_equal ["a1", "a2"], r.eval("return ARGV", { :argv => ["a1", "a2"] }) 58 | end 59 | end 60 | 61 | def test_evalsha 62 | target_version "2.5.9" do # 2.6-rc1 63 | assert_equal 0, r.evalsha(to_sha("return #KEYS")) 64 | assert_equal 0, r.evalsha(to_sha("return #ARGV")) 65 | assert_equal ["k1", "k2"], r.evalsha(to_sha("return KEYS"), ["k1", "k2"]) 66 | assert_equal ["a1", "a2"], r.evalsha(to_sha("return ARGV"), [], ["a1", "a2"]) 67 | end 68 | end 69 | 70 | def test_evalsha_with_options_hash 71 | target_version "2.5.9" do # 2.6-rc1 72 | assert_equal 0, r.evalsha(to_sha("return #KEYS"), {}) 73 | assert_equal 0, r.evalsha(to_sha("return #ARGV"), {}) 74 | assert_equal ["k1", "k2"], r.evalsha(to_sha("return KEYS"), { :keys => ["k1", "k2"] }) 75 | assert_equal ["a1", "a2"], r.evalsha(to_sha("return ARGV"), { :argv => ["a1", "a2"] }) 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /test/remote_server_control_commands_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | 5 | class TestRemoteServerControlCommands < Test::Unit::TestCase 6 | 7 | include Helper::Client 8 | 9 | def test_info 10 | keys = [ 11 | "redis_version", 12 | "uptime_in_seconds", 13 | "uptime_in_days", 14 | "connected_clients", 15 | "used_memory", 16 | "total_connections_received", 17 | "total_commands_processed", 18 | ] 19 | 20 | info = r.info 21 | 22 | keys.each do |k| 23 | msg = "expected #info to include #{k}" 24 | assert info.keys.include?(k), msg 25 | end 26 | end 27 | 28 | def test_info_commandstats 29 | target_version "2.5.7" do 30 | r.config(:resetstat) 31 | r.ping 32 | 33 | result = r.info(:commandstats) 34 | assert_equal "1", result["ping"]["calls"] 35 | end 36 | end 37 | 38 | def test_monitor_redis_lt_2_5_0 39 | return unless version < "2.5.0" 40 | 41 | log = [] 42 | 43 | wire = Wire.new do 44 | Redis.new(OPTIONS).monitor do |line| 45 | log << line 46 | break if log.size == 3 47 | end 48 | end 49 | 50 | Wire.pass while log.empty? # Faster than sleep 51 | 52 | r.set "foo", "s1" 53 | 54 | wire.join 55 | 56 | assert log[-1][%q{(db 15) "set" "foo" "s1"}] 57 | end 58 | 59 | def test_monitor_redis_gte_2_5_0 60 | return unless version >= "2.5.0" 61 | 62 | log = [] 63 | 64 | wire = Wire.new do 65 | Redis.new(OPTIONS).monitor do |line| 66 | log << line 67 | break if line =~ /set/ 68 | end 69 | end 70 | 71 | Wire.pass while log.empty? # Faster than sleep 72 | 73 | r.set "foo", "s1" 74 | 75 | wire.join 76 | 77 | assert log[-1] =~ /\b15\b.* "set" "foo" "s1"/ 78 | end 79 | 80 | def test_monitor_returns_value_for_break 81 | result = r.monitor do |line| 82 | break line 83 | end 84 | 85 | assert_equal result, "OK" 86 | end 87 | 88 | def test_echo 89 | assert_equal "foo bar baz\n", r.echo("foo bar baz\n") 90 | end 91 | 92 | def test_debug 93 | r.set "foo", "s1" 94 | 95 | assert r.debug(:object, "foo").kind_of?(String) 96 | end 97 | 98 | def test_object 99 | r.lpush "list", "value" 100 | 101 | assert_equal r.object(:refcount, "list"), 1 102 | assert_equal r.object(:encoding, "list"), "ziplist" 103 | assert r.object(:idletime, "list").kind_of?(Fixnum) 104 | end 105 | 106 | def test_sync 107 | redis_mock(:sync => lambda { "+OK" }) do |redis| 108 | assert_equal "OK", redis.sync 109 | end 110 | end 111 | 112 | def test_slowlog 113 | r.slowlog(:reset) 114 | result = r.slowlog(:len) 115 | assert_equal result, 0 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /test/commands_on_strings_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | require "lint/strings" 5 | 6 | class TestCommandsOnStrings < Test::Unit::TestCase 7 | 8 | include Helper::Client 9 | include Lint::Strings 10 | 11 | def test_mget 12 | r.set("foo", "s1") 13 | r.set("bar", "s2") 14 | 15 | assert_equal ["s1", "s2"] , r.mget("foo", "bar") 16 | assert_equal ["s1", "s2", nil], r.mget("foo", "bar", "baz") 17 | end 18 | 19 | def test_mget_mapped 20 | r.set("foo", "s1") 21 | r.set("bar", "s2") 22 | 23 | response = r.mapped_mget("foo", "bar") 24 | 25 | assert_equal "s1", response["foo"] 26 | assert_equal "s2", response["bar"] 27 | 28 | response = r.mapped_mget("foo", "bar", "baz") 29 | 30 | assert_equal "s1", response["foo"] 31 | assert_equal "s2", response["bar"] 32 | assert_equal nil , response["baz"] 33 | end 34 | 35 | def test_mapped_mget_in_a_pipeline_returns_hash 36 | r.set("foo", "s1") 37 | r.set("bar", "s2") 38 | 39 | result = r.pipelined do 40 | r.mapped_mget("foo", "bar") 41 | end 42 | 43 | assert_equal result[0], { "foo" => "s1", "bar" => "s2" } 44 | end 45 | 46 | def test_mset 47 | r.mset(:foo, "s1", :bar, "s2") 48 | 49 | assert_equal "s1", r.get("foo") 50 | assert_equal "s2", r.get("bar") 51 | end 52 | 53 | def test_mset_mapped 54 | r.mapped_mset(:foo => "s1", :bar => "s2") 55 | 56 | assert_equal "s1", r.get("foo") 57 | assert_equal "s2", r.get("bar") 58 | end 59 | 60 | def test_msetnx 61 | r.set("foo", "s1") 62 | assert_equal false, r.msetnx(:foo, "s2", :bar, "s3") 63 | assert_equal "s1", r.get("foo") 64 | assert_equal nil, r.get("bar") 65 | 66 | r.del("foo") 67 | assert_equal true, r.msetnx(:foo, "s2", :bar, "s3") 68 | assert_equal "s2", r.get("foo") 69 | assert_equal "s3", r.get("bar") 70 | end 71 | 72 | def test_msetnx_mapped 73 | r.set("foo", "s1") 74 | assert_equal false, r.mapped_msetnx(:foo => "s2", :bar => "s3") 75 | assert_equal "s1", r.get("foo") 76 | assert_equal nil, r.get("bar") 77 | 78 | r.del("foo") 79 | assert_equal true, r.mapped_msetnx(:foo => "s2", :bar => "s3") 80 | assert_equal "s2", r.get("foo") 81 | assert_equal "s3", r.get("bar") 82 | end 83 | 84 | def test_bitop 85 | try_encoding("UTF-8") do 86 | target_version "2.5.10" do 87 | r.set("foo", "a") 88 | r.set("bar", "b") 89 | 90 | r.bitop(:and, "foo&bar", "foo", "bar") 91 | assert_equal "\x60", r.get("foo&bar") 92 | r.bitop(:or, "foo|bar", "foo", "bar") 93 | assert_equal "\x63", r.get("foo|bar") 94 | r.bitop(:xor, "foo^bar", "foo", "bar") 95 | assert_equal "\x03", r.get("foo^bar") 96 | r.bitop(:not, "~foo", "foo") 97 | assert_equal "\x9E", r.get("~foo") 98 | end 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /test/lint/value_types.rb: -------------------------------------------------------------------------------- 1 | module Lint 2 | 3 | module ValueTypes 4 | 5 | def test_exists 6 | assert_equal false, r.exists("foo") 7 | 8 | r.set("foo", "s1") 9 | 10 | assert_equal true, r.exists("foo") 11 | end 12 | 13 | def test_type 14 | assert_equal "none", r.type("foo") 15 | 16 | r.set("foo", "s1") 17 | 18 | assert_equal "string", r.type("foo") 19 | end 20 | 21 | def test_keys 22 | r.set("f", "s1") 23 | r.set("fo", "s2") 24 | r.set("foo", "s3") 25 | 26 | assert_equal ["f","fo", "foo"], r.keys("f*").sort 27 | end 28 | 29 | def test_expire 30 | r.set("foo", "s1") 31 | assert r.expire("foo", 2) 32 | assert_in_range 0..2, r.ttl("foo") 33 | end 34 | 35 | def test_pexpire 36 | target_version "2.5.4" do 37 | r.set("foo", "s1") 38 | assert r.pexpire("foo", 2000) 39 | assert_in_range 0..2, r.ttl("foo") 40 | end 41 | end 42 | 43 | def test_expireat 44 | r.set("foo", "s1") 45 | assert r.expireat("foo", (Time.now + 2).to_i) 46 | assert_in_range 0..2, r.ttl("foo") 47 | end 48 | 49 | def test_pexpireat 50 | target_version "2.5.4" do 51 | r.set("foo", "s1") 52 | assert r.pexpireat("foo", (Time.now + 2).to_i * 1_000) 53 | assert_in_range 0..2, r.ttl("foo") 54 | end 55 | end 56 | 57 | def test_persist 58 | r.set("foo", "s1") 59 | r.expire("foo", 1) 60 | r.persist("foo") 61 | 62 | assert(-1 == r.ttl("foo")) 63 | end 64 | 65 | def test_ttl 66 | r.set("foo", "s1") 67 | r.expire("foo", 2) 68 | assert_in_range 0..2, r.ttl("foo") 69 | end 70 | 71 | def test_pttl 72 | target_version "2.5.4" do 73 | r.set("foo", "s1") 74 | r.expire("foo", 2) 75 | assert_in_range 1..2000, r.pttl("foo") 76 | end 77 | end 78 | 79 | def test_dump_and_restore 80 | target_version "2.5.7" do 81 | r.set("foo", "a") 82 | v = r.dump("foo") 83 | r.del("foo") 84 | 85 | assert r.restore("foo", 1000, v) 86 | assert_equal "a", r.get("foo") 87 | assert [0, 1].include? r.ttl("foo") 88 | 89 | r.rpush("bar", ["b", "c", "d"]) 90 | w = r.dump("bar") 91 | r.del("bar") 92 | 93 | assert r.restore("bar", 1000, w) 94 | assert_equal ["b", "c", "d"], r.lrange("bar", 0, -1) 95 | assert [0, 1].include? r.ttl("bar") 96 | end 97 | end 98 | 99 | def test_move 100 | r.select 14 101 | r.flushdb 102 | 103 | r.set "bar", "s3" 104 | 105 | r.select 15 106 | 107 | r.set "foo", "s1" 108 | r.set "bar", "s2" 109 | 110 | assert r.move("foo", 14) 111 | assert_equal nil, r.get("foo") 112 | 113 | assert !r.move("bar", 14) 114 | assert_equal "s2", r.get("bar") 115 | 116 | r.select 14 117 | 118 | assert_equal "s1", r.get("foo") 119 | assert_equal "s3", r.get("bar") 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /.order: -------------------------------------------------------------------------------- 1 | { 2 | "connection": [ 3 | "auth", 4 | "select", 5 | "ping", 6 | "echo", 7 | "quit" 8 | ], 9 | "server": [ 10 | "bgrewriteaof", 11 | "bgsave", 12 | "config", 13 | "dbsize", 14 | "debug", 15 | "flushall", 16 | "flushdb", 17 | "info", 18 | "lastsave", 19 | "monitor", 20 | "save", 21 | "shutdown", 22 | "slaveof", 23 | "slowlog", 24 | "sync", 25 | "time", 26 | "client" 27 | ], 28 | "generic": [ 29 | "persist", 30 | "expire", 31 | "expireat", 32 | "ttl", 33 | "pexpire", 34 | "pexpireat", 35 | "pttl", 36 | "dump", 37 | "restore", 38 | "del", 39 | "exists", 40 | "keys", 41 | "migrate", 42 | "move", 43 | "object", 44 | "randomkey", 45 | "rename", 46 | "renamenx", 47 | "sort", 48 | "type" 49 | ], 50 | "string": [ 51 | "decr", 52 | "decrby", 53 | "incr", 54 | "incrby", 55 | "incrbyfloat", 56 | "set", 57 | "setex", 58 | "psetex", 59 | "setnx", 60 | "mset", 61 | "mapped_mset", 62 | "msetnx", 63 | "mapped_msetnx", 64 | "get", 65 | "mget", 66 | "mapped_mget", 67 | "setrange", 68 | "getrange", 69 | "setbit", 70 | "getbit", 71 | "append", 72 | "bitcount", 73 | "getset", 74 | "strlen", 75 | "bitop" 76 | ], 77 | "list": [ 78 | "llen", 79 | "lpush", 80 | "lpushx", 81 | "rpush", 82 | "rpushx", 83 | "lpop", 84 | "rpop", 85 | "rpoplpush", 86 | "_bpop", 87 | "blpop", 88 | "brpop", 89 | "brpoplpush", 90 | "lindex", 91 | "linsert", 92 | "lrange", 93 | "lrem", 94 | "lset", 95 | "ltrim" 96 | ], 97 | "set": [ 98 | "scard", 99 | "sadd", 100 | "srem", 101 | "spop", 102 | "srandmember", 103 | "smove", 104 | "sismember", 105 | "smembers", 106 | "sdiff", 107 | "sdiffstore", 108 | "sinter", 109 | "sinterstore", 110 | "sunion", 111 | "sunionstore" 112 | ], 113 | "sorted_set": [ 114 | "zcard", 115 | "zadd", 116 | "zincrby", 117 | "zrem", 118 | "zscore", 119 | "zrange", 120 | "zrevrange", 121 | "zrank", 122 | "zrevrank", 123 | "zremrangebyrank", 124 | "zrangebyscore", 125 | "zrevrangebyscore", 126 | "zremrangebyscore", 127 | "zcount", 128 | "zinterstore", 129 | "zunionstore" 130 | ], 131 | "hash": [ 132 | "hlen", 133 | "hset", 134 | "hsetnx", 135 | "hmset", 136 | "mapped_hmset", 137 | "hget", 138 | "hmget", 139 | "mapped_hmget", 140 | "hdel", 141 | "hexists", 142 | "hincrby", 143 | "hincrbyfloat", 144 | "hkeys", 145 | "hvals", 146 | "hgetall" 147 | ], 148 | "pubsub": [ 149 | "publish", 150 | "subscribed?", 151 | "subscribe", 152 | "unsubscribe", 153 | "psubscribe", 154 | "punsubscribe" 155 | ], 156 | "transactions": [ 157 | "watch", 158 | "unwatch", 159 | "pipelined", 160 | "multi", 161 | "exec", 162 | "discard" 163 | ], 164 | "scripting": [ 165 | "script", 166 | "_eval", 167 | "eval", 168 | "evalsha" 169 | ] 170 | } -------------------------------------------------------------------------------- /lib/redis/connection/synchrony.rb: -------------------------------------------------------------------------------- 1 | require "redis/connection/command_helper" 2 | require "redis/connection/registry" 3 | require "redis/errors" 4 | require "em-synchrony" 5 | require "hiredis/reader" 6 | 7 | class Redis 8 | module Connection 9 | class RedisClient < EventMachine::Connection 10 | include EventMachine::Deferrable 11 | 12 | def post_init 13 | @req = nil 14 | @connected = false 15 | @reader = ::Hiredis::Reader.new 16 | end 17 | 18 | def connection_completed 19 | @connected = true 20 | succeed 21 | end 22 | 23 | def connected? 24 | @connected 25 | end 26 | 27 | def receive_data(data) 28 | @reader.feed(data) 29 | 30 | loop do 31 | begin 32 | reply = @reader.gets 33 | rescue RuntimeError => err 34 | @req.fail [:error, ProtocolError.new(err.message)] 35 | break 36 | end 37 | 38 | break if reply == false 39 | 40 | reply = CommandError.new(reply.message) if reply.is_a?(RuntimeError) 41 | @req.succeed [:reply, reply] 42 | end 43 | end 44 | 45 | def read 46 | @req = EventMachine::DefaultDeferrable.new 47 | EventMachine::Synchrony.sync @req 48 | end 49 | 50 | def send(data) 51 | callback { send_data data } 52 | end 53 | 54 | def unbind 55 | @connected = false 56 | if @req 57 | @req.fail [:error, Errno::ECONNRESET] 58 | @req = nil 59 | else 60 | fail 61 | end 62 | end 63 | end 64 | 65 | class Synchrony 66 | include Redis::Connection::CommandHelper 67 | 68 | def self.connect(config) 69 | if config[:scheme] == "unix" 70 | conn = EventMachine.connect_unix_domain(config[:path], RedisClient) 71 | else 72 | conn = EventMachine.connect(config[:host], config[:port], RedisClient) do |c| 73 | c.pending_connect_timeout = [config[:timeout], 0.1].max 74 | end 75 | end 76 | 77 | fiber = Fiber.current 78 | conn.callback { fiber.resume } 79 | conn.errback { fiber.resume :refused } 80 | 81 | raise Errno::ECONNREFUSED if Fiber.yield == :refused 82 | 83 | instance = new(conn) 84 | instance.timeout = config[:timeout] 85 | instance 86 | end 87 | 88 | def initialize(connection) 89 | @connection = connection 90 | end 91 | 92 | def connected? 93 | @connection && @connection.connected? 94 | end 95 | 96 | def timeout=(timeout) 97 | @timeout = timeout 98 | end 99 | 100 | def disconnect 101 | @connection.close_connection 102 | @connection = nil 103 | end 104 | 105 | def write(command) 106 | @connection.send(build_command(command)) 107 | end 108 | 109 | def read 110 | type, payload = @connection.read 111 | 112 | if type == :reply 113 | payload 114 | elsif type == :error 115 | raise payload 116 | else 117 | raise "Unknown type #{type.inspect}" 118 | end 119 | end 120 | end 121 | end 122 | end 123 | 124 | Redis::Connection.drivers << Redis::Connection::Synchrony 125 | -------------------------------------------------------------------------------- /test/lint/sets.rb: -------------------------------------------------------------------------------- 1 | module Lint 2 | 3 | module Sets 4 | 5 | def test_sadd 6 | assert_equal true, r.sadd("foo", "s1") 7 | assert_equal true, r.sadd("foo", "s2") 8 | assert_equal false, r.sadd("foo", "s1") 9 | 10 | assert_equal ["s1", "s2"], r.smembers("foo").sort 11 | end 12 | 13 | def test_variadic_sadd 14 | target_version "2.3.9" do # 2.4-rc6 15 | assert_equal 2, r.sadd("foo", ["s1", "s2"]) 16 | assert_equal 1, r.sadd("foo", ["s1", "s2", "s3"]) 17 | 18 | assert_equal ["s1", "s2", "s3"], r.smembers("foo").sort 19 | end 20 | end 21 | 22 | def test_srem 23 | r.sadd("foo", "s1") 24 | r.sadd("foo", "s2") 25 | 26 | assert_equal true, r.srem("foo", "s1") 27 | assert_equal false, r.srem("foo", "s3") 28 | 29 | assert_equal ["s2"], r.smembers("foo") 30 | end 31 | 32 | def test_variadic_srem 33 | target_version "2.3.9" do # 2.4-rc6 34 | r.sadd("foo", "s1") 35 | r.sadd("foo", "s2") 36 | r.sadd("foo", "s3") 37 | 38 | assert_equal 1, r.srem("foo", ["s1", "aaa"]) 39 | assert_equal 0, r.srem("foo", ["bbb", "ccc" "ddd"]) 40 | assert_equal 1, r.srem("foo", ["eee", "s3"]) 41 | 42 | assert_equal ["s2"], r.smembers("foo") 43 | end 44 | end 45 | 46 | def test_spop 47 | r.sadd "foo", "s1" 48 | r.sadd "foo", "s2" 49 | 50 | assert ["s1", "s2"].include?(r.spop("foo")) 51 | assert ["s1", "s2"].include?(r.spop("foo")) 52 | assert_equal nil, r.spop("foo") 53 | end 54 | 55 | def test_scard 56 | assert_equal 0, r.scard("foo") 57 | 58 | r.sadd "foo", "s1" 59 | 60 | assert_equal 1, r.scard("foo") 61 | 62 | r.sadd "foo", "s2" 63 | 64 | assert_equal 2, r.scard("foo") 65 | end 66 | 67 | def test_sismember 68 | assert_equal false, r.sismember("foo", "s1") 69 | 70 | r.sadd "foo", "s1" 71 | 72 | assert_equal true, r.sismember("foo", "s1") 73 | assert_equal false, r.sismember("foo", "s2") 74 | end 75 | 76 | def test_smembers 77 | assert_equal [], r.smembers("foo") 78 | 79 | r.sadd "foo", "s1" 80 | r.sadd "foo", "s2" 81 | 82 | assert_equal ["s1", "s2"], r.smembers("foo").sort 83 | end 84 | 85 | def test_srandmember 86 | r.sadd "foo", "s1" 87 | r.sadd "foo", "s2" 88 | 89 | 4.times do 90 | assert ["s1", "s2"].include?(r.srandmember("foo")) 91 | end 92 | 93 | assert_equal 2, r.scard("foo") 94 | end 95 | 96 | def test_srandmember_with_positive_count 97 | r.sadd "foo", "s1" 98 | r.sadd "foo", "s2" 99 | r.sadd "foo", "s3" 100 | r.sadd "foo", "s4" 101 | 102 | 4.times do 103 | assert !(["s1", "s2", "s3", "s4"] & r.srandmember("foo", 3)).empty? 104 | 105 | assert_equal 3, r.srandmember("foo", 3).size 106 | end 107 | 108 | assert_equal 4, r.scard("foo") 109 | end 110 | 111 | def test_srandmember_with_negative_count 112 | r.sadd "foo", "s1" 113 | r.sadd "foo", "s2" 114 | r.sadd "foo", "s3" 115 | r.sadd "foo", "s4" 116 | 117 | 4.times do 118 | assert !(["s1", "s2", "s3", "s4"] & r.srandmember("foo", -6)).empty? 119 | assert_equal 6, r.srandmember("foo", -6).size 120 | end 121 | 122 | assert_equal 4, r.scard("foo") 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /test/support/redis_mock.rb: -------------------------------------------------------------------------------- 1 | require "socket" 2 | 3 | module RedisMock 4 | class Server 5 | VERBOSE = false 6 | 7 | def initialize(port, options = {}, &block) 8 | @server = TCPServer.new(options[:host] || "127.0.0.1", port) 9 | @server.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true) 10 | end 11 | 12 | def start(&block) 13 | @thread = Thread.new { run(&block) } 14 | end 15 | 16 | # Bail out of @server.accept before closing the socket. This is required 17 | # to avoid EADDRINUSE after a couple of iterations. 18 | def shutdown 19 | @thread.terminate if @thread 20 | @server.close if @server 21 | rescue => ex 22 | $stderr.puts "Error closing mock server: #{ex.message}" if VERBOSE 23 | $stderr.puts ex.backtrace if VERBOSE 24 | end 25 | 26 | def run 27 | loop do 28 | session = @server.accept 29 | 30 | begin 31 | return if yield(session) == :exit 32 | ensure 33 | session.close 34 | end 35 | end 36 | rescue => ex 37 | $stderr.puts "Error running mock server: #{ex.message}" if VERBOSE 38 | $stderr.puts ex.backtrace if VERBOSE 39 | ensure 40 | @server.close 41 | end 42 | end 43 | 44 | MOCK_PORT = 6382 45 | 46 | # Starts a mock Redis server in a thread. 47 | # 48 | # The server will use the lambda handler passed as argument to handle 49 | # connections. For example: 50 | # 51 | # handler = lambda { |session| session.close } 52 | # RedisMock.start_with_handler(handler) do 53 | # # Every connection will be closed immediately 54 | # end 55 | # 56 | def self.start_with_handler(blk, options = {}) 57 | server = Server.new(MOCK_PORT, options) 58 | 59 | begin 60 | server.start(&blk) 61 | 62 | yield(MOCK_PORT) 63 | 64 | ensure 65 | server.shutdown 66 | end 67 | end 68 | 69 | # Starts a mock Redis server in a thread. 70 | # 71 | # The server will reply with a `+OK` to all commands, but you can 72 | # customize it by providing a hash. For example: 73 | # 74 | # RedisMock.start(:ping => lambda { "+PONG" }) do 75 | # assert_equal "PONG", Redis.new(:port => MOCK_PORT).ping 76 | # end 77 | # 78 | def self.start(commands, options = {}, &blk) 79 | handler = lambda do |session| 80 | while line = session.gets 81 | argv = Array.new(line[1..-3].to_i) do 82 | bytes = session.gets[1..-3].to_i 83 | arg = session.read(bytes) 84 | session.read(2) # Discard \r\n 85 | arg 86 | end 87 | 88 | command = argv.shift 89 | blk = commands[command.to_sym] 90 | blk ||= lambda { |*_| "+OK" } 91 | 92 | response = blk.call(*argv) 93 | 94 | # Convert a nil response to :close 95 | response ||= :close 96 | 97 | if response == :exit 98 | break :exit 99 | elsif response == :close 100 | break :close 101 | elsif response.is_a?(Array) 102 | session.write("*%d\r\n" % response.size) 103 | response.each do |e| 104 | session.write("$%d\r\n%s\r\n" % [e.length, e]) 105 | end 106 | else 107 | session.write(response) 108 | session.write("\r\n") unless response.end_with?("\r\n") 109 | end 110 | end 111 | end 112 | 113 | start_with_handler(handler, options, &blk) 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /test/distributed_scripting_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | 5 | class TestDistributedScripting < Test::Unit::TestCase 6 | 7 | include Helper::Distributed 8 | 9 | def to_sha(script) 10 | r.script(:load, script).first 11 | end 12 | 13 | def test_script_exists 14 | target_version "2.5.9" do # 2.6-rc1 15 | a = to_sha("return 1") 16 | b = a.succ 17 | 18 | assert_equal [true], r.script(:exists, a) 19 | assert_equal [false], r.script(:exists, b) 20 | assert_equal [[true]], r.script(:exists, [a]) 21 | assert_equal [[false]], r.script(:exists, [b]) 22 | assert_equal [[true, false]], r.script(:exists, [a, b]) 23 | end 24 | end 25 | 26 | def test_script_flush 27 | target_version "2.5.9" do # 2.6-rc1 28 | sha = to_sha("return 1") 29 | assert r.script(:exists, sha).first 30 | assert_equal ["OK"], r.script(:flush) 31 | assert !r.script(:exists, sha).first 32 | end 33 | end 34 | 35 | def test_script_kill 36 | target_version "2.5.9" do # 2.6-rc1 37 | redis_mock(:script => lambda { |arg| "+#{arg.upcase}" }) do |redis| 38 | assert_equal ["KILL"], redis.script(:kill) 39 | end 40 | end 41 | end 42 | 43 | def test_eval 44 | target_version "2.5.9" do # 2.6-rc1 45 | assert_raises(Redis::Distributed::CannotDistribute) do 46 | r.eval("return #KEYS") 47 | end 48 | 49 | assert_raises(Redis::Distributed::CannotDistribute) do 50 | r.eval("return KEYS", ["k1", "k2"]) 51 | end 52 | 53 | assert_equal ["k1"], r.eval("return KEYS", ["k1"]) 54 | assert_equal ["a1", "a2"], r.eval("return ARGV", ["k1"], ["a1", "a2"]) 55 | end 56 | end 57 | 58 | def test_eval_with_options_hash 59 | target_version "2.5.9" do # 2.6-rc1 60 | assert_raises(Redis::Distributed::CannotDistribute) do 61 | r.eval("return #KEYS", {}) 62 | end 63 | 64 | assert_raises(Redis::Distributed::CannotDistribute) do 65 | r.eval("return KEYS", { :keys => ["k1", "k2"] }) 66 | end 67 | 68 | assert_equal ["k1"], r.eval("return KEYS", { :keys => ["k1"] }) 69 | assert_equal ["a1", "a2"], r.eval("return ARGV", { :keys => ["k1"], :argv => ["a1", "a2"] }) 70 | end 71 | end 72 | 73 | def test_evalsha 74 | target_version "2.5.9" do # 2.6-rc1 75 | assert_raises(Redis::Distributed::CannotDistribute) do 76 | r.evalsha(to_sha("return #KEYS")) 77 | end 78 | 79 | assert_raises(Redis::Distributed::CannotDistribute) do 80 | r.evalsha(to_sha("return KEYS"), ["k1", "k2"]) 81 | end 82 | 83 | assert_equal ["k1"], r.evalsha(to_sha("return KEYS"), ["k1"]) 84 | assert_equal ["a1", "a2"], r.evalsha(to_sha("return ARGV"), ["k1"], ["a1", "a2"]) 85 | end 86 | end 87 | 88 | def test_evalsha_with_options_hash 89 | target_version "2.5.9" do # 2.6-rc1 90 | assert_raises(Redis::Distributed::CannotDistribute) do 91 | r.evalsha(to_sha("return #KEYS"), {}) 92 | end 93 | 94 | assert_raises(Redis::Distributed::CannotDistribute) do 95 | r.evalsha(to_sha("return KEYS"), { :keys => ["k1", "k2"] }) 96 | end 97 | 98 | assert_equal ["k1"], r.evalsha(to_sha("return KEYS"), { :keys => ["k1"] }) 99 | assert_equal ["a1", "a2"], r.evalsha(to_sha("return ARGV"), { :keys => ["k1"], :argv => ["a1", "a2"] }) 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /test/commands_on_value_types_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | require "lint/value_types" 5 | 6 | class TestCommandsOnValueTypes < Test::Unit::TestCase 7 | 8 | include Helper::Client 9 | include Lint::ValueTypes 10 | 11 | def test_del 12 | r.set "foo", "s1" 13 | r.set "bar", "s2" 14 | r.set "baz", "s3" 15 | 16 | assert_equal ["bar", "baz", "foo"], r.keys("*").sort 17 | 18 | assert_equal 1, r.del("foo") 19 | 20 | assert_equal ["bar", "baz"], r.keys("*").sort 21 | 22 | assert_equal 2, r.del("bar", "baz") 23 | 24 | assert_equal [], r.keys("*").sort 25 | end 26 | 27 | def test_del_with_array_argument 28 | r.set "foo", "s1" 29 | r.set "bar", "s2" 30 | r.set "baz", "s3" 31 | 32 | assert_equal ["bar", "baz", "foo"], r.keys("*").sort 33 | 34 | assert_equal 1, r.del(["foo"]) 35 | 36 | assert_equal ["bar", "baz"], r.keys("*").sort 37 | 38 | assert_equal 2, r.del(["bar", "baz"]) 39 | 40 | assert_equal [], r.keys("*").sort 41 | end 42 | 43 | def test_randomkey 44 | assert r.randomkey.to_s.empty? 45 | 46 | r.set("foo", "s1") 47 | 48 | assert_equal "foo", r.randomkey 49 | 50 | r.set("bar", "s2") 51 | 52 | 4.times do 53 | assert ["foo", "bar"].include?(r.randomkey) 54 | end 55 | end 56 | 57 | def test_rename 58 | r.set("foo", "s1") 59 | r.rename "foo", "bar" 60 | 61 | assert_equal "s1", r.get("bar") 62 | assert_equal nil, r.get("foo") 63 | end 64 | 65 | def test_renamenx 66 | r.set("foo", "s1") 67 | r.set("bar", "s2") 68 | 69 | assert_equal false, r.renamenx("foo", "bar") 70 | 71 | assert_equal "s1", r.get("foo") 72 | assert_equal "s2", r.get("bar") 73 | end 74 | 75 | def test_dbsize 76 | assert_equal 0, r.dbsize 77 | 78 | r.set("foo", "s1") 79 | 80 | assert_equal 1, r.dbsize 81 | end 82 | 83 | def test_flushdb 84 | r.set("foo", "s1") 85 | r.set("bar", "s2") 86 | 87 | assert_equal 2, r.dbsize 88 | 89 | r.flushdb 90 | 91 | assert_equal 0, r.dbsize 92 | end 93 | 94 | def test_flushall 95 | redis_mock(:flushall => lambda { "+FLUSHALL" }) do |redis| 96 | assert_equal "FLUSHALL", redis.flushall 97 | end 98 | end 99 | 100 | def test_migrate 101 | redis_mock(:migrate => lambda { |*args| args }) do |redis| 102 | options = { :host => "127.0.0.1", :port => 1234 } 103 | 104 | assert_raise(RuntimeError, /host not specified/) do 105 | redis.migrate("foo", options.reject { |key, _| key == :host }) 106 | end 107 | 108 | assert_raise(RuntimeError, /port not specified/) do 109 | redis.migrate("foo", options.reject { |key, _| key == :port }) 110 | end 111 | 112 | default_db = redis.client.db.to_i 113 | default_timeout = redis.client.timeout.to_i 114 | 115 | # Test defaults 116 | actual = redis.migrate("foo", options) 117 | expected = ["127.0.0.1", "1234", "foo", default_db.to_s, default_timeout.to_s] 118 | assert_equal expected, actual 119 | 120 | # Test db override 121 | actual = redis.migrate("foo", options.merge(:db => default_db + 1)) 122 | expected = ["127.0.0.1", "1234", "foo", (default_db + 1).to_s, default_timeout.to_s] 123 | assert_equal expected, actual 124 | 125 | # Test timeout override 126 | actual = redis.migrate("foo", options.merge(:timeout => default_timeout + 1)) 127 | expected = ["127.0.0.1", "1234", "foo", default_db.to_s, (default_timeout + 1).to_s] 128 | assert_equal expected, actual 129 | end 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /lib/redis/pipeline.rb: -------------------------------------------------------------------------------- 1 | class Redis 2 | unless defined?(::BasicObject) 3 | class BasicObject 4 | instance_methods.each { |meth| undef_method(meth) unless meth =~ /\A(__|instance_eval)/ } 5 | end 6 | end 7 | 8 | class Pipeline 9 | attr_accessor :db 10 | 11 | attr :futures 12 | 13 | def initialize 14 | @with_reconnect = true 15 | @shutdown = false 16 | @futures = [] 17 | end 18 | 19 | def with_reconnect? 20 | @with_reconnect 21 | end 22 | 23 | def without_reconnect? 24 | !@with_reconnect 25 | end 26 | 27 | def shutdown? 28 | @shutdown 29 | end 30 | 31 | def call(command, &block) 32 | # A pipeline that contains a shutdown should not raise ECONNRESET when 33 | # the connection is gone. 34 | @shutdown = true if command.first == :shutdown 35 | future = Future.new(command, block) 36 | @futures << future 37 | future 38 | end 39 | 40 | def call_pipeline(pipeline) 41 | @shutdown = true if pipeline.shutdown? 42 | @futures.concat(pipeline.futures) 43 | @db = pipeline.db 44 | nil 45 | end 46 | 47 | def commands 48 | @futures.map { |f| f._command } 49 | end 50 | 51 | def with_reconnect(val=true) 52 | @with_reconnect = false unless val 53 | yield 54 | end 55 | 56 | def without_reconnect(&blk) 57 | with_reconnect(false, &blk) 58 | end 59 | 60 | def finish(replies, &blk) 61 | if blk 62 | futures.each_with_index.map do |future, i| 63 | future._set(blk.call(replies[i])) 64 | end 65 | else 66 | futures.each_with_index.map do |future, i| 67 | future._set(replies[i]) 68 | end 69 | end 70 | end 71 | 72 | class Multi < self 73 | def finish(replies) 74 | exec = replies.last 75 | 76 | return if exec.nil? # The transaction failed because of WATCH. 77 | 78 | # EXEC command failed. 79 | raise exec if exec.is_a?(CommandError) 80 | 81 | if exec.size < futures.size 82 | # Some command wasn't recognized by Redis. 83 | raise replies.detect { |r| r.is_a?(CommandError) } 84 | end 85 | 86 | super(exec) do |reply| 87 | # Because an EXEC returns nested replies, hiredis won't be able to 88 | # convert an error reply to a CommandError instance itself. This is 89 | # specific to MULTI/EXEC, so we solve this here. 90 | reply.is_a?(::RuntimeError) ? CommandError.new(reply.message) : reply 91 | end 92 | end 93 | 94 | def commands 95 | [[:multi]] + super + [[:exec]] 96 | end 97 | end 98 | end 99 | 100 | class FutureNotReady < RuntimeError 101 | def initialize 102 | super("Value will be available once the pipeline executes.") 103 | end 104 | end 105 | 106 | class Future < BasicObject 107 | FutureNotReady = ::Redis::FutureNotReady.new 108 | 109 | def initialize(command, transformation) 110 | @command = command 111 | @transformation = transformation 112 | @object = FutureNotReady 113 | end 114 | 115 | def inspect 116 | "" 117 | end 118 | 119 | def _set(object) 120 | @object = @transformation ? @transformation.call(object) : object 121 | value 122 | end 123 | 124 | def _command 125 | @command 126 | end 127 | 128 | def value 129 | ::Kernel.raise(@object) if @object.kind_of?(::RuntimeError) 130 | @object 131 | end 132 | 133 | def is_a?(other) 134 | self.class.ancestors.include?(other) 135 | end 136 | 137 | def class 138 | Future 139 | end 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /test/commands_on_sorted_sets_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | require "lint/sorted_sets" 5 | 6 | class TestCommandsOnSortedSets < Test::Unit::TestCase 7 | 8 | include Helper::Client 9 | include Lint::SortedSets 10 | 11 | def test_zcount 12 | r.zadd "foo", 1, "s1" 13 | r.zadd "foo", 2, "s2" 14 | r.zadd "foo", 3, "s3" 15 | 16 | assert_equal 2, r.zcount("foo", 2, 3) 17 | end 18 | 19 | def test_zunionstore 20 | r.zadd "foo", 1, "s1" 21 | r.zadd "bar", 2, "s2" 22 | r.zadd "foo", 3, "s3" 23 | r.zadd "bar", 4, "s4" 24 | 25 | assert_equal 4, r.zunionstore("foobar", ["foo", "bar"]) 26 | assert_equal ["s1", "s2", "s3", "s4"], r.zrange("foobar", 0, -1) 27 | end 28 | 29 | def test_zunionstore_with_weights 30 | r.zadd "foo", 1, "s1" 31 | r.zadd "foo", 3, "s3" 32 | r.zadd "bar", 20, "s2" 33 | r.zadd "bar", 40, "s4" 34 | 35 | assert_equal 4, r.zunionstore("foobar", ["foo", "bar"]) 36 | assert_equal ["s1", "s3", "s2", "s4"], r.zrange("foobar", 0, -1) 37 | 38 | assert_equal 4, r.zunionstore("foobar", ["foo", "bar"], :weights => [10, 1]) 39 | assert_equal ["s1", "s2", "s3", "s4"], r.zrange("foobar", 0, -1) 40 | end 41 | 42 | def test_zunionstore_with_aggregate 43 | r.zadd "foo", 1, "s1" 44 | r.zadd "foo", 2, "s2" 45 | r.zadd "bar", 4, "s2" 46 | r.zadd "bar", 3, "s3" 47 | 48 | assert_equal 3, r.zunionstore("foobar", ["foo", "bar"]) 49 | assert_equal ["s1", "s3", "s2"], r.zrange("foobar", 0, -1) 50 | 51 | assert_equal 3, r.zunionstore("foobar", ["foo", "bar"], :aggregate => :min) 52 | assert_equal ["s1", "s2", "s3"], r.zrange("foobar", 0, -1) 53 | 54 | assert_equal 3, r.zunionstore("foobar", ["foo", "bar"], :aggregate => :max) 55 | assert_equal ["s1", "s3", "s2"], r.zrange("foobar", 0, -1) 56 | end 57 | 58 | def test_zinterstore 59 | r.zadd "foo", 1, "s1" 60 | r.zadd "bar", 2, "s1" 61 | r.zadd "foo", 3, "s3" 62 | r.zadd "bar", 4, "s4" 63 | 64 | assert_equal 1, r.zinterstore("foobar", ["foo", "bar"]) 65 | assert_equal ["s1"], r.zrange("foobar", 0, -1) 66 | end 67 | 68 | def test_zinterstore_with_weights 69 | r.zadd "foo", 1, "s1" 70 | r.zadd "foo", 2, "s2" 71 | r.zadd "foo", 3, "s3" 72 | r.zadd "bar", 20, "s2" 73 | r.zadd "bar", 30, "s3" 74 | r.zadd "bar", 40, "s4" 75 | 76 | assert_equal 2, r.zinterstore("foobar", ["foo", "bar"]) 77 | assert_equal ["s2", "s3"], r.zrange("foobar", 0, -1) 78 | 79 | assert_equal 2, r.zinterstore("foobar", ["foo", "bar"], :weights => [10, 1]) 80 | assert_equal ["s2", "s3"], r.zrange("foobar", 0, -1) 81 | 82 | assert_equal 40.0, r.zscore("foobar", "s2") 83 | assert_equal 60.0, r.zscore("foobar", "s3") 84 | end 85 | 86 | def test_zinterstore_with_aggregate 87 | r.zadd "foo", 1, "s1" 88 | r.zadd "foo", 2, "s2" 89 | r.zadd "foo", 3, "s3" 90 | r.zadd "bar", 20, "s2" 91 | r.zadd "bar", 30, "s3" 92 | r.zadd "bar", 40, "s4" 93 | 94 | assert_equal 2, r.zinterstore("foobar", ["foo", "bar"]) 95 | assert_equal ["s2", "s3"], r.zrange("foobar", 0, -1) 96 | assert_equal 22.0, r.zscore("foobar", "s2") 97 | assert_equal 33.0, r.zscore("foobar", "s3") 98 | 99 | assert_equal 2, r.zinterstore("foobar", ["foo", "bar"], :aggregate => :min) 100 | assert_equal ["s2", "s3"], r.zrange("foobar", 0, -1) 101 | assert_equal 2.0, r.zscore("foobar", "s2") 102 | assert_equal 3.0, r.zscore("foobar", "s3") 103 | 104 | assert_equal 2, r.zinterstore("foobar", ["foo", "bar"], :aggregate => :max) 105 | assert_equal ["s2", "s3"], r.zrange("foobar", 0, -1) 106 | assert_equal 20.0, r.zscore("foobar", "s2") 107 | assert_equal 30.0, r.zscore("foobar", "s3") 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /test/lint/lists.rb: -------------------------------------------------------------------------------- 1 | module Lint 2 | 3 | module Lists 4 | 5 | def test_lpush 6 | r.lpush "foo", "s1" 7 | r.lpush "foo", "s2" 8 | 9 | assert_equal 2, r.llen("foo") 10 | assert_equal "s2", r.lpop("foo") 11 | end 12 | 13 | def test_variadic_lpush 14 | target_version "2.3.9" do # 2.4-rc6 15 | assert_equal 3, r.lpush("foo", ["s1", "s2", "s3"]) 16 | assert_equal 3, r.llen("foo") 17 | assert_equal "s3", r.lpop("foo") 18 | end 19 | end 20 | 21 | def test_lpushx 22 | r.lpushx "foo", "s1" 23 | r.lpush "foo", "s2" 24 | r.lpushx "foo", "s3" 25 | 26 | assert_equal 2, r.llen("foo") 27 | assert_equal ["s3", "s2"], r.lrange("foo", 0, -1) 28 | end 29 | 30 | def test_rpush 31 | r.rpush "foo", "s1" 32 | r.rpush "foo", "s2" 33 | 34 | assert_equal 2, r.llen("foo") 35 | assert_equal "s2", r.rpop("foo") 36 | end 37 | 38 | def test_variadic_rpush 39 | target_version "2.3.9" do # 2.4-rc6 40 | assert_equal 3, r.rpush("foo", ["s1", "s2", "s3"]) 41 | assert_equal 3, r.llen("foo") 42 | assert_equal "s3", r.rpop("foo") 43 | end 44 | end 45 | 46 | def test_rpushx 47 | r.rpushx "foo", "s1" 48 | r.rpush "foo", "s2" 49 | r.rpushx "foo", "s3" 50 | 51 | assert_equal 2, r.llen("foo") 52 | assert_equal ["s2", "s3"], r.lrange("foo", 0, -1) 53 | end 54 | 55 | def test_llen 56 | r.rpush "foo", "s1" 57 | r.rpush "foo", "s2" 58 | 59 | assert_equal 2, r.llen("foo") 60 | end 61 | 62 | def test_lrange 63 | r.rpush "foo", "s1" 64 | r.rpush "foo", "s2" 65 | r.rpush "foo", "s3" 66 | 67 | assert_equal ["s2", "s3"], r.lrange("foo", 1, -1) 68 | assert_equal ["s1", "s2"], r.lrange("foo", 0, 1) 69 | 70 | assert_equal [], r.lrange("bar", 0, -1) 71 | end 72 | 73 | def test_ltrim 74 | r.rpush "foo", "s1" 75 | r.rpush "foo", "s2" 76 | r.rpush "foo", "s3" 77 | 78 | r.ltrim "foo", 0, 1 79 | 80 | assert_equal 2, r.llen("foo") 81 | assert_equal ["s1", "s2"], r.lrange("foo", 0, -1) 82 | end 83 | 84 | def test_lindex 85 | r.rpush "foo", "s1" 86 | r.rpush "foo", "s2" 87 | 88 | assert_equal "s1", r.lindex("foo", 0) 89 | assert_equal "s2", r.lindex("foo", 1) 90 | end 91 | 92 | def test_lset 93 | r.rpush "foo", "s1" 94 | r.rpush "foo", "s2" 95 | 96 | assert_equal "s2", r.lindex("foo", 1) 97 | assert r.lset("foo", 1, "s3") 98 | assert_equal "s3", r.lindex("foo", 1) 99 | 100 | assert_raise Redis::CommandError do 101 | r.lset("foo", 4, "s3") 102 | end 103 | end 104 | 105 | def test_lrem 106 | r.rpush "foo", "s1" 107 | r.rpush "foo", "s2" 108 | 109 | assert_equal 1, r.lrem("foo", 1, "s1") 110 | assert_equal ["s2"], r.lrange("foo", 0, -1) 111 | end 112 | 113 | def test_lpop 114 | r.rpush "foo", "s1" 115 | r.rpush "foo", "s2" 116 | 117 | assert_equal 2, r.llen("foo") 118 | assert_equal "s1", r.lpop("foo") 119 | assert_equal 1, r.llen("foo") 120 | end 121 | 122 | def test_rpop 123 | r.rpush "foo", "s1" 124 | r.rpush "foo", "s2" 125 | 126 | assert_equal 2, r.llen("foo") 127 | assert_equal "s2", r.rpop("foo") 128 | assert_equal 1, r.llen("foo") 129 | end 130 | 131 | def test_linsert 132 | r.rpush "foo", "s1" 133 | r.rpush "foo", "s3" 134 | r.linsert "foo", :before, "s3", "s2" 135 | 136 | assert_equal ["s1", "s2", "s3"], r.lrange("foo", 0, -1) 137 | 138 | assert_raise(Redis::CommandError) do 139 | r.linsert "foo", :anywhere, "s3", "s2" 140 | end 141 | end 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /lib/redis/hash_ring.rb: -------------------------------------------------------------------------------- 1 | require 'zlib' 2 | 3 | class Redis 4 | class HashRing 5 | 6 | POINTS_PER_SERVER = 160 # this is the default in libmemcached 7 | 8 | attr_reader :ring, :sorted_keys, :replicas, :nodes 9 | 10 | # nodes is a list of objects that have a proper to_s representation. 11 | # replicas indicates how many virtual points should be used pr. node, 12 | # replicas are required to improve the distribution. 13 | def initialize(nodes=[], replicas=POINTS_PER_SERVER) 14 | @replicas = replicas 15 | @ring = {} 16 | @nodes = [] 17 | @sorted_keys = [] 18 | nodes.each do |node| 19 | add_node(node) 20 | end 21 | end 22 | 23 | # Adds a `node` to the hash ring (including a number of replicas). 24 | def add_node(node) 25 | @nodes << node 26 | @replicas.times do |i| 27 | key = Zlib.crc32("#{node.id}:#{i}") 28 | @ring[key] = node 29 | @sorted_keys << key 30 | end 31 | @sorted_keys.sort! 32 | end 33 | 34 | def remove_node(node) 35 | @nodes.reject!{|n| n.id == node.id} 36 | @replicas.times do |i| 37 | key = Zlib.crc32("#{node.id}:#{i}") 38 | @ring.delete(key) 39 | @sorted_keys.reject! {|k| k == key} 40 | end 41 | end 42 | 43 | # get the node in the hash ring for this key 44 | def get_node(key) 45 | get_node_pos(key)[0] 46 | end 47 | 48 | def get_node_pos(key) 49 | return [nil,nil] if @ring.size == 0 50 | crc = Zlib.crc32(key) 51 | idx = HashRing.binary_search(@sorted_keys, crc) 52 | return [@ring[@sorted_keys[idx]], idx] 53 | end 54 | 55 | def iter_nodes(key) 56 | return [nil,nil] if @ring.size == 0 57 | _, pos = get_node_pos(key) 58 | @ring.size.times do |n| 59 | yield @ring[@sorted_keys[(pos+n) % @ring.size]] 60 | end 61 | end 62 | 63 | class << self 64 | 65 | # gem install RubyInline to use this code 66 | # Native extension to perform the binary search within the hashring. 67 | # There's a pure ruby version below so this is purely optional 68 | # for performance. In testing 20k gets and sets, the native 69 | # binary search shaved about 12% off the runtime (9sec -> 8sec). 70 | begin 71 | require 'inline' 72 | inline do |builder| 73 | builder.c <<-EOM 74 | int binary_search(VALUE ary, unsigned int r) { 75 | int upper = RARRAY_LEN(ary) - 1; 76 | int lower = 0; 77 | int idx = 0; 78 | 79 | while (lower <= upper) { 80 | idx = (lower + upper) / 2; 81 | 82 | VALUE continuumValue = RARRAY_PTR(ary)[idx]; 83 | unsigned int l = NUM2UINT(continuumValue); 84 | if (l == r) { 85 | return idx; 86 | } 87 | else if (l > r) { 88 | upper = idx - 1; 89 | } 90 | else { 91 | lower = idx + 1; 92 | } 93 | } 94 | if (upper < 0) { 95 | upper = RARRAY_LEN(ary) - 1; 96 | } 97 | return upper; 98 | } 99 | EOM 100 | end 101 | rescue Exception 102 | # Find the closest index in HashRing with value <= the given value 103 | def binary_search(ary, value, &block) 104 | upper = ary.size - 1 105 | lower = 0 106 | idx = 0 107 | 108 | while(lower <= upper) do 109 | idx = (lower + upper) / 2 110 | comp = ary[idx] <=> value 111 | 112 | if comp == 0 113 | return idx 114 | elsif comp > 0 115 | upper = idx - 1 116 | else 117 | lower = idx + 1 118 | end 119 | end 120 | 121 | if upper < 0 122 | upper = ary.size - 1 123 | end 124 | return upper 125 | end 126 | 127 | end 128 | end 129 | 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /test/lint/hashes.rb: -------------------------------------------------------------------------------- 1 | module Lint 2 | 3 | module Hashes 4 | 5 | def test_hset_and_hget 6 | r.hset("foo", "f1", "s1") 7 | 8 | assert_equal "s1", r.hget("foo", "f1") 9 | end 10 | 11 | def test_hsetnx 12 | r.hset("foo", "f1", "s1") 13 | r.hsetnx("foo", "f1", "s2") 14 | 15 | assert_equal "s1", r.hget("foo", "f1") 16 | 17 | r.del("foo") 18 | r.hsetnx("foo", "f1", "s2") 19 | 20 | assert_equal "s2", r.hget("foo", "f1") 21 | end 22 | 23 | def test_hdel 24 | r.hset("foo", "f1", "s1") 25 | 26 | assert_equal "s1", r.hget("foo", "f1") 27 | 28 | assert_equal 1, r.hdel("foo", "f1") 29 | 30 | assert_equal nil, r.hget("foo", "f1") 31 | end 32 | 33 | def test_variadic_hdel 34 | target_version "2.3.9" do 35 | r.hset("foo", "f1", "s1") 36 | r.hset("foo", "f2", "s2") 37 | 38 | assert_equal "s1", r.hget("foo", "f1") 39 | assert_equal "s2", r.hget("foo", "f2") 40 | 41 | assert_equal 2, r.hdel("foo", ["f1", "f2"]) 42 | 43 | assert_equal nil, r.hget("foo", "f1") 44 | assert_equal nil, r.hget("foo", "f2") 45 | end 46 | end 47 | 48 | def test_hexists 49 | assert_equal false, r.hexists("foo", "f1") 50 | 51 | r.hset("foo", "f1", "s1") 52 | 53 | assert r.hexists("foo", "f1") 54 | end 55 | 56 | def test_hlen 57 | assert_equal 0, r.hlen("foo") 58 | 59 | r.hset("foo", "f1", "s1") 60 | 61 | assert_equal 1, r.hlen("foo") 62 | 63 | r.hset("foo", "f2", "s2") 64 | 65 | assert_equal 2, r.hlen("foo") 66 | end 67 | 68 | def test_hkeys 69 | assert_equal [], r.hkeys("foo") 70 | 71 | r.hset("foo", "f1", "s1") 72 | r.hset("foo", "f2", "s2") 73 | 74 | assert_equal ["f1", "f2"], r.hkeys("foo") 75 | end 76 | 77 | def test_hvals 78 | assert_equal [], r.hvals("foo") 79 | 80 | r.hset("foo", "f1", "s1") 81 | r.hset("foo", "f2", "s2") 82 | 83 | assert_equal ["s1", "s2"], r.hvals("foo") 84 | end 85 | 86 | def test_hgetall 87 | assert({} == r.hgetall("foo")) 88 | 89 | r.hset("foo", "f1", "s1") 90 | r.hset("foo", "f2", "s2") 91 | 92 | assert({"f1" => "s1", "f2" => "s2"} == r.hgetall("foo")) 93 | end 94 | 95 | def test_hmset 96 | r.hmset("hash", "foo1", "bar1", "foo2", "bar2") 97 | 98 | assert_equal "bar1", r.hget("hash", "foo1") 99 | assert_equal "bar2", r.hget("hash", "foo2") 100 | end 101 | 102 | def test_hmset_with_invalid_arguments 103 | assert_raise(Redis::CommandError) do 104 | r.hmset("hash", "foo1", "bar1", "foo2", "bar2", "foo3") 105 | end 106 | end 107 | 108 | def test_mapped_hmset 109 | r.mapped_hmset("foo", :f1 => "s1", :f2 => "s2") 110 | 111 | assert_equal "s1", r.hget("foo", "f1") 112 | assert_equal "s2", r.hget("foo", "f2") 113 | end 114 | 115 | def test_hmget 116 | r.hset("foo", "f1", "s1") 117 | r.hset("foo", "f2", "s2") 118 | r.hset("foo", "f3", "s3") 119 | 120 | assert_equal ["s2", "s3"], r.hmget("foo", "f2", "f3") 121 | end 122 | 123 | def test_hmget_mapped 124 | r.hset("foo", "f1", "s1") 125 | r.hset("foo", "f2", "s2") 126 | r.hset("foo", "f3", "s3") 127 | 128 | assert({"f1" => "s1"} == r.mapped_hmget("foo", "f1")) 129 | assert({"f1" => "s1", "f2" => "s2"} == r.mapped_hmget("foo", "f1", "f2")) 130 | end 131 | 132 | def test_hincrby 133 | r.hincrby("foo", "f1", 1) 134 | 135 | assert_equal "1", r.hget("foo", "f1") 136 | 137 | r.hincrby("foo", "f1", 2) 138 | 139 | assert_equal "3", r.hget("foo", "f1") 140 | 141 | r.hincrby("foo", "f1", -1) 142 | 143 | assert_equal "2", r.hget("foo", "f1") 144 | end 145 | 146 | def test_hincrbyfloat 147 | target_version "2.5.4" do 148 | r.hincrbyfloat("foo", "f1", 1.23) 149 | 150 | assert_equal "1.23", r.hget("foo", "f1") 151 | 152 | r.hincrbyfloat("foo", "f1", 0.77) 153 | 154 | assert_equal "2", r.hget("foo", "f1") 155 | 156 | r.hincrbyfloat("foo", "f1", -0.1) 157 | 158 | assert_equal "1.9", r.hget("foo", "f1") 159 | end 160 | end 161 | end 162 | end 163 | -------------------------------------------------------------------------------- /test/url_param_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | 5 | class TestUrlParam < Test::Unit::TestCase 6 | 7 | include Helper::Client 8 | 9 | def test_url_defaults_to_______________ 10 | redis = Redis.new 11 | 12 | assert_equal "127.0.0.1", redis.client.host 13 | assert_equal 6379, redis.client.port 14 | assert_equal 0, redis.client.db 15 | assert_equal nil, redis.client.password 16 | end 17 | 18 | def test_allows_to_pass_in_a_url 19 | redis = Redis.new :url => "redis://:secr3t@foo.com:999/2" 20 | 21 | assert_equal "foo.com", redis.client.host 22 | assert_equal 999, redis.client.port 23 | assert_equal 2, redis.client.db 24 | assert_equal "secr3t", redis.client.password 25 | end 26 | 27 | def test_allows_to_pass_in_a_url_with_string_key 28 | redis = Redis.new "url" => "redis://:secr3t@foo.com:999/2" 29 | 30 | assert_equal "foo.com", redis.client.host 31 | assert_equal 999, redis.client.port 32 | assert_equal 2, redis.client.db 33 | assert_equal "secr3t", redis.client.password 34 | end 35 | 36 | def test_unescape_password_from_url 37 | redis = Redis.new :url => "redis://:secr3t%3A@foo.com:999/2" 38 | 39 | assert_equal "secr3t:", redis.client.password 40 | end 41 | 42 | def test_unescape_password_from_url_with_string_key 43 | redis = Redis.new "url" => "redis://:secr3t%3A@foo.com:999/2" 44 | 45 | assert_equal "secr3t:", redis.client.password 46 | end 47 | 48 | def test_does_not_unescape_password_when_explicitly_passed 49 | redis = Redis.new :url => "redis://:secr3t%3A@foo.com:999/2", :password => "secr3t%3A" 50 | 51 | assert_equal "secr3t%3A", redis.client.password 52 | end 53 | 54 | def test_does_not_unescape_password_when_explicitly_passed_with_string_key 55 | redis = Redis.new :url => "redis://:secr3t%3A@foo.com:999/2", "password" => "secr3t%3A" 56 | 57 | assert_equal "secr3t%3A", redis.client.password 58 | end 59 | 60 | def test_override_url_if_path_option_is_passed 61 | redis = Redis.new :url => "redis://:secr3t@foo.com/foo:999/2", :path => "/tmp/redis.sock" 62 | 63 | assert_equal "/tmp/redis.sock", redis.client.path 64 | assert_equal nil, redis.client.host 65 | assert_equal nil, redis.client.port 66 | end 67 | 68 | def test_override_url_if_path_option_is_passed_with_string_key 69 | redis = Redis.new :url => "redis://:secr3t@foo.com/foo:999/2", "path" => "/tmp/redis.sock" 70 | 71 | assert_equal "/tmp/redis.sock", redis.client.path 72 | assert_equal nil, redis.client.host 73 | assert_equal nil, redis.client.port 74 | end 75 | 76 | def test_overrides_url_if_another_connection_option_is_passed 77 | redis = Redis.new :url => "redis://:secr3t@foo.com:999/2", :port => 1000 78 | 79 | assert_equal "foo.com", redis.client.host 80 | assert_equal 1000, redis.client.port 81 | assert_equal 2, redis.client.db 82 | assert_equal "secr3t", redis.client.password 83 | end 84 | 85 | def test_overrides_url_if_another_connection_option_is_passed_with_string_key 86 | redis = Redis.new :url => "redis://:secr3t@foo.com:999/2", "port" => 1000 87 | 88 | assert_equal "foo.com", redis.client.host 89 | assert_equal 1000, redis.client.port 90 | assert_equal 2, redis.client.db 91 | assert_equal "secr3t", redis.client.password 92 | end 93 | 94 | def test_does_not_overrides_url_if_a_nil_option_is_passed 95 | redis = Redis.new :url => "redis://:secr3t@foo.com:999/2", :port => nil 96 | 97 | assert_equal "foo.com", redis.client.host 98 | assert_equal 999, redis.client.port 99 | assert_equal 2, redis.client.db 100 | assert_equal "secr3t", redis.client.password 101 | end 102 | 103 | def test_does_not_overrides_url_if_a_nil_option_is_passed_with_string_key 104 | redis = Redis.new :url => "redis://:secr3t@foo.com:999/2", "port" => nil 105 | 106 | assert_equal "foo.com", redis.client.host 107 | assert_equal 999, redis.client.port 108 | assert_equal 2, redis.client.db 109 | assert_equal "secr3t", redis.client.password 110 | end 111 | 112 | def test_does_not_modify_the_passed_options 113 | options = { :url => "redis://:secr3t@foo.com:999/2" } 114 | 115 | Redis.new(options) 116 | 117 | assert({ :url => "redis://:secr3t@foo.com:999/2" } == options) 118 | end 119 | 120 | def test_uses_redis_url_over_default_if_available 121 | ENV["REDIS_URL"] = "redis://:secr3t@foo.com:999/2" 122 | 123 | redis = Redis.new 124 | 125 | assert_equal "foo.com", redis.client.host 126 | assert_equal 999, redis.client.port 127 | assert_equal 2, redis.client.db 128 | assert_equal "secr3t", redis.client.password 129 | 130 | ENV.delete("REDIS_URL") 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /test/distributed_commands_requiring_clustering_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | 5 | class TestDistributedCommandsRequiringClustering < Test::Unit::TestCase 6 | 7 | include Helper::Distributed 8 | 9 | def test_rename 10 | r.set("{qux}foo", "s1") 11 | r.rename "{qux}foo", "{qux}bar" 12 | 13 | assert_equal "s1", r.get("{qux}bar") 14 | assert_equal nil, r.get("{qux}foo") 15 | end 16 | 17 | def test_renamenx 18 | r.set("{qux}foo", "s1") 19 | r.set("{qux}bar", "s2") 20 | 21 | assert_equal false, r.renamenx("{qux}foo", "{qux}bar") 22 | 23 | assert_equal "s1", r.get("{qux}foo") 24 | assert_equal "s2", r.get("{qux}bar") 25 | end 26 | 27 | def test_brpoplpush 28 | r.rpush "{qux}foo", "s1" 29 | r.rpush "{qux}foo", "s2" 30 | 31 | assert_equal "s2", r.brpoplpush("{qux}foo", "{qux}bar", :timeout => 1) 32 | assert_equal ["s2"], r.lrange("{qux}bar", 0, -1) 33 | end 34 | 35 | def test_rpoplpush 36 | r.rpush "{qux}foo", "s1" 37 | r.rpush "{qux}foo", "s2" 38 | 39 | assert_equal "s2", r.rpoplpush("{qux}foo", "{qux}bar") 40 | assert_equal ["s2"], r.lrange("{qux}bar", 0, -1) 41 | assert_equal "s1", r.rpoplpush("{qux}foo", "{qux}bar") 42 | assert_equal ["s1", "s2"], r.lrange("{qux}bar", 0, -1) 43 | end 44 | 45 | def test_smove 46 | r.sadd "{qux}foo", "s1" 47 | r.sadd "{qux}bar", "s2" 48 | 49 | assert r.smove("{qux}foo", "{qux}bar", "s1") 50 | assert r.sismember("{qux}bar", "s1") 51 | end 52 | 53 | def test_sinter 54 | r.sadd "{qux}foo", "s1" 55 | r.sadd "{qux}foo", "s2" 56 | r.sadd "{qux}bar", "s2" 57 | 58 | assert_equal ["s2"], r.sinter("{qux}foo", "{qux}bar") 59 | end 60 | 61 | def test_sinterstore 62 | r.sadd "{qux}foo", "s1" 63 | r.sadd "{qux}foo", "s2" 64 | r.sadd "{qux}bar", "s2" 65 | 66 | r.sinterstore("{qux}baz", "{qux}foo", "{qux}bar") 67 | 68 | assert_equal ["s2"], r.smembers("{qux}baz") 69 | end 70 | 71 | def test_sunion 72 | r.sadd "{qux}foo", "s1" 73 | r.sadd "{qux}foo", "s2" 74 | r.sadd "{qux}bar", "s2" 75 | r.sadd "{qux}bar", "s3" 76 | 77 | assert_equal ["s1", "s2", "s3"], r.sunion("{qux}foo", "{qux}bar").sort 78 | end 79 | 80 | def test_sunionstore 81 | r.sadd "{qux}foo", "s1" 82 | r.sadd "{qux}foo", "s2" 83 | r.sadd "{qux}bar", "s2" 84 | r.sadd "{qux}bar", "s3" 85 | 86 | r.sunionstore("{qux}baz", "{qux}foo", "{qux}bar") 87 | 88 | assert_equal ["s1", "s2", "s3"], r.smembers("{qux}baz").sort 89 | end 90 | 91 | def test_sdiff 92 | r.sadd "{qux}foo", "s1" 93 | r.sadd "{qux}foo", "s2" 94 | r.sadd "{qux}bar", "s2" 95 | r.sadd "{qux}bar", "s3" 96 | 97 | assert_equal ["s1"], r.sdiff("{qux}foo", "{qux}bar") 98 | assert_equal ["s3"], r.sdiff("{qux}bar", "{qux}foo") 99 | end 100 | 101 | def test_sdiffstore 102 | r.sadd "{qux}foo", "s1" 103 | r.sadd "{qux}foo", "s2" 104 | r.sadd "{qux}bar", "s2" 105 | r.sadd "{qux}bar", "s3" 106 | 107 | r.sdiffstore("{qux}baz", "{qux}foo", "{qux}bar") 108 | 109 | assert_equal ["s1"], r.smembers("{qux}baz") 110 | end 111 | 112 | def test_sort 113 | r.set("{qux}foo:1", "s1") 114 | r.set("{qux}foo:2", "s2") 115 | 116 | r.rpush("{qux}bar", "1") 117 | r.rpush("{qux}bar", "2") 118 | 119 | assert_equal ["s1"], r.sort("{qux}bar", :get => "{qux}foo:*", :limit => [0, 1]) 120 | assert_equal ["s2"], r.sort("{qux}bar", :get => "{qux}foo:*", :limit => [0, 1], :order => "desc alpha") 121 | end 122 | 123 | def test_sort_with_an_array_of_gets 124 | r.set("{qux}foo:1:a", "s1a") 125 | r.set("{qux}foo:1:b", "s1b") 126 | 127 | r.set("{qux}foo:2:a", "s2a") 128 | r.set("{qux}foo:2:b", "s2b") 129 | 130 | r.rpush("{qux}bar", "1") 131 | r.rpush("{qux}bar", "2") 132 | 133 | assert_equal [["s1a", "s1b"]], r.sort("{qux}bar", :get => ["{qux}foo:*:a", "{qux}foo:*:b"], :limit => [0, 1]) 134 | assert_equal [["s2a", "s2b"]], r.sort("{qux}bar", :get => ["{qux}foo:*:a", "{qux}foo:*:b"], :limit => [0, 1], :order => "desc alpha") 135 | assert_equal [["s1a", "s1b"], ["s2a", "s2b"]], r.sort("{qux}bar", :get => ["{qux}foo:*:a", "{qux}foo:*:b"]) 136 | end 137 | 138 | def test_sort_with_store 139 | r.set("{qux}foo:1", "s1") 140 | r.set("{qux}foo:2", "s2") 141 | 142 | r.rpush("{qux}bar", "1") 143 | r.rpush("{qux}bar", "2") 144 | 145 | r.sort("{qux}bar", :get => "{qux}foo:*", :store => "{qux}baz") 146 | assert_equal ["s1", "s2"], r.lrange("{qux}baz", 0, -1) 147 | end 148 | 149 | def test_bitop 150 | target_version "2.5.10" do 151 | r.set("{qux}foo", "a") 152 | r.set("{qux}bar", "b") 153 | 154 | r.bitop(:and, "{qux}foo&bar", "{qux}foo", "{qux}bar") 155 | assert_equal "\x60", r.get("{qux}foo&bar") 156 | r.bitop(:or, "{qux}foo|bar", "{qux}foo", "{qux}bar") 157 | assert_equal "\x63", r.get("{qux}foo|bar") 158 | r.bitop(:xor, "{qux}foo^bar", "{qux}foo", "{qux}bar") 159 | assert_equal "\x03", r.get("{qux}foo^bar") 160 | r.bitop(:not, "{qux}~foo", "{qux}foo") 161 | assert_equal "\x9E", r.get("{qux}~foo") 162 | end 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /test/lint/blocking_commands.rb: -------------------------------------------------------------------------------- 1 | module Lint 2 | 3 | module BlockingCommands 4 | 5 | def setup 6 | super 7 | 8 | r.rpush("{zap}foo", "s1") 9 | r.rpush("{zap}foo", "s2") 10 | r.rpush("{zap}bar", "s1") 11 | r.rpush("{zap}bar", "s2") 12 | end 13 | 14 | def to_protocol(obj) 15 | case obj 16 | when String 17 | "$#{obj.length}\r\n#{obj}\r\n" 18 | when Array 19 | "*#{obj.length}\r\n" + obj.map { |e| to_protocol(e) }.join 20 | else 21 | fail 22 | end 23 | end 24 | 25 | def mock(options = {}, &blk) 26 | commands = { 27 | :blpop => lambda do |*args| 28 | sleep options[:delay] if options.has_key?(:delay) 29 | to_protocol([args.first, args.last]) 30 | end, 31 | :brpop => lambda do |*args| 32 | sleep options[:delay] if options.has_key?(:delay) 33 | to_protocol([args.first, args.last]) 34 | end, 35 | :brpoplpush => lambda do |*args| 36 | sleep options[:delay] if options.has_key?(:delay) 37 | to_protocol(args.last) 38 | end, 39 | } 40 | 41 | redis_mock(commands, &blk) 42 | end 43 | 44 | def test_blpop 45 | assert_equal ["{zap}foo", "s1"], r.blpop("{zap}foo") 46 | assert_equal ["{zap}foo", "s2"], r.blpop(["{zap}foo"]) 47 | assert_equal ["{zap}bar", "s1"], r.blpop(["{zap}bar", "{zap}foo"]) 48 | assert_equal ["{zap}bar", "s2"], r.blpop(["{zap}foo", "{zap}bar"]) 49 | end 50 | 51 | def test_blpop_timeout 52 | mock do |r| 53 | assert_equal ["{zap}foo", "0"], r.blpop("{zap}foo") 54 | assert_equal ["{zap}foo", "1"], r.blpop("{zap}foo", :timeout => 1) 55 | end 56 | end 57 | 58 | def test_blpop_with_old_prototype 59 | assert_equal ["{zap}foo", "s1"], r.blpop("{zap}foo", 0) 60 | assert_equal ["{zap}foo", "s2"], r.blpop("{zap}foo", 0) 61 | assert_equal ["{zap}bar", "s1"], r.blpop("{zap}bar", "{zap}foo", 0) 62 | assert_equal ["{zap}bar", "s2"], r.blpop("{zap}foo", "{zap}bar", 0) 63 | end 64 | 65 | def test_blpop_timeout_with_old_prototype 66 | mock do |r| 67 | assert_equal ["{zap}foo", "0"], r.blpop("{zap}foo", 0) 68 | assert_equal ["{zap}foo", "1"], r.blpop("{zap}foo", 1) 69 | end 70 | end 71 | 72 | def test_brpop 73 | assert_equal ["{zap}foo", "s2"], r.brpop("{zap}foo") 74 | assert_equal ["{zap}foo", "s1"], r.brpop(["{zap}foo"]) 75 | assert_equal ["{zap}bar", "s2"], r.brpop(["{zap}bar", "{zap}foo"]) 76 | assert_equal ["{zap}bar", "s1"], r.brpop(["{zap}foo", "{zap}bar"]) 77 | end 78 | 79 | def test_brpop_timeout 80 | mock do |r| 81 | assert_equal ["{zap}foo", "0"], r.brpop("{zap}foo") 82 | assert_equal ["{zap}foo", "1"], r.brpop("{zap}foo", :timeout => 1) 83 | end 84 | end 85 | 86 | def test_brpop_with_old_prototype 87 | assert_equal ["{zap}foo", "s2"], r.brpop("{zap}foo", 0) 88 | assert_equal ["{zap}foo", "s1"], r.brpop("{zap}foo", 0) 89 | assert_equal ["{zap}bar", "s2"], r.brpop("{zap}bar", "{zap}foo", 0) 90 | assert_equal ["{zap}bar", "s1"], r.brpop("{zap}foo", "{zap}bar", 0) 91 | end 92 | 93 | def test_brpop_timeout_with_old_prototype 94 | mock do |r| 95 | assert_equal ["{zap}foo", "0"], r.brpop("{zap}foo", 0) 96 | assert_equal ["{zap}foo", "1"], r.brpop("{zap}foo", 1) 97 | end 98 | end 99 | 100 | def test_brpoplpush 101 | assert_equal "s2", r.brpoplpush("{zap}foo", "{zap}qux") 102 | assert_equal ["s2"], r.lrange("{zap}qux", 0, -1) 103 | end 104 | 105 | def test_brpoplpush_timeout 106 | mock do |r| 107 | assert_equal "0", r.brpoplpush("{zap}foo", "{zap}bar") 108 | assert_equal "1", r.brpoplpush("{zap}foo", "{zap}bar", :timeout => 1) 109 | end 110 | end 111 | 112 | def test_brpoplpush_with_old_prototype 113 | assert_equal "s2", r.brpoplpush("{zap}foo", "{zap}qux", 0) 114 | assert_equal ["s2"], r.lrange("{zap}qux", 0, -1) 115 | end 116 | 117 | def test_brpoplpush_timeout_with_old_prototype 118 | mock do |r| 119 | assert_equal "0", r.brpoplpush("{zap}foo", "{zap}bar", 0) 120 | assert_equal "1", r.brpoplpush("{zap}foo", "{zap}bar", 1) 121 | end 122 | end 123 | 124 | driver(:ruby, :hiredis) do 125 | def test_blpop_socket_timeout 126 | mock(:delay => 1 + OPTIONS[:timeout] * 2) do |r| 127 | assert_raises(Redis::TimeoutError) do 128 | r.blpop("{zap}foo", :timeout => 1) 129 | end 130 | end 131 | end 132 | 133 | def test_brpop_socket_timeout 134 | mock(:delay => 1 + OPTIONS[:timeout] * 2) do |r| 135 | assert_raises(Redis::TimeoutError) do 136 | r.brpop("{zap}foo", :timeout => 1) 137 | end 138 | end 139 | end 140 | 141 | def test_brpoplpush_socket_timeout 142 | mock(:delay => 1 + OPTIONS[:timeout] * 2) do |r| 143 | assert_raises(Redis::TimeoutError) do 144 | r.brpoplpush("{zap}foo", "{zap}bar", :timeout => 1) 145 | end 146 | end 147 | end 148 | end 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /test/publish_subscribe_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | 5 | class TestPublishSubscribe < Test::Unit::TestCase 6 | 7 | include Helper::Client 8 | 9 | class TestError < StandardError 10 | end 11 | 12 | def test_subscribe_and_unsubscribe 13 | @subscribed = false 14 | @unsubscribed = false 15 | 16 | wire = Wire.new do 17 | r.subscribe("foo") do |on| 18 | on.subscribe do |channel, total| 19 | @subscribed = true 20 | @t1 = total 21 | end 22 | 23 | on.message do |channel, message| 24 | if message == "s1" 25 | r.unsubscribe 26 | @message = message 27 | end 28 | end 29 | 30 | on.unsubscribe do |channel, total| 31 | @unsubscribed = true 32 | @t2 = total 33 | end 34 | end 35 | end 36 | 37 | # Wait until the subscription is active before publishing 38 | Wire.pass while !@subscribed 39 | 40 | Redis.new(OPTIONS).publish("foo", "s1") 41 | 42 | wire.join 43 | 44 | assert @subscribed 45 | assert_equal 1, @t1 46 | assert @unsubscribed 47 | assert_equal 0, @t2 48 | assert_equal "s1", @message 49 | end 50 | 51 | def test_psubscribe_and_punsubscribe 52 | @subscribed = false 53 | @unsubscribed = false 54 | 55 | wire = Wire.new do 56 | r.psubscribe("f*") do |on| 57 | on.psubscribe do |pattern, total| 58 | @subscribed = true 59 | @t1 = total 60 | end 61 | 62 | on.pmessage do |pattern, channel, message| 63 | if message == "s1" 64 | r.punsubscribe 65 | @message = message 66 | end 67 | end 68 | 69 | on.punsubscribe do |pattern, total| 70 | @unsubscribed = true 71 | @t2 = total 72 | end 73 | end 74 | end 75 | 76 | # Wait until the subscription is active before publishing 77 | Wire.pass while !@subscribed 78 | 79 | Redis.new(OPTIONS).publish("foo", "s1") 80 | 81 | wire.join 82 | 83 | assert @subscribed 84 | assert_equal 1, @t1 85 | assert @unsubscribed 86 | assert_equal 0, @t2 87 | assert_equal "s1", @message 88 | end 89 | 90 | def test_subscribe_connection_usable_after_raise 91 | @subscribed = false 92 | 93 | wire = Wire.new do 94 | begin 95 | r.subscribe("foo") do |on| 96 | on.subscribe do |channel, total| 97 | @subscribed = true 98 | end 99 | 100 | on.message do |channel, message| 101 | raise TestError 102 | end 103 | end 104 | rescue TestError 105 | end 106 | end 107 | 108 | # Wait until the subscription is active before publishing 109 | Wire.pass while !@subscribed 110 | 111 | Redis.new(OPTIONS).publish("foo", "s1") 112 | 113 | wire.join 114 | 115 | assert_equal "PONG", r.ping 116 | end 117 | 118 | def test_psubscribe_connection_usable_after_raise 119 | @subscribed = false 120 | 121 | wire = Wire.new do 122 | begin 123 | r.psubscribe("f*") do |on| 124 | on.psubscribe do |pattern, total| 125 | @subscribed = true 126 | end 127 | 128 | on.pmessage do |pattern, channel, message| 129 | raise TestError 130 | end 131 | end 132 | rescue TestError 133 | end 134 | end 135 | 136 | # Wait until the subscription is active before publishing 137 | Wire.pass while !@subscribed 138 | 139 | Redis.new(OPTIONS).publish("foo", "s1") 140 | 141 | wire.join 142 | 143 | assert_equal "PONG", r.ping 144 | end 145 | 146 | def test_subscribe_within_subscribe 147 | @channels = [] 148 | 149 | wire = Wire.new do 150 | r.subscribe("foo") do |on| 151 | on.subscribe do |channel, total| 152 | @channels << channel 153 | 154 | r.subscribe("bar") if channel == "foo" 155 | r.unsubscribe if channel == "bar" 156 | end 157 | end 158 | end 159 | 160 | wire.join 161 | 162 | assert_equal ["foo", "bar"], @channels 163 | end 164 | 165 | def test_other_commands_within_a_subscribe 166 | assert_raise Redis::CommandError do 167 | r.subscribe("foo") do |on| 168 | on.subscribe do |channel, total| 169 | r.set("bar", "s2") 170 | end 171 | end 172 | end 173 | end 174 | 175 | def test_subscribe_without_a_block 176 | assert_raise LocalJumpError do 177 | r.subscribe("foo") 178 | end 179 | end 180 | 181 | def test_unsubscribe_without_a_subscribe 182 | assert_raise RuntimeError do 183 | r.unsubscribe 184 | end 185 | 186 | assert_raise RuntimeError do 187 | r.punsubscribe 188 | end 189 | end 190 | 191 | def test_subscribe_past_a_timeout 192 | # For some reason, a thread here doesn't reproduce the issue. 193 | sleep = %{sleep #{OPTIONS[:timeout] * 2}} 194 | publish = %{echo "publish foo bar\r\n" | nc 127.0.0.1 #{OPTIONS[:port]}} 195 | cmd = [sleep, publish].join("; ") 196 | 197 | IO.popen(cmd, "r+") do |pipe| 198 | received = false 199 | 200 | r.subscribe "foo" do |on| 201 | on.message do |channel, message| 202 | received = true 203 | r.unsubscribe 204 | end 205 | end 206 | 207 | assert received 208 | end 209 | end 210 | end 211 | -------------------------------------------------------------------------------- /test/connection_handling_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | 5 | class TestConnectionHandling < Test::Unit::TestCase 6 | 7 | include Helper::Client 8 | 9 | def test_auth 10 | commands = { 11 | :auth => lambda { |password| $auth = password; "+OK" }, 12 | :get => lambda { |key| $auth == "secret" ? "$3\r\nbar" : "$-1" }, 13 | } 14 | 15 | redis_mock(commands, :password => "secret") do |redis| 16 | assert_equal "bar", redis.get("foo") 17 | end 18 | end 19 | 20 | def test_ping 21 | assert_equal "PONG", r.ping 22 | end 23 | 24 | def test_select 25 | r.set "foo", "bar" 26 | 27 | r.select 14 28 | assert_equal nil, r.get("foo") 29 | 30 | r.client.disconnect 31 | 32 | assert_equal nil, r.get("foo") 33 | end 34 | 35 | def test_quit 36 | r.quit 37 | 38 | assert !r.client.connected? 39 | end 40 | 41 | def test_shutdown 42 | commands = { 43 | :shutdown => lambda { :exit } 44 | } 45 | 46 | redis_mock(commands) do |redis| 47 | # SHUTDOWN does not reply: test that it does not raise here. 48 | assert_equal nil, redis.shutdown 49 | end 50 | end 51 | 52 | def test_shutdown_with_error 53 | connections = 0 54 | commands = { 55 | :select => lambda { |*_| connections += 1; "+OK\r\n" }, 56 | :connections => lambda { ":#{connections}\r\n" }, 57 | :shutdown => lambda { "-ERR could not shutdown\r\n" } 58 | } 59 | 60 | redis_mock(commands) do |redis| 61 | connections = redis.connections 62 | 63 | # SHUTDOWN replies with an error: test that it gets raised 64 | assert_raise Redis::CommandError do 65 | redis.shutdown 66 | end 67 | 68 | # The connection should remain in tact 69 | assert_equal connections, redis.connections 70 | end 71 | end 72 | 73 | def test_shutdown_from_pipeline 74 | commands = { 75 | :shutdown => lambda { :exit } 76 | } 77 | 78 | redis_mock(commands) do |redis| 79 | result = redis.pipelined do 80 | redis.shutdown 81 | end 82 | 83 | assert_equal nil, result 84 | assert !redis.client.connected? 85 | end 86 | end 87 | 88 | def test_shutdown_with_error_from_pipeline 89 | connections = 0 90 | commands = { 91 | :select => lambda { |*_| connections += 1; "+OK\r\n" }, 92 | :connections => lambda { ":#{connections}\r\n" }, 93 | :shutdown => lambda { "-ERR could not shutdown\r\n" } 94 | } 95 | 96 | redis_mock(commands) do |redis| 97 | connections = redis.connections 98 | 99 | # SHUTDOWN replies with an error: test that it gets raised 100 | assert_raise Redis::CommandError do 101 | redis.pipelined do 102 | redis.shutdown 103 | end 104 | end 105 | 106 | # The connection should remain in tact 107 | assert_equal connections, redis.connections 108 | end 109 | end 110 | 111 | def test_shutdown_from_multi_exec 112 | commands = { 113 | :multi => lambda { "+OK\r\n" }, 114 | :shutdown => lambda { "+QUEUED\r\n" }, 115 | :exec => lambda { :exit } 116 | } 117 | 118 | redis_mock(commands) do |redis| 119 | result = redis.multi do 120 | redis.shutdown 121 | end 122 | 123 | assert_equal nil, result 124 | assert !redis.client.connected? 125 | end 126 | end 127 | 128 | def test_shutdown_with_error_from_multi_exec 129 | connections = 0 130 | commands = { 131 | :select => lambda { |*_| connections += 1; "+OK\r\n" }, 132 | :connections => lambda { ":#{connections}\r\n" }, 133 | :multi => lambda { "+OK\r\n" }, 134 | :shutdown => lambda { "+QUEUED\r\n" }, 135 | :exec => lambda { "*1\r\n-ERR could not shutdown\r\n" } 136 | } 137 | 138 | redis_mock(commands) do |redis| 139 | connections = redis.connections 140 | 141 | # SHUTDOWN replies with an error: test that it gets returned 142 | # We should test for Redis::CommandError here, but hiredis doesn't yet do 143 | # custom error classes. 144 | err = nil 145 | 146 | begin 147 | redis.multi { redis.shutdown } 148 | rescue => err 149 | end 150 | 151 | assert err.kind_of?(StandardError) 152 | 153 | # The connection should remain intact 154 | assert_equal connections, redis.connections 155 | end 156 | end 157 | 158 | def test_slaveof 159 | redis_mock(:slaveof => lambda { |host, port| "+SLAVEOF #{host} #{port}" }) do |redis| 160 | assert_equal "SLAVEOF somehost 6381", redis.slaveof("somehost", 6381) 161 | end 162 | end 163 | 164 | def test_bgrewriteaof 165 | redis_mock(:bgrewriteaof => lambda { "+BGREWRITEAOF" }) do |redis| 166 | assert_equal "BGREWRITEAOF", redis.bgrewriteaof 167 | end 168 | end 169 | 170 | def test_config_get 171 | assert r.config(:get, "*")["timeout"] != nil 172 | 173 | config = r.config(:get, "timeout") 174 | assert_equal ["timeout"], config.keys 175 | assert config.values.compact.size > 0 176 | end 177 | 178 | def test_config_set 179 | begin 180 | assert_equal "OK", r.config(:set, "timeout", 200) 181 | assert_equal "200", r.config(:get, "*")["timeout"] 182 | 183 | assert_equal "OK", r.config(:set, "timeout", 100) 184 | assert_equal "100", r.config(:get, "*")["timeout"] 185 | ensure 186 | r.config :set, "timeout", 300 187 | end 188 | end 189 | end 190 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path("../lib", File.dirname(__FILE__)) 2 | $:.unshift File.expand_path(File.dirname(__FILE__)) 3 | 4 | require "test/unit" 5 | require "logger" 6 | require "stringio" 7 | 8 | begin 9 | require "ruby-debug" 10 | rescue LoadError 11 | end 12 | 13 | $VERBOSE = true 14 | 15 | ENV["conn"] ||= "ruby" 16 | 17 | require "redis" 18 | require "redis/distributed" 19 | require "redis/connection/#{ENV["conn"]}" 20 | 21 | require "support/redis_mock" 22 | require "support/connection/#{ENV["conn"]}" 23 | 24 | PORT = 6381 25 | OPTIONS = {:port => PORT, :db => 15, :timeout => Float(ENV["TIMEOUT"] || 0.1)} 26 | NODES = ["redis://127.0.0.1:#{PORT}/15"] 27 | 28 | def init(redis) 29 | begin 30 | redis.select 14 31 | redis.flushdb 32 | redis.select 15 33 | redis.flushdb 34 | redis 35 | rescue Redis::CannotConnectError 36 | puts <<-EOS 37 | 38 | Cannot connect to Redis. 39 | 40 | Make sure Redis is running on localhost, port #{PORT}. 41 | This testing suite connects to the database 15. 42 | 43 | Try this once: 44 | 45 | $ rake clean 46 | 47 | Then run the build again: 48 | 49 | $ rake 50 | 51 | EOS 52 | exit 1 53 | end 54 | end 55 | 56 | def driver(*drivers, &blk) 57 | if drivers.map(&:to_s).include?(ENV["conn"]) 58 | class_eval(&blk) 59 | end 60 | end 61 | 62 | module Helper 63 | 64 | def run(runner) 65 | if respond_to?(:around) 66 | around { super(runner) } 67 | else 68 | super 69 | end 70 | end 71 | 72 | def silent 73 | verbose, $VERBOSE = $VERBOSE, false 74 | 75 | begin 76 | yield 77 | ensure 78 | $VERBOSE = verbose 79 | end 80 | end 81 | 82 | def with_external_encoding(encoding) 83 | original_encoding = Encoding.default_external 84 | 85 | begin 86 | silent { Encoding.default_external = Encoding.find(encoding) } 87 | yield 88 | ensure 89 | silent { Encoding.default_external = original_encoding } 90 | end 91 | end 92 | 93 | def try_encoding(encoding, &block) 94 | if defined?(Encoding) 95 | with_external_encoding(encoding, &block) 96 | else 97 | yield 98 | end 99 | end 100 | 101 | class Version 102 | 103 | include Comparable 104 | 105 | attr :parts 106 | 107 | def initialize(v) 108 | case v 109 | when Version 110 | @parts = v.parts 111 | else 112 | @parts = v.to_s.split(".") 113 | end 114 | end 115 | 116 | def <=>(other) 117 | other = Version.new(other) 118 | length = [self.parts.length, other.parts.length].max 119 | length.times do |i| 120 | a, b = self.parts[i], other.parts[i] 121 | 122 | return -1 if a.nil? 123 | return +1 if b.nil? 124 | return a.to_i <=> b.to_i if a != b 125 | end 126 | 127 | 0 128 | end 129 | end 130 | 131 | module Generic 132 | 133 | include Helper 134 | 135 | attr_reader :log 136 | attr_reader :redis 137 | 138 | alias :r :redis 139 | 140 | def setup 141 | @log = StringIO.new 142 | @redis = init _new_client 143 | 144 | # Run GC to make sure orphaned connections are closed. 145 | GC.start 146 | end 147 | 148 | def teardown 149 | @redis.quit if @redis 150 | end 151 | 152 | def redis_mock(commands, options = {}, &blk) 153 | RedisMock.start(commands, options) do |port| 154 | yield _new_client(options.merge(:port => port)) 155 | end 156 | end 157 | 158 | def redis_mock_with_handler(handler, options = {}, &blk) 159 | RedisMock.start_with_handler(handler, options) do |port| 160 | yield _new_client(options.merge(:port => port)) 161 | end 162 | end 163 | 164 | def assert_in_range(range, value) 165 | assert range.include?(value), "expected #{value} to be in #{range.inspect}" 166 | end 167 | 168 | def target_version(target) 169 | if version < target 170 | skip("Requires Redis > #{target}") if respond_to?(:skip) 171 | else 172 | yield 173 | end 174 | end 175 | end 176 | 177 | module Client 178 | 179 | include Generic 180 | 181 | def version 182 | Version.new(redis.info["redis_version"]) 183 | end 184 | 185 | private 186 | 187 | def _format_options(options) 188 | OPTIONS.merge(:logger => ::Logger.new(@log)).merge(options) 189 | end 190 | 191 | def _new_client(options = {}) 192 | Redis.new(_format_options(options).merge(:driver => ENV["conn"])) 193 | end 194 | end 195 | 196 | module Distributed 197 | 198 | include Generic 199 | 200 | def version 201 | Version.new(redis.info.first["redis_version"]) 202 | end 203 | 204 | private 205 | 206 | def _format_options(options) 207 | { 208 | :timeout => OPTIONS[:timeout], 209 | :logger => ::Logger.new(@log), 210 | }.merge(options) 211 | end 212 | 213 | def _new_client(options = {}) 214 | Redis::Distributed.new(NODES, _format_options(options).merge(:driver => ENV["conn"])) 215 | end 216 | end 217 | 218 | # Basic support for `skip` in 1.8.x 219 | # Note: YOU MUST use `return skip(message)` in order to appropriately bail 220 | # from a running test. 221 | module Skipable 222 | Skipped = Class.new(RuntimeError) 223 | 224 | def skip(message = nil, bt = caller) 225 | return super if defined?(super) 226 | 227 | $stderr.puts("SKIPPED: #{self} #{message || 'no reason given'}") 228 | end 229 | end 230 | end 231 | -------------------------------------------------------------------------------- /test/pipelining_commands_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | 5 | class TestPipeliningCommands < Test::Unit::TestCase 6 | 7 | include Helper::Client 8 | 9 | def test_bulk_commands 10 | r.pipelined do 11 | r.lpush "foo", "s1" 12 | r.lpush "foo", "s2" 13 | end 14 | 15 | assert_equal 2, r.llen("foo") 16 | assert_equal "s2", r.lpop("foo") 17 | assert_equal "s1", r.lpop("foo") 18 | end 19 | 20 | def test_multi_bulk_commands 21 | r.pipelined do 22 | r.mset("foo", "s1", "bar", "s2") 23 | r.mset("baz", "s3", "qux", "s4") 24 | end 25 | 26 | assert_equal "s1", r.get("foo") 27 | assert_equal "s2", r.get("bar") 28 | assert_equal "s3", r.get("baz") 29 | assert_equal "s4", r.get("qux") 30 | end 31 | 32 | def test_bulk_and_multi_bulk_commands_mixed 33 | r.pipelined do 34 | r.lpush "foo", "s1" 35 | r.lpush "foo", "s2" 36 | r.mset("baz", "s3", "qux", "s4") 37 | end 38 | 39 | assert_equal 2, r.llen("foo") 40 | assert_equal "s2", r.lpop("foo") 41 | assert_equal "s1", r.lpop("foo") 42 | assert_equal "s3", r.get("baz") 43 | assert_equal "s4", r.get("qux") 44 | end 45 | 46 | def test_multi_bulk_and_bulk_commands_mixed 47 | r.pipelined do 48 | r.mset("baz", "s3", "qux", "s4") 49 | r.lpush "foo", "s1" 50 | r.lpush "foo", "s2" 51 | end 52 | 53 | assert_equal 2, r.llen("foo") 54 | assert_equal "s2", r.lpop("foo") 55 | assert_equal "s1", r.lpop("foo") 56 | assert_equal "s3", r.get("baz") 57 | assert_equal "s4", r.get("qux") 58 | end 59 | 60 | def test_pipelined_with_an_empty_block 61 | assert_nothing_raised do 62 | r.pipelined do 63 | end 64 | end 65 | 66 | assert_equal 0, r.dbsize 67 | end 68 | 69 | def test_returning_the_result_of_a_pipeline 70 | result = r.pipelined do 71 | r.set "foo", "bar" 72 | r.get "foo" 73 | r.get "bar" 74 | end 75 | 76 | assert_equal ["OK", "bar", nil], result 77 | end 78 | 79 | def test_assignment_of_results_inside_the_block 80 | r.pipelined do 81 | @first = r.sadd("foo", 1) 82 | @second = r.sadd("foo", 1) 83 | end 84 | 85 | assert_equal true, @first.value 86 | assert_equal false, @second.value 87 | end 88 | 89 | # Although we could support accessing the values in these futures, 90 | # it doesn't make a lot of sense. 91 | def test_assignment_of_results_inside_the_block_with_errors 92 | assert_raise(Redis::CommandError) do 93 | r.pipelined do 94 | r.doesnt_exist 95 | @first = r.sadd("foo", 1) 96 | @second = r.sadd("foo", 1) 97 | end 98 | end 99 | 100 | assert_raise(Redis::FutureNotReady) { @first.value } 101 | assert_raise(Redis::FutureNotReady) { @second.value } 102 | end 103 | 104 | def test_assignment_of_results_inside_a_nested_block 105 | r.pipelined do 106 | @first = r.sadd("foo", 1) 107 | 108 | r.pipelined do 109 | @second = r.sadd("foo", 1) 110 | end 111 | end 112 | 113 | assert_equal true, @first.value 114 | assert_equal false, @second.value 115 | end 116 | 117 | def test_futures_raise_when_confused_with_something_else 118 | r.pipelined do 119 | @result = r.sadd("foo", 1) 120 | end 121 | 122 | assert_raise(NoMethodError) { @result.to_s } 123 | end 124 | 125 | def test_futures_raise_when_trying_to_access_their_values_too_early 126 | r.pipelined do 127 | assert_raise(Redis::FutureNotReady) do 128 | r.sadd("foo", 1).value 129 | end 130 | end 131 | end 132 | 133 | def test_futures_can_be_identified 134 | r.pipelined do 135 | @result = r.sadd("foo", 1) 136 | end 137 | 138 | assert_equal true, @result.is_a?(Redis::Future) 139 | if defined?(::BasicObject) 140 | assert_equal true, @result.is_a?(::BasicObject) 141 | end 142 | assert_equal Redis::Future, @result.class 143 | end 144 | 145 | def test_returning_the_result_of_an_empty_pipeline 146 | result = r.pipelined do 147 | end 148 | 149 | assert_equal [], result 150 | end 151 | 152 | def test_nesting_pipeline_blocks 153 | r.pipelined do 154 | r.set("foo", "s1") 155 | r.pipelined do 156 | r.set("bar", "s2") 157 | end 158 | end 159 | 160 | assert_equal "s1", r.get("foo") 161 | assert_equal "s2", r.get("bar") 162 | end 163 | 164 | def test_info_in_a_pipeline_returns_hash 165 | result = r.pipelined do 166 | r.info 167 | end 168 | 169 | assert result.first.kind_of?(Hash) 170 | end 171 | 172 | def test_config_get_in_a_pipeline_returns_hash 173 | result = r.pipelined do 174 | r.config(:get, "*") 175 | end 176 | 177 | assert result.first.kind_of?(Hash) 178 | end 179 | 180 | def test_hgetall_in_a_pipeline_returns_hash 181 | r.hmset("hash", "field", "value") 182 | result = r.pipelined do 183 | r.hgetall("hash") 184 | end 185 | 186 | assert_equal result.first, { "field" => "value" } 187 | end 188 | 189 | def test_keys_in_a_pipeline 190 | r.set("key", "value") 191 | result = r.pipelined do 192 | r.keys("*") 193 | end 194 | 195 | assert_equal ["key"], result.first 196 | end 197 | 198 | def test_pipeline_yields_a_connection 199 | r.pipelined do |p| 200 | p.set("foo", "bar") 201 | end 202 | 203 | assert_equal "bar", r.get("foo") 204 | end 205 | 206 | def test_pipeline_select 207 | r.select 1 208 | r.set("db", "1") 209 | 210 | r.pipelined do |p| 211 | p.select 2 212 | p.set("db", "2") 213 | end 214 | 215 | r.select 1 216 | assert_equal "1", r.get("db") 217 | 218 | r.select 2 219 | assert_equal "2", r.get("db") 220 | end 221 | 222 | def test_pipeline_select_client_db 223 | r.select 1 224 | r.pipelined do |p2| 225 | p2.select 2 226 | end 227 | 228 | assert_equal 2, r.client.db 229 | end 230 | 231 | def test_nested_pipeline_select_client_db 232 | r.select 1 233 | r.pipelined do |p2| 234 | p2.select 2 235 | p2.pipelined do |p3| 236 | p3.select 3 237 | end 238 | end 239 | 240 | assert_equal 3, r.client.db 241 | end 242 | end 243 | -------------------------------------------------------------------------------- /test/lint/strings.rb: -------------------------------------------------------------------------------- 1 | module Lint 2 | 3 | module Strings 4 | 5 | def test_set_and_get 6 | r.set("foo", "s1") 7 | 8 | assert_equal "s1", r.get("foo") 9 | end 10 | 11 | def test_set_and_get_with_brackets 12 | r["foo"] = "s1" 13 | 14 | assert_equal "s1", r["foo"] 15 | end 16 | 17 | def test_set_and_get_with_brackets_and_symbol 18 | r[:foo] = "s1" 19 | 20 | assert_equal "s1", r[:foo] 21 | end 22 | 23 | def test_set_and_get_with_newline_characters 24 | r.set("foo", "1\n") 25 | 26 | assert_equal "1\n", r.get("foo") 27 | end 28 | 29 | def test_set_and_get_with_non_string_value 30 | value = ["a", "b"] 31 | 32 | r.set("foo", value) 33 | 34 | assert_equal value.to_s, r.get("foo") 35 | end 36 | 37 | def test_set_and_get_with_ascii_characters 38 | if defined?(Encoding) 39 | with_external_encoding("ASCII-8BIT") do 40 | (0..255).each do |i| 41 | str = "#{i.chr}---#{i.chr}" 42 | r.set("foo", str) 43 | 44 | assert_equal str, r.get("foo") 45 | end 46 | end 47 | end 48 | end 49 | 50 | def test_set_with_ex 51 | target_version "2.6.12" do 52 | r.set("foo", "bar", :ex => 2) 53 | assert_in_range 0..2, r.ttl("foo") 54 | end 55 | end 56 | 57 | def test_set_with_px 58 | target_version "2.6.12" do 59 | r.set("foo", "bar", :px => 2000) 60 | assert_in_range 0..2, r.ttl("foo") 61 | end 62 | end 63 | 64 | def test_set_with_nx 65 | target_version "2.6.12" do 66 | r.set("foo", "qux", :nx => true) 67 | assert !r.set("foo", "bar", :nx => true) 68 | assert_equal "qux", r.get("foo") 69 | 70 | r.del("foo") 71 | assert r.set("foo", "bar", :nx => true) 72 | assert_equal "bar", r.get("foo") 73 | end 74 | end 75 | 76 | def test_set_with_xx 77 | target_version "2.6.12" do 78 | r.set("foo", "qux") 79 | assert r.set("foo", "bar", :xx => true) 80 | assert_equal "bar", r.get("foo") 81 | 82 | r.del("foo") 83 | assert !r.set("foo", "bar", :xx => true) 84 | end 85 | end 86 | 87 | def test_setex 88 | assert r.setex("foo", 1, "bar") 89 | assert_equal "bar", r.get("foo") 90 | assert [0, 1].include? r.ttl("foo") 91 | end 92 | 93 | def test_setex_with_non_string_value 94 | value = ["b", "a", "r"] 95 | 96 | assert r.setex("foo", 1, value) 97 | assert_equal value.to_s, r.get("foo") 98 | assert [0, 1].include? r.ttl("foo") 99 | end 100 | 101 | def test_psetex 102 | target_version "2.5.4" do 103 | assert r.psetex("foo", 1000, "bar") 104 | assert_equal "bar", r.get("foo") 105 | assert [0, 1].include? r.ttl("foo") 106 | end 107 | end 108 | 109 | def test_psetex_with_non_string_value 110 | target_version "2.5.4" do 111 | value = ["b", "a", "r"] 112 | 113 | assert r.psetex("foo", 1000, value) 114 | assert_equal value.to_s, r.get("foo") 115 | assert [0, 1].include? r.ttl("foo") 116 | end 117 | end 118 | 119 | def test_getset 120 | r.set("foo", "bar") 121 | 122 | assert_equal "bar", r.getset("foo", "baz") 123 | assert_equal "baz", r.get("foo") 124 | end 125 | 126 | def test_getset_with_non_string_value 127 | r.set("foo", "zap") 128 | 129 | value = ["b", "a", "r"] 130 | 131 | assert_equal "zap", r.getset("foo", value) 132 | assert_equal value.to_s, r.get("foo") 133 | end 134 | 135 | def test_setnx 136 | r.set("foo", "qux") 137 | assert !r.setnx("foo", "bar") 138 | assert_equal "qux", r.get("foo") 139 | 140 | r.del("foo") 141 | assert r.setnx("foo", "bar") 142 | assert_equal "bar", r.get("foo") 143 | end 144 | 145 | def test_setnx_with_non_string_value 146 | value = ["b", "a", "r"] 147 | 148 | r.set("foo", "qux") 149 | assert !r.setnx("foo", value) 150 | assert_equal "qux", r.get("foo") 151 | 152 | r.del("foo") 153 | assert r.setnx("foo", value) 154 | assert_equal value.to_s, r.get("foo") 155 | end 156 | 157 | def test_incr 158 | assert_equal 1, r.incr("foo") 159 | assert_equal 2, r.incr("foo") 160 | assert_equal 3, r.incr("foo") 161 | end 162 | 163 | def test_incrby 164 | assert_equal 1, r.incrby("foo", 1) 165 | assert_equal 3, r.incrby("foo", 2) 166 | assert_equal 6, r.incrby("foo", 3) 167 | end 168 | 169 | def test_incrbyfloat 170 | target_version "2.5.4" do 171 | assert_equal 1.23, r.incrbyfloat("foo", 1.23) 172 | assert_equal 2 , r.incrbyfloat("foo", 0.77) 173 | assert_equal 1.9 , r.incrbyfloat("foo", -0.1) 174 | end 175 | end 176 | 177 | def test_decr 178 | r.set("foo", 3) 179 | 180 | assert_equal 2, r.decr("foo") 181 | assert_equal 1, r.decr("foo") 182 | assert_equal 0, r.decr("foo") 183 | end 184 | 185 | def test_decrby 186 | r.set("foo", 6) 187 | 188 | assert_equal 3, r.decrby("foo", 3) 189 | assert_equal 1, r.decrby("foo", 2) 190 | assert_equal 0, r.decrby("foo", 1) 191 | end 192 | 193 | def test_append 194 | r.set "foo", "s" 195 | r.append "foo", "1" 196 | 197 | assert_equal "s1", r.get("foo") 198 | end 199 | 200 | def test_getbit 201 | r.set("foo", "a") 202 | 203 | assert_equal 1, r.getbit("foo", 1) 204 | assert_equal 1, r.getbit("foo", 2) 205 | assert_equal 0, r.getbit("foo", 3) 206 | assert_equal 0, r.getbit("foo", 4) 207 | assert_equal 0, r.getbit("foo", 5) 208 | assert_equal 0, r.getbit("foo", 6) 209 | assert_equal 1, r.getbit("foo", 7) 210 | end 211 | 212 | def test_setbit 213 | r.set("foo", "a") 214 | 215 | r.setbit("foo", 6, 1) 216 | 217 | assert_equal "c", r.get("foo") 218 | end 219 | 220 | def test_bitcount 221 | target_version "2.5.10" do 222 | r.set("foo", "abcde") 223 | 224 | assert_equal 10, r.bitcount("foo", 1, 3) 225 | assert_equal 17, r.bitcount("foo", 0, -1) 226 | end 227 | end 228 | 229 | def test_getrange 230 | r.set("foo", "abcde") 231 | 232 | assert_equal "bcd", r.getrange("foo", 1, 3) 233 | assert_equal "abcde", r.getrange("foo", 0, -1) 234 | end 235 | 236 | def test_setrange 237 | r.set("foo", "abcde") 238 | 239 | r.setrange("foo", 1, "bar") 240 | 241 | assert_equal "abare", r.get("foo") 242 | end 243 | 244 | def test_setrange_with_non_string_value 245 | r.set("foo", "abcde") 246 | 247 | value = ["b", "a", "r"] 248 | 249 | r.setrange("foo", 2, value) 250 | 251 | assert_equal "ab#{value.to_s}", r.get("foo") 252 | end 253 | 254 | def test_strlen 255 | r.set "foo", "lorem" 256 | 257 | assert_equal 5, r.strlen("foo") 258 | end 259 | end 260 | end 261 | -------------------------------------------------------------------------------- /test/transactions_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | 5 | class TestTransactions < Test::Unit::TestCase 6 | 7 | include Helper::Client 8 | 9 | def test_multi_discard 10 | r.multi 11 | 12 | assert_equal "QUEUED", r.set("foo", "1") 13 | assert_equal "QUEUED", r.get("foo") 14 | 15 | r.discard 16 | 17 | assert_equal nil, r.get("foo") 18 | end 19 | 20 | def test_multi_exec_with_a_block 21 | r.multi do |multi| 22 | multi.set "foo", "s1" 23 | end 24 | 25 | assert_equal "s1", r.get("foo") 26 | end 27 | 28 | def test_multi_exec_with_a_block_doesn_t_return_replies_for_multi_and_exec 29 | r1, r2, nothing_else = r.multi do |multi| 30 | multi.set "foo", "s1" 31 | multi.get "foo" 32 | end 33 | 34 | assert_equal "OK", r1 35 | assert_equal "s1", r2 36 | assert_equal nil, nothing_else 37 | end 38 | 39 | def test_assignment_inside_multi_exec_block 40 | r.multi do |m| 41 | @first = m.sadd("foo", 1) 42 | @second = m.sadd("foo", 1) 43 | end 44 | 45 | assert_equal true, @first.value 46 | assert_equal false, @second.value 47 | end 48 | 49 | # Although we could support accessing the values in these futures, 50 | # it doesn't make a lot of sense. 51 | def test_assignment_inside_multi_exec_block_with_delayed_command_errors 52 | assert_raise(Redis::CommandError) do 53 | r.multi do |m| 54 | @first = m.set("foo", "s1") 55 | @second = m.incr("foo") # not an integer 56 | @third = m.lpush("foo", "value") # wrong kind of value 57 | end 58 | end 59 | 60 | assert_equal "OK", @first.value 61 | assert_raise(Redis::CommandError) { @second.value } 62 | assert_raise(Redis::FutureNotReady) { @third.value } 63 | end 64 | 65 | def test_assignment_inside_multi_exec_block_with_immediate_command_errors 66 | assert_raise(Redis::CommandError) do 67 | r.multi do |m| 68 | m.doesnt_exist 69 | @first = m.sadd("foo", 1) 70 | @second = m.sadd("foo", 1) 71 | end 72 | end 73 | 74 | assert_raise(Redis::FutureNotReady) { @first.value } 75 | assert_raise(Redis::FutureNotReady) { @second.value } 76 | end 77 | 78 | def test_raise_immediate_errors_in_multi_exec 79 | assert_raise(RuntimeError) do 80 | r.multi do |multi| 81 | multi.set "bar", "s2" 82 | raise "Some error" 83 | multi.set "baz", "s3" 84 | end 85 | end 86 | 87 | assert_equal nil, r.get("bar") 88 | assert_equal nil, r.get("baz") 89 | end 90 | 91 | def test_transformed_replies_as_return_values_for_multi_exec_block 92 | info, _ = r.multi do |m| 93 | r.info 94 | end 95 | 96 | assert info.kind_of?(Hash) 97 | end 98 | 99 | def test_transformed_replies_inside_multi_exec_block 100 | r.multi do |m| 101 | @info = r.info 102 | end 103 | 104 | assert @info.value.kind_of?(Hash) 105 | end 106 | 107 | def test_raise_command_errors_in_multi_exec 108 | assert_raise(Redis::CommandError) do 109 | r.multi do |m| 110 | m.set("foo", "s1") 111 | m.incr("foo") # not an integer 112 | m.lpush("foo", "value") # wrong kind of value 113 | end 114 | end 115 | 116 | assert_equal "s1", r.get("foo") 117 | end 118 | 119 | def test_raise_command_errors_when_accessing_futures_after_multi_exec 120 | begin 121 | r.multi do |m| 122 | m.set("foo", "s1") 123 | @counter = m.incr("foo") # not an integer 124 | end 125 | rescue Exception 126 | # Not gonna deal with it 127 | end 128 | 129 | # We should test for Redis::Error here, but hiredis doesn't yet do 130 | # custom error classes. 131 | err = nil 132 | begin 133 | @counter.value 134 | rescue => err 135 | end 136 | 137 | assert err.kind_of?(RuntimeError) 138 | end 139 | 140 | def test_multi_with_a_block_yielding_the_client 141 | r.multi do |multi| 142 | multi.set "foo", "s1" 143 | end 144 | 145 | assert_equal "s1", r.get("foo") 146 | end 147 | 148 | def test_raise_command_error_when_exec_fails 149 | redis_mock(:exec => lambda { |*_| "-ERROR" }) do |redis| 150 | assert_raise(Redis::CommandError) do 151 | redis.multi do |m| 152 | m.set "foo", "s1" 153 | end 154 | end 155 | end 156 | end 157 | 158 | def test_watch 159 | res = r.watch "foo" 160 | 161 | assert_equal "OK", res 162 | end 163 | 164 | def test_watch_with_an_unmodified_key 165 | r.watch "foo" 166 | r.multi do |multi| 167 | multi.set "foo", "s1" 168 | end 169 | 170 | assert_equal "s1", r.get("foo") 171 | end 172 | 173 | def test_watch_with_an_unmodified_key_passed_as_array 174 | r.watch ["foo", "bar"] 175 | r.multi do |multi| 176 | multi.set "foo", "s1" 177 | end 178 | 179 | assert_equal "s1", r.get("foo") 180 | end 181 | 182 | def test_watch_with_a_modified_key 183 | r.watch "foo" 184 | r.set "foo", "s1" 185 | res = r.multi do |multi| 186 | multi.set "foo", "s2" 187 | end 188 | 189 | assert_equal nil, res 190 | assert_equal "s1", r.get("foo") 191 | end 192 | 193 | def test_watch_with_a_modified_key_passed_as_array 194 | r.watch ["foo", "bar"] 195 | r.set "foo", "s1" 196 | res = r.multi do |multi| 197 | multi.set "foo", "s2" 198 | end 199 | 200 | assert_equal nil, res 201 | assert_equal "s1", r.get("foo") 202 | end 203 | 204 | def test_watch_with_a_block_and_an_unmodified_key 205 | result = r.watch "foo" do |rd| 206 | 207 | assert_same r, rd 208 | 209 | rd.multi do |multi| 210 | multi.set "foo", "s1" 211 | end 212 | end 213 | 214 | assert_equal ["OK"], result 215 | assert_equal "s1", r.get("foo") 216 | end 217 | 218 | def test_watch_with_a_block_and_a_modified_key 219 | result = r.watch "foo" do |rd| 220 | 221 | assert_same r, rd 222 | 223 | rd.set "foo", "s1" 224 | rd.multi do |multi| 225 | multi.set "foo", "s2" 226 | end 227 | end 228 | 229 | assert_equal nil, result 230 | assert_equal "s1", r.get("foo") 231 | end 232 | 233 | def test_watch_with_a_block_that_raises_an_exception 234 | r.set("foo", "s1") 235 | 236 | begin 237 | r.watch "foo" do 238 | raise "test" 239 | end 240 | rescue RuntimeError 241 | end 242 | 243 | r.set("foo", "s2") 244 | 245 | # If the watch was still set from within the block above, this multi/exec 246 | # would fail. This proves that raising an exception above unwatches. 247 | r.multi do |multi| 248 | multi.set "foo", "s3" 249 | end 250 | 251 | assert_equal "s3", r.get("foo") 252 | end 253 | 254 | def test_unwatch_with_a_modified_key 255 | r.watch "foo" 256 | r.set "foo", "s1" 257 | r.unwatch 258 | r.multi do |multi| 259 | multi.set "foo", "s2" 260 | end 261 | 262 | assert_equal "s2", r.get("foo") 263 | end 264 | end 265 | -------------------------------------------------------------------------------- /test/lint/sorted_sets.rb: -------------------------------------------------------------------------------- 1 | module Lint 2 | 3 | module SortedSets 4 | 5 | Infinity = 1.0/0.0 6 | 7 | def test_zadd 8 | assert_equal 0, r.zcard("foo") 9 | assert_equal true, r.zadd("foo", 1, "s1") 10 | assert_equal false, r.zadd("foo", 1, "s1") 11 | assert_equal 1, r.zcard("foo") 12 | end 13 | 14 | def test_variadic_zadd 15 | target_version "2.3.9" do # 2.4-rc6 16 | # Non-nested array with pairs 17 | assert_equal 0, r.zcard("foo") 18 | assert_equal 2, r.zadd("foo", [1, "s1", 2, "s2"]) 19 | assert_equal 1, r.zadd("foo", [4, "s1", 5, "s2", 6, "s3"]) 20 | assert_equal 3, r.zcard("foo") 21 | r.del "foo" 22 | 23 | # Nested array with pairs 24 | assert_equal 0, r.zcard("foo") 25 | assert_equal 2, r.zadd("foo", [[1, "s1"], [2, "s2"]]) 26 | assert_equal 1, r.zadd("foo", [[4, "s1"], [5, "s2"], [6, "s3"]]) 27 | assert_equal 3, r.zcard("foo") 28 | r.del "foo" 29 | 30 | # Wrong number of arguments 31 | assert_raise(Redis::CommandError) { r.zadd("foo", ["bar"]) } 32 | assert_raise(Redis::CommandError) { r.zadd("foo", ["bar", "qux", "zap"]) } 33 | end 34 | end 35 | 36 | def test_zrem 37 | r.zadd("foo", 1, "s1") 38 | r.zadd("foo", 2, "s2") 39 | 40 | assert_equal 2, r.zcard("foo") 41 | assert_equal true, r.zrem("foo", "s1") 42 | assert_equal false, r.zrem("foo", "s1") 43 | assert_equal 1, r.zcard("foo") 44 | end 45 | 46 | def test_variadic_zrem 47 | target_version "2.3.9" do # 2.4-rc6 48 | r.zadd("foo", 1, "s1") 49 | r.zadd("foo", 2, "s2") 50 | r.zadd("foo", 3, "s3") 51 | 52 | assert_equal 3, r.zcard("foo") 53 | assert_equal 1, r.zrem("foo", ["s1", "aaa"]) 54 | assert_equal 0, r.zrem("foo", ["bbb", "ccc" "ddd"]) 55 | assert_equal 1, r.zrem("foo", ["eee", "s3"]) 56 | assert_equal 1, r.zcard("foo") 57 | end 58 | end 59 | 60 | def test_zincrby 61 | rv = r.zincrby "foo", 1, "s1" 62 | assert_equal 1.0, rv 63 | 64 | rv = r.zincrby "foo", 10, "s1" 65 | assert_equal 11.0, rv 66 | 67 | rv = r.zincrby "bar", "-inf", "s1" 68 | assert_equal(-Infinity, rv) 69 | 70 | rv = r.zincrby "bar", "+inf", "s2" 71 | assert_equal(+Infinity, rv) 72 | end 73 | 74 | def test_zrank 75 | r.zadd "foo", 1, "s1" 76 | r.zadd "foo", 2, "s2" 77 | r.zadd "foo", 3, "s3" 78 | 79 | assert_equal 2, r.zrank("foo", "s3") 80 | end 81 | 82 | def test_zrevrank 83 | r.zadd "foo", 1, "s1" 84 | r.zadd "foo", 2, "s2" 85 | r.zadd "foo", 3, "s3" 86 | 87 | assert_equal 0, r.zrevrank("foo", "s3") 88 | end 89 | 90 | def test_zrange 91 | r.zadd "foo", 1, "s1" 92 | r.zadd "foo", 2, "s2" 93 | r.zadd "foo", 3, "s3" 94 | 95 | assert_equal ["s1", "s2"], r.zrange("foo", 0, 1) 96 | assert_equal [["s1", 1.0], ["s2", 2.0]], r.zrange("foo", 0, 1, :with_scores => true) 97 | assert_equal [["s1", 1.0], ["s2", 2.0]], r.zrange("foo", 0, 1, :withscores => true) 98 | 99 | r.zadd "bar", "-inf", "s1" 100 | r.zadd "bar", "+inf", "s2" 101 | assert_equal [["s1", -Infinity], ["s2", +Infinity]], r.zrange("bar", 0, 1, :with_scores => true) 102 | assert_equal [["s1", -Infinity], ["s2", +Infinity]], r.zrange("bar", 0, 1, :withscores => true) 103 | end 104 | 105 | def test_zrevrange 106 | r.zadd "foo", 1, "s1" 107 | r.zadd "foo", 2, "s2" 108 | r.zadd "foo", 3, "s3" 109 | 110 | assert_equal ["s3", "s2"], r.zrevrange("foo", 0, 1) 111 | assert_equal [["s3", 3.0], ["s2", 2.0]], r.zrevrange("foo", 0, 1, :with_scores => true) 112 | assert_equal [["s3", 3.0], ["s2", 2.0]], r.zrevrange("foo", 0, 1, :withscores => true) 113 | 114 | r.zadd "bar", "-inf", "s1" 115 | r.zadd "bar", "+inf", "s2" 116 | assert_equal [["s2", +Infinity], ["s1", -Infinity]], r.zrevrange("bar", 0, 1, :with_scores => true) 117 | assert_equal [["s2", +Infinity], ["s1", -Infinity]], r.zrevrange("bar", 0, 1, :withscores => true) 118 | end 119 | 120 | def test_zrangebyscore 121 | r.zadd "foo", 1, "s1" 122 | r.zadd "foo", 2, "s2" 123 | r.zadd "foo", 3, "s3" 124 | 125 | assert_equal ["s2", "s3"], r.zrangebyscore("foo", 2, 3) 126 | end 127 | 128 | def test_zrevrangebyscore 129 | r.zadd "foo", 1, "s1" 130 | r.zadd "foo", 2, "s2" 131 | r.zadd "foo", 3, "s3" 132 | 133 | assert_equal ["s3", "s2"], r.zrevrangebyscore("foo", 3, 2) 134 | end 135 | 136 | def test_zrangebyscore_with_limit 137 | r.zadd "foo", 1, "s1" 138 | r.zadd "foo", 2, "s2" 139 | r.zadd "foo", 3, "s3" 140 | r.zadd "foo", 4, "s4" 141 | 142 | assert_equal ["s2"], r.zrangebyscore("foo", 2, 4, :limit => [0, 1]) 143 | assert_equal ["s3"], r.zrangebyscore("foo", 2, 4, :limit => [1, 1]) 144 | assert_equal ["s3", "s4"], r.zrangebyscore("foo", 2, 4, :limit => [1, 2]) 145 | end 146 | 147 | def test_zrevrangebyscore_with_limit 148 | r.zadd "foo", 1, "s1" 149 | r.zadd "foo", 2, "s2" 150 | r.zadd "foo", 3, "s3" 151 | r.zadd "foo", 4, "s4" 152 | 153 | assert_equal ["s4"], r.zrevrangebyscore("foo", 4, 2, :limit => [0, 1]) 154 | assert_equal ["s3"], r.zrevrangebyscore("foo", 4, 2, :limit => [1, 1]) 155 | assert_equal ["s3", "s2"], r.zrevrangebyscore("foo", 4, 2, :limit => [1, 2]) 156 | end 157 | 158 | def test_zrangebyscore_with_withscores 159 | r.zadd "foo", 1, "s1" 160 | r.zadd "foo", 2, "s2" 161 | r.zadd "foo", 3, "s3" 162 | r.zadd "foo", 4, "s4" 163 | 164 | assert_equal [["s2", 2.0]], r.zrangebyscore("foo", 2, 4, :limit => [0, 1], :with_scores => true) 165 | assert_equal [["s3", 3.0]], r.zrangebyscore("foo", 2, 4, :limit => [1, 1], :with_scores => true) 166 | assert_equal [["s2", 2.0]], r.zrangebyscore("foo", 2, 4, :limit => [0, 1], :withscores => true) 167 | assert_equal [["s3", 3.0]], r.zrangebyscore("foo", 2, 4, :limit => [1, 1], :withscores => true) 168 | 169 | r.zadd "bar", "-inf", "s1" 170 | r.zadd "bar", "+inf", "s2" 171 | assert_equal [["s1", -Infinity]], r.zrangebyscore("bar", -Infinity, +Infinity, :limit => [0, 1], :with_scores => true) 172 | assert_equal [["s2", +Infinity]], r.zrangebyscore("bar", -Infinity, +Infinity, :limit => [1, 1], :with_scores => true) 173 | assert_equal [["s1", -Infinity]], r.zrangebyscore("bar", -Infinity, +Infinity, :limit => [0, 1], :withscores => true) 174 | assert_equal [["s2", +Infinity]], r.zrangebyscore("bar", -Infinity, +Infinity, :limit => [1, 1], :withscores => true) 175 | end 176 | 177 | def test_zrevrangebyscore_with_withscores 178 | r.zadd "foo", 1, "s1" 179 | r.zadd "foo", 2, "s2" 180 | r.zadd "foo", 3, "s3" 181 | r.zadd "foo", 4, "s4" 182 | 183 | assert_equal [["s4", 4.0]], r.zrevrangebyscore("foo", 4, 2, :limit => [0, 1], :with_scores => true) 184 | assert_equal [["s3", 3.0]], r.zrevrangebyscore("foo", 4, 2, :limit => [1, 1], :with_scores => true) 185 | assert_equal [["s4", 4.0]], r.zrevrangebyscore("foo", 4, 2, :limit => [0, 1], :withscores => true) 186 | assert_equal [["s3", 3.0]], r.zrevrangebyscore("foo", 4, 2, :limit => [1, 1], :withscores => true) 187 | 188 | r.zadd "bar", "-inf", "s1" 189 | r.zadd "bar", "+inf", "s2" 190 | assert_equal [["s2", +Infinity]], r.zrevrangebyscore("bar", +Infinity, -Infinity, :limit => [0, 1], :with_scores => true) 191 | assert_equal [["s1", -Infinity]], r.zrevrangebyscore("bar", +Infinity, -Infinity, :limit => [1, 1], :with_scores => true) 192 | assert_equal [["s2", +Infinity]], r.zrevrangebyscore("bar", +Infinity, -Infinity, :limit => [0, 1], :withscores => true) 193 | assert_equal [["s1", -Infinity]], r.zrevrangebyscore("bar", +Infinity, -Infinity, :limit => [1, 1], :withscores => true) 194 | end 195 | 196 | def test_zcard 197 | assert_equal 0, r.zcard("foo") 198 | 199 | r.zadd "foo", 1, "s1" 200 | 201 | assert_equal 1, r.zcard("foo") 202 | end 203 | 204 | def test_zscore 205 | r.zadd "foo", 1, "s1" 206 | 207 | assert_equal 1.0, r.zscore("foo", "s1") 208 | 209 | assert_equal nil, r.zscore("foo", "s2") 210 | assert_equal nil, r.zscore("bar", "s1") 211 | 212 | r.zadd "bar", "-inf", "s1" 213 | r.zadd "bar", "+inf", "s2" 214 | assert_equal(-Infinity, r.zscore("bar", "s1")) 215 | assert_equal(+Infinity, r.zscore("bar", "s2")) 216 | end 217 | 218 | def test_zremrangebyrank 219 | r.zadd "foo", 10, "s1" 220 | r.zadd "foo", 20, "s2" 221 | r.zadd "foo", 30, "s3" 222 | r.zadd "foo", 40, "s4" 223 | 224 | assert_equal 3, r.zremrangebyrank("foo", 1, 3) 225 | assert_equal ["s1"], r.zrange("foo", 0, -1) 226 | end 227 | 228 | def test_zremrangebyscore 229 | r.zadd "foo", 1, "s1" 230 | r.zadd "foo", 2, "s2" 231 | r.zadd "foo", 3, "s3" 232 | r.zadd "foo", 4, "s4" 233 | 234 | assert_equal 3, r.zremrangebyscore("foo", 2, 4) 235 | assert_equal ["s1"], r.zrange("foo", 0, -1) 236 | end 237 | end 238 | end 239 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redis-rb [![Build Status][travis-image]][travis-link] [![Inline docs][inchpages-image]][inchpages-link] 2 | 3 | [travis-image]: https://secure.travis-ci.org/redis/redis-rb.png?branch=master 4 | [travis-link]: http://travis-ci.org/redis/redis-rb 5 | [travis-home]: http://travis-ci.org/ 6 | [inchpages-image]: http://inch-pages.github.io/github/redis/redis-rb.png 7 | [inchpages-link]: http://inch-pages.github.io/github/redis/redis-rb 8 | 9 | A Ruby client library for [Redis][redis-home]. 10 | 11 | [redis-home]: http://redis.io 12 | 13 | A Ruby client that tries to match Redis' API one-to-one, while still 14 | providing an idiomatic interface. It features thread-safety, client-side 15 | sharding, pipelining, and an obsession for performance. 16 | 17 | ## Upgrading from 2.x to 3.0 18 | 19 | Please refer to the [CHANGELOG][changelog-3.0.0] for a summary of the 20 | most important changes, as well as a full list of changes. 21 | 22 | [changelog-3.0.0]: https://github.com/redis/redis-rb/blob/master/CHANGELOG.md#300 23 | 24 | ## Getting started 25 | 26 | As of version 2.0 this client only targets Redis version 2.0 and higher. 27 | You can use an older version of this client if you need to interface 28 | with a Redis instance older than 2.0, but this is no longer supported. 29 | 30 | You can connect to Redis by instantiating the `Redis` class: 31 | 32 | ```ruby 33 | require "redis" 34 | 35 | redis = Redis.new 36 | ``` 37 | 38 | This assumes Redis was started with a default configuration, and is 39 | listening on `localhost`, port 6379. If you need to connect to a remote 40 | server or a different port, try: 41 | 42 | ```ruby 43 | redis = Redis.new(:host => "10.0.1.1", :port => 6380, :db => 15) 44 | ``` 45 | 46 | You can also specify connection options as an URL: 47 | 48 | ```ruby 49 | redis = Redis.new(:url => "redis://:p4ssw0rd@10.0.1.1:6380/15") 50 | ``` 51 | 52 | By default, the client will try to read the `REDIS_URL` environment variable 53 | and use that as URL to connect to. The above statement is therefore equivalent 54 | to setting this environment variable and calling `Redis.new` without arguments. 55 | 56 | To connect to Redis listening on a Unix socket, try: 57 | 58 | ```ruby 59 | redis = Redis.new(:path => "/tmp/redis.sock") 60 | ``` 61 | 62 | To connect to a password protected Redis instance, use: 63 | 64 | ```ruby 65 | redis = Redis.new(:password => "mysecret") 66 | ``` 67 | 68 | The Redis class exports methods that are named identical to the commands 69 | they execute. The arguments these methods accept are often identical to 70 | the arguments specified on the [Redis website][redis-commands]. For 71 | instance, the `SET` and `GET` commands can be called like this: 72 | 73 | [redis-commands]: http://redis.io/commands 74 | 75 | ```ruby 76 | redis.set("mykey", "hello world") 77 | # => "OK" 78 | 79 | redis.get("mykey") 80 | # => "hello world" 81 | ``` 82 | 83 | All commands, their arguments and return values are documented, and 84 | available on [rdoc.info][rdoc]. 85 | 86 | [rdoc]: http://rdoc.info/github/redis/redis-rb/ 87 | 88 | ## Storing objects 89 | 90 | Redis only stores strings as values. If you want to store an object, you 91 | can use a serialization mechanism such as JSON: 92 | 93 | ```ruby 94 | require "json" 95 | 96 | redis.set "foo", [1, 2, 3].to_json 97 | # => OK 98 | 99 | JSON.parse(redis.get("foo")) 100 | # => [1, 2, 3] 101 | ``` 102 | 103 | ## Pipelining 104 | 105 | When multiple commands are executed sequentially, but are not dependent, 106 | the calls can be *pipelined*. This means that the client doesn't wait 107 | for reply of the first command before sending the next command. The 108 | advantage is that multiple commands are sent at once, resulting in 109 | faster overall execution. 110 | 111 | The client can be instructed to pipeline commands by using the 112 | `#pipelined` method. After the block is executed, the client sends all 113 | commands to Redis and gathers their replies. These replies are returned 114 | by the `#pipelined` method. 115 | 116 | ```ruby 117 | redis.pipelined do 118 | redis.set "foo", "bar" 119 | redis.incr "baz" 120 | end 121 | # => ["OK", 1] 122 | ``` 123 | 124 | ### Executing commands atomically 125 | 126 | You can use `MULTI/EXEC` to run a number of commands in an atomic 127 | fashion. This is similar to executing a pipeline, but the commands are 128 | preceded by a call to `MULTI`, and followed by a call to `EXEC`. Like 129 | the regular pipeline, the replies to the commands are returned by the 130 | `#multi` method. 131 | 132 | ```ruby 133 | redis.multi do 134 | redis.set "foo", "bar" 135 | redis.incr "baz" 136 | end 137 | # => ["OK", 1] 138 | ``` 139 | 140 | ### Futures 141 | 142 | Replies to commands in a pipeline can be accessed via the *futures* they 143 | emit (since redis-rb 3.0). All calls inside a pipeline block return a 144 | `Future` object, which responds to the `#value` method. When the 145 | pipeline has succesfully executed, all futures are assigned their 146 | respective replies and can be used. 147 | 148 | ```ruby 149 | redis.pipelined do 150 | @set = redis.set "foo", "bar" 151 | @incr = redis.incr "baz" 152 | end 153 | 154 | @set.value 155 | # => "OK" 156 | 157 | @incr.value 158 | # => 1 159 | ``` 160 | 161 | ## Error Handling 162 | 163 | In general, if something goes wrong you'll get an exception. For example, if 164 | it can't connect to the server a `Redis::CannotConnectError` error will be raised. 165 | 166 | ```ruby 167 | begin 168 | redis.ping 169 | rescue Exception => e 170 | e.inspect 171 | # => # 172 | 173 | e.message 174 | # => Timed out connecting to Redis on 10.0.1.1:6380 175 | end 176 | ``` 177 | 178 | See lib/redis/errors.rb for information about what exceptions are possible. 179 | 180 | 181 | ## Expert-Mode Options 182 | 183 | - `inherit_socket: true`: disable safety check that prevents a forked child 184 | from sharing a socket with its parent; this is potentially useful in order to mitigate connection churn when: 185 | - many short-lived forked children of one process need to talk 186 | to redis, AND 187 | - your own code prevents the parent process from using the redis 188 | connection while a child is alive 189 | 190 | Improper use of `inherit_socket` will result in corrupted and/or incorrect 191 | responses. 192 | 193 | ## Alternate drivers 194 | 195 | By default, redis-rb uses Ruby's socket library to talk with Redis. 196 | To use an alternative connection driver it should be specified as option 197 | when instantiating the client object. These instructions are only valid 198 | for **redis-rb 3.0**. For instructions on how to use alternate drivers from 199 | **redis-rb 2.2**, please refer to an [older README][readme-2.2.2]. 200 | 201 | [readme-2.2.2]: https://github.com/redis/redis-rb/blob/v2.2.2/README.md 202 | 203 | ### hiredis 204 | 205 | The hiredis driver uses the connection facility of hiredis-rb. In turn, 206 | hiredis-rb is a binding to the official hiredis client library. It 207 | optimizes for speed, at the cost of portability. Because it is a C 208 | extension, JRuby is not supported (by default). 209 | 210 | It is best to use hiredis when you have large replies (for example: 211 | `LRANGE`, `SMEMBERS`, `ZRANGE`, etc.) and/or use big pipelines. 212 | 213 | In your Gemfile, include hiredis: 214 | 215 | ```ruby 216 | gem "redis", "~> 3.0.1" 217 | gem "hiredis", "~> 0.4.5" 218 | ``` 219 | 220 | When instantiating the client object, specify hiredis: 221 | 222 | ```ruby 223 | redis = Redis.new(:driver => :hiredis) 224 | ``` 225 | 226 | ### synchrony 227 | 228 | The synchrony driver adds support for [em-synchrony][em-synchrony]. 229 | This makes redis-rb work with EventMachine's asynchronous I/O, while not 230 | changing the exposed API. The hiredis gem needs to be available as 231 | well, because the synchrony driver uses hiredis for parsing the Redis 232 | protocol. 233 | 234 | [em-synchrony]: https://github.com/igrigorik/em-synchrony 235 | 236 | In your Gemfile, include em-synchrony and hiredis: 237 | 238 | ```ruby 239 | gem "redis", "~> 3.0.1" 240 | gem "hiredis", "~> 0.4.5" 241 | gem "em-synchrony" 242 | ``` 243 | 244 | When instantiating the client object, specify synchrony: 245 | 246 | ```ruby 247 | redis = Redis.new(:driver => :synchrony) 248 | ``` 249 | 250 | ## Testing 251 | 252 | This library is tested using [Travis][travis-home], where it is tested 253 | against the following interpreters and drivers: 254 | 255 | * MRI 1.8.7 (drivers: ruby, hiredis) 256 | * MRI 1.9.2 (drivers: ruby, hiredis, synchrony) 257 | * MRI 1.9.3 (drivers: ruby, hiredis, synchrony) 258 | * MRI 2.0.0 (drivers: ruby, hiredis, synchrony) 259 | * JRuby 1.7 (1.8 mode) (drivers: ruby) 260 | * JRuby 1.7 (1.9 mode) (drivers: ruby) 261 | 262 | ## Contributors 263 | 264 | (ordered chronologically with more than 5 commits, see `git shortlog -sn` for 265 | all contributors) 266 | 267 | * Ezra Zygmuntowicz 268 | * Taylor Weibley 269 | * Matthew Clark 270 | * Brian McKinney 271 | * Luca Guidi 272 | * Salvatore Sanfillipo 273 | * Chris Wanstrath 274 | * Damian Janowski 275 | * Michel Martens 276 | * Nick Quaranto 277 | * Pieter Noordhuis 278 | * Ilya Grigorik 279 | 280 | ## Contributing 281 | 282 | [Fork the project](https://github.com/redis/redis-rb) and send pull 283 | requests. You can also ask for help at `#redis-rb` on Freenode. 284 | -------------------------------------------------------------------------------- /lib/redis/connection/ruby.rb: -------------------------------------------------------------------------------- 1 | require "redis/connection/registry" 2 | require "redis/connection/command_helper" 3 | require "redis/errors" 4 | require "socket" 5 | 6 | class Redis 7 | module Connection 8 | module SocketMixin 9 | 10 | CRLF = "\r\n".freeze 11 | 12 | def initialize(*args) 13 | super(*args) 14 | 15 | @timeout = nil 16 | @buffer = "" 17 | end 18 | 19 | def timeout=(timeout) 20 | if timeout && timeout > 0 21 | @timeout = timeout 22 | else 23 | @timeout = nil 24 | end 25 | end 26 | 27 | def read(nbytes) 28 | result = @buffer.slice!(0, nbytes) 29 | 30 | while result.bytesize < nbytes 31 | result << _read_from_socket(nbytes - result.bytesize) 32 | end 33 | 34 | result 35 | end 36 | 37 | def gets 38 | crlf = nil 39 | 40 | while (crlf = @buffer.index(CRLF)) == nil 41 | @buffer << _read_from_socket(1024) 42 | end 43 | 44 | @buffer.slice!(0, crlf + CRLF.bytesize) 45 | end 46 | 47 | def _read_from_socket(nbytes) 48 | begin 49 | read_nonblock(nbytes) 50 | 51 | rescue Errno::EWOULDBLOCK, Errno::EAGAIN 52 | if IO.select([self], nil, nil, @timeout) 53 | retry 54 | else 55 | raise Redis::TimeoutError 56 | end 57 | end 58 | 59 | rescue EOFError 60 | raise Errno::ECONNRESET 61 | end 62 | end 63 | 64 | if defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby" 65 | 66 | require "timeout" 67 | 68 | class TCPSocket < ::TCPSocket 69 | 70 | include SocketMixin 71 | 72 | def self.connect(host, port, timeout) 73 | Timeout.timeout(timeout) do 74 | sock = new(host, port) 75 | sock 76 | end 77 | rescue Timeout::Error 78 | raise TimeoutError 79 | end 80 | end 81 | 82 | if defined?(::UNIXSocket) 83 | 84 | class UNIXSocket < ::UNIXSocket 85 | 86 | include SocketMixin 87 | 88 | def self.connect(path, timeout) 89 | Timeout.timeout(timeout) do 90 | sock = new(path) 91 | sock 92 | end 93 | rescue Timeout::Error 94 | raise TimeoutError 95 | end 96 | 97 | # JRuby raises Errno::EAGAIN on #read_nonblock even when IO.select 98 | # says it is readable (1.6.6, in both 1.8 and 1.9 mode). 99 | # Use the blocking #readpartial method instead. 100 | 101 | def _read_from_socket(nbytes) 102 | readpartial(nbytes) 103 | 104 | rescue EOFError 105 | raise Errno::ECONNRESET 106 | end 107 | end 108 | 109 | end 110 | 111 | else 112 | 113 | class TCPSocket < ::Socket 114 | 115 | include SocketMixin 116 | 117 | def self.connect_addrinfo(ai, port, timeout) 118 | sock = new(::Socket.const_get(ai[0]), Socket::SOCK_STREAM, 0) 119 | sockaddr = ::Socket.pack_sockaddr_in(port, ai[3]) 120 | 121 | begin 122 | sock.connect_nonblock(sockaddr) 123 | rescue Errno::EINPROGRESS 124 | if IO.select(nil, [sock], nil, timeout) == nil 125 | raise TimeoutError 126 | end 127 | 128 | begin 129 | sock.connect_nonblock(sockaddr) 130 | rescue Errno::EISCONN 131 | end 132 | end 133 | 134 | sock 135 | end 136 | 137 | def self.connect(host, port, timeout) 138 | # Don't pass AI_ADDRCONFIG as flag to getaddrinfo(3) 139 | # 140 | # From the man page for getaddrinfo(3): 141 | # 142 | # If hints.ai_flags includes the AI_ADDRCONFIG flag, then IPv4 143 | # addresses are returned in the list pointed to by res only if the 144 | # local system has at least one IPv4 address configured, and IPv6 145 | # addresses are returned only if the local system has at least one 146 | # IPv6 address configured. The loopback address is not considered 147 | # for this case as valid as a configured address. 148 | # 149 | # We do want the IPv6 loopback address to be returned if applicable, 150 | # even if it is the only configured IPv6 address on the machine. 151 | # Also see: https://github.com/redis/redis-rb/pull/394. 152 | addrinfo = ::Socket.getaddrinfo(host, nil, Socket::AF_UNSPEC, Socket::SOCK_STREAM) 153 | 154 | # From the man page for getaddrinfo(3): 155 | # 156 | # Normally, the application should try using the addresses in the 157 | # order in which they are returned. The sorting function used 158 | # within getaddrinfo() is defined in RFC 3484 [...]. 159 | # 160 | addrinfo.each_with_index do |ai, i| 161 | begin 162 | return connect_addrinfo(ai, port, timeout) 163 | rescue SystemCallError 164 | # Raise if this was our last attempt. 165 | raise if addrinfo.length == i+1 166 | end 167 | end 168 | end 169 | end 170 | 171 | class UNIXSocket < ::Socket 172 | 173 | include SocketMixin 174 | 175 | def self.connect(path, timeout) 176 | sock = new(::Socket::AF_UNIX, Socket::SOCK_STREAM, 0) 177 | sockaddr = ::Socket.pack_sockaddr_un(path) 178 | 179 | begin 180 | sock.connect_nonblock(sockaddr) 181 | rescue Errno::EINPROGRESS 182 | if IO.select(nil, [sock], nil, timeout) == nil 183 | raise TimeoutError 184 | end 185 | 186 | begin 187 | sock.connect_nonblock(sockaddr) 188 | rescue Errno::EISCONN 189 | end 190 | end 191 | 192 | sock 193 | end 194 | end 195 | 196 | end 197 | 198 | class Ruby 199 | include Redis::Connection::CommandHelper 200 | 201 | MINUS = "-".freeze 202 | PLUS = "+".freeze 203 | COLON = ":".freeze 204 | DOLLAR = "$".freeze 205 | ASTERISK = "*".freeze 206 | 207 | def self.connect(config) 208 | if config[:scheme] == "unix" 209 | sock = UNIXSocket.connect(config[:path], config[:timeout]) 210 | else 211 | sock = TCPSocket.connect(config[:host], config[:port], config[:timeout]) 212 | end 213 | 214 | instance = new(sock) 215 | instance.timeout = config[:timeout] 216 | instance.set_tcp_keepalive config[:tcp_keepalive] 217 | instance 218 | end 219 | 220 | if [:SOL_SOCKET, :SO_KEEPALIVE, :SOL_TCP, :TCP_KEEPIDLE, :TCP_KEEPINTVL, :TCP_KEEPCNT].all?{|c| Socket.const_defined? c} 221 | def set_tcp_keepalive(keepalive) 222 | return unless keepalive.is_a?(Hash) 223 | 224 | @sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true) 225 | @sock.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE, keepalive[:time]) 226 | @sock.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL, keepalive[:intvl]) 227 | @sock.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, keepalive[:probes]) 228 | end 229 | 230 | def get_tcp_keepalive 231 | { 232 | :time => @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE).int, 233 | :intvl => @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL).int, 234 | :probes => @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT).int, 235 | } 236 | end 237 | else 238 | def set_tcp_keepalive(keepalive) 239 | end 240 | 241 | def get_tcp_keepalive 242 | { 243 | } 244 | end 245 | end 246 | 247 | def initialize(sock) 248 | @sock = sock 249 | end 250 | 251 | def connected? 252 | !! @sock 253 | end 254 | 255 | def disconnect 256 | @sock.close 257 | rescue 258 | ensure 259 | @sock = nil 260 | end 261 | 262 | def timeout=(timeout) 263 | if @sock.respond_to?(:timeout=) 264 | @sock.timeout = timeout 265 | end 266 | end 267 | 268 | def write(command) 269 | @sock.write(build_command(command)) 270 | end 271 | 272 | def read 273 | line = @sock.gets 274 | reply_type = line.slice!(0, 1) 275 | format_reply(reply_type, line) 276 | 277 | rescue Errno::EAGAIN 278 | raise TimeoutError 279 | end 280 | 281 | def format_reply(reply_type, line) 282 | case reply_type 283 | when MINUS then format_error_reply(line) 284 | when PLUS then format_status_reply(line) 285 | when COLON then format_integer_reply(line) 286 | when DOLLAR then format_bulk_reply(line) 287 | when ASTERISK then format_multi_bulk_reply(line) 288 | else raise ProtocolError.new(reply_type) 289 | end 290 | end 291 | 292 | def format_error_reply(line) 293 | CommandError.new(line.strip) 294 | end 295 | 296 | def format_status_reply(line) 297 | line.strip 298 | end 299 | 300 | def format_integer_reply(line) 301 | line.to_i 302 | end 303 | 304 | def format_bulk_reply(line) 305 | bulklen = line.to_i 306 | return if bulklen == -1 307 | reply = encode(@sock.read(bulklen)) 308 | @sock.read(2) # Discard CRLF. 309 | reply 310 | end 311 | 312 | def format_multi_bulk_reply(line) 313 | n = line.to_i 314 | return if n == -1 315 | 316 | Array.new(n) { read } 317 | end 318 | end 319 | end 320 | end 321 | 322 | Redis::Connection.drivers << Redis::Connection::Ruby 323 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 4.x (unreleased) 2 | 3 | ## Planned breaking changes: 4 | * `Redis#client` will no longer expose the underlying `Redis::Client`; 5 | it has not yet been determined how 4.0 will expose the underlying 6 | functionality, but we will make every attempt to provide a final minor 7 | release of 3.x that provides the new interfaces in order to facilitate 8 | a smooth transition. 9 | 10 | * Ruby 1.8.7 (and the 1.8 modes of JRuby and Rubinius) will no longer be 11 | supported; 1.8.x entered end-of-life in June of 2012 and stopped receiving 12 | security updates in June of 2013; continuing to support it would prevent 13 | the use of newer features of Ruby. 14 | 15 | # 3.1.0 16 | 17 | * Added debug log sanitization (#428). 18 | 19 | * Added support for HyperLogLog commands (Redis 2.8.9, #432). 20 | 21 | * Added support for `BITPOS` command (Redis 2.9.11, #412). 22 | 23 | * The client will now automatically reconnect after a fork (#414). 24 | 25 | * If you want to disable the fork-safety check and prefer to share the 26 | connection across child processes, you can now pass the `inherit_socket` 27 | option (#409). 28 | 29 | * If you want the client to attempt to reconnect more than once, you can now 30 | pass the `reconnect_attempts` option (#347) 31 | 32 | # 3.0.7 33 | 34 | * Added method `Redis#dup` to duplicate a Redis connection. 35 | 36 | * IPv6 support. 37 | 38 | # 3.0.6 39 | 40 | * Added support for `SCAN` and variants. 41 | 42 | # 3.0.5 43 | 44 | * Fix calling #select from a pipeline (#309). 45 | 46 | * Added method `Redis#connected?`. 47 | 48 | * Added support for `MIGRATE` (Redis 2.6). 49 | 50 | * Support extended SET command (#343, thanks to @benubois). 51 | 52 | # 3.0.4 53 | 54 | * Ensure #watch without a block returns "OK" (#332). 55 | 56 | * Make futures identifiable (#330). 57 | 58 | * Fix an issue preventing STORE in a SORT with multiple GETs (#328). 59 | 60 | # 3.0.3 61 | 62 | * Blocking list commands (`BLPOP`, `BRPOP`, `BRPOPLPUSH`) use a socket 63 | timeout equal to the sum of the command's timeout and the Redis 64 | client's timeout, instead of disabling socket timeout altogether. 65 | 66 | * Ruby 2.0 compatibility. 67 | 68 | * Added support for `DUMP` and `RESTORE` (Redis 2.6). 69 | 70 | * Added support for `BITCOUNT` and `BITOP` (Redis 2.6). 71 | 72 | * Call `#to_s` on value argument for `SET`, `SETEX`, `PSETEX`, `GETSET`, 73 | `SETNX`, and `SETRANGE`. 74 | 75 | # 3.0.2 76 | 77 | * Unescape CGI escaped password in URL. 78 | 79 | * Fix test to check availability of `UNIXSocket`. 80 | 81 | * Fix handling of score = +/- infinity for sorted set commands. 82 | 83 | * Replace array splats with concatenation where possible. 84 | 85 | * Raise if `EXEC` returns an error. 86 | 87 | * Passing a nil value in options hash no longer overwrites the default. 88 | 89 | * Allow string keys in options hash passed to `Redis.new` or 90 | `Redis.connect`. 91 | 92 | * Fix uncaught error triggering unrelated error (synchrony driver). 93 | 94 | See f7ffd5f1a628029691084de69e5b46699bb8b96d and #248. 95 | 96 | # 3.0.1 97 | 98 | * Fix reconnect logic not kicking in on a write error. 99 | 100 | See 427dbd52928af452f35aa0a57b621bee56cdcb18 and #238. 101 | 102 | # 3.0.0 103 | 104 | ### Upgrading from 2.x to 3.0 105 | 106 | The following items are the most important changes to review when 107 | upgrading from redis-rb 2.x. A full list of changes can be found below. 108 | 109 | * The methods for the following commands have changed the arguments they 110 | take, their return value, or both. 111 | 112 | * `BLPOP`, `BRPOP`, `BRPOPLPUSH` 113 | * `SORT` 114 | * `MSETNX` 115 | * `ZRANGE`, `ZREVRANGE`, `ZRANGEBYSCORE`, `ZREVRANGEBYSCORE` 116 | * `ZINCRBY`, `ZSCORE` 117 | 118 | * The return value from `#pipelined` and `#multi` no longer contains 119 | unprocessed replies, but the same replies that would be returned if 120 | the command had not been executed in these blocks. 121 | 122 | * The client raises custom errors on connection errors, instead of 123 | `RuntimeError` and errors in the `Errno` family. 124 | 125 | ### Changes 126 | 127 | * Added support for scripting commands (Redis 2.6). 128 | 129 | Scripts can be executed using `#eval` and `#evalsha`. Both can 130 | commands can either take two arrays to specify `KEYS` and `ARGV`, or 131 | take a hash containing `:keys` and `:argv` to specify `KEYS` and 132 | `ARGV`. 133 | 134 | ```ruby 135 | redis.eval("return ARGV[1] * ARGV[2]", :argv => [2, 3]) 136 | # => 6 137 | ``` 138 | 139 | Subcommands of the `SCRIPT` command can be executed via the 140 | `#script` method. 141 | 142 | For example: 143 | 144 | ```ruby 145 | redis.script(:load, "return ARGV[1] * ARGV[2]") 146 | # => "58db5d365a1922f32e7aa717722141ea9c2b0cf3" 147 | redis.script(:exists, "58db5d365a1922f32e7aa717722141ea9c2b0cf3") 148 | # => true 149 | redis.script(:flush) 150 | # => "OK" 151 | ``` 152 | 153 | * The repository now lives at [https://github.com/redis/redis-rb](https://github.com/redis/redis-rb). 154 | Thanks, Ezra! 155 | 156 | * Added support for `PEXPIRE`, `PEXPIREAT`, `PTTL`, `PSETEX`, 157 | `INCRYBYFLOAT`, `HINCRYBYFLOAT` and `TIME` (Redis 2.6). 158 | 159 | * `Redis.current` is now thread unsafe, because the client itself is thread safe. 160 | 161 | In the future you'll be able to do something like: 162 | 163 | ```ruby 164 | Redis.current = Redis::Pool.connect 165 | ``` 166 | 167 | This makes `Redis.current` actually usable in multi-threaded environments, 168 | while not affecting those running a single thread. 169 | 170 | * Change API for `BLPOP`, `BRPOP` and `BRPOPLPUSH`. 171 | 172 | Both `BLPOP` and `BRPOP` now take a single argument equal to a 173 | string key, or an array with string keys, followed by an optional 174 | hash with a `:timeout` key. When not specified, the timeout defaults 175 | to `0` to not time out. 176 | 177 | ```ruby 178 | redis.blpop(["list1", "list2"], :timeout => 1.0) 179 | ``` 180 | 181 | `BRPOPLPUSH` also takes an optional hash with a `:timeout` key as 182 | last argument for consistency. When not specified, the timeout 183 | defaults to `0` to not time out. 184 | 185 | ```ruby 186 | redis.brpoplpush("some_list", "another_list", :timeout => 1.0) 187 | ``` 188 | 189 | * When `SORT` is passed multiple key patterns to get via the `:get` 190 | option, it now returns an array per result element, holding all `GET` 191 | substitutions. 192 | 193 | * The `MSETNX` command now returns a boolean. 194 | 195 | * The `ZRANGE`, `ZREVRANGE`, `ZRANGEBYSCORE` and `ZREVRANGEBYSCORE` commands 196 | now return an array containing `[String, Float]` pairs when 197 | `:with_scores => true` is passed. 198 | 199 | For example: 200 | 201 | ```ruby 202 | redis.zrange("zset", 0, -1, :with_scores => true) 203 | # => [["foo", 1.0], ["bar", 2.0]] 204 | ``` 205 | 206 | * The `ZINCRBY` and `ZSCORE` commands now return a `Float` score instead 207 | of a string holding a representation of the score. 208 | 209 | * The client now raises custom exceptions where it makes sense. 210 | 211 | If by any chance you were rescuing low-level exceptions (`Errno::*`), 212 | you should now rescue as follows: 213 | 214 | Errno::ECONNRESET -> Redis::ConnectionError 215 | Errno::EPIPE -> Redis::ConnectionError 216 | Errno::ECONNABORTED -> Redis::ConnectionError 217 | Errno::EBADF -> Redis::ConnectionError 218 | Errno::EINVAL -> Redis::ConnectionError 219 | Errno::EAGAIN -> Redis::TimeoutError 220 | Errno::ECONNREFUSED -> Redis::CannotConnectError 221 | 222 | * Always raise exceptions originating from erroneous command invocation 223 | inside pipelines and MULTI/EXEC blocks. 224 | 225 | The old behavior (swallowing exceptions) could cause application bugs 226 | to go unnoticed. 227 | 228 | * Implement futures for assigning values inside pipelines and MULTI/EXEC 229 | blocks. Futures are assigned their value after the pipeline or 230 | MULTI/EXEC block has executed. 231 | 232 | ```ruby 233 | $redis.pipelined do 234 | @future = $redis.get "key" 235 | end 236 | 237 | puts @future.value 238 | ``` 239 | 240 | * Ruby 1.8.6 is officially not supported. 241 | 242 | * Support `ZCOUNT` in `Redis::Distributed` (Michael Dungan). 243 | 244 | * Pipelined commands now return the same replies as when called outside 245 | a pipeline. 246 | 247 | In the past, pipelined replies were returned without post-processing. 248 | 249 | * Support `SLOWLOG` command (Michael Bernstein). 250 | 251 | * Calling `SHUTDOWN` effectively disconnects the client (Stefan Kaes). 252 | 253 | * Basic support for mapping commands so that they can be renamed on the 254 | server. 255 | 256 | * Connecting using a URL now checks that a host is given. 257 | 258 | It's just a small sanity check, cf. #126 259 | 260 | * Support variadic commands introduced in Redis 2.4. 261 | 262 | # 2.2.2 263 | 264 | * Added method `Redis::Distributed#hsetnx`. 265 | 266 | # 2.2.1 267 | 268 | * Internal API: Client#call and family are now called with a single array 269 | argument, since splatting a large number of arguments (100K+) results in a 270 | stack overflow on 1.9.2. 271 | 272 | * The `INFO` command can optionally take a subcommand. When the subcommand is 273 | `COMMANDSTATS`, the client will properly format the returned statistics per 274 | command. Subcommands for `INFO` are available since Redis v2.3.0 (unstable). 275 | 276 | * Change `IO#syswrite` back to the buffered `IO#write` since some Rubies do 277 | short writes for large (1MB+) buffers and some don't (see issue #108). 278 | 279 | # 2.2.0 280 | 281 | * Added method `Redis#without_reconnect` that ensures the client will not try 282 | to reconnect when running the code inside the specified block. 283 | 284 | * Thread-safe by default. Thread safety can be explicitly disabled by passing 285 | `:thread_safe => false` as argument. 286 | 287 | * Commands called inside a MULTI/EXEC no longer raise error replies, since a 288 | successful EXEC means the commands inside the block were executed. 289 | 290 | * MULTI/EXEC blocks are pipelined. 291 | 292 | * Don't disconnect on error replies. 293 | 294 | * Use `IO#syswrite` instead of `IO#write` because write buffering is not 295 | necessary. 296 | 297 | * Connect to a unix socket by passing the `:path` option as argument. 298 | 299 | * The timeout value is coerced into a float, allowing sub-second timeouts. 300 | 301 | * Accept both `:with_scores` _and_ `:withscores` as argument to sorted set 302 | commands. 303 | 304 | * Use [hiredis](https://github.com/pietern/hiredis-rb) (v0.3 or higher) by 305 | requiring "redis/connection/hiredis". 306 | 307 | * Use [em-synchrony](https://github.com/igrigorik/em-synchrony) by requiring 308 | "redis/connection/synchrony". 309 | 310 | # 2.1.1 311 | 312 | See commit log. 313 | -------------------------------------------------------------------------------- /test/scanning_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path("helper", File.dirname(__FILE__)) 4 | 5 | unless defined?(Enumerator) 6 | Enumerator = Enumerable::Enumerator 7 | end 8 | 9 | class TestScanning < Test::Unit::TestCase 10 | 11 | include Helper::Client 12 | 13 | def test_scan_basic 14 | target_version "2.7.105" do 15 | r.debug :populate, 1000 16 | 17 | cursor = 0 18 | all_keys = [] 19 | loop { 20 | cursor, keys = r.scan cursor 21 | all_keys += keys 22 | break if cursor == "0" 23 | } 24 | 25 | assert_equal 1000, all_keys.uniq.size 26 | end 27 | end 28 | 29 | def test_scan_count 30 | target_version "2.7.105" do 31 | r.debug :populate, 1000 32 | 33 | cursor = 0 34 | all_keys = [] 35 | loop { 36 | cursor, keys = r.scan cursor, :count => 5 37 | all_keys += keys 38 | break if cursor == "0" 39 | } 40 | 41 | assert_equal 1000, all_keys.uniq.size 42 | end 43 | end 44 | 45 | def test_scan_match 46 | target_version "2.7.105" do 47 | r.debug :populate, 1000 48 | 49 | cursor = 0 50 | all_keys = [] 51 | loop { 52 | cursor, keys = r.scan cursor, :match => "key:1??" 53 | all_keys += keys 54 | break if cursor == "0" 55 | } 56 | 57 | assert_equal 100, all_keys.uniq.size 58 | end 59 | end 60 | 61 | def test_scan_each_enumerator 62 | target_version "2.7.105" do 63 | 64 | r.debug :populate, 1000 65 | 66 | scan_enumerator = r.scan_each 67 | assert_equal true, scan_enumerator.is_a?(::Enumerator) 68 | 69 | keys_from_scan = scan_enumerator.to_a.uniq 70 | all_keys = r.keys "*" 71 | 72 | assert all_keys.sort == keys_from_scan.sort 73 | end 74 | end 75 | 76 | def test_scan_each_enumerator_match 77 | target_version "2.7.105" do 78 | 79 | r.debug :populate, 1000 80 | 81 | keys_from_scan = r.scan_each(:match => "key:1??").to_a.uniq 82 | all_keys = r.keys "key:1??" 83 | 84 | assert all_keys.sort == keys_from_scan.sort 85 | end 86 | end 87 | 88 | def test_scan_each_block 89 | target_version "2.7.105" do 90 | 91 | r.debug :populate, 100 92 | 93 | keys_from_scan = [] 94 | r.scan_each {|key| 95 | keys_from_scan << key 96 | } 97 | 98 | all_keys = r.keys "*" 99 | 100 | assert all_keys.sort == keys_from_scan.uniq.sort 101 | end 102 | end 103 | 104 | def test_scan_each_block_match 105 | target_version "2.7.105" do 106 | 107 | r.debug :populate, 100 108 | 109 | keys_from_scan = [] 110 | r.scan_each(:match => "key:1?") {|key| 111 | keys_from_scan << key 112 | } 113 | 114 | all_keys = r.keys "key:1?" 115 | 116 | assert all_keys.sort == keys_from_scan.uniq.sort 117 | end 118 | end 119 | 120 | def test_sscan_with_encoding 121 | target_version "2.7.105" do 122 | [:intset, :hashtable].each do |enc| 123 | r.del "set" 124 | 125 | prefix = "" 126 | prefix = "ele:" if enc == :hashtable 127 | 128 | elements = [] 129 | 100.times { |j| elements << "#{prefix}#{j}" } 130 | 131 | r.sadd "set", elements 132 | 133 | assert_equal enc.to_s, r.object("encoding", "set") 134 | 135 | cursor = 0 136 | all_keys = [] 137 | loop { 138 | cursor, keys = r.sscan "set", cursor 139 | all_keys += keys 140 | break if cursor == "0" 141 | } 142 | 143 | assert_equal 100, all_keys.uniq.size 144 | end 145 | end 146 | end 147 | 148 | def test_sscan_each_enumerator 149 | target_version "2.7.105" do 150 | elements = [] 151 | 100.times { |j| elements << "ele:#{j}" } 152 | r.sadd "set", elements 153 | 154 | scan_enumerator = r.sscan_each("set") 155 | assert_equal true, scan_enumerator.is_a?(::Enumerator) 156 | 157 | keys_from_scan = scan_enumerator.to_a.uniq 158 | all_keys = r.smembers("set") 159 | 160 | assert all_keys.sort == keys_from_scan.sort 161 | end 162 | end 163 | 164 | def test_sscan_each_enumerator_match 165 | target_version "2.7.105" do 166 | elements = [] 167 | 100.times { |j| elements << "ele:#{j}" } 168 | r.sadd "set", elements 169 | 170 | keys_from_scan = r.sscan_each("set", :match => "ele:1?").to_a.uniq 171 | 172 | all_keys = r.smembers("set").grep(/^ele:1.$/) 173 | 174 | assert all_keys.sort == keys_from_scan.sort 175 | end 176 | end 177 | 178 | def test_sscan_each_enumerator_block 179 | target_version "2.7.105" do 180 | elements = [] 181 | 100.times { |j| elements << "ele:#{j}" } 182 | r.sadd "set", elements 183 | 184 | keys_from_scan = [] 185 | r.sscan_each("set") do |key| 186 | keys_from_scan << key 187 | end 188 | 189 | all_keys = r.smembers("set") 190 | 191 | assert all_keys.sort == keys_from_scan.uniq.sort 192 | end 193 | end 194 | 195 | def test_sscan_each_enumerator_block_match 196 | target_version "2.7.105" do 197 | elements = [] 198 | 100.times { |j| elements << "ele:#{j}" } 199 | r.sadd "set", elements 200 | 201 | keys_from_scan = [] 202 | r.sscan_each("set", :match => "ele:1?") do |key| 203 | keys_from_scan << key 204 | end 205 | 206 | all_keys = r.smembers("set").grep(/^ele:1.$/) 207 | 208 | assert all_keys.sort == keys_from_scan.uniq.sort 209 | end 210 | end 211 | 212 | def test_hscan_with_encoding 213 | target_version "2.7.105" do 214 | [:ziplist, :hashtable].each do |enc| 215 | r.del "set" 216 | 217 | count = 1000 218 | count = 30 if enc == :ziplist 219 | 220 | elements = [] 221 | count.times { |j| elements << "key:#{j}" << j.to_s } 222 | 223 | r.hmset "hash", *elements 224 | 225 | assert_equal enc.to_s, r.object("encoding", "hash") 226 | 227 | cursor = 0 228 | all_key_values = [] 229 | loop { 230 | cursor, key_values = r.hscan "hash", cursor 231 | all_key_values.concat key_values 232 | break if cursor == "0" 233 | } 234 | 235 | keys2 = [] 236 | all_key_values.each do |k, v| 237 | assert_equal "key:#{v}", k 238 | keys2 << k 239 | end 240 | 241 | assert_equal count, keys2.uniq.size 242 | end 243 | end 244 | end 245 | 246 | def test_hscan_each_enumerator 247 | target_version "2.7.105" do 248 | count = 1000 249 | elements = [] 250 | count.times { |j| elements << "key:#{j}" << j.to_s } 251 | r.hmset "hash", *elements 252 | 253 | scan_enumerator = r.hscan_each("hash") 254 | assert_equal true, scan_enumerator.is_a?(::Enumerator) 255 | 256 | keys_from_scan = scan_enumerator.to_a.uniq 257 | all_keys = r.hgetall("hash").to_a 258 | 259 | assert all_keys.sort == keys_from_scan.sort 260 | end 261 | end 262 | 263 | def test_hscan_each_enumerator_match 264 | target_version "2.7.105" do 265 | count = 100 266 | elements = [] 267 | count.times { |j| elements << "key:#{j}" << j.to_s } 268 | r.hmset "hash", *elements 269 | 270 | keys_from_scan = r.hscan_each("hash", :match => "key:1?").to_a.uniq 271 | all_keys = r.hgetall("hash").to_a.select{|k,v| k =~ /^key:1.$/} 272 | 273 | assert all_keys.sort == keys_from_scan.sort 274 | end 275 | end 276 | 277 | def test_hscan_each_block 278 | target_version "2.7.105" do 279 | count = 1000 280 | elements = [] 281 | count.times { |j| elements << "key:#{j}" << j.to_s } 282 | r.hmset "hash", *elements 283 | 284 | keys_from_scan = [] 285 | r.hscan_each("hash") do |field, value| 286 | keys_from_scan << [field, value] 287 | end 288 | all_keys = r.hgetall("hash").to_a 289 | 290 | assert all_keys.sort == keys_from_scan.uniq.sort 291 | end 292 | end 293 | 294 | def test_hscan_each_block_match 295 | target_version "2.7.105" do 296 | count = 1000 297 | elements = [] 298 | count.times { |j| elements << "key:#{j}" << j.to_s } 299 | r.hmset "hash", *elements 300 | 301 | keys_from_scan = [] 302 | r.hscan_each("hash", :match => "key:1?") do |field, value| 303 | keys_from_scan << [field, value] 304 | end 305 | all_keys = r.hgetall("hash").to_a.select{|k,v| k =~ /^key:1.$/} 306 | 307 | assert all_keys.sort == keys_from_scan.uniq.sort 308 | end 309 | end 310 | 311 | def test_zscan_with_encoding 312 | target_version "2.7.105" do 313 | [:ziplist, :skiplist].each do |enc| 314 | r.del "zset" 315 | 316 | count = 1000 317 | count = 30 if enc == :ziplist 318 | 319 | elements = [] 320 | count.times { |j| elements << j << "key:#{j}" } 321 | 322 | r.zadd "zset", elements 323 | 324 | assert_equal enc.to_s, r.object("encoding", "zset") 325 | 326 | cursor = 0 327 | all_key_scores = [] 328 | loop { 329 | cursor, key_scores = r.zscan "zset", cursor 330 | all_key_scores.concat key_scores 331 | break if cursor == "0" 332 | } 333 | 334 | keys2 = [] 335 | all_key_scores.each do |k, v| 336 | assert_equal true, v.is_a?(Float) 337 | assert_equal "key:#{Integer(v)}", k 338 | keys2 << k 339 | end 340 | 341 | assert_equal count, keys2.uniq.size 342 | end 343 | end 344 | end 345 | 346 | def test_zscan_each_enumerator 347 | target_version "2.7.105" do 348 | count = 1000 349 | elements = [] 350 | count.times { |j| elements << j << "key:#{j}" } 351 | r.zadd "zset", elements 352 | 353 | scan_enumerator = r.zscan_each "zset" 354 | assert_equal true, scan_enumerator.is_a?(::Enumerator) 355 | 356 | scores_from_scan = scan_enumerator.to_a.uniq 357 | member_scores = r.zrange("zset", 0, -1, :with_scores => true) 358 | 359 | assert member_scores.sort == scores_from_scan.sort 360 | end 361 | end 362 | 363 | def test_zscan_each_enumerator_match 364 | target_version "2.7.105" do 365 | count = 1000 366 | elements = [] 367 | count.times { |j| elements << j << "key:#{j}" } 368 | r.zadd "zset", elements 369 | 370 | scores_from_scan = r.zscan_each("zset", :match => "key:1??").to_a.uniq 371 | member_scores = r.zrange("zset", 0, -1, :with_scores => true) 372 | filtered_members = member_scores.select{|k,s| k =~ /^key:1..$/} 373 | 374 | assert filtered_members.sort == scores_from_scan.sort 375 | end 376 | end 377 | 378 | def test_zscan_each_block 379 | target_version "2.7.105" do 380 | count = 1000 381 | elements = [] 382 | count.times { |j| elements << j << "key:#{j}" } 383 | r.zadd "zset", elements 384 | 385 | scores_from_scan = [] 386 | r.zscan_each("zset") do |member, score| 387 | scores_from_scan << [member, score] 388 | end 389 | member_scores = r.zrange("zset", 0, -1, :with_scores => true) 390 | 391 | assert member_scores.sort == scores_from_scan.sort 392 | end 393 | end 394 | 395 | def test_zscan_each_block_match 396 | target_version "2.7.105" do 397 | count = 1000 398 | elements = [] 399 | count.times { |j| elements << j << "key:#{j}" } 400 | r.zadd "zset", elements 401 | 402 | scores_from_scan = [] 403 | r.zscan_each("zset", :match => "key:1??") do |member, score| 404 | scores_from_scan << [member, score] 405 | end 406 | member_scores = r.zrange("zset", 0, -1, :with_scores => true) 407 | filtered_members = member_scores.select{|k,s| k =~ /^key:1..$/} 408 | 409 | assert filtered_members.sort == scores_from_scan.sort 410 | end 411 | end 412 | 413 | end 414 | --------------------------------------------------------------------------------