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 +4 -4
- data/README.md +130 -0
- data/VERSION +1 -1
- data/lib/multi_redis.rb +146 -33
- data/lib/multi_redis/context.rb +28 -0
- data/lib/multi_redis/data.rb +30 -0
- metadata +8 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 61930ec26bd7e401367fff65403353e02a6d9c7f
|
|
4
|
+
data.tar.gz: b9ac5d3857fea25471585dda965067e5509bfee6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
[](http://badge.fury.io/rb/multi_redis)
|
|
4
6
|
[](https://gemnasium.com/AlphaHydrae/multi_redis)
|
|
5
7
|
[](http://travis-ci.org/AlphaHydrae/multi_redis)
|
|
6
8
|
[](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.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.
|
|
6
|
+
VERSION = '0.2.0'
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
@redis = nil
|
|
9
|
+
@mutex = Mutex.new
|
|
10
|
+
@executing = false
|
|
11
|
+
@operations = []
|
|
12
|
+
@arguments = []
|
|
8
13
|
|
|
9
|
-
|
|
14
|
+
def self.redis= redis
|
|
15
|
+
@redis = redis
|
|
16
|
+
end
|
|
10
17
|
|
|
11
|
-
|
|
18
|
+
def self.redis
|
|
19
|
+
@redis
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.execute *args, &block
|
|
12
23
|
|
|
13
|
-
|
|
14
|
-
@
|
|
15
|
-
@
|
|
16
|
-
@
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
54
|
+
self
|
|
28
55
|
end
|
|
56
|
+
end
|
|
29
57
|
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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
|
-
|
|
38
|
-
@data = Hash.new
|
|
39
|
-
@results = []
|
|
129
|
+
DSL.new(self).instance_eval &block
|
|
40
130
|
end
|
|
41
131
|
|
|
42
|
-
def
|
|
43
|
-
if
|
|
44
|
-
|
|
132
|
+
def execute *args
|
|
133
|
+
if MultiRedis.executing?
|
|
134
|
+
MultiRedis.register_operation self, *args
|
|
45
135
|
else
|
|
46
|
-
|
|
136
|
+
Executor.new([ self ], args: [ args ], redis: @redis).execute.first
|
|
47
137
|
end
|
|
48
138
|
end
|
|
49
139
|
|
|
50
|
-
def
|
|
51
|
-
@
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
58
|
-
@
|
|
170
|
+
def execute context, *args
|
|
171
|
+
@target.instance_exec *args.unshift(context), &@block
|
|
59
172
|
end
|
|
60
173
|
|
|
61
|
-
def
|
|
62
|
-
@
|
|
174
|
+
def multi_type
|
|
175
|
+
@multi_type
|
|
63
176
|
end
|
|
64
177
|
end
|
|
65
178
|
end
|
|
66
179
|
|
|
67
|
-
|
|
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.
|
|
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-
|
|
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:
|
|
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:
|
|
178
|
+
summary: Pattern to execute separate redis-rb operations in the same command pipeline
|
|
179
|
+
or multi/exec.
|
|
176
180
|
test_files: []
|