multi_redis 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []