multi_redis 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0e26f3631e84ad497b9f973cf0bb51e7beadcaa1
4
- data.tar.gz: e0c768e42521ff6d4ef1cd82a9927eedf77d7658
3
+ metadata.gz: 61930ec26bd7e401367fff65403353e02a6d9c7f
4
+ data.tar.gz: b9ac5d3857fea25471585dda965067e5509bfee6
5
5
  SHA512:
6
- metadata.gz: 0bfab1e32e01f57ea87bdac95b3be65316fc22528f5638997407f51e84aa3ac80ffe0b65cbe3c70d38c314c41f0dddfb3a139043ae0c40c28a1d5e616209c5a0
7
- data.tar.gz: a1eeac5efc8f63d1603ab45c70b0566f09231d34a719fb819dd30da6f688ce300095fed0736f244a5a01590dea6769e5de70daa60c9fa08be34a878398a7a870
6
+ metadata.gz: a1b888ede24eea2c0efb87dfc3db0e4b0cebdad0bd4114c101af9d34afe7d428976b38148d6bba12da701546f8083ad3d898ac0211d273adcc7f85ca8162a1d9
7
+ data.tar.gz: 39bee42c79812c41203c46029cfdebb3bbb07f6754bc47188f08a0065ee57ec804ad14e37cbefed9714123835fc9bd2ff83e1c981ffd5de6fa58adb53392c088
data/README.md CHANGED
@@ -1,10 +1,140 @@
1
1
  # Multi Redis
2
2
 
3
+ **Pattern to execute separate [redis-rb](https://github.com/redis/redis-rb) operations in the same command pipeline or multi/exec.**
4
+
3
5
  [![Gem Version](https://badge.fury.io/rb/multi_redis.png)](http://badge.fury.io/rb/multi_redis)
4
6
  [![Dependency Status](https://gemnasium.com/AlphaHydrae/multi_redis.png)](https://gemnasium.com/AlphaHydrae/multi_redis)
5
7
  [![Build Status](https://secure.travis-ci.org/AlphaHydrae/multi_redis.png)](http://travis-ci.org/AlphaHydrae/multi_redis)
6
8
  [![Coverage Status](https://coveralls.io/repos/AlphaHydrae/multi_redis/badge.png?branch=master)](https://coveralls.io/r/AlphaHydrae/multi_redis?branch=master)
7
9
 
10
+ ## Installation
11
+
12
+ Put this in your Gemfile:
13
+
14
+ ```rb
15
+ gem 'multi_redis', '~> 0.1.0'
16
+ ```
17
+
18
+ Then run `bundle install`.
19
+
20
+ ## Usage
21
+
22
+ Assume you have two separate methods that call redis:
23
+
24
+ ```rb
25
+ $redis = Redis.new
26
+ $redis.set 'key1', 'foo'
27
+ $redis.set 'key2', 'bar'
28
+ $redis.set 'key3', 'baz'
29
+
30
+ class MyRedisClass
31
+
32
+ def do_stuff
33
+
34
+ # run two calls atomically in a MULTI/EXEC
35
+ values = $redis.multi do
36
+ $redis.get 'key1'
37
+ $redis.getset 'key2', 'newvalue'
38
+ end
39
+
40
+ "value 1 is #{values[0]}, value 2 is #{values[1]}"
41
+ end
42
+
43
+ def do_other_stuff
44
+ value = $redis.get 'key3'
45
+ "hey #{value}"
46
+ end
47
+ end
48
+
49
+ o = MyRedisClass.new
50
+ o.do_stuff #=> "value 1 is foo, value 2 is bar"
51
+ o.do_other_stuff #=> "hey baz"
52
+ ```
53
+
54
+ This works, but the redis client executes two separate requests to the server:
55
+
56
+ ```
57
+ Request 1:
58
+ - MULTI
59
+ - GET foo
60
+ - GETSET bar newvalue
61
+ - EXEC
62
+
63
+ Request 2:
64
+ - GET baz
65
+ ```
66
+
67
+ The client will wait for the response from the first request before starting the second one.
68
+ One round trip could be saved by executing the second request in the same MULTI/EXEC block.
69
+ But it would be hard to refactor these two methods to do that while still keeping them separate.
70
+
71
+ Multi Redis provides a pattern to structure this code so that your separate redis calls may be executed together in one request when needed.
72
+
73
+ ```rb
74
+ $redis = Redis.new
75
+ $redis.set 'key1', 'foo'
76
+ $redis.set 'key2', 'bar'
77
+ $redis.set 'key3', 'baz'
78
+
79
+ # Create a redis operation, i.e. an operation that performs redis calls.
80
+ do_stuff = MultiRedis::Operation.new do
81
+
82
+ # Multi blocks will be run atomically in a MULTI/EXEC.
83
+ # All redis commands will return futures inside this block, so you can't use the values immediately.
84
+ # Store futures in the provided data object for later use.
85
+ multi do |mr|
86
+ mr.data.value1 = $redis.get 'key1'
87
+ mr.data.value2 = $redis.getset 'key2', 'newvalue'
88
+ end
89
+
90
+ # Run blocks are executed after the previous multi block (or blocks) are completed and all futures have been resolved.
91
+ # The data object now contains the values of the futures you stored.
92
+ run do |mr|
93
+ "value 1 is #{mr.data.value1}, value 2 is #{mr.data.value2}"
94
+ end
95
+ end
96
+
97
+ # The return value of the operation is that of the last run block.
98
+ result = do_stuff.execute #=> "value 1 is foo, value 2 is bar"
99
+
100
+ # Create the other redis operation.
101
+ do_other_stuff = MultiRedis::Operation.new do
102
+
103
+ multi do |mr|
104
+ mr.data.value = $redis.get 'key3'
105
+ end
106
+
107
+ run do |mr|
108
+ "hey #{mr.data.value}"
109
+ end
110
+ end
111
+
112
+ result = do_other_stuff.execute #=> "hey baz"
113
+ ```
114
+
115
+ The two operations can still be executed separately like before, but they can also be combined through Multi Redis:
116
+
117
+ ```rb
118
+ MultiRedis.execute do_stuff, do_other_stuff
119
+ ```
120
+
121
+ All redis calls get grouped into the same MULTI/EXEC:
122
+
123
+ ```
124
+ One request:
125
+ - MULTI
126
+ - GET foo
127
+ - GETSET bar newvalue
128
+ - GET baz
129
+ - EXEC
130
+ ```
131
+
132
+ The array of results is also returned by the `execute` call:
133
+
134
+ ```rb
135
+ MultiRedis.execute do_stuff, do_other_stuff #=> [ 'value 1 is foo, value 2 is bar', 'hey baz' ]
136
+ ```
137
+
8
138
  ## Meta
9
139
 
10
140
  * **Author:** Simon Oulevay (Alpha Hydrae)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.2.0
data/lib/multi_redis.rb CHANGED
@@ -1,67 +1,180 @@
1
1
  require 'ostruct'
2
2
  require 'redis'
3
+ require 'thread'
3
4
 
4
5
  module MultiRedis
5
- VERSION = '0.1.0'
6
+ VERSION = '0.2.0'
6
7
 
7
- class Operation
8
+ @redis = nil
9
+ @mutex = Mutex.new
10
+ @executing = false
11
+ @operations = []
12
+ @arguments = []
8
13
 
9
- def initialize *args, &block
14
+ def self.redis= redis
15
+ @redis = redis
16
+ end
10
17
 
11
- options = args.last.kind_of?(Hash) ? args.pop : {}
18
+ def self.redis
19
+ @redis
20
+ end
21
+
22
+ def self.execute *args, &block
12
23
 
13
- @target = args.shift || self
14
- @redis = options[:redis] || Redis.new
15
- @data = Data.new
16
- @block = block
24
+ operations, arguments = @mutex.synchronize do
25
+ @operations = args.dup
26
+ @arguments = []
27
+ @executing = true
28
+ yield if block_given?
29
+ @executing = false
30
+ [ @operations.dup.tap{ |ops| @operations.clear }, @arguments ]
17
31
  end
18
32
 
19
- def execute
20
- instance_eval &@block
33
+ Executor.new(operations, args: arguments).execute
34
+ end
35
+
36
+ def self.executing?
37
+ @executing
38
+ end
39
+
40
+ def self.register_operation op, *args
41
+ op.tap do |op|
42
+ @operations << op
43
+ @arguments << args
21
44
  end
45
+ end
22
46
 
23
- def multi &block
24
- @data.to_a = @redis.multi do
25
- @target.instance_exec @redis, @data, &block
47
+ module Extension
48
+
49
+ def multi_redis_operation symbol, options = {}, &block
50
+ op = Operation.new self, options, &block
51
+ define_method symbol do |*args|
52
+ op.execute *args
26
53
  end
27
- @data.resolve_futures!
54
+ self
28
55
  end
56
+ end
29
57
 
30
- def run &block
31
- @target.instance_exec @data, &block
58
+ class Executor
59
+
60
+ def initialize operations, options = {}
61
+ @operations = operations
62
+ @arguments = options[:args] || []
63
+ @redis = options[:redis]
64
+ end
65
+
66
+ def execute options = {}
67
+
68
+ redis = @redis || MultiRedis.redis
69
+ contexts = Array.new(@operations.length){ |i| Context.new redis }
70
+ stacks = @operations.collect{ |op| op.steps.dup }
71
+ args = stacks.collect.with_index{ |a,i| @arguments[i] || [] }
72
+ final_results = Array.new @operations.length
73
+
74
+ while stacks.any? &:any?
75
+
76
+ # execute all non-multi steps
77
+ stacks.each_with_index do |steps,i|
78
+ final_results[i] = steps.shift.execute(contexts[i], args[i]) while steps.first && !steps.first.multi_type
79
+ end
80
+
81
+ # execute all pipelined steps, if any
82
+ pipelined_steps = stacks.collect{ |steps| steps.first && steps.first.multi_type == :pipelined ? steps.shift : nil }
83
+ if pipelined_steps.any?
84
+ results = []
85
+ redis.pipelined do
86
+ pipelined_steps.each_with_index do |step,i|
87
+ if step
88
+ final_results[i] = step.execute(contexts[i], args[i])
89
+ contexts[i].last_results = redis.client.futures[results.length, redis.client.futures.length]
90
+ results += contexts[i].last_results
91
+ end
92
+ end
93
+ end
94
+ pipelined_steps.each_with_index{ |step,i| contexts[i].resolve_futures! if step }
95
+ end
96
+
97
+ # execute all multi steps, if any
98
+ multi_steps = stacks.collect{ |steps| steps.first && steps.first.multi_type == :multi ? steps.shift : nil }
99
+ if multi_steps.any?
100
+ results = []
101
+ redis.multi do
102
+ multi_steps.each_with_index do |step,i|
103
+ if step
104
+ final_results[i] = step.execute(contexts[i], args[i])
105
+ contexts[i].last_results = redis.client.futures[results.length, redis.client.futures.length]
106
+ results += contexts[i].last_results
107
+ end
108
+ end
109
+ end
110
+ multi_steps.each_with_index{ |step,i| contexts[i].resolve_futures! if step }
111
+ end
112
+ end
113
+
114
+ final_results
32
115
  end
33
116
  end
34
117
 
35
- class Data
118
+ class Operation
119
+ attr_reader :steps
120
+
121
+ def initialize *args, &block
122
+
123
+ options = args.last.kind_of?(Hash) ? args.pop : {}
124
+
125
+ @target = args.shift || options[:target] || self
126
+ @redis = options[:redis]
127
+ @steps = []
36
128
 
37
- def initialize
38
- @data = Hash.new
39
- @results = []
129
+ DSL.new(self).instance_eval &block
40
130
  end
41
131
 
42
- def method_missing symbol, *args, &block
43
- if m = symbol.to_s.match(/\A(.*)\=\Z/)
44
- @data[m[1].to_sym] = args[0]
132
+ def execute *args
133
+ if MultiRedis.executing?
134
+ MultiRedis.register_operation self, *args
45
135
  else
46
- super symbol, *args, &block
136
+ Executor.new([ self ], args: [ args ], redis: @redis).execute.first
47
137
  end
48
138
  end
49
139
 
50
- def resolve_futures!
51
- @data.each_key do |k|
52
- self.class.send(:define_method, k){ @data[k].value } if @data[k].is_a? Redis::Future
53
- #@data[k] = @data[k].value if @data[k].is_a? Redis::Future
140
+ def add_step multi_type = nil, &block
141
+ @steps << Step.new(@target, multi_type, block)
142
+ end
143
+
144
+ class DSL
145
+
146
+ def initialize op
147
+ @op = op
54
148
  end
149
+
150
+ def multi &block
151
+ @op.add_step :multi, &block
152
+ end
153
+
154
+ def pipelined &block
155
+ @op.add_step :pipelined, &block
156
+ end
157
+
158
+ def run &block
159
+ @op.add_step &block
160
+ end
161
+ end
162
+ end
163
+
164
+ class Step
165
+
166
+ def initialize target, multi_type, block
167
+ @target, @multi_type, @block = target, multi_type, block
55
168
  end
56
169
 
57
- def to_a
58
- @results
170
+ def execute context, *args
171
+ @target.instance_exec *args.unshift(context), &@block
59
172
  end
60
173
 
61
- def to_a= results
62
- @results = results
174
+ def multi_type
175
+ @multi_type
63
176
  end
64
177
  end
65
178
  end
66
179
 
67
- #Dir[File.join File.dirname(__FILE__), File.basename(__FILE__, '.*'), '*.rb'].each{ |lib| require lib }
180
+ Dir[File.join File.dirname(__FILE__), File.basename(__FILE__, '.*'), '*.rb'].each{ |lib| require lib }
@@ -0,0 +1,28 @@
1
+
2
+ module MultiRedis
3
+
4
+ class Context
5
+ attr_accessor :last_results
6
+
7
+ def initialize redis
8
+ @last_results = []
9
+ @data = Data.new
10
+ @redis = redis
11
+ end
12
+
13
+ def redis
14
+ @redis
15
+ end
16
+
17
+ def data
18
+ @data
19
+ end
20
+
21
+ def resolve_futures!
22
+ @data.contents.each_key do |k|
23
+ @data.contents[k] = @data.contents[k].value if @data.contents[k].is_a? Redis::Future
24
+ end
25
+ @last_results.collect!{ |r| r.is_a?(Redis::Future) ? r.value : r }
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,30 @@
1
+
2
+ module MultiRedis
3
+
4
+ class Data
5
+ attr_reader :contents
6
+
7
+ def initialize
8
+ @contents = Hash.new
9
+ end
10
+
11
+ def [] k
12
+ @contents[k]
13
+ end
14
+
15
+ def []= k, v
16
+ @contents[k.to_sym] = v
17
+ end
18
+
19
+ def method_missing symbol, *args, &block
20
+ if @contents.key? symbol
21
+ @contents[symbol]
22
+ elsif m = symbol.to_s.match(/\A(.*)\=\Z/)
23
+ raise "Reserved name" if respond_to? acc = m[1].to_sym
24
+ @contents[acc] = args[0]
25
+ else
26
+ super symbol, *args, &block
27
+ end
28
+ end
29
+ end
30
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: multi_redis
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simon Oulevay
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-01-06 00:00:00.000000000 Z
11
+ date: 2014-01-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -136,7 +136,8 @@ dependencies:
136
136
  - - ">="
137
137
  - !ruby/object:Gem::Version
138
138
  version: '0'
139
- description: Multi redis.
139
+ description: Allows you to organize your redis calls in separate classes but still
140
+ execute them atomically with pipelined or multi.
140
141
  email: git@alphahydrae.com
141
142
  executables: []
142
143
  extensions: []
@@ -149,6 +150,8 @@ files:
149
150
  - README.md
150
151
  - VERSION
151
152
  - lib/multi_redis.rb
153
+ - lib/multi_redis/context.rb
154
+ - lib/multi_redis/data.rb
152
155
  homepage: http://github.com/AlphaHydrae/multi_redis
153
156
  licenses:
154
157
  - MIT
@@ -172,5 +175,6 @@ rubyforge_project:
172
175
  rubygems_version: 2.2.0
173
176
  signing_key:
174
177
  specification_version: 4
175
- summary: Multi redis.
178
+ summary: Pattern to execute separate redis-rb operations in the same command pipeline
179
+ or multi/exec.
176
180
  test_files: []