Skip to content

Commit

Permalink
Support for pipelining, optional blocks, and returning values
Browse files Browse the repository at this point in the history
  • Loading branch information
mrdanadams committed Jan 5, 2013
1 parent 1633928 commit 03104b3
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 5 deletions.
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Ruby gem for easily pipelining REDIS commands
https://github.com/mrdanadams/redis_pipeliner

(Inspired by this blog post on [5-10x Speed Ups by Pipeling Multiple REDIS Commands in Ruby](http://mrdanadams.com/2012/pipeline-redis-commands-ruby/) by [Dan Adams](http://mrdanadams.com).)

[Pipelining in REDIS](https://github.com/redis/redis-rb#pipelining) is a great way to stay performant when executing multiple commands. It should also be easy to use.

## Usage

Basic usage involves:

1. Enqueueing a number of REDIS commands inside a `pipelined` block
2. Doing something with the results either afterwards or inside blocks specific to each command.

Ex: (a bit contrived...)

```ruby
# Put a bunch of values in a few different hashes
redis = Redis.connect
redis.hset "h1", "foo", 1
redis.hset "h2", "bar", 2
redis.hest "h3", "baz", 3

# Get the values pipelined and sum them up
values = RedisPipeliner.pipeline redis do |p|
# This would normally be 3 round-trips
p.enqueue redis.hget("h1", "foo")
p.enqueue redis.hget("h2", "bar")
p.enqueue redis.hget("h3", "baz")
end
values.map(&:to_i).inject(&:+).should == 6
```

You can also pass in a block to be called for each value rather than operating on the values afterwards:

```ruby
results = []
RedisPipeliner.pipeline redis do |p|
[%w(h1 foo), %w(h2 bar), %w(h3 baz)].each do |pair|
p.enqueue redis.hget(pair[0], pair[1]) do |value|
# referencing pair inside the block
results << pair[1] + value
end
end
end
results.first.should == "foo1"
```

See the specs for executable examples.
14 changes: 13 additions & 1 deletion lib/redis_pipeliner.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
require "redis_pipeliner/version"
require "redis"
require "redis_pipeliner/pipeliner"

module RedisPipeliner
# Your code goes here...
class << self
# Convenience for creating a pipeline, enqueueing, and blocking until the results are processed.
def pipeline(redis, &proc)
pipeliner = RedisPipeliner::Pipeliner.new(redis)
redis.pipelined do
proc.call pipeliner
end

pipeliner.values
end
end
end
44 changes: 44 additions & 0 deletions lib/redis_pipeliner/pipeliner.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
module RedisPipeliner
# Enqueues commands in a pipeline and waits until they are finished.
# Usage pattern is to call #enqueue with each REDIS future and a block to process it,
# then call #wait outside the Redis.pipelined call.
class Pipeliner
def initialize(redis)
@redis = redis
@commands = []
end

# Adds a command (a Future, actually) with an option block to call when the Future has been realized.
def enqueue(future, &proc)
@commands << { future: future, callback: proc }
end

# Blocks until all Futures have been realized and returns the values.
# This should be called _outside_ the Redis.pipelined call.
# Note that the value enqueue is the REDIS return value, not the value returned by any passed block.
# Nil values will be included in the return values (if that's what REDIS gives us).
def values
return @values unless @values.nil?

@values = []
@commands.each do |cmd|
while cmd[:future].value.is_a?(Redis::FutureNotReady)
sleep(1.0 / 100.0)
end

v = cmd[:future].value
cmd[:callback].call(v) unless cmd[:callback].nil?
@values << v
end

@values
end

# Returns the enqueue REDIS commands
def commands
@commands.map {|h| h[:future] }
end

alias_method :wait, :values
end
end
9 changes: 5 additions & 4 deletions redis_pipeliner.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ Gem::Specification.new do |s|
s.version = RedisPipeliner::VERSION
s.authors = ["Dan Adams"]
s.email = ["mr.danadams@gmail.com"]
s.homepage = "http://mrdanadams.com"
s.summary = %q{Utility for easily pipelining commands in REDIS}
s.description = %q{Utility for easily pipelining commands in REDIS}
s.homepage = "https://github.com/mrdanadams/redis_pipeliner"
s.summary = %q{Easy pipelining of REDIS commands}
s.description = %q{Easy pipelining of REDIS commands}

s.rubyforge_project = "redis_pipeliner"

Expand All @@ -18,6 +18,7 @@ Gem::Specification.new do |s|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.require_paths = ["lib"]

s.add_runtime_dependency "redis"
s.add_dependency "redis"
s.add_development_dependency "rspec"
s.add_development_dependency "pry"
end
61 changes: 61 additions & 0 deletions spec/pipeliner_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
require 'spec_helper'

describe RedisPipeliner do
let :redis do
Redis.connect
end

it 'supports basic usage' do
redis.del "testhash"
redis.hset "testhash", "bar", 1
redis.hset "testhash", "foo", 2

redis.hmget("testhash", "foo", "bar").map(&:to_i).inject(&:+).should == 3

total = 0
values = RedisPipeliner.pipeline redis do |pipe|
%w(bar foo).each do |key|
pipe.enqueue redis.hget("testhash", key) do |result|
total += result.to_i
end
end
end
values.map(&:to_i).inject(&:+).should == 3
end

it 'can reference variables outside the proce' do
redis.del "testhash"
redis.hset "testhash", "bar", 1
redis.hset "testhash", "foo", 2

results = []
RedisPipeliner.pipeline redis do |pipe|
%w(bar foo).each do |key|
pipe.enqueue redis.hget("testhash", key) do |result|
results << key+result
end
end
end

results.should == %w(bar1 foo2)
end

it 'should pipelines commands and return values' do
redis.del "testhash"
redis.hset "testhash", "bar", 1
redis.hset "testhash", "foo", 2

pipeliner = RedisPipeliner::Pipeliner.new(redis)
redis.pipelined do
%w(bar foo).each do |key|
# allows not having a block
pipeliner.enqueue redis.hget("testhash", key)
end
end

pipeliner.commands.first.value.should_not be_nil # you can't test this class for type...

pipeliner.values.map(&:to_i).inject(&:+).should == 3
pipeliner.values.should === pipeliner.values
end
end
3 changes: 3 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
require 'redis_pipeliner'
require 'pry'

# This file was generated by the `rspec --init` command. Conventionally, all
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
# Require this file using `require "spec_helper"` to ensure that it is only
Expand Down

0 comments on commit 03104b3

Please sign in to comment.