pax_global_header00006660000000000000000000000064133711111410014503gustar00rootroot0000000000000052 comment=61bb83eef86d8dc730f3352ba1d0aabe548a4d3a hiredis-rb-0.6.3/000077500000000000000000000000001337111114100135415ustar00rootroot00000000000000hiredis-rb-0.6.3/.gitignore000066400000000000000000000003151337111114100155300ustar00rootroot00000000000000*.rbc ext/**/*.log ext/**/Makefile ext/**/*.conf *.so *.bundle .bundle .config .yardoc Gemfile.lock InstalledFiles _yardoc coverage doc/ lib/bundler/man pkg rdoc spec/reports test/tmp test/version_tmp tmp hiredis-rb-0.6.3/.gitmodules000066400000000000000000000001401337111114100157110ustar00rootroot00000000000000[submodule "vendor/hiredis"] path = vendor/hiredis url = https://github.com/redis/hiredis.git hiredis-rb-0.6.3/.travis.yml000066400000000000000000000003561337111114100156560ustar00rootroot00000000000000language: ruby sudo: false rvm: - 1.9.3 - 2.0.0 - 2.1.10 - 2.2.10 - 2.3.8 - 2.4.5 - 2.5.3 - ruby-2.6.0-preview2 - jruby-19mode notifications: email: on_success: never recipients: - badboy@archlinux.us hiredis-rb-0.6.3/CHANGELOG.md000066400000000000000000000057141337111114100153610ustar00rootroot00000000000000### 0.6.3 (2018-11-08) * Fix the packaged gem ### 0.6.2 (2018-11-08) * Update to use mew hiredis 0.14 * Fix bugs with values being garbage collected while still in use ### 0.6.1 (2015-12-29) * Disable compilation of C extension on unsupported platforms (such as Windows) ### 0.6.0 (2015-02-08) * Update to use new hiredis 0.12 * Small fixes to make tests work correctly * Do not test on 1.8.7, 1.9.2 or ree anymore ### 0.5.3 * Add license to gemspec (see #28). ### 0.5.2 * Fix build issue on FreeBSD (see #24). ### 0.5.1 * Fix memory leak for MRI >= 1.9.2 introduced in 0.5.0 (see #22). ### 0.5.0 * Update calls to deprecated Ruby functions with their non-deprecated equivalents (see #20 and f85e8c65). * Update hiredis to 0.11.0. * Reduced number of objects to garbage collect on Rubinius (see #13). * Configurable `make` command (see #5). ### 0.4.5 * The protocol reader now forces all strings to be encoded using `Encoding.default_external`. ### 0.4.4 * Make tests explicitly require files from the local tree to prevent files from the search path to be accidentally required. ### 0.4.3 * Fix bug that caused EAGAIN to be raised after the cumulative time spent waiting for the socket to become readable/writable exceeded the connection-wide timeout. ### 0.4.2 (unreleased) * Use patched version of hiredis to support multi bulk depth of 2. ### 0.4.1 * Block indefinitely when timeout is set to zero. ### 0.4.0 * Refactor both the pure Ruby and the native connection class to use non-blocking I/O. The code now uses `IO.select` for the pure Ruby connection class, and `rb_thread_select` for the native connection class, to detect if a socket is readable/writable. This makes the code more portable (w.r.t. timeouts on connect/read/write), and more friendly towards threads running in the same interpreter (they can now be properly scheduled while hiredis blocks on select(2)). * Add `#flush` method to connection class that flushes the write buffer to the socket. This buffer was previously only flushed whenever `#read` was called. ### 0.3.2 * Always statically link to the bundled hiredis version instead of searching the system-wide paths. * Update hiredis to 0.10.0. ### 0.3.1 * Fix bug where one or more arguments passed to #write were garbage collected before being appended to the write buffer. ### 0.3.0 * Modify `Connection#connect` and `Connection#connect_unix` to accept an extra timeout argument. When connecting times out, `Errno::ETIMEDOUT` is raised. The timeout value should be given as number of microseconds to wait. * Add support for connecting to Unix sockets via `Connection#connect_unix`. * Drop dependency on redis-rb so it can be used independently, or in another library that doesn't require redis-rb. * Add pure Ruby protocol parser and connection class to use as fallback when the extension cannot be loaded. These classes have the same API as the extension and use the same unit tests to ensure compatibility. hiredis-rb-0.6.3/COPYING000066400000000000000000000027141337111114100146000ustar00rootroot00000000000000Copyright (c) 2010-2012, Pieter Noordhuis All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Redis nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. hiredis-rb-0.6.3/Gemfile000066400000000000000000000000451337111114100150330ustar00rootroot00000000000000source "http://rubygems.org" gemspec hiredis-rb-0.6.3/README.md000066400000000000000000000121161337111114100150210ustar00rootroot00000000000000# hiredis-rb [![Build Status](https://travis-ci.org/redis/hiredis-rb.svg?branch=master)](https://travis-ci.org/redis/hiredis-rb) Ruby extension that wraps [hiredis](http://github.com/redis/hiredis). Both the synchronous connection API and a separate protocol reader are supported. It is primarily intended to speed up parsing multi bulk replies. ## Install Install with Rubygems: gem install hiredis ## Usage Hiredis can be used as standalone library, or be used together with redis-rb. The latter adds in support for hiredis in 2.2. ### redis-rb To use hiredis from redis-rb, it needs to be available in Ruby's load path. Using Bundler, this comes down to adding the following lines: ``` ruby gem "hiredis", "~> 0.6.0" gem "redis", ">= 3.2.0" ``` To use hiredis with redis-rb, you need to require `redis/connection/hiredis` before creating a new connection. This makes sure that hiredis will be used instead of the pure Ruby connection code and protocol parser. Doing so in the Gemfile is done by adding a `:require` option to the line adding the redis-rb dependency: ``` ruby gem "redis", ">= 3.2.0", :require => ["redis", "redis/connection/hiredis"] ``` You can use Redis normally, as you would with the pure Ruby version. ### Standalone: Connection A connection to Redis can be opened by creating an instance of `Hiredis::Connection` and calling `#connect`: ``` ruby conn = Hiredis::Connection.new conn.connect("127.0.0.1", 6379) ``` Commands can be written to Redis by calling `#write` with an array of arguments. You can call write more than once, resulting in a pipeline of commands. ``` ruby conn.write ["SET", "speed", "awesome"] conn.write ["GET", "speed"] ``` After commands are written, use `#read` to receive the subsequent replies. Make sure **not** to call `#read` more than you have replies to read, or the connection will block indefinitely. You _can_ use this feature to implement a subscriber (for Redis Pub/Sub). ``` ruby >> conn.read => "OK" >> conn.read => "awesome" ``` When the connection was closed by the server, an error of the type `Hiredis::Connection::EOFError` will be raised. For all I/O related errors, the Ruby built-in `Errno::XYZ` errors will be raised. All other errors (such as a protocol error) result in a `RuntimeError`. You can skip loading everything and just load `Hiredis::Connection` by requiring `hiredis/connection`. ### Standalone: Reply parser Only using hiredis for the reply parser can be very useful in scenarios where the I/O is already handled by another component (such as EventMachine). You can skip loading everything and just load `Hiredis::Reader` by requiring `hiredis/reader`. Use `#feed` on an instance of `Hiredis::Reader` to feed the stream parser with new data. Use `#read` to get the parsed replies one by one: ``` ruby >> reader = Hiredis::Reader.new >> reader.feed("*2\r\n$7\r\nawesome\r\n$5\r\narray\r\n") >> reader.gets => ["awesome", "array"] ``` ## Benchmarks These numbers were generated by running `benchmark/throughput.rb` against `ruby 1.9.2p180 (2011-02-18 revision 30909) [x86_64-darwin10.6.0]`. The benchmark compares redis-rb with the Ruby connection and protocol code against redis-rb with hiredis to handle I/O and reply parsing. For simple line or bulk replies, the throughput improvement is insignificant, while the larger multi bulk responses will have a noticeable higher throughput. user system total real redis-rb: 1x SET pipeline, 10000 times 0.260000 0.140000 0.400000 ( 0.726216) hiredis: 1x SET pipeline, 10000 times 0.170000 0.130000 0.300000 ( 0.602332) redis-rb: 10x SET pipeline, 10000 times 1.550000 0.660000 2.210000 ( 2.231505) hiredis: 10x SET pipeline, 10000 times 0.400000 0.140000 0.540000 ( 0.995266) redis-rb: 1x GET pipeline, 10000 times 0.270000 0.150000 0.420000 ( 0.730322) hiredis: 1x GET pipeline, 10000 times 0.170000 0.130000 0.300000 ( 0.604060) redis-rb: 10x GET pipeline, 10000 times 1.480000 0.640000 2.120000 ( 2.122215) hiredis: 10x GET pipeline, 10000 times 0.380000 0.130000 0.510000 ( 0.925731) redis-rb: 1x LPUSH pipeline, 10000 times 0.260000 0.150000 0.410000 ( 0.725292) hiredis: 1x LPUSH pipeline, 10000 times 0.160000 0.130000 0.290000 ( 0.592466) redis-rb: 10x LPUSH pipeline, 10000 times 1.540000 0.660000 2.200000 ( 2.202709) hiredis: 10x LPUSH pipeline, 10000 times 0.360000 0.130000 0.490000 ( 0.940361) redis-rb: 1x LRANGE(100) pipeline, 1000 times 0.240000 0.020000 0.260000 ( 0.307504) hiredis: 1x LRANGE(100) pipeline, 1000 times 0.050000 0.020000 0.070000 ( 0.100293) redis-rb: 1x LRANGE(1000) pipeline, 1000 times 2.100000 0.030000 2.130000 ( 2.353551) hiredis: 1x LRANGE(1000) pipeline, 1000 times 0.320000 0.030000 0.350000 ( 0.472789) ## License This code is released under the BSD license, after the license of hiredis. hiredis-rb-0.6.3/Rakefile000066400000000000000000000022651337111114100152130ustar00rootroot00000000000000require "bundler" Bundler::GemHelper.install_tasks require "rbconfig" require "rake/testtask" require "rake/extensiontask" if RUBY_PLATFORM =~ /java|mswin|mingw/i task :rebuild do # no-op end else Rake::ExtensionTask.new('hiredis_ext') do |task| # Pass --with-foo-config args to extconf.rb task.config_options = ARGV[1..-1] || [] task.lib_dir = File.join(*['lib', 'hiredis', 'ext']) end namespace :hiredis do task :clean do # Fetch hiredis if not present if !File.directory?("vendor/hiredis/.git") system("git submodule update --init") end RbConfig::CONFIG['configure_args'] =~ /with-make-prog\=(\w+)/ make_program = $1 || ENV['make'] unless make_program then make_program = (/mswin/ =~ RUBY_PLATFORM) ? 'nmake' : 'make' end system("cd vendor/hiredis && #{make_program} clean") end end # "rake clean" should also clean bundled hiredis Rake::Task[:clean].enhance(['hiredis:clean']) # Build from scratch task :rebuild => [:clean, :compile] end task :default => [:rebuild, :test] desc "Run tests" Rake::TestTask.new(:test) do |t| t.pattern = 'test/**/*_test.rb' t.verbose = true end hiredis-rb-0.6.3/appveyor.yml000066400000000000000000000005601337111114100161320ustar00rootroot00000000000000environment: matrix: - ruby_version: "21" - ruby_version: "22" install: - SET PATH=C:\Ruby%ruby_version%\bin;%PATH% - ruby --version - ruby -e 'p([:engine, RUBY_ENGINE, :platform, RUBY_PLATFORM])' - gem --version - gem install bundler --no-document - bundler --version - bundle install --retry=3 test_script: - bundle exec rake build: off hiredis-rb-0.6.3/benchmark/000077500000000000000000000000001337111114100154735ustar00rootroot00000000000000hiredis-rb-0.6.3/benchmark/reader.rb000066400000000000000000000030231337111114100172600ustar00rootroot00000000000000# Compare throughput of pure-Ruby reply reader vs extension # # Run with # # $ ruby -Ilib benchmark/reader.rb # require "benchmark" require "hiredis/ruby/reader" begin require "hiredis/ext/reader" rescue LoadError end N = 100_000 def benchmark(b, title, klass, pipeline = 1) reader = klass.new data = "+OK\r\n" GC.start b.report("#{title}: Status reply") do (N / pipeline).times do pipeline.times { reader.feed(data) } pipeline.times { reader.gets } end end data = "$10\r\nxxxxxxxxxx\r\n" GC.start b.report("#{title}: Bulk reply (10 bytes)") do (N / pipeline).times do pipeline.times { reader.feed(data) } pipeline.times { reader.gets } end end data = "*10\r\n" 10.times { data << "$10\r\nxxxxxxxxxx\r\n" } GC.start b.report("#{title}: Multi-bulk reply (10x 10 bytes)") do (N / pipeline).times do pipeline.times { reader.feed(data) } pipeline.times { reader.gets } end end data = "*1\r\n#{data}" GC.start b.report("#{title}: Nested multi-bulk reply (1x 10x 10 bytes)") do (N / pipeline).times do pipeline.times { reader.feed(data) } pipeline.times { reader.gets } end end end pipeline = (ARGV.shift || 1).to_i puts "\n%d replies in pipelines of %d replies\n" % [N, pipeline] Benchmark.bm(50) do |b| if defined?(Hiredis::Ext::Reader) benchmark(b, "Ext", Hiredis::Ext::Reader, pipeline) puts end if defined?(Hiredis::Ruby::Reader) benchmark(b, "Ruby", Hiredis::Ruby::Reader, pipeline) puts end end hiredis-rb-0.6.3/benchmark/throughput.rb000066400000000000000000000032161337111114100202330ustar00rootroot00000000000000# Compare performance of redis-rb with and without hiredis # # Run with # # $ ruby -Ilib benchmark/throughput.rb # require "rubygems" require "benchmark" require "redis/connection/hiredis" require "redis/connection/ruby" require "redis" DB = 9 $ruby = Redis.new(:db => DB) $ruby.client.instance_variable_set(:@connection,Redis::Connection::Ruby.new) $hiredis = Redis.new(:db => DB) $hiredis.client.instance_variable_set(:@connection,Redis::Connection::Hiredis.new) # make sure both are connected $ruby.ping $hiredis.ping # test if db is empty if $ruby.dbsize > 0 STDERR.puts "Database \##{DB} is not empty!" exit 1 end def without_gc GC.start GC.disable yield ensure GC.enable end def pipeline(b,num,size,title,cmd) commands = size.times.map { cmd } x = without_gc { b.report("redis-rb: %2dx #{title} pipeline, #{num} times" % size) { num.times { $ruby.client.call_pipelined(commands) } } } y = without_gc { b.report(" hiredis: %2dx #{title} pipeline, #{num} times" % size) { num.times { $hiredis.client.call_pipelined(commands) } } } puts "%.1fx" % [1 / (y.real / x.real)] end Benchmark.bm(50) do |b| pipeline(b,10000, 1, "SET", %w(set foo bar)) pipeline(b,10000,10, "SET", %w(set foo bar)) puts pipeline(b,10000, 1, "GET", %w(get foo)) pipeline(b,10000,10, "GET", %w(get foo)) puts pipeline(b,10000, 1, "LPUSH", %w(lpush list fooz)) pipeline(b,10000,10, "LPUSH", %w(lpush list fooz)) puts pipeline(b,1000, 1, "LRANGE(100)", %w(lrange list 0 99)) puts pipeline(b,1000, 1, "LRANGE(1000)", %w(lrange list 0 999)) puts # Clean up... $ruby.flushdb end hiredis-rb-0.6.3/examples/000077500000000000000000000000001337111114100153575ustar00rootroot00000000000000hiredis-rb-0.6.3/examples/em.rb000066400000000000000000000035361337111114100163140ustar00rootroot00000000000000# Example of using hiredis-rb in pub/sub with EventMachine. # # Make sure you have both EventMachine and hiredis installed. # Then, run the following command twice with pub *and* sub to see # messages passing through Redis. # # ruby -rubygems -Ilib examples/em.rb [pub|sub] # require "eventmachine" require "hiredis/reader" module Hiredis module EM class Connection < ::EM::Connection CRLF = "\r\n".freeze def initialize super @reader = Reader.new @callbacks = [] end def receive_data(data) @reader.feed(data) until (reply = @reader.gets) == false receive_reply(reply) end end def receive_reply(reply) callback = @callbacks.shift callback.call(reply) if callback end def send_command(*args) args = args.flatten send_data("*" + args.size.to_s + CRLF) args.each do |arg| arg = arg.to_s send_data("$" + arg.size.to_s + CRLF + arg + CRLF) end end def method_missing(sym, *args, &callback) send_command(sym, *args) @callbacks.push callback end end end end $cnt = 0 class Publisher < Hiredis::EM::Connection def post_init publish! end def publish! publish "channel", "hithere" do |reply| $cnt += 1 publish! end end end class Subscriber < Hiredis::EM::Connection def post_init subscribe "channel" end def receive_reply(reply) $cnt += 1 end end EventMachine.run do klass = case ARGV.shift when "pub" Publisher when "sub" Subscriber else raise "Specify pub or sub" end num = (ARGV.shift || 5).to_i num.times { EventMachine.connect("localhost", 6379, klass) } EventMachine::PeriodicTimer.new(1) do print "%s: %6d\r" % [klass.name, $cnt] STDOUT.flush $cnt = 0 end end hiredis-rb-0.6.3/ext/000077500000000000000000000000001337111114100143415ustar00rootroot00000000000000hiredis-rb-0.6.3/ext/hiredis_ext/000077500000000000000000000000001337111114100166505ustar00rootroot00000000000000hiredis-rb-0.6.3/ext/hiredis_ext/connection.c000066400000000000000000000341361337111114100211620ustar00rootroot00000000000000#include #include #include "hiredis_ext.h" typedef struct redisParentContext { redisContext *context; struct timeval *timeout; } redisParentContext; static void parent_context_try_free_context(redisParentContext *pc) { if (pc->context) { redisFree(pc->context); pc->context = NULL; } } static void parent_context_try_free_timeout(redisParentContext *pc) { if (pc->timeout) { free(pc->timeout); pc->timeout = NULL; } } static void parent_context_try_free(redisParentContext *pc) { parent_context_try_free_context(pc); parent_context_try_free_timeout(pc); } static void parent_context_mark(redisParentContext *pc) { // volatile until rb_gc_mark volatile VALUE root; if (pc->context && pc->context->reader) { root = (VALUE)redisReaderGetObject(pc->context->reader); if (root != 0 && TYPE(root) == T_ARRAY) { rb_gc_mark(root); } } } static void parent_context_free(redisParentContext *pc) { parent_context_try_free(pc); free(pc); } static void parent_context_raise(redisParentContext *pc) { int err; char errstr[1024]; /* Copy error and free context */ err = pc->context->err; snprintf(errstr,sizeof(errstr),"%s",pc->context->errstr); parent_context_try_free(pc); switch(err) { case REDIS_ERR_IO: /* Raise native Ruby I/O error */ rb_sys_fail(0); break; case REDIS_ERR_EOF: /* Raise native Ruby EOFError */ rb_raise(rb_eEOFError,"%s",errstr); break; default: /* Raise something else */ rb_raise(rb_eRuntimeError,"%s",errstr); } } static VALUE connection_parent_context_alloc(VALUE klass) { redisParentContext *pc = malloc(sizeof(*pc)); pc->context = NULL; pc->timeout = NULL; return Data_Wrap_Struct(klass, parent_context_mark, parent_context_free, pc); } /* * The rb_fdset_t API was introduced in Ruby 1.9.1. The rb_fd_thread_select * function was introduced in a later version. Therefore, there are one more * versions where we cannot simply test HAVE_RB_FD_INIT and be done, we have to * explicitly test for HAVE_RB_THREAD_FD_SELECT (also see extconf.rb). */ #ifdef HAVE_RB_THREAD_FD_SELECT typedef rb_fdset_t _fdset_t; #define _fd_zero(f) rb_fd_zero(f) #define _fd_set(n, f) rb_fd_set(n, f) #define _fd_clr(n, f) rb_fd_clr(n, f) #define _fd_isset(n, f) rb_fd_isset(n, f) #define _fd_copy(d, s, n) rb_fd_copy(d, s, n) #define _fd_ptr(f) rb_fd_ptr(f) #define _fd_init(f) rb_fd_init(f) #define _fd_term(f) rb_fd_term(f) #define _fd_max(f) rb_fd_max(f) #define _thread_fd_select(n, r, w, e, t) rb_thread_fd_select(n, r, w, e, t) #else typedef fd_set _fdset_t; #define _fd_zero(f) FD_ZERO(f) #define _fd_set(n, f) FD_SET(n, f) #define _fd_clr(n, f) FD_CLR(n, f) #define _fd_isset(n, f) FD_ISSET(n, f) #define _fd_copy(d, s, n) (*(d) = *(s)) #define _fd_ptr(f) (f) #define _fd_init(f) FD_ZERO(f) #define _fd_term(f) (void)(f) #define _fd_max(f) FD_SETSIZE #define _thread_fd_select(n, r, w, e, t) rb_thread_select(n, r, w, e, t) #endif static int __wait_readable(int fd, const struct timeval *timeout, int *isset) { struct timeval to; struct timeval *toptr = NULL; _fdset_t fds; /* Be cautious: a call to rb_fd_init to initialize the rb_fdset_t structure * must be paired with a call to rb_fd_term to free it. */ _fd_init(&fds); _fd_set(fd, &fds); /* rb_thread_{fd_,}select modifies the passed timeval, so we pass a copy */ if (timeout != NULL) { memcpy(&to, timeout, sizeof(to)); toptr = &to; } if (_thread_fd_select(fd + 1, &fds, NULL, NULL, toptr) < 0) { _fd_term(&fds); return -1; } if (_fd_isset(fd, &fds) && isset) { *isset = 1; } _fd_term(&fds); return 0; } static int __wait_writable(int fd, const struct timeval *timeout, int *isset) { struct timeval to; struct timeval *toptr = NULL; _fdset_t fds; /* Be cautious: a call to rb_fd_init to initialize the rb_fdset_t structure * must be paired with a call to rb_fd_term to free it. */ _fd_init(&fds); _fd_set(fd, &fds); /* rb_thread_{fd_,}select modifies the passed timeval, so we pass a copy */ if (timeout != NULL) { memcpy(&to, timeout, sizeof(to)); toptr = &to; } if (_thread_fd_select(fd + 1, NULL, &fds, NULL, toptr) < 0) { _fd_term(&fds); return -1; } if (_fd_isset(fd, &fds) && isset) { *isset = 1; } _fd_term(&fds); return 0; } static VALUE connection_generic_connect(VALUE self, redisContext *c, VALUE arg_timeout) { redisParentContext *pc; struct timeval tv; struct timeval *timeout = NULL; int writable = 0; int optval = 0; socklen_t optlen = sizeof(optval); Data_Get_Struct(self,redisParentContext,pc); if (c->err) { char buf[1024]; int err; /* Copy error and free context */ err = c->err; snprintf(buf,sizeof(buf),"%s",c->errstr); redisFree(c); if (err == REDIS_ERR_IO) { /* Raise native Ruby I/O error */ rb_sys_fail(0); } else { /* Raise something else */ rb_raise(rb_eRuntimeError,"%s",buf); } } /* Default to context-wide timeout setting */ if (pc->timeout != NULL) { timeout = pc->timeout; } /* Override timeout when timeout argument is available */ if (arg_timeout != Qnil) { tv.tv_sec = NUM2INT(arg_timeout) / 1000000; tv.tv_usec = NUM2INT(arg_timeout) % 1000000; timeout = &tv; } /* Wait for socket to become writable */ if (__wait_writable(c->fd, timeout, &writable) < 0) { goto sys_fail; } if (!writable) { errno = ETIMEDOUT; goto sys_fail; } /* Check for socket error */ if (getsockopt(c->fd, SOL_SOCKET, SO_ERROR, &optval, &optlen) < 0) { goto sys_fail; } if (optval) { errno = optval; goto sys_fail; } parent_context_try_free_context(pc); pc->context = c; pc->context->reader->fn = &redisExtReplyObjectFunctions; return Qnil; sys_fail: redisFree(c); rb_sys_fail(0); } static VALUE connection_connect(int argc, VALUE *argv, VALUE self) { redisContext *c; VALUE arg_host = Qnil; VALUE arg_port = Qnil; VALUE arg_timeout = Qnil; if (argc == 2 || argc == 3) { arg_host = argv[0]; arg_port = argv[1]; if (argc == 3) { arg_timeout = argv[2]; /* Sanity check */ if (NUM2INT(arg_timeout) <= 0) { rb_raise(rb_eArgError, "timeout should be positive"); } } } else { rb_raise(rb_eArgError, "invalid number of arguments"); } c = redisConnectNonBlock(StringValuePtr(arg_host), NUM2INT(arg_port)); return connection_generic_connect(self,c,arg_timeout); } static VALUE connection_connect_unix(int argc, VALUE *argv, VALUE self) { redisContext *c; VALUE arg_path = Qnil; VALUE arg_timeout = Qnil; if (argc == 1 || argc == 2) { arg_path = argv[0]; if (argc == 2) { arg_timeout = argv[1]; /* Sanity check */ if (NUM2INT(arg_timeout) <= 0) { rb_raise(rb_eArgError, "timeout should be positive"); } } } else { rb_raise(rb_eArgError, "invalid number of arguments"); } c = redisConnectUnixNonBlock(StringValuePtr(arg_path)); return connection_generic_connect(self,c,arg_timeout); } static VALUE connection_is_connected(VALUE self) { redisParentContext *pc; Data_Get_Struct(self,redisParentContext,pc); if (pc->context && !pc->context->err) return Qtrue; else return Qfalse; } static VALUE connection_disconnect(VALUE self) { redisParentContext *pc; Data_Get_Struct(self,redisParentContext,pc); if (!pc->context) rb_raise(rb_eRuntimeError,"%s","not connected"); parent_context_try_free(pc); return Qnil; } static VALUE connection_write(VALUE self, VALUE command) { redisParentContext *pc; int argc; char **argv = NULL; size_t *alen = NULL; int i; /* Commands should be an array of commands, where each command * is an array of string arguments. */ if (TYPE(command) != T_ARRAY) rb_raise(rb_eArgError,"%s","not an array"); Data_Get_Struct(self,redisParentContext,pc); if (!pc->context) rb_raise(rb_eRuntimeError,"%s","not connected"); argc = (int)RARRAY_LEN(command); argv = malloc(argc*sizeof(char*)); alen = malloc(argc*sizeof(size_t)); for (i = 0; i < argc; i++) { /* Replace arguments in the arguments array to prevent their string * equivalents to be garbage collected before this loop is done. */ VALUE entry = rb_obj_as_string(rb_ary_entry(command, i)); rb_ary_store(command, i, entry); argv[i] = RSTRING_PTR(entry); alen[i] = RSTRING_LEN(entry); } redisAppendCommandArgv(pc->context,argc,(const char**)argv,alen); free(argv); free(alen); return Qnil; } static VALUE connection_flush(VALUE self) { redisParentContext *pc; redisContext *c; int wdone = 0; Data_Get_Struct(self,redisParentContext,pc); if (!pc->context) rb_raise(rb_eRuntimeError, "not connected"); c = pc->context; while (!wdone) { errno = 0; if (redisBufferWrite(c, &wdone) == REDIS_ERR) { /* Socket error */ parent_context_raise(pc); } if (errno == EAGAIN) { int writable = 0; if (__wait_writable(c->fd, pc->timeout, &writable) < 0) { rb_sys_fail(0); } if (!writable) { errno = EAGAIN; rb_sys_fail(0); } } } return Qnil; } static int __get_reply(redisParentContext *pc, VALUE *reply) { redisContext *c = pc->context; int wdone = 0; void *aux = NULL; /* Try to read pending replies */ if (redisGetReplyFromReader(c,&aux) == REDIS_ERR) { /* Protocol error */ return -1; } if (aux == NULL) { /* Write until the write buffer is drained */ while (!wdone) { errno = 0; if (redisBufferWrite(c, &wdone) == REDIS_ERR) { /* Socket error */ return -1; } if (errno == EAGAIN) { int writable = 0; if (__wait_writable(c->fd, pc->timeout, &writable) < 0) { rb_sys_fail(0); } if (!writable) { errno = EAGAIN; rb_sys_fail(0); } } } /* Read until there is a full reply */ while (aux == NULL) { errno = 0; if (redisBufferRead(c) == REDIS_ERR) { /* Socket error */ return -1; } if (errno == EAGAIN) { int readable = 0; if (__wait_readable(c->fd, pc->timeout, &readable) < 0) { rb_sys_fail(0); } if (!readable) { errno = EAGAIN; rb_sys_fail(0); } /* Retry */ continue; } if (redisGetReplyFromReader(c,&aux) == REDIS_ERR) { /* Protocol error */ return -1; } } } /* Set reply object */ if (reply != NULL) { *reply = (VALUE)aux; } return 0; } static VALUE connection_read(VALUE self) { redisParentContext *pc; volatile VALUE reply; Data_Get_Struct(self,redisParentContext,pc); if (!pc->context) rb_raise(rb_eRuntimeError, "not connected"); if (__get_reply(pc,&reply) == -1) parent_context_raise(pc); return reply; } static VALUE connection_set_timeout(VALUE self, VALUE usecs) { redisParentContext *pc; struct timeval *ptr; Data_Get_Struct(self,redisParentContext,pc); if (NUM2INT(usecs) < 0) { rb_raise(rb_eArgError, "timeout cannot be negative"); } else { parent_context_try_free_timeout(pc); /* A timeout equal to zero means not to time out. This translates to a * NULL timeout for select(2). Only allocate and populate the timeout * when it is a positive integer. */ if (NUM2INT(usecs) > 0) { ptr = malloc(sizeof(*ptr)); ptr->tv_sec = NUM2INT(usecs) / 1000000; ptr->tv_usec = NUM2INT(usecs) % 1000000; pc->timeout = ptr; } } return Qnil; } static VALUE connection_fileno(VALUE self) { redisParentContext *pc; Data_Get_Struct(self,redisParentContext,pc); if (!pc->context) rb_raise(rb_eRuntimeError, "not connected"); return INT2NUM(pc->context->fd); } VALUE klass_connection; void InitConnection(VALUE mod) { klass_connection = rb_define_class_under(mod, "Connection", rb_cObject); rb_global_variable(&klass_connection); rb_define_alloc_func(klass_connection, connection_parent_context_alloc); rb_define_method(klass_connection, "connect", connection_connect, -1); rb_define_method(klass_connection, "connect_unix", connection_connect_unix, -1); rb_define_method(klass_connection, "connected?", connection_is_connected, 0); rb_define_method(klass_connection, "disconnect", connection_disconnect, 0); rb_define_method(klass_connection, "timeout=", connection_set_timeout, 1); rb_define_method(klass_connection, "fileno", connection_fileno, 0); rb_define_method(klass_connection, "write", connection_write, 1); rb_define_method(klass_connection, "flush", connection_flush, 0); rb_define_method(klass_connection, "read", connection_read, 0); } hiredis-rb-0.6.3/ext/hiredis_ext/extconf.rb000066400000000000000000000023151337111114100206440ustar00rootroot00000000000000require 'mkmf' build_hiredis = true unless have_header('sys/socket.h') puts "Could not find (Likely Windows)." puts "Skipping building hiredis. The slower, pure-ruby implementation will be used instead." build_hiredis = false end RbConfig::MAKEFILE_CONFIG['CC'] = ENV['CC'] if ENV['CC'] hiredis_dir = File.join(File.dirname(__FILE__), %w{.. .. vendor hiredis}) unless File.directory?(hiredis_dir) STDERR.puts "vendor/hiredis missing, please checkout its submodule..." exit 1 end RbConfig::CONFIG['configure_args'] =~ /with-make-prog\=(\w+)/ make_program = $1 || ENV['make'] make_program ||= case RUBY_PLATFORM when /mswin/ 'nmake' when /(bsd|solaris)/ 'gmake' else 'make' end if build_hiredis # Make sure hiredis is built... Dir.chdir(hiredis_dir) do success = system("#{make_program} static") raise "Building hiredis failed" if !success end # Statically link to hiredis (mkmf can't do this for us) $CFLAGS << " -I#{hiredis_dir}" $LDFLAGS << " #{hiredis_dir}/libhiredis.a" have_func("rb_thread_fd_select") create_makefile('hiredis/ext/hiredis_ext') else File.open("Makefile", "wb") do |f| dummy_makefile(".").each do |line| f.puts(line) end end end hiredis-rb-0.6.3/ext/hiredis_ext/hiredis_ext.c000066400000000000000000000005721337111114100213270ustar00rootroot00000000000000#include #include #include #include "hiredis_ext.h" VALUE mod_hiredis; VALUE mod_ext; void Init_hiredis_ext() { mod_hiredis = rb_define_module("Hiredis"); mod_ext = rb_define_module_under(mod_hiredis,"Ext"); rb_global_variable(&mod_hiredis); rb_global_variable(&mod_ext); InitReader(mod_ext); InitConnection(mod_ext); } hiredis-rb-0.6.3/ext/hiredis_ext/hiredis_ext.h000066400000000000000000000017471337111114100213410ustar00rootroot00000000000000#ifndef __HIREDIS_EXT_H #define __HIREDIS_EXT_H /* Defined for Rubinius. This indicates a char* obtained * through RSTRING_PTR is never modified in place. With this * define Rubinius can disable the slow copy back mechanisms * to make sure strings are updated at the Ruby side. */ #define RSTRING_NOT_MODIFIED #include "hiredis.h" #include "ruby.h" /* Defined in hiredis_ext.c */ extern VALUE mod_hiredis; /* Defined in reader.c */ extern redisReplyObjectFunctions redisExtReplyObjectFunctions; extern VALUE klass_reader; extern void InitReader(VALUE module); /* Defined in connection.c */ extern VALUE klass_connection; extern VALUE error_eof; extern void InitConnection(VALUE module); /* Borrowed from Nokogiri */ #ifndef RSTRING_PTR #define RSTRING_PTR(s) (RSTRING(s)->ptr) #endif #ifndef RSTRING_LEN #define RSTRING_LEN(s) (RSTRING(s)->len) #endif #ifndef RARRAY_PTR #define RARRAY_PTR(a) RARRAY(a)->ptr #endif #ifndef RARRAY_LEN #define RARRAY_LEN(a) RARRAY(a)->len #endif #endif hiredis-rb-0.6.3/ext/hiredis_ext/reader.c000066400000000000000000000077351337111114100202720ustar00rootroot00000000000000#include #include "hiredis_ext.h" /* Force encoding on new strings? */ static VALUE enc_klass; static ID enc_default_external = 0; static ID str_force_encoding = 0; /* Add VALUE to parent when the redisReadTask has a parent. * Note that the parent should always be of type T_ARRAY. */ static void *tryParentize(const redisReadTask *task, VALUE v) { if (task && task->parent != NULL) { volatile VALUE parent = (VALUE)task->parent->obj; assert(TYPE(parent) == T_ARRAY); rb_ary_store(parent,task->idx,v); } return (void*)v; } static void *createStringObject(const redisReadTask *task, char *str, size_t len) { volatile VALUE v, enc; v = rb_str_new(str,len); /* Force default external encoding if possible. */ if (enc_default_external) { enc = rb_funcall(enc_klass,enc_default_external,0); v = rb_funcall(v,str_force_encoding,1,enc); } if (task->type == REDIS_REPLY_ERROR) { v = rb_funcall(rb_eRuntimeError,rb_intern("new"),1,v); } return tryParentize(task,v); } static void *createArrayObject(const redisReadTask *task, int elements) { volatile VALUE v = rb_ary_new2(elements); return tryParentize(task,v); } static void *createIntegerObject(const redisReadTask *task, long long value) { volatile VALUE v = LL2NUM(value); return tryParentize(task,v); } static void *createNilObject(const redisReadTask *task) { return tryParentize(task,Qnil); } static void freeObject(void *ptr) { /* Garbage collection will clean things up. */ } /* Declare our set of reply object functions only once. */ redisReplyObjectFunctions redisExtReplyObjectFunctions = { createStringObject, createArrayObject, createIntegerObject, createNilObject, freeObject }; static void reader_mark(redisReader *reader) { // volatile until rb_gc_mark volatile VALUE root = (VALUE)reader->reply; // FIXME - PCO - checking root for 0 is checkign to see if the value is // Qfalse. I suspect that is not what is intended here. Checking the // redisReader code might clarify. It would be unfortunate if the reply, a // void* was using NULL to indicate not set but that may be the nature of // the redisReader library. It is worth checking anyway. if (root != 0 && TYPE(root) == T_ARRAY) rb_gc_mark(root); } static VALUE reader_allocate(VALUE klass) { redisReader *reader = redisReaderCreate(); reader->fn = &redisExtReplyObjectFunctions; return Data_Wrap_Struct(klass, reader_mark, redisReaderFree, reader); } static VALUE reader_feed(VALUE klass, VALUE str) { redisReader *reader; if (TYPE(str) != T_STRING) rb_raise(rb_eTypeError, "not a string"); Data_Get_Struct(klass, redisReader, reader); redisReaderFeed(reader,RSTRING_PTR(str),(size_t)RSTRING_LEN(str)); return INT2NUM(0); } static VALUE reader_gets(VALUE klass) { redisReader *reader; volatile VALUE reply; Data_Get_Struct(klass, redisReader, reader); if (redisReaderGetReply(reader,(void**)&reply) != REDIS_OK) rb_raise(rb_eRuntimeError,"%s",reader->errstr); return reply; } VALUE klass_reader; void InitReader(VALUE mod) { klass_reader = rb_define_class_under(mod, "Reader", rb_cObject); rb_global_variable(&klass_reader); rb_define_alloc_func(klass_reader, reader_allocate); rb_define_method(klass_reader, "feed", reader_feed, 1); rb_define_method(klass_reader, "gets", reader_gets, 0); /* If the Encoding class is present, #default_external should be used to * determine the encoding for new strings. The "enc_default_external" * ID is non-zero when encoding should be set on new strings. */ if (rb_const_defined(rb_cObject, rb_intern("Encoding"))) { enc_klass = rb_const_get(rb_cObject, rb_intern("Encoding")); enc_default_external = rb_intern("default_external"); str_force_encoding = rb_intern("force_encoding"); rb_global_variable(&enc_klass); } else { enc_default_external = 0; } } hiredis-rb-0.6.3/hiredis.gemspec000066400000000000000000000017611337111114100165420ustar00rootroot00000000000000require File.expand_path("../lib/hiredis/version", __FILE__) Gem::Specification.new do |s| s.name = "hiredis" s.version = Hiredis::VERSION s.homepage = "http://github.com/redis/hiredis-rb" s.authors = ["Pieter Noordhuis"] s.email = ["pcnoordhuis@gmail.com"] s.license = 'BSD-3-Clause' s.summary = "Ruby wrapper for hiredis (protocol serialization/deserialization and blocking I/O)" s.description = s.summary s.require_path = "lib" s.files = [] if RUBY_PLATFORM =~ /java/ s.platform = "java" else s.extensions = Dir["ext/**/extconf.rb"] s.files += Dir["ext/**/*.{rb,c,h}"] s.files += Dir["vendor/hiredis/*.{c,h}"] - Dir["vendor/hiredis/example*"] + Dir["vendor/hiredis/COPYING"] + Dir["vendor/hiredis/Makefile"] end s.files += Dir["lib/**/*.rb"] s.files += %w(COPYING Rakefile) s.add_development_dependency "rake", "10.0" s.add_development_dependency "rake-compiler", "~> 0.7.1" s.add_development_dependency "minitest", "~> 5.5.1" end hiredis-rb-0.6.3/lib/000077500000000000000000000000001337111114100143075ustar00rootroot00000000000000hiredis-rb-0.6.3/lib/hiredis.rb000066400000000000000000000000671337111114100162660ustar00rootroot00000000000000require "hiredis/version" require "hiredis/connection" hiredis-rb-0.6.3/lib/hiredis/000077500000000000000000000000001337111114100157365ustar00rootroot00000000000000hiredis-rb-0.6.3/lib/hiredis/connection.rb000066400000000000000000000004411337111114100204210ustar00rootroot00000000000000module Hiredis begin require "hiredis/ext/connection" Connection = Ext::Connection rescue LoadError warn "WARNING: could not load hiredis extension, using (slower) pure Ruby implementation." require "hiredis/ruby/connection" Connection = Ruby::Connection end end hiredis-rb-0.6.3/lib/hiredis/ext/000077500000000000000000000000001337111114100165365ustar00rootroot00000000000000hiredis-rb-0.6.3/lib/hiredis/ext/connection.rb000066400000000000000000000007071337111114100212260ustar00rootroot00000000000000require "hiredis/ext/hiredis_ext" require "hiredis/version" require "socket" module Hiredis module Ext class Connection alias :_disconnect :disconnect def disconnect _disconnect ensure @sock = nil end alias :_read :read def read _read rescue ::EOFError raise Errno::ECONNRESET end def sock @sock ||= Socket.for_fd(fileno) end end end end hiredis-rb-0.6.3/lib/hiredis/ext/reader.rb000066400000000000000000000000741337111114100203260ustar00rootroot00000000000000require "hiredis/ext/hiredis_ext" require "hiredis/version" hiredis-rb-0.6.3/lib/hiredis/reader.rb000066400000000000000000000004111337111114100175210ustar00rootroot00000000000000module Hiredis begin require "hiredis/ext/reader" Reader = Ext::Reader rescue LoadError warn "WARNING: could not load hiredis extension, using (slower) pure Ruby implementation." require "hiredis/ruby/reader" Reader = Ruby::Reader end end hiredis-rb-0.6.3/lib/hiredis/ruby/000077500000000000000000000000001337111114100167175ustar00rootroot00000000000000hiredis-rb-0.6.3/lib/hiredis/ruby/connection.rb000066400000000000000000000154511337111114100214110ustar00rootroot00000000000000require "socket" require "hiredis/ruby/reader" require "hiredis/version" module Hiredis module Ruby class Connection if defined?(RUBY_ENGINE) && RUBY_ENGINE == "rbx" def self.errno_to_class Errno::Mapping end else def self.errno_to_class @mapping ||= Hash[Errno.constants.map do |name| klass = Errno.const_get(name) [klass.const_get("Errno"), klass] end] end end if defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby" require "timeout" def _connect(host, port, timeout) sock = nil begin Timeout.timeout(timeout) do sock = TCPSocket.new(host, port) end rescue SocketError => se raise se.message rescue Timeout::Error raise Errno::ETIMEDOUT end sock end def _connect_unix(path, timeout) sock = nil begin Timeout.timeout(timeout) do sock = UNIXSocket.new(path) end rescue SocketError => se raise se.message rescue Timeout::Error raise Errno::ETIMEDOUT end sock end def _write(sock, data, timeout) begin Timeout.timeout(timeout) do sock.write(data) end rescue Timeout::Error raise Errno::EAGAIN end end else def _connect(host, port, timeout) error = nil sock = nil # Resolve address begin addrinfo = Socket.getaddrinfo(host, port, Socket::AF_UNSPEC, Socket::SOCK_STREAM) rescue SocketError => se raise se.message end addrinfo.each do |_, port, name, addr, af| begin sockaddr = Socket.pack_sockaddr_in(port, addr) sock = _connect_sockaddr(af, sockaddr, timeout) rescue => aux case aux when Errno::EAFNOSUPPORT, Errno::ECONNREFUSED error = aux next else # Re-raise raise end else # No errors, awesome! break end end unless sock # Re-raise last error since the last try obviously failed raise error if error # This code path should not happen: getaddrinfo should always return # at least one record, which should either succeed or fail and leave # and error to raise. raise end sock end def _connect_unix(path, timeout) sockaddr = Socket.pack_sockaddr_un(path) _connect_sockaddr(Socket::AF_UNIX, sockaddr, timeout) end def _write(sock, data, timeout) data.force_encoding("binary") if data.respond_to?(:force_encoding) begin nwritten = @sock.write_nonblock(data) while nwritten < string_size(data) data = data[nwritten..-1] nwritten = @sock.write_nonblock(data) end rescue Errno::EAGAIN if IO.select([], [@sock], [], timeout) # Writable, try again retry else # Timed out, raise raise Errno::EAGAIN end end end def _connect_sockaddr(af, sockaddr, timeout) sock = Socket.new(af, Socket::SOCK_STREAM, 0) begin sock.connect_nonblock(sockaddr) rescue Errno::EINPROGRESS if IO.select(nil, [sock], nil, timeout) # Writable, check for errors optval = sock.getsockopt(Socket::SOL_SOCKET, Socket::SO_ERROR) errno = optval.unpack("i").first # Raise socket error if there is any raise self.class.errno_to_class[errno] if errno > 0 else # Timeout (TODO: replace with own Timeout class) raise Errno::ETIMEDOUT end end sock rescue sock.close if sock # Re-raise raise end private :_connect_sockaddr end attr_reader :sock def initialize @sock = nil @timeout = nil end def connected? !! @sock end def connect(host, port, usecs = nil) # Temporarily override timeout on #connect timeout = usecs ? (usecs / 1_000_000.0) : @timeout # Optionally disconnect current socket disconnect if connected? sock = _connect(host, port, timeout) sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1 @reader = ::Hiredis::Ruby::Reader.new @sock = sock nil end def connect_unix(path, usecs = 0) # Temporarily override timeout on #connect timeout = usecs ? (usecs / 1_000_000.0) : @timeout # Optionally disconnect current socket disconnect if connected? sock = _connect_unix(path, timeout) @reader = ::Hiredis::Ruby::Reader.new @sock = sock nil end def disconnect @sock.close rescue ensure @sock = nil end def timeout=(usecs) raise ArgumentError.new("timeout cannot be negative") if usecs < 0 if usecs == 0 @timeout = nil else @timeout = usecs / 1_000_000.0 end nil end def fileno raise "not connected" unless connected? @sock.fileno end COMMAND_DELIMITER = "\r\n".freeze def write(args) command = [] command << "*#{args.size}" args.each do |arg| arg = arg.to_s command << "$#{string_size arg}" command << arg end data = command.join(COMMAND_DELIMITER) + COMMAND_DELIMITER _write(@sock, data, @timeout) nil end # No-op for now.. def flush end def read raise "not connected" unless connected? while (reply = @reader.gets) == false begin @reader.feed @sock.read_nonblock(1024) rescue Errno::EAGAIN if IO.select([@sock], [], [], @timeout) # Readable, try again retry else # Timed out, raise raise Errno::EAGAIN end end end reply rescue ::EOFError raise Errno::ECONNRESET end protected 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 end hiredis-rb-0.6.3/lib/hiredis/ruby/reader.rb000066400000000000000000000074621337111114100205170ustar00rootroot00000000000000require "hiredis/version" module Hiredis module Ruby class Reader def initialize @buffer = Buffer.new @task = Task.new(@buffer) end def feed(data) @buffer << data end def gets reply = @task.process @buffer.discard! reply end protected class Task # Use lookup table to map reply types to methods method_index = {} method_index[?-] = :process_error_reply method_index[?+] = :process_status_reply method_index[?:] = :process_integer_reply method_index[?$] = :process_bulk_reply method_index[?*] = :process_multi_bulk_reply METHOD_INDEX = method_index.freeze attr_reader :parent attr_reader :depth attr_accessor :multi_bulk def initialize(buffer, parent = nil, depth = 0) @buffer, @parent, @depth = buffer, parent, depth end # Note: task depth is not checked. def child @child ||= Task.new(@buffer, self, depth + 1) end def root parent ? parent.root : self end def reset! @line = @type = @multi_bulk = nil end def process_error_reply RuntimeError.new(@line) end def process_status_reply @line end def process_integer_reply @line.to_i end def process_bulk_reply bulk_length = @line.to_i return nil if bulk_length < 0 # Caught by caller function when false @buffer.read(bulk_length, 2) end def process_multi_bulk_reply multi_bulk_length = @line.to_i if multi_bulk_length > 0 @multi_bulk ||= [] # We know the multi bulk is not complete when this path is taken. while (element = child.process) != false @multi_bulk << element return @multi_bulk if @multi_bulk.length == multi_bulk_length end false elsif multi_bulk_length == 0 [] else nil end end def process_protocol_error raise "Protocol error" end def process @line ||= @buffer.read_line return false if @line == false @type ||= @line.slice!(0) reply = send(METHOD_INDEX[@type] || :process_protocol_error) reset! if reply != false reply end end class Buffer CRLF = "\r\n".freeze def initialize @buffer = "" @length = @pos = 0 end def <<(data) @length += data.length @buffer << data end def length @length end def empty? @length == 0 end def discard! if @length == 0 @buffer = "" @length = @pos = 0 else if @pos >= 1024 @buffer.slice!(0, @pos) @length -= @pos @pos = 0 end end end def read(bytes, skip = 0) start = @pos stop = start + bytes + skip return false if @length < stop @pos = stop force_encoding @buffer[start, bytes] end def read_line start = @pos stop = @buffer.index(CRLF, @pos) return false unless stop @pos = stop + 2 # include CRLF force_encoding @buffer[start, stop - start] end private if "".respond_to?(:force_encoding) def force_encoding(str) str.force_encoding(Encoding.default_external) end else def force_encoding(str) str end end end end end end hiredis-rb-0.6.3/lib/hiredis/version.rb000066400000000000000000000000471337111114100177510ustar00rootroot00000000000000module Hiredis VERSION = "0.6.3" end hiredis-rb-0.6.3/test/000077500000000000000000000000001337111114100145205ustar00rootroot00000000000000hiredis-rb-0.6.3/test/connection_test.rb000066400000000000000000000222331337111114100202450ustar00rootroot00000000000000# encoding: utf-8 require_relative 'helper' module ConnectionTests attr_reader :hiredis DEFAULT_PORT = 6380 def sockopt(sock, opt, unpack = "i") sock.getsockopt(Socket::SOL_SOCKET, opt).unpack("i").first end class Netcat def initialize(port) @listener = TCPServer.new(port) end def close @listener.close end def run begin @sock = @listener.accept_nonblock rescue Errno::EAGAIN begin IO.select([@listener]) rescue IOError # Happens when listener gets closed return end retry end end def read(*args) wait_for_sock @sock.read(*args) end def write(*args) wait_for_sock @sock.write(*args) end def close_read(*args) wait_for_sock @sock.close_read(*args) end def close_write(*args) wait_for_sock @sock.close_write(*args) end private def wait_for_sock stop = Time.now + 1 Thread.pass while @sock == nil && Time.now < stop end end def listen(port = DEFAULT_PORT) nc = Netcat.new(port) thread = Thread.new do nc.run end begin yield nc ensure nc.close end thread.join end def test_connect_wrong_host ex = assert_raises RuntimeError do hiredis.connect("nonexisting", 6379) end assert ex.message =~ /(can't resolve)|(name or service not known)|(nodename nor servname provided, or not known)/i end def test_connect_wrong_port assert_raises Errno::ECONNREFUSED do hiredis.connect("localhost", 6380) end end def test_connected_tcp socket = TCPServer.new("127.0.0.1", 6380) assert !hiredis.connected? hiredis.connect("127.0.0.1", DEFAULT_PORT) assert hiredis.connected? hiredis.disconnect assert !hiredis.connected? ensure socket.close if socket end def test_connected_tcp_has_fileno socket = TCPServer.new("127.0.0.1", 6380) hiredis.connect("127.0.0.1", DEFAULT_PORT) assert hiredis.fileno > $stderr.fileno ensure socket.close if socket end def test_connect_unix path = "/tmp/hiredis-rb-test.sock" File.unlink(path) if File.exist?(path) socket = UNIXServer.new(path) assert !hiredis.connected? hiredis.connect_unix(path) assert hiredis.connected? hiredis.disconnect assert !hiredis.connected? ensure socket.close if socket end def test_connect_unix_has_fileno path = "/tmp/hiredis-rb-test.sock" File.unlink(path) if File.exist?(path) socket = UNIXServer.new(path) hiredis.connect_unix(path) assert hiredis.fileno > $stderr.fileno ensure socket.close if socket end def test_fileno_when_disconnected assert_raises RuntimeError, "not connected" do hiredis.fileno end end def test_wrong_value_for_timeout assert_raises ArgumentError do hiredis.timeout = -10 end end def test_connect_tcp_with_timeout hiredis.timeout = 200_000 t = Time.now assert_raises Errno::ETIMEDOUT do hiredis.connect("1.1.1.1", 59876) end assert 210_000 > (Time.now - t) end def test_connect_tcp_with_timeout_override hiredis.timeout = 1_000_000 t = Time.now assert_raises Errno::ETIMEDOUT do hiredis.connect("1.1.1.1", 59876, 200_000) end assert 210_000 > (Time.now - t) end def test_connect_tcp_without_timeout hiredis.timeout = 0 finished = false thread = Thread.new do hiredis.connect("1.1.1.1", 59876) finished = true end sleep(0.5) # double of default timeout assert !finished thread.kill end def test_read_when_disconnected assert_raises RuntimeError, "not connected" do hiredis.read end end def test_read_against_eof listen do |server| hiredis.connect("localhost", 6380) hiredis.write(["QUIT"]) # Reply to QUIT and disconnect server.write "+OK\r\n" server.close_write # Reply for QUIT can be read assert_equal "OK", hiredis.read # Next read should raise assert_raises Errno::ECONNRESET do hiredis.read end end end def test_symbol_in_argument_list listen do |server| hiredis.connect("localhost", 6380) hiredis.write([:info]) server.write "$2\r\nhi\r\n" assert_kind_of String, hiredis.read end end def test_read_against_timeout listen do |_| hiredis.connect("localhost", DEFAULT_PORT) hiredis.timeout = 10_000 assert_raises Errno::EAGAIN do hiredis.read end end end def test_read_without_timeout listen do |_| hiredis.connect("localhost", DEFAULT_PORT) hiredis.timeout = 0 finished = false thread = Thread.new do hiredis.read finished = true end sleep(0.5) # double of default timeout assert !finished thread.kill end end # Test that the Hiredis thread is scheduled after some time while waiting for # the descriptor to be readable. def test_read_against_timeout_with_other_thread thread = Thread.new do sleep 0.1 while true end listen do |_| hiredis.connect("localhost", DEFAULT_PORT) hiredis.timeout = 10_000 assert_raises Errno::EAGAIN do hiredis.read end end ensure thread.kill end def test_raise_on_error_reply listen do |server| hiredis.connect("localhost", 6380) hiredis.write(["GET"]) server.write "-ERR wrong number of arguments\r\n" err = hiredis.read assert_match /wrong number of arguments/i, err.message assert_kind_of RuntimeError, err end end def test_recover_from_partial_write listen do |server| hiredis.connect("localhost", 6380) # Find out send buffer size sndbuf = sockopt(hiredis.sock, Socket::SO_SNDBUF) # Make request that saturates the send buffer hiredis.write(["x" * sndbuf]) # Flush and disconnect to signal EOF hiredis.flush hiredis.disconnect # Compare to data received on the other end formatted = "*1\r\n$#{sndbuf}\r\n#{"x" * sndbuf}\r\n" assert formatted == server.read end end # # This does not have consistent outcome for different operating systems... # # def test_eagain_on_write # listen do |server| # hiredis.connect("localhost", 6380) # hiredis.timeout = 100_000 # # Find out buffer sizes # sndbuf = sockopt(hiredis.sock, Socket::SO_SNDBUF) # rcvbuf = sockopt(hiredis.sock, Socket::SO_RCVBUF) # # Make request that fills both the remote receive buffer and the local # # send buffer. This assumes that the size of the receive buffer on the # # remote end is equal to our local receive buffer size. # assert_raises Errno::EAGAIN do # hiredis.write(["x" * rcvbuf * 2]) # hiredis.write(["x" * sndbuf * 2]) # hiredis.flush # end # end # end def test_eagain_on_write_followed_by_remote_drain listen do |server| hiredis.connect("localhost", 6380) hiredis.timeout = 100_000 # Find out buffer sizes sndbuf = sockopt(hiredis.sock, Socket::SO_SNDBUF) rcvbuf = sockopt(hiredis.sock, Socket::SO_RCVBUF) # This thread starts reading the server buffer after 50ms. This will # cause the local write to first return EAGAIN, wait for the socket to # become writable with select(2) and retry. begin thread = Thread.new do sleep(0.050) loop do server.read(1024) end end # Make request that fills both the remote receive buffer and the local # send buffer. This assumes that the size of the receive buffer on the # remote end is equal to our local receive buffer size. hiredis.write(["x" * rcvbuf]) hiredis.write(["x" * sndbuf]) hiredis.flush hiredis.disconnect ensure thread.kill end end end def test_no_eagain_after_cumulative_wait_exceeds_timeout listen do |server| hiredis.connect("localhost", 6380) hiredis.timeout = 10_000 begin thread = Thread.new do loop do sleep(0.001) server.write("+ok\r\n") end end # The read timeout for this connection is 10 milliseconds. # To compensate for the overhead of parsing the reply and the chance # not having to wait because the reply is already present in the OS # buffers, continue until we have waited at least 5x the timeout. waited = 0 while waited < 50_000 t1 = Time.now hiredis.read t2 = Time.now waited += (t2 - t1) * 1_000_000 end ensure thread.kill end end end end if defined?(Hiredis::Ruby::Connection) class RubyConnectionTest < Minitest::Test include ConnectionTests def setup @hiredis = Hiredis::Ruby::Connection.new @hiredis.timeout = 250_000 end end end if defined?(Hiredis::Ext::Connection) class ExtConnectionTest < Minitest::Test include ConnectionTests def setup @hiredis = Hiredis::Ext::Connection.new @hiredis.timeout = 250_000 end end end hiredis-rb-0.6.3/test/helper.rb000066400000000000000000000002711337111114100163240ustar00rootroot00000000000000# encoding: utf-8 require 'minitest/autorun' require_relative '../lib/hiredis/ext/connection' unless RUBY_PLATFORM =~ /java|mswin|mingw/i require_relative '../lib/hiredis/ruby/reader' hiredis-rb-0.6.3/test/reader_test.rb000066400000000000000000000060701337111114100173510ustar00rootroot00000000000000# encoding: utf-8 require_relative 'helper' module ReaderTests def silent verbose, $VERBOSE = $VERBOSE, false begin yield ensure $VERBOSE = verbose end end def with_external_encoding(encoding) original_encoding = Encoding.default_external begin silent { Encoding.default_external = Encoding.find(encoding) } yield ensure silent { Encoding.default_external = original_encoding } end end def test_false_on_empty_buffer assert_equal false, @reader.gets end def test_nil @reader.feed("$-1\r\n") assert_equal nil, @reader.gets end def test_integer value = 2**63-1 # largest 64-bit signed integer @reader.feed(":#{value.to_s}\r\n") assert_equal value, @reader.gets end def test_status_string @reader.feed("+status\r\n") assert_equal "status", @reader.gets end def test_error_string @reader.feed("-error\r\n") error = @reader.gets assert_equal RuntimeError, error.class assert_equal "error", error.message end def test_errors_in_nested_multi_bulk @reader.feed("*2\r\n-err0\r\n-err1\r\n") errors = @reader.gets 2.times do |i| assert_equal RuntimeError, errors[i].class assert_equal "err#{i}", errors[i].message end end def test_empty_bulk_string @reader.feed("$0\r\n\r\n") assert_equal "", @reader.gets end def test_bulk_string @reader.feed("$5\r\nhello\r\n") assert_equal "hello", @reader.gets end def test_bulk_string_encoding string = "שלום" protocol = "$%d\r\n%s\r\n" % [string.bytesize, string] protocol.force_encoding "ASCII-8BIT" @reader.feed(protocol) with_external_encoding("UTF-8") do assert_equal string, @reader.gets end end def test_bulk_string_encoding_chunked string = "שלום" protocol = "$%d\r\n%s\r\n" % [string.bytesize, string] protocol.force_encoding "ASCII-8BIT" protocol.each_char do |c| @reader.feed(c) end with_external_encoding("UTF-8") do assert_equal string, @reader.gets end end def test_null_multi_bulk @reader.feed("*-1\r\n") assert_equal nil, @reader.gets end def test_empty_multi_bulk @reader.feed("*0\r\n") assert_equal [], @reader.gets end def test_multi_bulk @reader.feed("*2\r\n$5\r\nhello\r\n$5\r\nworld\r\n") assert_equal ["hello", "world"], @reader.gets end def test_nested_multi_bulk @reader.feed("*2\r\n*2\r\n$5\r\nhello\r\n$5\r\nworld\r\n$1\r\n!\r\n") assert_equal [["hello", "world"], "!"], @reader.gets end def test_nested_multi_bulk_redux @reader.feed("*2\r\n*2\r\n*1\r\n$5\r\nhello\r\n$5\r\nworld\r\n$1\r\n!\r\n") assert_equal [[["hello"], "world"], "!"], @reader.gets end end if defined?(Hiredis::Ruby::Reader) class RubyReaderTest < Minitest::Test include ReaderTests def setup @reader = Hiredis::Ruby::Reader.new end end end if defined?(Hiredis::Ext::Reader) class ExtReaderTest < Minitest::Test include ReaderTests def setup @reader = Hiredis::Ext::Reader.new end end end hiredis-rb-0.6.3/vendor/000077500000000000000000000000001337111114100150365ustar00rootroot00000000000000hiredis-rb-0.6.3/vendor/hiredis/000077500000000000000000000000001337111114100164655ustar00rootroot00000000000000