pax_global_header00006660000000000000000000000064123542721370014520gustar00rootroot0000000000000052 comment=2b4d6b718a90c5ea527c4ca1c0dc6c07b40106c7 em-hiredis-0.3.0/000077500000000000000000000000001235427213700135465ustar00rootroot00000000000000em-hiredis-0.3.0/.gitignore000066400000000000000000000000611235427213700155330ustar00rootroot00000000000000*.gem .bundle Gemfile.lock pkg/* .DS_Store *.swp em-hiredis-0.3.0/.rspec000066400000000000000000000000111235427213700146530ustar00rootroot00000000000000--colour em-hiredis-0.3.0/CHANGELOG.md000066400000000000000000000022421235427213700153570ustar00rootroot00000000000000# Changelog ## 0.3.0 (2014-06-30) [NEW] Lua script support (see README for details). [NEW] `Client#reconnect!` method which disconnects, reconfigures, and reconnects. [CHANGED] Locking abstraction re-implemented using lua (safer and simpler) [mdpye]. [CHANGED] Hiredis dependency updated to 0.5.x ## 0.2.1 (2013-04-22) [NEW] Support for connecting to redis on a unix socket. [CHANGED] Redis error reply message now used as message for RedisError. ## 0.2.0 (2013-04-05) [NEW] Richer interface for pubsub (accessible via `client.pubsub`). See example in `examples/pubsub.rb`. [NEW] Better failure handling: * Clients now emit the following events: connected, reconnected, disconnected, reconnect_failed (passes the number of consecutive failures) * Client is considered failed after 4 consecutive failures * Fails all queued commands when client failed * Can now reconfiure and reconnect an exising client * Reconnect timeout can be configured (defaults to 0.5s) [NEW] Added `EM::Hiredis::Lock` and `EM::Hiredis::PersistentLock` [CHANGE] When a redis command fails, the errback is now always passed an `EM::Hiredis::Error`. [FIX] Fixed info parsing for Redis 2.6 em-hiredis-0.3.0/Gemfile000066400000000000000000000000461235427213700150410ustar00rootroot00000000000000source "http://rubygems.org" gemspec em-hiredis-0.3.0/LICENCE000066400000000000000000000020461235427213700145350ustar00rootroot00000000000000Copyright (C) 2011 by Martyn Loughran Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. em-hiredis-0.3.0/README.md000066400000000000000000000142711235427213700150320ustar00rootroot00000000000000# em-hiredis ## What A Redis client for EventMachine designed to be fast and simple. ## Why I wanted a client which: * used the C hiredis library to parse redis replies * had a convenient API for pubsub * exposed the state of the underlying redis connections so that custom failover logic could be written outside the library Also, is no longer maintained. ## Getting started Connect to redis: require 'em-hiredis' redis = EM::Hiredis.connect Or, connect to redis with a redis URL (for a different host, port, password, DB) redis = EM::Hiredis.connect("redis://:secretpassword@example.com:9000/4") Commands may be sent immediately. Any commands sent while connecting to redis will be queued. All redis commands are available without any remapping of names, and return a deferrable redis.set('foo', 'bar').callback { redis.get('foo').callback { |value| p [:returned, value] } } If redis replies with an error (for example you called a hash operation against a set or the database is full), or if the redis connection disconnects before the command returns, the deferrable will fail. redis.sadd('aset', 'member').callback { response_deferrable = redis.hget('aset', 'member') response_deferrable.errback { |e| p e # => # p e.redis_error # => # } } As a shortcut, if you're only interested in binding to the success case you can simply provide a block to any command redis.get('foo') { |value| p [:returned, value] } ## Understanding the state of the connection When a connection to redis server closes, a `:disconnected` event will be emitted and the connection will be immediately reconnect. If the connection reconnects a `:connected` event will be emitted. If a reconnect fails to connect, a `:reconnect_failed` event will be emitted (rather than `:disconnected`) with the number of consecutive failures, and the connection will be retried after a timeout (defaults to 0.5s, can be set via `EM::Hiredis.reconnect_timeout=`). If a client fails to reconnect 4 consecutive times then a `:failed` event will be emitted, and any queued redis commands will be failed (otherwise they would be queued forever waiting for a reconnect). ## Pubsub The way pubsub works in redis is that once a subscribe has been made on a connection, it's only possible to send (p)subscribe or (p)unsubscribe commands on that connection. The connection will also receive messages which are not replies to commands. The regular `EM::Hiredis::Client` no longer understands pubsub messages - this logic has been moved to `EM::Hiredis::PubsubClient`. The pubsub client can either be initialized directly (see code) or you can get one connected to the same redis server by calling `#pubsub` on an existing `EM::Hiredis::Client` instance. Pubsub can either be used in em-hiredis in a close-to-the-metal fashion, or you can use the convenience functionality for binding blocks to subscriptions if you prefer (recommended). ### Close to the metal pubsub interface Basically just bind to `:message` and `:pmessage` events: # Create two connections, one will be used for subscribing redis = EM::Hiredis.connect pubsub = redis.pubsub pubsub.subscribe('bar.0').callback { puts "Subscribed" } pubsub.psubscribe('bar.*') pubsub.on(:message) { |channel, message| p [:message, channel, message] } pubsub.on(:pmessage) { |key, channel, message| p [:pmessage, key, channel, message] } EM.add_periodic_timer(1) { redis.publish("bar.#{rand(2)}", "hello").errback { |e| p [:publisherror, e] } } ### Richer pubsub interface If you pass a block to `subscribe` or `psubscribe`, the passed block will be called whenever a message arrives on that subscription: redis = EM::Hiredis.connect puts "Subscribing" redis.pubsub.subscribe("foo") { |msg| p [:sub1, msg] } redis.pubsub.psubscribe("f*") { |msg| p [:sub2, msg] } EM.add_periodic_timer(1) { redis.publish("foo", "Hello") } EM.add_timer(5) { puts "Unsubscribing sub1" redis.pubsub.unsubscribe("foo") } It's possible to subscribe to the same channel multiple time and just unsubscribe a single callback using `unsubscribe_proc` or `punsubscribe_proc`. ## Lua You can of course call EVAL or EVALSHA directly; the following is a higher-level API. Registering a named command on a redis client defines a ruby method with the given name on the client: redis.register_script(:multiply, <<-END) return redis.call('get', KEYS[1]) * ARGV[1] END The method can be called in a very similar way to any other redis command; the only difference is that the first argument must be an array of keys, and the second (optional) an array of values. # Multiplies the value at key foo by 2 redis.multiply(['foo'], [2]).callback { ... } Lua commands are submitted to redis using EVALSHA for efficiency. If redis replies with a NOSCRIPT error, the command is automatically re-submitted with EVAL; this is totally transparent to your code and the intermediate 'failure' will not be passed to your errback. You may register scripts globally, in which case they will be available to all clients: EM::Hiredis::Client.register_script(:multiply, <<-END) return redis.call('get', KEYS[1]) * ARGV[1] END As a final convenience, it is possible to load all lua scripts from a directory automatically. All `.lua` files in the directory will be registered, and named according to filename (so a file called `sum.lua` becomes available as `redis.sum(...)`). EM::Hiredis::Client.load_scripts_from('./lua_scripts') For examples see `examples/lua.rb` or `lib/em-hiredis/lock_lua`. ## Developing You need bundler and a local redis server running on port 6379 to run the test suite. # WARNING: The tests call flushdb on db 9 - this clears all keys! bundle exec rake Run an individual spec: bundle exec rspec spec/redis_commands_spec.rb Many thanks to the em-redis gem for getting this gem bootstrapped with some tests. em-hiredis-0.3.0/Rakefile000066400000000000000000000002751235427213700152170ustar00rootroot00000000000000require 'bundler' Bundler::GemHelper.install_tasks require 'rspec/core/rake_task' desc 'Default: run specs.' task :default => :spec desc "Run specs" RSpec::Core::RakeTask.new do |t| end em-hiredis-0.3.0/em-hiredis.gemspec000066400000000000000000000017651235427213700171520ustar00rootroot00000000000000# -*- encoding: utf-8 -*- $:.push File.expand_path("../lib", __FILE__) require "em-hiredis/version" Gem::Specification.new do |s| s.name = "em-hiredis" s.version = EventMachine::Hiredis::VERSION s.platform = Gem::Platform::RUBY s.authors = ["Martyn Loughran"] s.email = ["me@mloughran.com"] s.homepage = "http://github.com/mloughran/em-hiredis" s.summary = %q{Eventmachine redis client} s.description = %q{Eventmachine redis client using hiredis native parser} s.add_dependency 'eventmachine', '~> 1.0' s.add_dependency 'hiredis', '~> 0.5.0' s.add_development_dependency 'em-spec', '~> 0.2.5' s.add_development_dependency 'rspec', '~> 2.6.0' s.add_development_dependency 'rake' s.rubyforge_project = "em-hiredis" s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } s.require_paths = ["lib"] end em-hiredis-0.3.0/examples/000077500000000000000000000000001235427213700153645ustar00rootroot00000000000000em-hiredis-0.3.0/examples/getting_started.rb000066400000000000000000000005711235427213700211030ustar00rootroot00000000000000$:.unshift(File.expand_path('../../lib', __FILE__)) require 'em-hiredis' EM.run { redis = EM::Hiredis.connect redis.sadd('aset', 'member').callback { response_deferrable = redis.hget('aset', 'member') response_deferrable.errback { |e| p e # => # p e.redis_error } } } em-hiredis-0.3.0/examples/lua/000077500000000000000000000000001235427213700161455ustar00rootroot00000000000000em-hiredis-0.3.0/examples/lua/sum.lua000066400000000000000000000001301235427213700174460ustar00rootroot00000000000000local a = redis.call('get', KEYS[1]) local b = redis.call('get', KEYS[2]) return a + b em-hiredis-0.3.0/examples/lua_example.rb000066400000000000000000000015441235427213700202110ustar00rootroot00000000000000$:.unshift(File.expand_path('../../lib', __FILE__)) require 'em-hiredis' EM.run do scripts_dir = File.expand_path("../lua", __FILE__) EM::Hiredis::Client.load_scripts_from(scripts_dir) redis = EM::Hiredis.connect redis.register_script(:multiply, <<-END) return redis.call('get', KEYS[1]) * ARGV[1] END redis.set('foo', 42).callback { redis.set('bar', 8).callback { # Multiply is defined above. # It is passed one key and one argument. redis.multiply(['foo'], [2]).callback { |v| puts "Multiply returned: #{v}" }.errback { |e| puts "Multiply error: #{e}" } # Sum is a lua script defined in sum.lua. # It is passed two keys. redis.sum(['foo', 'bar']).callback { |sum| puts "Sum returned: #{sum}" }.errback { |e| puts "Sum error: #{e}" } } } end em-hiredis-0.3.0/examples/pubsub_basics.rb000066400000000000000000000006571235427213700205450ustar00rootroot00000000000000$:.unshift(File.expand_path('../../lib', __FILE__)) require 'em-hiredis' EM.run { redis = EM::Hiredis.connect puts "Subscribing" redis.pubsub.subscribe("foo") { |msg| p [:sub1, msg] } redis.pubsub.psubscribe("f*") { |msg| p [:sub2, msg] } EM.add_periodic_timer(1) { redis.publish("foo", "Hello") } EM.add_timer(5) { puts "Unsubscribing sub1" redis.pubsub.unsubscribe("foo") } } em-hiredis-0.3.0/examples/pubsub_more.rb000066400000000000000000000027171235427213700202420ustar00rootroot00000000000000$:.unshift(File.expand_path('../../lib', __FILE__)) require 'em-hiredis' EM.run { redis = EM::Hiredis.connect # If you pass a block to subscribe it will be called whenever a message # is received on this channel redis.pubsub.subscribe('foo') { |message| puts "Block received #{message}" } # You can also pass any other object which responds to call if you wish callback = Proc.new { |message| "Proc received #{message}" } df = redis.pubsub.subscribe('foo', callback) # All calls return a deferrable df.callback { |reply| p [:subscription_succeeded, reply] } # Passing such an object is useful if you want to unsubscribe redis.pubsub.unsubscribe_proc('foo', callback) # Or if you want to call a method on a certain object class Thing def receive_message(message) puts "Thing received #{message}" end end redis.pubsub.subscribe('bar', Thing.new.method(:receive_message)) # You can also get all the following raw events: # message pmessage subscribe unsubscribe psubscribe punsubscribe redis.pubsub.on(:message) { |channel, message| p [:message_received, channel, message] } redis.pubsub.on(:unsubscribe) { |channel, remaining_subscriptions| p [:unsubscribe_happened, channel, remaining_subscriptions] } EM.add_timer(1) { # You can also unsubscribe completely from a channel redis.pubsub.unsubscribe('foo') # Publishing events redis.publish('bar', 'Hello') } } em-hiredis-0.3.0/examples/pubsub_raw.rb000066400000000000000000000011211235427213700200550ustar00rootroot00000000000000$:.unshift(File.expand_path('../../lib', __FILE__)) require 'em-hiredis' EM.run { # Create two connections, one will be used for subscribing redis = EM::Hiredis.connect pubsub = redis.pubsub pubsub.subscribe('bar.0').callback { puts "Subscribed" } pubsub.psubscribe('bar.*') pubsub.on(:message) { |channel, message| p [:message, channel, message] } pubsub.on(:pmessage) { |key, channel, message| p [:pmessage, key, channel, message] } EM.add_periodic_timer(1) { redis.publish("bar.#{rand(2)}", "hello").errback { |e| p [:publisherror, e] } } } em-hiredis-0.3.0/lib/000077500000000000000000000000001235427213700143145ustar00rootroot00000000000000em-hiredis-0.3.0/lib/em-hiredis.rb000066400000000000000000000032611235427213700166710ustar00rootroot00000000000000require 'eventmachine' module EventMachine module Hiredis # All em-hiredis errors should descend from EM::Hiredis::Error class Error < RuntimeError; end # An error reply from Redis. The actual error retuned by ::Hiredis will be # wrapped in the redis_error accessor. class RedisError < Error attr_accessor :redis_error end class << self attr_accessor :reconnect_timeout end self.reconnect_timeout = 0.5 def self.setup(uri = nil) uri = uri || ENV["REDIS_URL"] || "redis://127.0.0.1:6379/0" client = Client.new client.configure(uri) client end # Connects to redis and returns a client instance # # Will connect in preference order to the provided uri, the REDIS_URL # environment variable, or localhost:6379 # # TCP connections are supported via redis://:password@host:port/db (only # host and port components are required) # # Unix socket uris are supported, e.g. unix:///tmp/redis.sock, however # it's not possible to set the db or password - use initialize instead in # this case def self.connect(uri = nil) client = setup(uri) client.connect client end def self.logger=(logger) @@logger = logger end def self.logger @@logger ||= begin require 'logger' log = Logger.new(STDOUT) log.level = Logger::WARN log end end autoload :Lock, 'em-hiredis/lock' autoload :PersistentLock, 'em-hiredis/persistent_lock' end end require 'em-hiredis/event_emitter' require 'em-hiredis/connection' require 'em-hiredis/base_client' require 'em-hiredis/client' require 'em-hiredis/pubsub_client' em-hiredis-0.3.0/lib/em-hiredis/000077500000000000000000000000001235427213700163425ustar00rootroot00000000000000em-hiredis-0.3.0/lib/em-hiredis/base_client.rb000066400000000000000000000137341235427213700211470ustar00rootroot00000000000000require 'uri' module EventMachine::Hiredis # Emits the following events # # * :connected - on successful connection or reconnection # * :reconnected - on successful reconnection # * :disconnected - no longer connected, when previously in connected state # * :reconnect_failed(failure_number) - a reconnect attempt failed # This event is passed number of failures so far (1,2,3...) # * :monitor # class BaseClient include EventEmitter include EM::Deferrable attr_reader :host, :port, :password, :db def initialize(host = 'localhost', port = 6379, password = nil, db = nil) @host, @port, @password, @db = host, port, password, db @defs = [] @command_queue = [] @reconnect_failed_count = 0 @reconnect_timer = nil @failed = false self.on(:failed) { @failed = true @command_queue.each do |df, _, _| df.fail(Error.new("Redis connection in failed state")) end @command_queue = [] } end # Configure the redis connection to use # # In usual operation, the uri should be passed to initialize. This method # is useful for example when failing over to a slave connection at runtime # def configure(uri_string) uri = URI(uri_string) if uri.scheme == "unix" @host = uri.path @port = nil else @host = uri.host @port = uri.port @password = uri.password path = uri.path[1..-1] @db = path.to_i # Empty path => 0 end end # Disconnect then reconnect the redis connection. # # Pass optional uri - e.g. to connect to a different redis server. # Any pending redis commands will be failed, but during the reconnection # new commands will be queued and sent after connected. # def reconnect!(new_uri = nil) @connection.close_connection configure(new_uri) if new_uri @auto_reconnect = true EM.next_tick { reconnect_connection } end def connect @auto_reconnect = true @connection = EM.connect(@host, @port, Connection, @host, @port) @connection.on(:closed) do if @connected @defs.each { |d| d.fail(Error.new("Redis disconnected")) } @defs = [] @deferred_status = nil @connected = false if @auto_reconnect # Next tick avoids reconnecting after for example EM.stop EM.next_tick { reconnect } end emit(:disconnected) EM::Hiredis.logger.info("#{@connection} Disconnected") else if @auto_reconnect @reconnect_failed_count += 1 @reconnect_timer = EM.add_timer(EM::Hiredis.reconnect_timeout) { @reconnect_timer = nil reconnect } emit(:reconnect_failed, @reconnect_failed_count) EM::Hiredis.logger.info("#{@connection} Reconnect failed") if @reconnect_failed_count >= 4 emit(:failed) self.fail(Error.new("Could not connect after 4 attempts")) end end end end @connection.on(:connected) do @connected = true @reconnect_failed_count = 0 @failed = false select(@db) unless @db == 0 auth(@password) if @password @command_queue.each do |df, command, args| @connection.send_command(command, args) @defs.push(df) end @command_queue = [] emit(:connected) EM::Hiredis.logger.info("#{@connection} Connected") succeed if @reconnecting @reconnecting = false emit(:reconnected) end end @connection.on(:message) do |reply| if RuntimeError === reply raise "Replies out of sync: #{reply.inspect}" if @defs.empty? deferred = @defs.shift error = RedisError.new(reply.message) error.redis_error = reply deferred.fail(error) if deferred else handle_reply(reply) end end @connected = false @reconnecting = false return self end # Indicates that commands have been sent to redis but a reply has not yet # been received # # This can be useful for example to avoid stopping the # eventmachine reactor while there are outstanding commands # def pending_commands? @connected && @defs.size > 0 end def connected? @connected end def select(db, &blk) @db = db method_missing(:select, db, &blk) end def auth(password, &blk) @password = password method_missing(:auth, password, &blk) end def close_connection EM.cancel_timer(@reconnect_timer) if @reconnect_timer @auto_reconnect = false @connection.close_connection_after_writing end # Note: This method doesn't disconnect if already connected. You probably # want to use `reconnect!` def reconnect_connection @auto_reconnect = true EM.cancel_timer(@reconnect_timer) if @reconnect_timer reconnect end private def method_missing(sym, *args) deferred = EM::DefaultDeferrable.new # Shortcut for defining the callback case with just a block deferred.callback { |result| yield(result) } if block_given? if @connected @connection.send_command(sym, args) @defs.push(deferred) elsif @failed deferred.fail(Error.new("Redis connection in failed state")) else @command_queue << [deferred, sym, args] end deferred end def reconnect @reconnecting = true @connection.reconnect @host, @port EM::Hiredis.logger.info("#{@connection} Reconnecting") end def handle_reply(reply) if @defs.empty? if @monitoring emit(:monitor, reply) else raise "Replies out of sync: #{reply.inspect}" end else deferred = @defs.shift deferred.succeed(reply) if deferred end end end end em-hiredis-0.3.0/lib/em-hiredis/client.rb000066400000000000000000000055421235427213700201530ustar00rootroot00000000000000require 'digest/sha1' module EventMachine::Hiredis class Client < BaseClient def self.connect(host = 'localhost', port = 6379) new(host, port).connect end def self.load_scripts_from(dir) Dir.glob("#{dir}/*.lua").each do |f| name = Regexp.new(/([^\/]*)\.lua$/).match(f)[1] lua = File.open(f, 'r').read EM::Hiredis.logger.debug { "Registering script: #{name}" } EM::Hiredis::Client.register_script(name, lua) end end def self.register_script(name, lua) sha = Digest::SHA1.hexdigest(lua) self.send(:define_method, name.to_sym) { |keys, args=[]| eval_script(lua, sha, keys, args) } end def register_script(name, lua) sha = Digest::SHA1.hexdigest(lua) singleton = class << self; self end singleton.send(:define_method, name.to_sym) { |keys, args=[]| eval_script(lua, sha, keys, args) } end def eval_script(lua, lua_sha, keys, args) df = EM::DefaultDeferrable.new method_missing(:evalsha, lua_sha, keys.size, *keys, *args).callback( &df.method(:succeed) ).errback { |e| if e.kind_of?(RedisError) && e.redis_error.message.start_with?("NOSCRIPT") self.eval(lua, keys.size, *keys, *args) .callback(&df.method(:succeed)).errback(&df.method(:fail)) else df.fail(e) end } df end def monitor(&blk) @monitoring = true method_missing(:monitor, &blk) end def info df = method_missing(:info) df.callback { |response| info = {} response.each_line do |line| key, value = line.split(":", 2) info[key.to_sym] = value.chomp if value end df.succeed(info) } df.callback { |info| yield info } if block_given? df end def info_commandstats(&blk) hash_processor = lambda do |response| commands = {} response.each_line do |line| command, data = line.split(':') if data c = commands[command.sub('cmdstat_', '').to_sym] = {} data.split(',').each do |d| k, v = d.split('=') c[k.to_sym] = v =~ /\./ ? v.to_f : v.to_i end end end blk.call(commands) end method_missing(:info, 'commandstats', &hash_processor) end # Gives access to a richer interface for pubsub subscriptions on a # separate redis connection # def pubsub @pubsub ||= begin PubsubClient.new(@host, @port, @password, @db).connect end end def subscribe(*channels) raise "Use pubsub client" end def unsubscribe(*channels) raise "Use pubsub client" end def psubscribe(channel) raise "Use pubsub client" end def punsubscribe(channel) raise "Use pubsub client" end end end em-hiredis-0.3.0/lib/em-hiredis/connection.rb000066400000000000000000000023651235427213700210340ustar00rootroot00000000000000require 'hiredis/reader' module EventMachine::Hiredis class Connection < EM::Connection include EventMachine::Hiredis::EventEmitter def initialize(host, port) super @host, @port = host, port @name = "[em-hiredis #{@host}:#{@port}]" end def reconnect(host, port) super @host, @port = host, port end def connection_completed @reader = ::Hiredis::Reader.new emit(:connected) end def receive_data(data) @reader.feed(data) until (reply = @reader.gets) == false emit(:message, reply) end end def unbind emit(:closed) end def send_command(command, args) send_data(command(command, *args)) end def to_s @name end protected COMMAND_DELIMITER = "\r\n" def command(*args) command = [] command << "*#{args.size}" args.each do |arg| arg = arg.to_s command << "$#{string_size arg}" command << arg end command.join(COMMAND_DELIMITER) + COMMAND_DELIMITER end if "".respond_to?(:bytesize) def string_size(string) string.to_s.bytesize end else def string_size(string) string.to_s.size end end end end em-hiredis-0.3.0/lib/em-hiredis/event_emitter.rb000066400000000000000000000010331235427213700215360ustar00rootroot00000000000000module EventMachine::Hiredis module EventEmitter def on(event, &listener) _listeners[event] << listener end def emit(event, *args) _listeners[event].each { |l| l.call(*args) } end def remove_listener(event, &listener) _listeners[event].delete(listener) end def remove_all_listeners(event) _listeners.delete(event) end def listeners(event) _listeners[event] end private def _listeners @_listeners ||= Hash.new { |h,k| h[k] = [] } end end end em-hiredis-0.3.0/lib/em-hiredis/lock.rb000066400000000000000000000052161235427213700176230ustar00rootroot00000000000000require 'securerandom' module EM::Hiredis # Cross-process re-entrant lock, backed by redis class Lock EM::Hiredis::Client.load_scripts_from(File.expand_path("../lock_lua", __FILE__)) # Register a callback which will be called 1s before the lock expires # This is an informational callback, there is no hard guarantee on the timing # of its invocation because the callback firing and lock key expiry are handled # by different clocks (the client process and redis server respectively) def onexpire(&blk); @onexpire = blk; end def initialize(redis, key, timeout) unless timeout.kind_of?(Fixnum) && timeout >= 1 raise "Timeout must be an integer and >= 1s" end @redis, @key, @timeout = redis, key, timeout @token = SecureRandom.hex end # Acquire the lock # # This is a re-entrant lock, re-acquiring will succeed and extend the timeout # # Returns a deferrable which either succeeds if the lock can be acquired, or fails if it cannot. def acquire df = EM::DefaultDeferrable.new @redis.lock_acquire([@key], [@token, @timeout]).callback { |success| if (success) EM::Hiredis.logger.debug "#{to_s} acquired" EM.cancel_timer(@expire_timer) if @expire_timer @expire_timer = EM.add_timer(@timeout - 1) { EM::Hiredis.logger.debug "#{to_s} Expires in 1s" @onexpire.call if @onexpire } df.succeed else EM::Hiredis.logger.debug "#{to_s} failed to acquire" df.fail("Lock is not available") end }.errback { |e| EM::Hiredis.logger.error "#{to_s} Error acquiring lock #{e}" df.fail(e) } df end # Release the lock # # Returns a deferrable def unlock EM.cancel_timer(@expire_timer) if @expire_timer df = EM::DefaultDeferrable.new @redis.lock_release([@key], [@token]).callback { |keys_removed| if keys_removed > 0 EM::Hiredis.logger.debug "#{to_s} released" df.succeed else EM::Hiredis.logger.debug "#{to_s} could not release, not held" df.fail("Cannot release a lock we do not hold") end }.errback { |e| EM::Hiredis.logger.error "#{to_s} Error releasing lock #{e}" df.fail(e) } df end # This should not be used in normal operation. # Force clear without regard to who owns the lock. def clear EM::Hiredis.logger.warn "#{to_s} Force clearing lock (unsafe)" EM.cancel_timer(@expire_timer) if @expire_timer @redis.del(@key) end def to_s "[lock #{@key}]" end end end em-hiredis-0.3.0/lib/em-hiredis/lock_lua/000077500000000000000000000000001235427213700201335ustar00rootroot00000000000000em-hiredis-0.3.0/lib/em-hiredis/lock_lua/lock_acquire.lua000066400000000000000000000007641235427213700233060ustar00rootroot00000000000000-- Set key to token with expiry of timeout, if: -- - It doesn't exist -- - It exists and already has value of token (further set extends timeout) -- Used to implement a re-entrant lock. local key = KEYS[1] local token = ARGV[1] local timeout = ARGV[2] local value = redis.call('get', key) if value == token or not value then -- Great, either we hold the lock or it's free for us to take return redis.call('setex', key, timeout, token) else -- Someone else has it return false end em-hiredis-0.3.0/lib/em-hiredis/lock_lua/lock_release.lua000066400000000000000000000003071235427213700232660ustar00rootroot00000000000000-- Deletes a key only if it has the value supplied as token local key = KEYS[1] local token = ARGV[1] if redis.call('get', key) == token then return redis.call('del', key) else return 0 end em-hiredis-0.3.0/lib/em-hiredis/persistent_lock.rb000066400000000000000000000044651235427213700221100ustar00rootroot00000000000000module EM::Hiredis # A lock that automatically re-acquires a lock before it loses it # # The lock is configured with the following two parameters # # :lock_timeout - Specifies how long each lock is acquired for. Setting # this low means that locks need to be re-acquired very often, but a long # timout means that a process that fails without cleaning up after itself # (i.e. without releasing it's underlying lock) will block the anther # process from picking up this lock # replaced for a long while # :retry_interval - Specifies how frequently to retry acquiring the lock in # the case that the lock is held by another process, or there's an error # communicating with redis # class PersistentLock def onlocked(&blk); @onlocked = blk; self; end def onunlocked(&blk); @onunlocked = blk; self; end def initialize(redis, key, options = {}) @redis, @key = redis, key @timeout = options[:lock_timeout] || 100 @retry_timeout = options[:retry_interval] || 60 @lock = EM::Hiredis::Lock.new(redis, key, @timeout) @locked = false EM.next_tick { @running = true acquire } end # Acquire the lock (called automatically by initialize) def acquire return unless @running @lock.acquire.callback { if !@locked @onlocked.call if @onlocked @locked = true end # Re-acquire lock near the end of the period @extend_timer = EM.add_timer(@timeout.to_f * 2 / 3) { acquire() } }.errback { |e| if @locked # We were previously locked @onunlocked.call if @onunlocked @locked = false end if e.kind_of?(EM::Hiredis::RedisError) err = e.redis_error EM::Hiredis.logger.warn "Unexpected error acquiring #{@lock} #{err}" end @retry_timer = EM.add_timer(@retry_timeout) { acquire() unless @locked } } end def stop @running = false EM.cancel_timer(@extend_timer) if @extend_timer EM.cancel_timer(@retry_timer) if @retry_timer if @locked # We were previously locked @onunlocked.call if @onunlocked @locked = false end @lock.unlock end def locked? @locked end end end em-hiredis-0.3.0/lib/em-hiredis/pubsub_client.rb000066400000000000000000000133151235427213700215300ustar00rootroot00000000000000module EventMachine::Hiredis class PubsubClient < BaseClient PUBSUB_MESSAGES = %w{message pmessage subscribe unsubscribe psubscribe punsubscribe}.freeze def initialize(host='localhost', port='6379', password=nil, db=nil) @subs, @psubs = [], [] @pubsub_defs = Hash.new { |h,k| h[k] = [] } super end def connect @sub_callbacks = Hash.new { |h, k| h[k] = [] } @psub_callbacks = Hash.new { |h, k| h[k] = [] } # Resubsubscribe to channels on reconnect on(:reconnected) { raw_send_command(:subscribe, @subs) if @subs.any? raw_send_command(:psubscribe, @psubs) if @psubs.any? } super end # Subscribe to a pubsub channel # # If an optional proc / block is provided then it will be called when a # message is received on this channel # # @return [Deferrable] Redis subscribe call # def subscribe(channel, proc = nil, &block) if cb = proc || block @sub_callbacks[channel] << cb end @subs << channel raw_send_command(:subscribe, [channel]) return pubsub_deferrable(channel) end # Unsubscribe all callbacks for a given channel # # @return [Deferrable] Redis unsubscribe call # def unsubscribe(channel) @sub_callbacks.delete(channel) @subs.delete(channel) raw_send_command(:unsubscribe, [channel]) return pubsub_deferrable(channel) end # Unsubscribe a given callback from a channel. Will unsubscribe from redis # if there are no remaining subscriptions on this channel # # @return [Deferrable] Succeeds when the unsubscribe has completed or # fails if callback could not be found. Note that success may happen # immediately in the case that there are other callbacks for the same # channel (and therefore no unsubscription from redis is necessary) # def unsubscribe_proc(channel, proc) df = EM::DefaultDeferrable.new if @sub_callbacks[channel].delete(proc) if @sub_callbacks[channel].any? # Succeed deferrable immediately - no need to unsubscribe df.succeed else unsubscribe(channel).callback { |_| df.succeed } end else df.fail end return df end # Pattern subscribe to a pubsub channel # # If an optional proc / block is provided then it will be called (with the # channel name and message) when a message is received on a matching # channel # # @return [Deferrable] Redis psubscribe call # def psubscribe(pattern, proc = nil, &block) if cb = proc || block @psub_callbacks[pattern] << cb end @psubs << pattern raw_send_command(:psubscribe, [pattern]) return pubsub_deferrable(pattern) end # Pattern unsubscribe all callbacks for a given pattern # # @return [Deferrable] Redis punsubscribe call # def punsubscribe(pattern) @psub_callbacks.delete(pattern) @psubs.delete(pattern) raw_send_command(:punsubscribe, [pattern]) return pubsub_deferrable(pattern) end # Unsubscribe a given callback from a pattern. Will unsubscribe from redis # if there are no remaining subscriptions on this pattern # # @return [Deferrable] Succeeds when the punsubscribe has completed or # fails if callback could not be found. Note that success may happen # immediately in the case that there are other callbacks for the same # pattern (and therefore no punsubscription from redis is necessary) # def punsubscribe_proc(pattern, proc) df = EM::DefaultDeferrable.new if @psub_callbacks[pattern].delete(proc) if @psub_callbacks[pattern].any? # Succeed deferrable immediately - no need to punsubscribe df.succeed else punsubscribe(pattern).callback { |_| df.succeed } end else df.fail end return df end private # Send a command to redis without adding a deferrable for it. This is # useful for commands for which replies work or need to be treated # differently def raw_send_command(sym, args) if @connected @connection.send_command(sym, args) else callback do @connection.send_command(sym, args) end end return nil end def pubsub_deferrable(channel) df = EM::DefaultDeferrable.new @pubsub_defs[channel].push(df) df end def handle_reply(reply) if reply && PUBSUB_MESSAGES.include?(reply[0]) # reply can be nil # Note: pmessage is the only message with 4 arguments kind, subscription, d1, d2 = *reply case kind.to_sym when :message if @sub_callbacks.has_key?(subscription) @sub_callbacks[subscription].each { |cb| cb.call(d1) } end # Arguments are channel, message payload emit(:message, subscription, d1) when :pmessage if @psub_callbacks.has_key?(subscription) @psub_callbacks[subscription].each { |cb| cb.call(d1, d2) } end # Arguments are original pattern, channel, message payload emit(:pmessage, subscription, d1, d2) else if @pubsub_defs[subscription].any? df = @pubsub_defs[subscription].shift df.succeed(d1) # Cleanup empty arrays if @pubsub_defs[subscription].empty? @pubsub_defs.delete(subscription) end end # Also emit the event, as an alternative to using the deferrables emit(kind.to_sym, subscription, d1) end else super end end end end em-hiredis-0.3.0/lib/em-hiredis/version.rb000066400000000000000000000001051235427213700203500ustar00rootroot00000000000000module EventMachine module Hiredis VERSION = "0.3.0" end end em-hiredis-0.3.0/spec/000077500000000000000000000000001235427213700145005ustar00rootroot00000000000000em-hiredis-0.3.0/spec/base_client_spec.rb000066400000000000000000000061421235427213700203120ustar00rootroot00000000000000require 'spec_helper' describe EM::Hiredis::BaseClient do it "should be able to connect to redis (required for all tests!)" do em { redis = EM::Hiredis.connect redis.callback { done } redis.errback { puts "CHECK THAT THE REDIS SERVER IS RUNNING ON PORT 6379" fail } } end it "should emit an event on reconnect failure, with the retry count" do # Assumes there is no redis server on 9999 connect(1, "redis://localhost:9999/") do |redis| expected = 1 redis.on(:reconnect_failed) { |count| count.should == expected expected += 1 done if expected == 3 } end end it "should emit disconnected when the connection closes" do connect do |redis| redis.on(:disconnected) { done } redis.close_connection end end it "should fail the client deferrable after 4 unsuccessful attempts" do connect(1, "redis://localhost:9999/") do |redis| events = [] redis.on(:reconnect_failed) { |count| events << count } redis.errback { |error| error.class.should == EM::Hiredis::Error error.message.should == 'Could not connect after 4 attempts' events.should == [1,2,3,4] done } end end it "should fail commands immediately when in failed state" do connect(1, "redis://localhost:9999/") do |redis| redis.fail redis.get('foo').errback { |error| error.class.should == EM::Hiredis::Error error.message.should == 'Redis connection in failed state' done } end end it "should fail queued commands when entering failed state" do connect(1, "redis://localhost:9999/") do |redis| redis.get('foo').errback { |error| error.class.should == EM::Hiredis::Error error.message.should == 'Redis connection in failed state' done } redis.fail end end it "should allow reconfiguring the client at runtime" do connect(1, "redis://localhost:9999/") do |redis| redis.on(:reconnect_failed) { redis.configure("redis://localhost:6379/9") redis.info { done } } end end it "should allow connection to be reconnected" do connect do |redis| redis.on(:reconnected) { done } # Wait for first connection to complete redis.callback { redis.reconnect_connection } end end it "should wrap error responses returned by redis" do connect do |redis| redis.sadd('foo', 'bar') { df = redis.get('foo') df.callback { fail "Should have received error response from redis" } df.errback { |e| e.class.should == EM::Hiredis::RedisError e.should be_kind_of(EM::Hiredis::Error) msg = "WRONGTYPE Operation against a key holding the wrong kind of value" e.message.should == msg # This is the wrapped error from redis: e.redis_error.class.should == RuntimeError e.redis_error.message.should == msg done } } end end end em-hiredis-0.3.0/spec/connection_spec.rb000066400000000000000000000025501235427213700202000ustar00rootroot00000000000000require 'spec_helper' describe EventMachine::Hiredis, "connecting" do let(:replies) do # shove db number into PING reply since redis has no way # of exposing the currently selected DB replies = { :select => lambda { |db| $db = db; "+OK" }, :ping => lambda { "+PONG #{$db}" }, :auth => lambda { |password| $auth = password; "+OK" }, :get => lambda { |key| $auth == "secret" ? "$3\r\nbar" : "$-1" }, } end def connect_to_mock(url, &blk) redis_mock(replies) do connect(1, url, &blk) end end it "doesn't call select by default" do connect_to_mock("redis://localhost:6380/") do |redis| redis.ping do |response| response.should == "PONG " done end end end it "selects the right db" do connect_to_mock("redis://localhost:6380/9") do |redis| redis.ping do |response| response.should == "PONG 9" done end end end it "authenticates with a password" do connect_to_mock("redis://:secret@localhost:6380/9") do |redis| redis.get("foo") do |response| response.should == "bar" done end end end it "rejects a bad password" do connect_to_mock("redis://:failboat@localhost:6380/9") do |redis| redis.get("foo") do |response| response.should be_nil done end end end end em-hiredis-0.3.0/spec/live_redis_protocol_spec.rb000066400000000000000000000242551235427213700221150ustar00rootroot00000000000000require 'spec_helper' describe EventMachine::Hiredis, "connected to an empty db" do it "sets a string value" do connect do |redis| redis.set("foo", "bar") do |r| r.should == "OK" done end end end it "increments the value of a string" do connect do |redis| redis.incr "foo" do |r| r.should == 1 redis.incr "foo" do |r| r.should == 2 done end end end end it "increments the value of a string by an amount" do connect do |redis| redis.incrby "foo", 10 do |r| r.should == 10 done end end end it "decrements the value of a string" do connect do |redis| redis.incr "foo" do |r| r.should == 1 redis.decr "foo" do |r| r.should == 0 done end end end end it "decrement the value of a string by an amount" do connect do |redis| redis.incrby "foo", 20 do |r| r.should == 20 redis.decrby "foo", 10 do |r| r.should == 10 done end end end end it "can 'lpush' to a nonexistent list" do connect do |redis| redis.lpush("foo", "bar") do |r| r.should == 1 done end end end it "can 'rpush' to a nonexistent list" do connect do |redis| redis.rpush("foo", "bar") do |r| r.should == 1 done end end end it "gets the size of the database" do connect do |redis| redis.dbsize do |r| r.should == 0 done end end end it "adds a member to a nonexistent set" do connect do |redis| redis.sadd("set_foo", "bar") do |r| r.should == 1 done end end end it "reads info about the db" do connect do |redis| redis.info do |info| info[:redis_version].should_not be_nil done end end end it "can save the db" do connect do |redis| redis.save do |r| r.should == "OK" done end end end it "can save the db in the background" do connect do |redis| redis.bgsave do |r| r.should == "Background saving started" done end end end end describe EventMachine::Hiredis, "connected to a db containing some simple string-valued keys" do def set(&blk) connect do |redis| redis.flushdb redis.set "a", "b" redis.set "x", "y" blk.call(redis) end end it "fetches the values of multiple keys" do set do |redis| redis.mget "a", "x" do |r| r.should == ["b", "y"] done end end end it "fetches all the keys" do set do |redis| redis.keys "*" do |r| r.sort.should == ["a", "x"] done end end end it "sets a value if a key doesn't exist" do set do |redis| redis.setnx "a", "foo" do |r| r.should == 0 redis.setnx "zzz", "foo" do |r| r.should == 1 done end end end end it "tests for the existence of a key" do set do |redis| redis.exists "a" do |r| r.should == 1 redis.exists "zzz" do |r| r.should == 0 done end end end end it "deletes a key" do set do |redis| redis.del "a" do |r| r.should == 1 redis.exists "a" do |r| r.should == 0 redis.del "a" do |r| r.should == 0 done end end end end end it "detects the type of a key, existing or not" do set do |redis| redis.type "a" do |r| r.should == "string" redis.type "zzz" do |r| r.should == "none" done end end end end it "renames a key" do set do |redis| redis.rename "a", "x" do |r| redis.get "x" do |r| r.should == "b" done end end end end it "renames a key unless it exists" do set do |redis| redis.renamenx "a", "x" do |r| r.should == 0 redis.renamenx "a", "zzz" do |r| r.should == 1 redis.get "zzz" do |r| r.should == "b" done end end end end end end describe EventMachine::Hiredis, "connected to a db containing a list" do def set(&blk) connect do |redis| redis.flushdb redis.lpush "foo", "c" redis.lpush "foo", "b" redis.lpush "foo", "a" blk.call(redis) end end it "sets a list member and 'lindex' to retrieve it" do set do |redis| redis.lset("foo", 1, "bar") do |r| redis.lindex("foo", 1) do |r| r.should == "bar" done end end end end it "pushes onto tail of the list" do set do |redis| redis.rpush "foo", "d" do |r| r.should == 4 redis.rpop "foo" do |r| r.should == "d" done end end end end it "pushes onto the head of the list" do set do |redis| redis.lpush "foo", "d" do |r| r.should == 4 redis.lpop "foo" do |r| r.should == "d" done end end end end it "pops off the tail of the list" do set do |redis| redis.rpop("foo") do |r| r.should == "c" done end end end it "pops off the tail of the list" do set do |redis| redis.lpop("foo") do |r| r.should == "a" done end end end it "gets a range of values from a list" do set do |redis| redis.lrange("foo", 0, 1) do |r| r.should == ["a", "b"] done end end end it "trims a list" do set do |redis| redis.ltrim("foo", 0, 1) do |r| r.should == "OK" redis.llen("foo") do |r| r.should == 2 done end end end end it "removes a list element" do set do |redis| redis.lrem("foo", 0, "a") do |r| r.should == 1 redis.llen("foo") do |r| r.should == 2 done end end end end it "detects the type of a list" do set do |redis| redis.type "foo" do |r| r.should == "list" done end end end end describe EventMachine::Hiredis, "connected to a db containing two sets" do def set(&blk) connect do |redis| redis.flushdb redis.sadd "foo", "a" redis.sadd "foo", "b" redis.sadd "foo", "c" redis.sadd "bar", "c" redis.sadd "bar", "d" redis.sadd "bar", "e" blk.call(redis) end end it "finds a set's cardinality" do set do |redis| redis.scard("foo") do |r| r.should == 3 done end end end it "adds a new member to a set unless it is a duplicate" do set do |redis| redis.sadd("foo", "d") do |r| r.should == 1 # success redis.sadd("foo", "a") do |r| r.should == 0 # failure redis.scard("foo") do |r| r.should == 4 done end end end end end it "removes a set member if it exists" do set do |redis| redis.srem("foo", "a") do |r| r.should == 1 redis.srem("foo", "z") do |r| r.should == 0 redis.scard("foo") do |r| r.should == 2 done end end end end end it "retrieves a set's members" do set do |redis| redis.smembers("foo") do |r| r.sort.should == ["a", "b", "c"] done end end end it "detects set membership" do set do |redis| redis.sismember("foo", "a") do |r| r.should == 1 redis.sismember("foo", "z") do |r| r.should == 0 done end end end end it "finds the sets' intersection" do set do |redis| redis.sinter("foo", "bar") do |r| r.should == ["c"] done end end end it "finds and stores the sets' intersection" do set do |redis| redis.sinterstore("baz", "foo", "bar") do |r| r.should == 1 redis.smembers("baz") do |r| r.should == ["c"] done end end end end it "finds the sets' union" do set do |redis| redis.sunion("foo", "bar") do |r| r.sort.should == ["a","b","c","d","e"] done end end end it "finds and stores the sets' union" do set do |redis| redis.sunionstore("baz", "foo", "bar") do |r| r.should == 5 redis.smembers("baz") do |r| r.sort.should == ["a","b","c","d","e"] done end end end end it "detects the type of a set" do set do |redis| redis.type "foo" do |r| r.should == "set" done end end end end describe EventMachine::Hiredis, "connected to a db containing three linked lists" do def set(&blk) connect do |redis| redis.flushdb redis.rpush "foo", "a" redis.rpush "foo", "b" redis.set "a_sort", "2" redis.set "b_sort", "1" redis.set "a_data", "foo" redis.set "b_data", "bar" blk.call(redis) end end it "collates a sorted set of data" do set do |redis| redis.sort("foo", "BY", "*_sort", "GET", "*_data") do |r| r.should == ["bar", "foo"] done end end end it "gets keys selectively" do set do |redis| redis.keys "a_*" do |r| r.sort.should == ["a_sort", "a_data"].sort done end end end end describe EventMachine::Hiredis, "when reconnecting" do it "select previously selected dataset" do connect(3) do |redis| #simulate disconnect redis.set('foo', 'a') { redis.close_connection_after_writing } EventMachine.add_timer(2) do redis.get('foo') do |r| r.should == 'a' redis.get('non_existing') do |r| r.should == nil done end end end end end end describe EventMachine::Hiredis, "when closing_connection" do it "should fail deferred commands" do errored = false connect do |redis| op = redis.blpop 'empty_list' op.callback { fail } op.errback { EM.stop } redis.close_connection EM.add_timer(1) { fail } end end end em-hiredis-0.3.0/spec/lock_spec.rb000066400000000000000000000050521235427213700167710ustar00rootroot00000000000000require 'spec_helper' describe EventMachine::Hiredis::Lock do def start(timeout = 1) connect(timeout) do |redis| @redis = redis yield end end def new_lock EventMachine::Hiredis::Lock.new(@redis, "test-lock", 2) end it "can be acquired" do start { new_lock.acquire.callback { done }.errback { |e| fail e } } end it "is re-entrant" do start { lock = new_lock lock.acquire.callback { lock.acquire.callback { done }.errback { |e| fail e } }.errback { |e| fail e } } end it "is exclusive" do start { new_lock.acquire.callback { new_lock.acquire.errback { done }.callback { fail "Should not be able to acquire lock from different client" } }.errback { |e| fail e } } end it "can be released and taken by another instance" do start { lock = new_lock lock.acquire.callback { lock.unlock.callback { new_lock.acquire.callback { done }.errback { |e| fail e } }.errback { |e| fail e } }.errback { |e| fail e } } end it "times out" do start(3) { new_lock.acquire.callback { EM.add_timer(2) { new_lock.acquire.callback { done }.errback { |e| fail e } } }.errback { |e| fail e } } end it "extends timeout on re-entry" do start(4) { lock = new_lock lock.acquire.callback { EM.add_timer(1) { lock.acquire.callback { EM.add_timer(1.5) { # Check it's still locked by initial instance new_lock.acquire.errback { done }.callback { |e| fail e } } }.errback { |e| fail e } } }.errback { |e| fail e } } end it "fails to release if it has not been taken" do start { new_lock.unlock.errback { done }.callback { fail "Released lock which had not been taken" } } end it "fails to release if taken by another instance" do start { new_lock.acquire.callback { new_lock.unlock.errback { done }.callback { fail "Released lock belonging to another instance" } }.errback { |e| fail e } } end end em-hiredis-0.3.0/spec/pubsub_spec.rb000066400000000000000000000231251235427213700173420ustar00rootroot00000000000000require 'spec_helper' describe EventMachine::Hiredis::PubsubClient, '(un)subscribe' do describe "subscribing" do it "should return deferrable which succeeds with subscribe call result" do connect do |redis| df = redis.pubsub.subscribe("channel") { } df.should be_kind_of(EventMachine::DefaultDeferrable) df.callback { |subscription_count| # Subscribe response from redis - indicates that subscription has # succeeded and that the current connection has a single # subscription subscription_count.should == 1 done } end end it "should run the passed block when message received" do connect do |redis| redis.pubsub.subscribe("channel") { |message| message.should == 'hello' done }.callback { redis.publish('channel', 'hello') } end end it "should run the passed proc when message received on channel" do connect do |redis| proc = Proc.new { |message| message.should == 'hello' done } redis.pubsub.subscribe("channel", proc).callback { redis.publish('channel', 'hello') } end end end describe "unsubscribing" do it "should allow unsubscribing a single callback without unsubscribing from redis" do connect do |redis| proc1 = Proc.new { |message| fail } proc2 = Proc.new { |message| message.should == 'hello' done } redis.pubsub.subscribe("channel", proc1) redis.pubsub.subscribe("channel", proc2).callback { redis.pubsub.unsubscribe_proc("channel", proc1) redis.publish("channel", "hello") } end end it "should unsubscribe from redis on last proc unsubscription" do connect do |redis| proc = Proc.new { |message| } redis.pubsub.subscribe("channel", proc).callback { |subs_count| subs_count.should == 1 redis.pubsub.unsubscribe_proc("channel", proc).callback { # Slightly awkward way to check that unsubscribe happened: redis.pubsub.subscribe('channel2').callback { |count| # If count is 1 this implies that channel unsubscribed count.should == 1 done } } } end end it "should allow unsubscribing from redis channel, including all callbacks, and return deferrable for redis unsubscribe" do connect do |redis| # Raw pubsub event redis.pubsub.on('message') { |channel, message| fail } # Block subscription redis.pubsub.subscribe("channel") { |m| fail } # block # Proc example df = redis.pubsub.subscribe("channel", Proc.new { |m| fail }) df.callback { redis.pubsub.unsubscribe("channel").callback { |remaining_subs| remaining_subs.should == 0 redis.publish("channel", "hello") { done } } } end end end it "should expose raw pubsub events from redis" do channel = "channel" callback_count = 0 connect do |redis| redis.pubsub.on(:subscribe) { |channel, subscription_count| # 2. Get subscribe callback callback_count += 1 channel.should == channel subscription_count.should == 1 # 3. Publish on channel redis.publish(channel, 'foo') } redis.pubsub.on(:message) { |channel, message| # 4. Get message callback callback_count += 1 channel.should == channel message.should == 'foo' callback_count.should == 2 done } # 1. Subscribe to channel redis.pubsub.subscribe(channel) end end it "should resubscribe to all channels on reconnect" do callback_count = 0 connect do |redis| # 1. Subscribe to channels redis.pubsub.subscribe('channel1') { callback_count += 1 } redis.pubsub.subscribe('channel2') { callback_count += 1 EM.next_tick { # 4. Success if both messages have been received callback_count.should == 2 done } }.callback { |subscription_count| subscription_count.should == 2 # 2. Subscriptions complete. Now force disconnect redis.pubsub.instance_variable_get(:@connection).close_connection EM.add_timer(0.1) { # 3. After giving time to reconnect publish to both channels redis.publish('channel1', 'foo') redis.publish('channel2', 'bar') } } end end end describe EventMachine::Hiredis::PubsubClient, 'p(un)subscribe' do describe "psubscribing" do it "should return deferrable which succeeds with psubscribe call result" do connect do |redis| df = redis.pubsub.psubscribe("channel") { } df.should be_kind_of(EventMachine::DefaultDeferrable) df.callback { |subscription_count| # Subscribe response from redis - indicates that subscription has # succeeded and that the current connection has a single # subscription subscription_count.should == 1 done } end end it "should run the passed block when message received" do connect do |redis| redis.pubsub.psubscribe("channel:*") { |channel, message| channel.should == 'channel:foo' message.should == 'hello' done }.callback { redis.publish('channel:foo', 'hello') } end end it "should run the passed proc when message received on channel" do connect do |redis| proc = Proc.new { |channel, message| channel.should == 'channel:foo' message.should == 'hello' done } redis.pubsub.psubscribe("channel:*", proc).callback { redis.publish('channel:foo', 'hello') } end end end describe "punsubscribing" do it "should allow punsubscribing a single callback without punsubscribing from redis" do connect do |redis| proc1 = Proc.new { |channel, message| fail } proc2 = Proc.new { |channel, message| channel.should == 'channel:foo' message.should == 'hello' done } redis.pubsub.psubscribe("channel:*", proc1) redis.pubsub.psubscribe("channel:*", proc2).callback { redis.pubsub.punsubscribe_proc("channel:*", proc1) redis.publish("channel:foo", "hello") } end end it "should punsubscribe from redis on last proc punsubscription" do connect do |redis| proc = Proc.new { |message| } redis.pubsub.psubscribe("channel:*", proc).callback { |subs_count| subs_count.should == 1 redis.pubsub.punsubscribe_proc("channel:*", proc).callback { # Slightly awkward way to check that unsubscribe happened: redis.pubsub.psubscribe('channel2').callback { |count| # If count is 1 this implies that channel unsubscribed count.should == 1 done } } } end end it "should allow punsubscribing from redis channel, including all callbacks, and return deferrable for redis punsubscribe" do connect do |redis| # Raw pubsub event redis.pubsub.on('pmessage') { |pattern, channel, message| fail } # Block subscription redis.pubsub.psubscribe("channel") { |c, m| fail } # block # Proc example df = redis.pubsub.psubscribe("channel", Proc.new { |c, m| fail }) df.callback { redis.pubsub.punsubscribe("channel").callback { |remaining_subs| remaining_subs.should == 0 redis.publish("channel", "hello") { done } } } end end end it "should expose raw pattern pubsub events from redis" do callback_count = 0 connect do |redis| redis.pubsub.on(:psubscribe) { |pattern, subscription_count| # 2. Get subscribe callback callback_count += 1 pattern.should == "channel:*" subscription_count.should == 1 # 3. Publish on channel redis.publish('channel:foo', 'foo') } redis.pubsub.on(:pmessage) { |pattern, channel, message| # 4. Get message callback callback_count += 1 pattern.should == 'channel:*' channel.should == 'channel:foo' message.should == 'foo' callback_count.should == 2 done } # 1. Subscribe to channel redis.pubsub.psubscribe('channel:*') end end it "should resubscribe to all pattern subscriptions on reconnect" do callback_count = 0 connect do |redis| # 1. Subscribe to channels redis.pubsub.psubscribe('foo:*') { |channel, message| channel.should == 'foo:a' message.should == 'hello foo' callback_count += 1 } redis.pubsub.psubscribe('bar:*') { |channel, message| channel.should == 'bar:b' message.should == 'hello bar' callback_count += 1 EM.next_tick { # 4. Success if both messages have been received callback_count.should == 2 done } }.callback { |subscription_count| subscription_count.should == 2 # 2. Subscriptions complete. Now force disconnect redis.pubsub.instance_variable_get(:@connection).close_connection EM.add_timer(0.1) { # 3. After giving time to reconnect publish to both channels redis.publish('foo:a', 'hello foo') redis.publish('bar:b', 'hello bar') } } end end end em-hiredis-0.3.0/spec/redis_commands_spec.rb000066400000000000000000000641421235427213700210350ustar00rootroot00000000000000require 'spec_helper' describe EventMachine::Hiredis, "commands" do it "pings" do connect do |redis| redis.ping { |r| r.should == 'PONG'; done } end end it "SETs and GETs a key" do connect do |redis| redis.set('foo', 'nik') redis.get('foo') { |r| r.should == 'nik'; done } end end it "handles trailing newline characters" do connect do |redis| redis.set('foo', "bar\n") redis.get('foo') { |r| r.should == "bar\n"; done } end end it "stores and retrieves all possible characters at the beginning and the end of a string" do connect do |redis| (0..255).each do |char_idx| string = "#{char_idx.chr}---#{char_idx.chr}" if RUBY_VERSION > "1.9" string.force_encoding("UTF-8") end redis.set('foo', string) redis.get('foo') { |r| r.should == string } end redis.ping { done } end end it "SETs a key with an expiry" do connect do |redis| timeout(3) redis.setex('foo', 1, 'bar') redis.get('foo') { |r| r.should == 'bar' } EventMachine.add_timer(2) do redis.get('foo') { |r| r.should == nil } redis.ping { done } end end end it "gets TTL for a key" do connect do |redis| redis.setex('foo', 1, 'bar') redis.ttl('foo') { |r| r.should == 1; done } end end it "can SETNX" do connect do |redis| redis.set('foo', 'nik') redis.get('foo') { |r| r.should == 'nik' } redis.setnx 'foo', 'bar' redis.get('foo') { |r| r.should == 'nik' } redis.ping { done } end end it "can GETSET" do connect do |redis| redis.set('foo', 'bar') redis.getset('foo', 'baz') { |r| r.should == 'bar' } redis.get('foo') { |r| r.should == 'baz'; done } end end it "can INCR a key" do connect do |redis| redis.del('counter') redis.incr('counter') { |r| r.should == 1 } redis.incr('counter') { |r| r.should == 2 } redis.incr('counter') { |r| r.should == 3 } redis.ping { done } end end it "can INCRBY a key" do connect do |redis| redis.del('counter') redis.incrby('counter', 1) { |r| r.should == 1 } redis.incrby('counter', 2) { |r| r.should == 3 } redis.incrby('counter', 3) { |r| r.should == 6 } redis.ping { done } end end it "can DECR a key" do connect do |redis| redis.del('counter') redis.incr('counter') { |r| r.should == 1 } redis.incr('counter') { |r| r.should == 2 } redis.incr('counter') { |r| r.should == 3 } redis.decr('counter') { |r| r.should == 2 } redis.decrby('counter', 2) { |r| r.should == 0; done } end end it "can RANDOMKEY" do connect do |redis| redis.set('foo', 'bar') redis.randomkey { |r| r.should_not == nil; done } end end it "can RENAME a key" do connect do |redis| redis.del 'foo' redis.del 'bar' redis.set('foo', 'hi') redis.rename 'foo', 'bar' redis.get('bar') { |r| r.should == 'hi' ; done } end end it "can RENAMENX a key" do connect do |redis| redis.del 'foo' redis.del 'bar' redis.set('foo', 'hi') redis.set('bar', 'ohai') redis.renamenx 'foo', 'bar' redis.get('bar') { |r| r.should == 'ohai' ; done } end end it "can get DBSIZE of the database" do connect do |redis| redis.set('foo1', 'bar') redis.set('foo2', 'baz') redis.set('foo3', 'bat') redis.dbsize do |r| r.should == 3 done end end end it "can EXPIRE a key" do connect do |redis| timeout(3) redis.set('foo', 'bar') redis.expire 'foo', 1 redis.get('foo') { |r| r.should == "bar" } EventMachine.add_timer(2) do redis.get('foo') { |r| r.should == nil } redis.ping { done } end end end it "can check if a key EXISTS" do connect do |redis| redis.set 'foo', 'nik' redis.exists('foo') { |r| r.should == 1 } redis.del 'foo' redis.exists('foo') { |r| r.should == 0 ; done } end end it "can list KEYS" do connect do |redis| redis.keys("f*") { |keys| keys.each { |key| @r.del key } } redis.set('f', 'nik') redis.set('fo', 'nak') redis.set('foo', 'qux') redis.keys("f*") { |r| r.sort.should == ['f', 'fo', 'foo'].sort } redis.ping { done } end end it "returns a random key (RANDOMKEY)" do connect do |redis| redis.set("foo", "bar") redis.randomkey do |r| redis.exists(r) do |e| e.should == 1 done end end end end it "should be able to check the TYPE of a key" do connect do |redis| redis.set('foo', 'nik') redis.type('foo') { |r| r.should == "string" } redis.del 'foo' redis.type('foo') { |r| r.should == "none" ; done } end end it "pushes to the head of a list (LPUSH)" do connect do |redis| redis.lpush "list", 'hello' redis.lpush "list", 42 redis.type('list') { |r| r.should == "list" } redis.llen('list') { |r| r.should == 2 } redis.lpop('list') { |r| r.should == '42'; done } end end it "pushes to the tail of a list (RPUSH)" do connect do |redis| redis.rpush "list", 'hello' redis.type('list') { |r| r.should == "list" } redis.llen('list') { |r| r.should == 1 ; done } end end it "pops the tail of a list (RPOP)" do connect do |redis| redis.rpush "list", 'hello' redis.rpush"list", 'goodbye' redis.type('list') { |r| r.should == "list" } redis.llen('list') { |r| r.should == 2 } redis.rpop('list') { |r| r.should == 'goodbye'; done } end end it "pop the head of a list (LPOP)" do connect do |redis| redis.rpush "list", 'hello' redis.rpush "list", 'goodbye' redis.type('list') { |r| r.should == "list" } redis.llen('list') { |r| r.should == 2 } redis.lpop('list') { |r| r.should == 'hello'; done } end end it "gets the length of a list (LLEN)" do connect do |redis| redis.rpush "list", 'hello' redis.rpush "list", 'goodbye' redis.type('list') { |r| r.should == "list" } redis.llen('list') { |r| r.should == 2 ; done } end end it "gets a range of values from a list (LRANGE)" do connect do |redis| redis.rpush "list", 'hello' redis.rpush "list", 'goodbye' redis.rpush "list", '1' redis.rpush "list", '2' redis.rpush "list", '3' redis.type('list') { |r| r.should == "list" } redis.llen('list') { |r| r.should == 5 } redis.lrange('list', 2, -1) { |r| r.should == ['1', '2', '3']; done } end end it "trims a list (LTRIM)" do connect do |redis| redis.rpush "list", 'hello' redis.rpush "list", 'goodbye' redis.rpush "list", '1' redis.rpush "list", '2' redis.rpush "list", '3' redis.type('list') { |r| r.should == "list" } redis.llen('list') { |r| r.should == 5 } redis.ltrim 'list', 0, 1 redis.llen('list') { |r| r.should == 2 } redis.lrange('list', 0, -1) { |r| r.should == ['hello', 'goodbye']; done } end end it "gets a value by indexing into a list (LINDEX)" do connect do |redis| redis.rpush "list", 'hello' redis.rpush "list", 'goodbye' redis.type('list') { |r| r.should == "list" } redis.llen('list') { |r| r.should == 2 } redis.lindex('list', 1) { |r| r.should == 'goodbye'; done } end end it "sets a value by indexing into a list (LSET)" do connect do |redis| redis.rpush "list", 'hello' redis.rpush "list", 'hello' redis.type('list') { |r| r.should == "list" } redis.llen('list') { |r| r.should == 2 } redis.lset('list', 1, 'goodbye') { |r| r.should == 'OK' } redis.lindex('list', 1) { |r| r.should == 'goodbye'; done } end end it "removes values from a list (LREM)" do connect do |redis| redis.rpush "list", 'hello' redis.rpush "list", 'goodbye' redis.type('list') { |r| r.should == "list" } redis.llen('list') { |r| r.should == 2 } redis.lrem('list', 1, 'hello') { |r| r.should == 1 } redis.lrange('list', 0, -1) { |r| r.should == ['goodbye']; done } end end it "pops values from a list and push them onto a temp list(RPOPLPUSH)" do connect do |redis| redis.rpush "list", 'one' redis.rpush "list", 'two' redis.rpush "list", 'three' redis.type('list') { |r| r.should == "list" } redis.llen('list') { |r| r.should == 3 } redis.lrange('list', 0, -1) { |r| r.should == ['one', 'two', 'three'] } redis.lrange('tmp', 0, -1) { |r| r.should == [] } redis.rpoplpush('list', 'tmp') { |r| r.should == 'three' } redis.lrange('tmp', 0, -1) { |r| r.should == ['three'] } redis.rpoplpush('list', 'tmp') { |r| r.should == 'two' } redis.lrange('tmp', 0, -1) { |r| r.should == ['two', 'three'] } redis.rpoplpush('list', 'tmp') { |r| r.should == 'one' } redis.lrange('tmp', 0, -1) { |r| r.should == ['one', 'two', 'three']; done } end end it "adds members to a set (SADD)" do connect do |redis| redis.sadd "set", 'key1' redis.sadd "set", 'key2' redis.type('set') { |r| r.should == "set" } redis.scard('set') { |r| r.should == 2 } redis.smembers('set') { |r| r.sort.should == ['key1', 'key2'].sort; done } end end it "deletes members to a set (SREM)" do connect do |redis| redis.sadd "set", 'key1' redis.sadd "set", 'key2' redis.type('set') { |r| r.should == "set" } redis.scard('set') { |r| r.should == 2 } redis.smembers('set') { |r| r.sort.should == ['key1', 'key2'].sort } redis.srem('set', 'key1') redis.scard('set') { |r| r.should == 1 } redis.smembers('set') { |r| r.should == ['key2']; done } end end it "returns and remove random key from set (SPOP)" do connect do |redis| redis.sadd "set_pop", "key1" redis.sadd "set_pop", "key2" redis.spop("set_pop") { |r| r.should_not == nil } redis.scard("set_pop") { |r| r.should == 1; done } end end it "returns random key without delete the key from a set (SRANDMEMBER)" do connect do |redis| redis.sadd "set_srandmember", "key1" redis.sadd "set_srandmember", "key2" redis.srandmember("set_srandmember") { |r| r.should_not == nil } redis.scard("set_srandmember") { |r| r.should == 2; done } end end it "counts the members of a set (SCARD)" do connect do |redis| redis.sadd "set", 'key1' redis.sadd "set", 'key2' redis.type('set') { |r| r.should == "set" } redis.scard('set') { |r| r.should == 2; done } end end it "tests for set membership (SISMEMBER)" do connect do |redis| redis.sadd "set", 'key1' redis.sadd "set", 'key2' redis.type('set') { |r| r.should == "set" } redis.scard('set') { |r| r.should == 2 } redis.sismember('set', 'key1') { |r| r.should == 1 } redis.sismember('set', 'key2') { |r| r.should == 1 } redis.sismember('set', 'notthere') { |r| r.should == 0; done } end end it "intersects sets (SINTER)" do connect do |redis| redis.sadd "set", 'key1' redis.sadd "set", 'key2' redis.sadd "set2", 'key2' redis.sinter('set', 'set2') { |r| r.should == ['key2']; done } end end it "intersects set and stores the results in a key (SINTERSTORE)" do connect do |redis| redis.sadd "set", 'key1' redis.sadd "set", 'key2' redis.sadd "set2", 'key2' redis.sinterstore('newone', 'set', 'set2') { |r| r.should == 1 } redis.smembers('newone') { |r| r.should == ['key2']; done } end end it "performs set unions (SUNION)" do connect do |redis| redis.sadd "set", 'key1' redis.sadd "set", 'key2' redis.sadd "set2", 'key2' redis.sadd "set2", 'key3' redis.sunion('set', 'set2') { |r| r.sort.should == ['key1','key2','key3'].sort; done } end end it "performs a set union and store the results in a key (SUNIONSTORE)" do connect do |redis| redis.sadd "set", 'key1' redis.sadd "set", 'key2' redis.sadd "set2", 'key2' redis.sadd "set2", 'key3' redis.sunionstore('newone', 'set', 'set2') { |r| r.should == 3 } redis.smembers('newone') { |r| r.sort.should == ['key1','key2','key3'].sort; done } end end it "takes a set difference (SDIFF)" do connect do |redis| redis.sadd "set", 'a' redis.sadd "set", 'b' redis.sadd "set2", 'b' redis.sadd "set2", 'c' redis.sdiff('set', 'set2') { |r| r.should == ['a']; done } end end it "takes set difference and store the results in a key (SDIFFSTORE)" do connect do |redis| redis.sadd "set", 'a' redis.sadd "set", 'b' redis.sadd "set2", 'b' redis.sadd "set2", 'c' redis.sdiffstore('newone', 'set', 'set2') redis.smembers('newone') { |r| r.should == ['a']; done } end end it "moves elements from one set to another (SMOVE)" do connect do |redis| redis.sadd 'set1', 'a' redis.sadd 'set1', 'b' redis.sadd 'set2', 'x' redis.smove('set1', 'set2', 'a') { |r| r.should == 1 } redis.sismember('set2', 'a') { |r| r.should == 1 } redis.del('set1') { done } end end it "counts the members of a zset" do connect do |redis| redis.sadd "set", 'key1' redis.sadd "set", 'key2' redis.zadd 'zset', 1, 'set' redis.zcount('zset') { |r| r.should == 1 } redis.del('set') redis.del('zset') { done } end end it "adds members to a zset" do connect do |redis| redis.sadd "set", 'key1' redis.sadd "set", 'key2' redis.zadd 'zset', 1, 'set' redis.zrange('zset', 0, 1) { |r| r.should == ['set'] } redis.zcount('zset') { |r| r.should == 1 } redis.del('set') redis.del('zset') { done } end end it "deletes members to a zset" do connect do |redis| redis.sadd "set", 'key1' redis.sadd "set", 'key2' redis.type?('set') { |r| r.should == "set" } redis.sadd "set2", 'key3' redis.sadd "set2", 'key4' redis.type?('set2') { |r| r.should == "set" } redis.zadd 'zset', 1, 'set' redis.zcount('zset') { |r| r.should == 1 } redis.zadd 'zset', 2, 'set2' redis.zcount('zset') { |r| r.should == 2 } redis.zset_delete 'zset', 'set' redis.zcount('zset') { |r| r.should == 1 } redis.del('set') redis.del('set2') redis.del('zset') { done } end end it "gets a range of values from a zset" do connect do |redis| redis.sadd "set", 'key1' redis.sadd "set", 'key2' redis.sadd "set2", 'key3' redis.sadd "set2", 'key4' redis.sadd "set3", 'key1' redis.type?('set') { |r| r.should == 'set' } redis.type?('set2') { |r| r.should == 'set' } redis.type?('set3') { |r| r.should == 'set' } redis.zadd 'zset', 1, 'set' redis.zadd 'zset', 2, 'set2' redis.zadd 'zset', 3, 'set3' redis.zcount('zset') { |r| r.should == 3 } redis.zrange('zset', 0, 3) { |r| r.should == ['set', 'set2', 'set3'] } redis.del('set') redis.del('set2') redis.del('set3') redis.del('zset') { done } end end it "gets a reverse range of values from a zset" do connect do |redis| redis.sadd "set", 'key1' redis.sadd "set", 'key2' redis.sadd "set2", 'key3' redis.sadd "set2", 'key4' redis.sadd "set3", 'key1' redis.type?('set') { |r| r.should == 'set' } redis.type?('set2') { |r| r.should == 'set' } redis.type?('set3') { |r| r.should == 'set' } redis.zadd 'zset', 1, 'set' redis.zadd 'zset', 2, 'set2' redis.zadd 'zset', 3, 'set3' redis.zcount('zset') { |r| r.should == 3 } redis.zrevrange('zset', 0, 3) { |r| r.should == ['set3', 'set2', 'set'] } redis.del('set') redis.del('set2') redis.del('set3') redis.del('zset') { done } end end it "gets a range by score of values from a zset" do connect do |redis| redis.sadd "set", 'key1' redis.sadd "set", 'key2' redis.sadd "set2", 'key3' redis.sadd "set2", 'key4' redis.sadd "set3", 'key1' redis.sadd "set4", 'key4' redis.zadd 'zset', 1, 'set' redis.zadd 'zset', 2, 'set2' redis.zadd 'zset', 3, 'set3' redis.zadd 'zset', 4, 'set4' redis.zcount('zset') { |r| r.should == 4 } redis.zrangebyscore('zset', 2, 3) { |r| r.should == ['set2', 'set3'] } redis.del('set') redis.del('set2') redis.del('set3') redis.del('set4') redis.del('zset') { done } end end it "gets a score for a specific value in a zset (ZSCORE)" do connect do |redis| redis.zadd "zset", 23, "value" redis.zscore("zset", "value") { |r| r.should == "23" } redis.zscore("zset", "value2") { |r| r.should == nil } redis.zscore("unknown_zset", "value") { |r| r.should == nil } redis.del("zset") { done } end end it "increments a range score of a zset (ZINCRBY)" do connect do |redis| # create a new zset redis.zincrby "hackers", 1965, "Yukihiro Matsumoto" redis.zscore("hackers", "Yukihiro Matsumoto") { |r| r.should == "1965" } # add a new element redis.zincrby "hackers", 1912, "Alan Turing" redis.zscore("hackers", "Alan Turing") { |r| r.should == "1912" } # update the score redis.zincrby "hackers", 100, "Alan Turing" # yeah, we are making Turing a bit younger redis.zscore("hackers", "Alan Turing") { |r| r.should == "2012" } # attempt to update a key that's not a zset redis.set("i_am_not_a_zet", "value") # shouldn't raise error anymore redis.zincrby("i_am_not_a_zet", 23, "element") { |r| r.should == nil } redis.del("hackers") redis.del("i_am_not_a_zet") { done } end end it "provides info (INFO)" do connect do |redis| redis.info do |r| [:redis_version, :total_connections_received, :connected_clients, :total_commands_processed, :connected_slaves, :uptime_in_seconds, :used_memory, :uptime_in_days].each do |x| r.keys.include?(x).should == true end done end end end it "provides commandstats (INFO COMMANDSTATS)" do connect do |redis| redis.info_commandstats do |r| r[:get][:calls].should be_a_kind_of(Integer) r[:get][:usec].should be_a_kind_of(Integer) r[:get][:usec_per_call].should be_a_kind_of(Float) done end end end it "flushes the database (FLUSHDB)" do connect do |redis| redis.set('key1', 'keyone') redis.set('key2', 'keytwo') redis.keys('*') { |r| r.sort.should == ['key1', 'key2'].sort } redis.flushdb redis.keys('*') { |r| r.should == []; done } end end it "SELECTs database" do connect do |redis| redis.set("foo", "bar") do |set_response| redis.select("10") do |select_response| redis.get("foo") do |get_response| get_response.should == nil; done end end end end end it "SELECTs database without a callback" do connect do |redis| redis.select("9") redis.incr("foo") do |response| response.should == 1 done end end end it "provides the last save time (LASTSAVE)" do connect do |redis| redis.lastsave do |savetime| Time.at(savetime).class.should == Time Time.at(savetime).should <= Time.now done end end end it "can MGET keys" do connect do |redis| redis.set('foo', 1000) redis.set('bar', 2000) redis.mget('foo', 'bar') { |r| r.should == ['1000', '2000'] } redis.mget('foo', 'bar', 'baz') { |r| r.should == ['1000', '2000', nil] } redis.ping { done } end end it "can MSET values" do connect do |redis| redis.mset "key1", "value1", "key2", "value2" redis.get('key1') { |r| r.should == "value1" } redis.get('key2') { |r| r.should == "value2"; done } end end it "can MSETNX values" do connect do |redis| redis.msetnx "keynx1", "valuenx1", "keynx2", "valuenx2" redis.mget('keynx1', 'keynx2') { |r| r.should == ["valuenx1", "valuenx2"] } redis.set("keynx1", "value1") redis.set("keynx2", "value2") redis.msetnx "keynx1", "valuenx1", "keynx2", "valuenx2" redis.mget('keynx1', 'keynx2') { |r| r.should == ["value1", "value2"]; done } end end it "can BGSAVE" do connect do |redis| redis.bgsave do |r| ['OK', 'Background saving started'].include?(r).should == true done end end end it "can ECHO" do connect do |redis| redis.echo("message in a bottle\n") { |r| r.should == "message in a bottle\n"; done } end end it "runs MULTI without a block" do connect do |redis| redis.multi redis.get("key1") { |r| r.should == "QUEUED" } redis.discard { done } end end it "runs MULTI/EXEC" do connect do |redis| redis.multi redis.set "key1", "value1" redis.exec redis.get("key1") { |r| r.should == "value1" } begin redis.multi redis.set "key2", "value2" raise "Some error" redis.set "key3", "value3" redis.exec rescue redis.discard end redis.get("key2") { |r| r.should == nil } redis.get("key3") { |r| r.should == nil; done} end end it "sets and get hash values" do connect do |redis| redis.hset("rush", "signals", "1982") { |r| r.should == 1 } redis.hexists("rush", "signals") { |r| r.should == 1 } redis.hget("rush", "signals") { |r| r.should == "1982"; done } end end it "deletes hash values" do connect do |redis| redis.hset("rush", "YYZ", "1981") redis.hdel("rush", "YYZ") { |r| r.should == 1 } redis.hexists("rush", "YYZ") { |r| r.should == 0; done } end end end describe EventMachine::Hiredis, "with hash values" do def set(&blk) connect do |redis| redis.hset("rush", "permanent waves", "1980") redis.hset("rush", "moving pictures", "1981") redis.hset("rush", "signals", "1982") blk.call(redis) end end it "gets the length of the hash" do set do |redis| redis.hlen("rush") { |r| r.should == 3 } redis.hlen("yyz") { |r| r.should == 0; done } end end it "gets the keys and values of the hash" do set do |redis| redis.hkeys("rush") { |r| r.should == ["permanent waves", "moving pictures", "signals"] } redis.hvals("rush") { |r| r.should == %w[1980 1981 1982] } redis.hvals("yyz") { |r| r.should == []; done } end end it "returns all hash values" do set do |redis| redis.hgetall("rush") do |r| r.should == [ "permanent waves", "1980", "moving pictures", "1981", "signals" , "1982" ] end redis.hgetall("yyz") { |r| r.should == []; done } end end end describe EventMachine::Hiredis, "with nested multi-bulk response" do def set(&blk) connect do |redis| redis.set 'user:one:id', 'id-one' redis.set 'user:two:id', 'id-two' redis.sadd "user:one:interests", "first-interest" redis.sadd "user:one:interests", "second-interest" redis.sadd "user:two:interests", "third-interest" blk.call(redis) end end it "returns array of arrays" do set do |redis| redis.multi redis.smembers "user:one:interests" redis.smembers "user:two:interests" redis.exec do |interests_one, interests_two| interests_one.sort.should == ["first-interest", "second-interest"] interests_two.should == ['third-interest'] end redis.mget("user:one:id", "user:two:id") do |user_ids| user_ids.should == ['id-one', 'id-two'] done end end end end describe EventMachine::Hiredis, "monitor" do it "returns monitored commands" do connect do |redis| # 1. Create 2nd connection to send traffic to monitor redis2 = EventMachine::Hiredis.connect("redis://localhost:6379/") redis2.callback { # 2. Monitor after command has connected redis.monitor do |reply| reply.should == "OK" # 3. Command which should show up in monitor output redis2.get('foo') end } redis.on(:monitor) do |line| line.should =~ /foo/ done end end end end describe EventMachine::Hiredis, "sorting" do context "with some simple sorting data" do def set(&blk) connect do |redis| redis.set('dog_1', 'louie') redis.rpush 'Dogs', 1 redis.set('dog_2', 'lucy') redis.rpush 'Dogs', 2 redis.set('dog_3', 'max') redis.rpush 'Dogs', 3 redis.set('dog_4', 'taj') redis.rpush 'Dogs', 4 blk.call(redis) end end it "sorts with a limit" do set do |redis| redis.sort('Dogs', "GET", 'dog_*', "LIMIT", "0", "1") do |r| r.should == ['louie'] done end end end it "sorts with a limit and order" do set do |redis| redis.sort('Dogs', "GET", 'dog_*', "LIMIT", "0", "1", "desc", "alpha") do |r| r.should == ['taj'] done end end end end context "with more complex sorting data" do def set(&blk) connect do |redis| redis.set('dog:1:name', 'louie') redis.set('dog:1:breed', 'mutt') redis.rpush 'dogs', 1 redis.set('dog:2:name', 'lucy') redis.set('dog:2:breed', 'poodle') redis.rpush 'dogs', 2 redis.set('dog:3:name', 'max') redis.set('dog:3:breed', 'hound') redis.rpush 'dogs', 3 redis.set('dog:4:name', 'taj') redis.set('dog:4:breed', 'terrier') redis.rpush 'dogs', 4 blk.call(redis) end end it "handles multiple GETs" do set do |redis| redis.sort('dogs', 'GET', 'dog:*:name', 'GET', 'dog:*:breed', 'LIMIT', '0', '1') do |r| r.should == ['louie', 'mutt'] done end end end it "handles multiple GETs with an order" do set do |redis| redis.sort('dogs', 'GET', 'dog:*:name', 'GET', 'dog:*:breed', 'LIMIT', '0', '1', 'desc', 'alpha') do |r| r.should == ['taj', 'terrier'] done end end end end end em-hiredis-0.3.0/spec/spec_helper.rb000066400000000000000000000006511235427213700173200ustar00rootroot00000000000000$:.unshift File.expand_path(File.dirname(__FILE__) + "/../lib") require 'em-hiredis' require 'rspec' require 'em-spec/rspec' require 'support/connection_helper' require 'support/redis_mock' require 'stringio' RSpec.configure do |config| config.include ConnectionHelper config.include EventMachine::SpecHelper config.include RedisMock::Helper end # This speeds the tests up a bit EM::Hiredis.reconnect_timeout = 0.01 em-hiredis-0.3.0/spec/support/000077500000000000000000000000001235427213700162145ustar00rootroot00000000000000em-hiredis-0.3.0/spec/support/connection_helper.rb000066400000000000000000000005261235427213700222420ustar00rootroot00000000000000module ConnectionHelper # Use db 9 for tests to avoid flushing the main db # It would be nice if there was a standard db number for testing... def connect(timeout = 1, url = "redis://localhost:6379/9", &blk) em(timeout) do redis = EventMachine::Hiredis.connect(url) redis.flushdb blk.call(redis) end end end em-hiredis-0.3.0/spec/support/redis_mock.rb000066400000000000000000000027251235427213700206660ustar00rootroot00000000000000# nabbed from redis-rb, thanks! require "socket" module RedisMock def self.start(port = 6380) server = TCPServer.new("127.0.0.1", port) loop do session = server.accept while line = session.gets parts = Array.new(line[1..-3].to_i) do bytes = session.gets[1..-3].to_i argument = session.read(bytes) session.read(2) # Discard \r\n argument end response = yield(*parts) if response.nil? session.shutdown(Socket::SHUT_RDWR) break else session.write(response) session.write("\r\n") end end end end module Helper # Forks the current process and starts a new mock Redis server on # port 6380. # # The server will reply with a `+OK` to all commands, but you can # customize it by providing a hash. For example: # # redis_mock(:ping => lambda { "+PONG" }) do # assert_equal "PONG", Redis.new(:port => 6380).ping # end # def redis_mock(replies = {}) begin pid = fork do trap("TERM") { exit } RedisMock.start do |command, *args| (replies[command.to_sym] || lambda { |*_| "+OK" }).call(*args) end end sleep 1 # Give time for the socket to start listening. yield ensure if pid Process.kill("TERM", pid) Process.wait(pid) end end end end end em-hiredis-0.3.0/spec/url_param_spec.rb000066400000000000000000000020651235427213700200240ustar00rootroot00000000000000# adapted from redis-rb require 'spec_helper' describe EventMachine::Hiredis, "URL parsing" do it "defaults URL defaults to 127.0.0.1:6379" do redis = EventMachine::Hiredis.setup redis.host.should == "127.0.0.1" redis.port.should == 6379 redis.db.should == 0 redis.password.should == nil end it "allows to pass in a URL" do redis = EventMachine::Hiredis.setup "redis://:secr3t@foo.com:999/2" redis.host.should == "foo.com" redis.port.should == 999 redis.db.should == 2 redis.password.should == "secr3t" end it "does not modify the passed options" do options = "redis://:secr3t@foo.com:999/2" redis = EventMachine::Hiredis.setup(options) options.should == "redis://:secr3t@foo.com:999/2" end it "uses REDIS_URL over default if available" do ENV["REDIS_URL"] = "redis://:secr3t@foo.com:999/2" redis = EventMachine::Hiredis.setup redis.host.should == "foo.com" redis.port.should == 999 redis.db.should == 2 redis.password.should == "secr3t" ENV.delete("REDIS_URL") end end