Code Monkey home page Code Monkey logo

em-hiredis's Introduction

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, https://github.com/madsimian/em-redis 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://:[email protected]: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 # => #<EventMachine::Hiredis::RedisError: Error reply from redis (wrapped in redis_error)>
    p e.redis_error # => #<RuntimeError: ERR Operation against a key holding the wrong kind of value>
  }
}

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*") { |channel, 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.

Inactivity checks

Sometimes a network connection may hang in ways which are difficult to detect or involve very long timeouts before they can be detected from the application layer. This is especially true of Redis Pubsub connections, as they are not request-response driven. It is very difficult for a listening client to descern between a hung connection and a server with nothing to say.

To start an application layer ping-pong mechanism for testing connection liveness, call the following at any time on a client:

redis.configure_inactivity_check(5, 3)

This configures a PING command to be sent if 5 seconds elapse without receiving any data from the server, and a reconnection to be triggered if a futher 3 seconds elapse after the PING is submitted.

This configuration is per client, you may choose different value for clients with different expected traffic patterns, or activate it on some and not at all on others.

PING and Pubsub

Because the Redis Pubsub protocol limits the set of valid commands on a connection once it is in "Pubsub" mode, PING is not supported in this case (though it may be in future, see redis/redis#420). In order to create some valid request-response traffic on the connection, a Pubsub connection will issue SUBSCRIBE "__em-hiredis-ping", followed by a corresponding UNSUBSCRIBE immediately on success of the subscribe. While less than ideal, this is the case where an application layer inactivity check is most valuable, and so the trade off is reasonable until PING is supported correctly on Pubsub connections.

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's People

Contributors

abrom avatar benpickles avatar bmxpert1 avatar dgraham avatar doersf avatar mdpye avatar mloughran avatar olleolleolle avatar peikk0 avatar pietern avatar qrush avatar thibaut avatar titanous avatar zubkonst avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

em-hiredis's Issues

Em-hiredis doesn't appear to support unix domain sockets

From the redis docs: "The typical latency of a 1 GBits/s network is about 200 us, while the latency with a Unix domain socket can be as low as 30 us. "

It doesn't appear that URI can handle such a thing as: redis:///tmp/redis.sock

Here is a super ugly, quick and dirty, monkey patch to support it in em-hiredis. I humbly apologize for all of its overwhelming ugliness.

require 'em-hiredis'

module EventMachine
module Hiredis
def self.setup(uri = nil)
if uri =~ /^redis:////
Client.new(uri.split(/^redis:///)[1],nil,nil,nil)
else
url = URI(uri || ENV["REDIS_URL"] || "redis://127.0.0.1:6379/0")
Client.new(url.host, url.port, url.password, url.path[1..-1])
end
end
end
end

EventMachine.run {
redis = EM::Hiredis.connect("redis:///tmp/redis.sock")
}

You have to add this to your redis server's conf:

create a unix domain socket to listen on

unixsocket /tmp/redis.sock

set permissions for the socket

unixsocketperm 755

EM::Hiredis.connect creates 2 connection when using just pubsub?

Hello,

If im trying to use just the subscription part of pubsub:

redis = EM::Hiredis.connect
redis.pubsub.subscribe("foo") { |msg|
  p [:sub1, msg]
} 

Would Hiredis always create two connections to redis?

Meaning I would have to do this to really close them both ?

redis.pubsub.close_connection
redis.close_connection

Also, a related question- do I have to unsubscribe before closing the pubsub connection?

Thanks!

pubsub command

The client uses the pubsub method to create and return the pubsub client. This masks the redis PUBSUB command.
There are two ways I can think of around this

  1. Change the name of the pubsub method to something like pubsub_client
  2. Pass self to PubsubClient.new in the pubsub method and keep a reference to it in the Pubsubclient object. Forward the call to this Client object in a method called pubsub on the PubsubClient object.

em-hiredis-0.3.0 doesn't even load, MacOSX stock Ruby

Trying to get a package that depends on em-hiredis up and running, I pulled the latest and get the following:

$ irb

require 'em-hiredis'
SyntaxError: /Library/Ruby/Gems/1.8/gems/em-hiredis-0.3.0/lib/em-hiredis/client.rb:20: syntax error, unexpected '=', expecting '|'
self.send(:define_method, name.to_sym) { |keys, args=[]|
^
/Library/Ruby/Gems/1.8/gems/em-hiredis-0.3.0/lib/em-hiredis/client.rb:22: syntax error, unexpected '}', expecting kEND
/Library/Ruby/Gems/1.8/gems/em-hiredis-0.3.0/lib/em-hiredis/client.rb:28: syntax error, unexpected '=', expecting '|'
singleton.send(:define_method, name.to_sym) { |keys, args=[]|
^
/Library/Ruby/Gems/1.8/gems/em-hiredis-0.3.0/lib/em-hiredis/client.rb:30: syntax error, unexpected '}', expecting kEND
/Library/Ruby/Gems/1.8/gems/em-hiredis-0.3.0/lib/em-hiredis/client.rb:35: syntax error, unexpected tSTAR, expecting tAMPER
..., lua_sha, keys.size, *keys, *args).callback(
^
/Library/Ruby/Gems/1.8/gems/em-hiredis-0.3.0/lib/em-hiredis/client.rb:39: syntax error, unexpected tSTAR, expecting tAMPER
self.eval(lua, keys.size, *keys, *args)
^
/Library/Ruby/Gems/1.8/gems/em-hiredis-0.3.0/lib/em-hiredis/client.rb:110: syntax error, unexpected $end, expecting kEND
from /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/rubygems/custom_require.rb:31:in gem_original_require' from /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/rubygems/custom_require.rb:31:inrequire'
from /Library/Ruby/Gems/1.8/gems/em-hiredis-0.3.0/lib/em-hiredis.rb:64
from /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/rubygems/custom_require.rb:36:in gem_original_require' from /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/rubygems/custom_require.rb:36:inrequire'
from (irb):1

Please update gem dependency: hiredis

I was installing faye gem, but compiling failed.

gem install hiredis -v 0.3.2
Building native extensions.  This could take a while...
ERROR:  Error installing hiredis:
    ERROR: Failed to build gem native extension.

        /Users/bryann83/.rbenv/versions/ree-1.8.7-2011.03/bin/ruby extconf.rb
cc -std=c99 -pedantic -c -O3 -fPIC -Wall -W -Wstrict-prototypes -Wwrite-strings   -arch i386 -arch x86_64 -g -ggdb  net.c
clang: warning: argument unused during compilation: '-ggdb'
cc -std=c99 -pedantic -c -O3 -fPIC -Wall -W -Wstrict-prototypes -Wwrite-strings   -arch i386 -arch x86_64 -g -ggdb  hiredis.c
clang: warning: argument unused during compilation: '-ggdb'
hiredis.c:797:31: error: second argument to 'va_arg' is of incomplete type 'void'
                    va_arg(ap,void);
                    ~~~~~~~~~~^~~~~
/usr/bin/../lib/clang/3.1/include/stdarg.h:35:50: note: expanded from macro 'va_arg'
#define va_arg(ap, type)    __builtin_va_arg(ap, type)
                                                 ^
1 error generated.
make: *** [hiredis.o] Error 1
creating Makefile

details:

ruby -v

ruby 1.8.7 (2011-02-18 patchlevel 334) [i686-darwin11.3.0], MBARI 0x6770, Ruby Enterprise Edition 2011.03

brew info redis
redis 2.4.7
http://redis.io/
/usr/local/Cellar/redis/2.4.7 (9 files, 464K)

installed hiredis v0.4.4 without problem, but latest Faye is using >= em-hiredis v0.0.1 and em-hiredis is currently using ~> hiredis 0.3.2

I was not sure if ruby ree-1.8.7 is causing this, but work project is using ree-1.8.7 :-/

Simple use of EM::Synchrony#sync causes 'root fiber' FiberError -- my fault?

This program

require 'em-synchrony' ## v1.0.0                                                                                                                               
require 'em-hiredis'   ## v0.1.0                                                                                                                               

module EventMachine
  module Hiredis
    class Client

      def self.connect(host = 'localhost', port = 6379)
        conn = new(host, port)
        EM::Synchrony.sync conn.connect
    conn
      end

      alias :old_method_missing :method_missing
      def method_missing(sym, *args)
        EM::Synchrony.sync old_method_missing(sym, *args)
      end
    end
  end
end

EventMachine.synchrony do
  redis = EM::Hiredis.connect

  redis.set('foo', 'bar')
  puts redis.get('foo')

  EM.stop
end

dies like this


$ ruby /tmp/reddy.rb 
/home/blt/.rvm/gems/ruby-1.9.3-p0/gems/em-synchrony-1.0.0/lib/em-synchrony.rb:58:in `yield': can't yield from root fiber (FiberError)
    from /home/blt/.rvm/gems/ruby-1.9.3-p0/gems/em-synchrony-1.0.0/lib/em-synchrony.rb:58:in `sync'
    from /tmp/reddy.rb:16:in `method_missing'
    from /home/blt/.rvm/gems/ruby-1.9.3-p0/gems/em-hiredis-0.1.0/lib/em-hiredis/client.rb:119:in `select'
    from /home/blt/.rvm/gems/ruby-1.9.3-p0/gems/em-hiredis-0.1.0/lib/em-hiredis/client.rb:38:in `block in connect'
    from /home/blt/.rvm/gems/ruby-1.9.3-p0/gems/em-hiredis-0.1.0/lib/em-hiredis/event_emitter.rb:8:in `call'
    from /home/blt/.rvm/gems/ruby-1.9.3-p0/gems/em-hiredis-0.1.0/lib/em-hiredis/event_emitter.rb:8:in `block in emit'
    from /home/blt/.rvm/gems/ruby-1.9.3-p0/gems/em-hiredis-0.1.0/lib/em-hiredis/event_emitter.rb:8:in `each'
    from /home/blt/.rvm/gems/ruby-1.9.3-p0/gems/em-hiredis-0.1.0/lib/em-hiredis/event_emitter.rb:8:in `emit'
    from /home/blt/.rvm/gems/ruby-1.9.3-p0/gems/em-hiredis-0.1.0/lib/em-hiredis/connection.rb:15:in `connection_completed'
    from /home/blt/.rvm/gems/ruby-1.9.3-p0/gems/eventmachine-1.0.0.beta.4/lib/eventmachine.rb:179:in `run_machine'
    from /home/blt/.rvm/gems/ruby-1.9.3-p0/gems/eventmachine-1.0.0.beta.4/lib/eventmachine.rb:179:in `run'
    from /home/blt/.rvm/gems/ruby-1.9.3-p0/gems/em-synchrony-1.0.0/lib/em-synchrony.rb:27:in `synchrony'
    from /tmp/reddy.rb:22:in `<main>'

I find this deeply confusing. Why doesn't it work and am I at fault? If so, what can I do differently? Unless I've glossed over something, this is kosher, per the em-synchrony README.

"stack level too deep" error with large splatted array parameters

There seems to be a well-know issue with passing too many parameters to a function in Ruby. It's easy to hit this with when using Redis. The Redis client appears to have a way around this by passing a single array type as an argument. I am wondering how i can achieve the same with EM::Hiredis.

require "em-hiredis"

EM::run do
    redis = EM::Hiredis::connect("redis://localhost:6379/5")

    bigRange = (1..1000000)
    p "Starting big sunion."
    redis.sunion(*bigRange)
    # now "stack level too deep" error occurs
    p "Got out alive."
end

REF: redis/redis-rb#122

Recent changes seem to have broken some downstream projects?

I was having an issue with Firehose that I had to resolve by downgrading to the previous release (0.1.1 or something) of em-redis. The issue seemed to be with maintaining the connection to redis; I can try to post more specifics about what I was seeing if that might help but just wanted to let maintainers know something might be up here.

close_connection doesn't seem to work

I'm using em-hiredis v0.1.0 with cramp 0.15.1 and after I call close_connection on @pub and @sub the redis server still says says there are two connections.

  def close_redis
    @subscriptions.each do |sub|
      @sub.unsubscribe(sub)
    end

    @pub.close_connection
    @sub.close_connection
  end

Nested callbacks not working

Thanks for the great library!

I'm hitting a weird problem. In some cases a set() is silently failing. It's not setting the value in redis and it isn't calling the callback or errback.

This works:

get = $redis.get('foo')
set = $redis.set("bar", "2") # succeeds
get.callback { |to|
  puts('hi') # shows up

and this doesn't:

get = $redis.get('foo')
get.callback { |to|
  set = $redis.set("bar", "2") # fails without calling set.callback or set.errback
  puts('hi') # shows up

Am I doing something wrong or is this a bug?

Thanks!

Dan

warnings when running Rails test suite (instance variable not initialized)

Seeing a lot of these when running the Rails 5 beta 2 tests:

/Users/bronson/.gem/ruby/2.2.2/gems/em-hiredis-0.3.1/lib/em-hiredis/base_client.rb:231: warning: instance variable @inactivity_timer not initialized
/Users/bronson/.gem/ruby/2.2.2/gems/em-hiredis-0.3.1/lib/em-hiredis/base_client.rb:236: warning: instance variable @inactivity_trigger_secs not initialized

Easy workaround appears to be to put @inactivity_timer = @inactivity_trigger_secs = nil in BaseClient's initializer. Can submit a PR for that if you want.

Configuring redis:// url without port defaults the port to.... zero?

Hi,

We're using Faye::Redis running in Puma under JRuby. We had an issue for quite a while where Faye communication would just.... stop.

It turns out we were using a url like redis://host without specifying the default port (6379). After diving through the code, I found that em-hiredis defaults the port to nil here and then passes the nil port along to EM.connect.

Diving into Eventmachine, it appears that it calls to_i on the port here (at least in JRuby). This means that it is trying to connect to port 0, which doesn't make a lot of sense. Furthermore, there is no exception. It appears that em-hiredis simply blocks on IO until the Puma connection times out.

I'm not sure what the correct fix would be. My gut feeling is that em-hiredis should simply default the port to 6379. I also feel that Eventmachine shouldn't accept a nil port, because a missing port can't mean anything reasonable.

Thanks, let me know what you think!

Subscribing to a symbol should work.

If you subscribe to a symbol using PubsubClient, the subscription is made successfully, but the callback is never called because when a message is received it searches the subscriptions hash for a string and misses the entry at the symbol.

Database is not selected automatically

redis://user:password@localhost:6379/2

Using this URI to connect to Hiredis, the database is not selected automatically. It always defaults to DB 0. Inspecting the Client object after creation, the instance variable @db has the correct value.

@db = '2'

But when I'm doing a manual select of the DB, it works like a charm.

con = EM::Hiredis.connect('redis://user:password@localhost:6379/2')
con.select('2')

Thanks Marc

Ship new gem version

Please! 0.1.0 or 1.0.0, let's ship it! If there's anything I can do please let me know.

Handle connection timeouts / refusals

The client retries connections forever in case of failure, and there's no way for the client's client to detect that. Need a callback that can indicate that the redis client is in an error state - perhaps a callback that gets invoked in the else condition of the on(:closed) block in client.rb[1] - it could provide a count of the number of sequential failed connections so far.

I'll tackle this if no one else is already on it / sees an easy way. Just checked out the code and the spec suite hung for a very long time after four tests, so it'll need to wait until I have a bit more time to devote to debugging.

[1] https://github.com/mloughran/em-hiredis/blob/master/lib/em-hiredis/client.rb#L35

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.