redis-em-mutex 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/BENCHMARK.md ADDED
@@ -0,0 +1,134 @@
1
+ BENCHMARK
2
+ =========
3
+
4
+ To measure the performance of {Redis::EM::Mutex} I've wrote a simple script called `benchmark_mutex.rb`.
5
+ The script is included in respository.
6
+
7
+ Below are the results of running tests against the following versions:
8
+
9
+ - redis-em-mutex v0.1.2
10
+ - redis-em-mutex v0.2.3
11
+ - redis-em-mutex v0.3.0 - "pure" handler
12
+ - redis-em-mutex v0.3.0 - "script" handler
13
+
14
+ To run theese tests type:
15
+
16
+ ```sh
17
+ cp benchmark_mutex.rb /tmp/
18
+
19
+ git reset --hard v0.1.2
20
+ ruby /tmp/benchmark_mutex.rb
21
+
22
+ git reset --hard v0.2.3
23
+ ruby /tmp/benchmark_mutex.rb
24
+
25
+ git reset --hard v0.3.0
26
+ REDIS_EM_MUTEX_HANDLER=pure ruby benchmark_mutex.rb
27
+ REDIS_EM_MUTEX_HANDLER=script ruby benchmark_mutex.rb
28
+ ```
29
+
30
+ Here are the results of running those tests on Quad Core Xeon machine
31
+ with redis-server 2.6.9 and ruby 1.9.3p374 (2013-01-15 revision 38858) [x86_64-linux].
32
+
33
+ The results may vary.
34
+
35
+ Test 1
36
+ ======
37
+
38
+ Lock/unlock 1000 times using 10 concurrent fibers.
39
+
40
+ ```
41
+ Version: 0.1.2, handler: N/A
42
+ lock/unlock 1000 times with concurrency: 10
43
+ user system total real
44
+ keys: 1 0.590000 0.220000 0.810000 ( 1.124918)
45
+ keys: 2 0.640000 0.220000 0.860000 ( 1.214577)
46
+ keys: 3 0.660000 0.200000 0.860000 ( 1.245093)
47
+ keys: 5 0.770000 0.140000 0.910000 ( 1.322475)
48
+ keys:10 0.890000 0.210000 1.100000 ( 1.456293)
49
+
50
+ Version: 0.2.3, handler: N/A
51
+ lock/unlock 1000 times with concurrency: 10
52
+ user system total real
53
+ keys: 1 0.640000 0.200000 0.840000 ( 1.105686)
54
+ keys: 2 1.090000 0.420000 1.510000 ( 2.014079)
55
+ keys: 3 1.450000 0.500000 1.950000 ( 2.738510)
56
+ keys: 5 1.940000 0.820000 2.760000 ( 4.136856)
57
+ keys:10 3.360000 1.900000 5.260000 ( 8.232977)
58
+
59
+ Version: 0.3.0, handler: Redis::EM::Mutex::PureHandlerMixin
60
+ lock/unlock 1000 times with concurrency: 10
61
+ user system total real
62
+ keys: 1 0.610000 0.230000 0.840000 ( 0.864242)
63
+ keys: 2 0.640000 0.220000 0.860000 ( 0.914122)
64
+ keys: 3 0.660000 0.260000 0.920000 ( 0.947300)
65
+ keys: 5 0.730000 0.250000 0.980000 ( 1.007862)
66
+ keys:10 0.840000 0.230000 1.070000 ( 1.315885)
67
+
68
+ Version: 0.3.0, handler: Redis::EM::Mutex::ScriptHandlerMixin
69
+ lock/unlock 1000 times with concurrency: 10
70
+ user system total real
71
+ keys: 1 0.290000 0.110000 0.400000 ( 0.633668)
72
+ keys: 2 0.280000 0.150000 0.430000 ( 0.714378)
73
+ keys: 3 0.290000 0.100000 0.390000 ( 0.657861)
74
+ keys: 5 0.430000 0.100000 0.530000 ( 0.775208)
75
+ keys:10 0.330000 0.150000 0.480000 ( 0.904942)
76
+ ```
77
+
78
+ Test 2
79
+ ======
80
+
81
+ run 100 fibers which will repeat the following actions:
82
+
83
+ - sleep some predefined time
84
+ - lock
85
+ - write some value to redis key
86
+ - increase value in redis key
87
+ - read value from that key
88
+ - delete that key
89
+ - unlock
90
+
91
+ Wait 5 seconds and then tell all the fibers to quit.
92
+ The wall time also includes the "cooling time" which lasted
93
+ after 5 seconds has elapsed to the moment where all the fibers
94
+ have ceased processing.
95
+ The resulting value is how many times the above actions were repeated
96
+ during that period.
97
+
98
+ ```
99
+ Version: 0.1.2, handler: N/A
100
+ lock/write/incr/read/del/unlock in 5 seconds + cooldown period:
101
+ result user system total real
102
+ keys: 1 3256 2.290000 1.230000 3.520000 ( 5.135926)
103
+ keys: 2 3179 2.260000 1.000000 3.260000 ( 5.124043)
104
+ keys: 3 3072 2.160000 1.170000 3.330000 ( 5.128032)
105
+ keys: 5 2859 2.280000 1.070000 3.350000 ( 5.132027)
106
+ keys:10 2564 2.460000 0.860000 3.320000 ( 5.151968)
107
+
108
+ Version: 0.2.3, handler: N/A
109
+ lock/write/incr/read/del/unlock in 5 seconds + cooldown period:
110
+ result user system total real
111
+ keys: 1 3274 2.480000 1.010000 3.490000 ( 5.111753)
112
+ keys: 2 2429 2.740000 1.270000 4.010000 ( 5.204065)
113
+ keys: 3 1855 2.380000 1.210000 3.590000 ( 5.256041)
114
+ keys: 5 1309 2.890000 1.250000 4.140000 ( 5.376043)
115
+ keys:10 710 3.120000 1.190000 4.310000 ( 5.763981)
116
+
117
+ Version: 0.3.0, handler: Redis::EM::Mutex::PureHandlerMixin
118
+ lock/write/incr/read/del/unlock in 5 seconds + cooldown period:
119
+ result user system total real
120
+ keys: 1 3795 2.490000 1.400000 3.890000 ( 5.108474)
121
+ keys: 2 3788 2.600000 1.400000 4.000000 ( 5.108037)
122
+ keys: 3 3921 2.800000 1.170000 3.970000 ( 5.120059)
123
+ keys: 5 3641 2.820000 1.140000 3.960000 ( 5.112036)
124
+ keys:10 2661 2.860000 1.130000 3.990000 ( 5.152105)
125
+
126
+ Version: 0.3.0, handler: Redis::EM::Mutex::ScriptHandlerMixin
127
+ lock/write/incr/read/del/unlock in 5 seconds + cooldown period:
128
+ result user system total real
129
+ keys: 1 5177 1.980000 1.020000 3.000000 ( 5.079791)
130
+ keys: 2 5460 1.600000 1.030000 2.630000 ( 5.080049)
131
+ keys: 3 5322 1.560000 1.000000 2.560000 ( 5.088010)
132
+ keys: 5 4685 1.620000 0.810000 2.430000 ( 5.084035)
133
+ keys:10 4347 1.600000 0.770000 2.370000 ( 5.111976)
134
+ ```
@@ -1,5 +1,14 @@
1
+ 0.3.0
2
+ - fixed: optimized pure handler
3
+ - added redis/em-connection-pool no more em-synchrony/connection_pool dependency
4
+ - added redis gem dependency updated to 3.0.2
5
+ - added owner_ident specs
6
+ - added redis-lua script handler
7
+ - added #can_refresh_expired? handler feature detection
8
+ - added handlers
9
+
1
10
  0.2.3
2
- - fixed: rare but possible race condition in unlock!
11
+ - fixed: rare unlock! race condition introduced in 0.2.1
3
12
  manifesting as deadlock exception when no deadlock should occur
4
13
 
5
14
  0.2.2
data/README.md CHANGED
@@ -17,9 +17,9 @@ FEATURES
17
17
  * no CPU-intensive sleep/polling while waiting for lock to become available
18
18
  * fibers waiting for the lock are signalled via Redis channel as soon as the lock
19
19
  has been released (~< 1 ms)
20
+ * alternative fast "script" handler (server-side LUA script based - redis-server 2.6.x)
20
21
  * multi-locks (all-or-nothing) locking (to prevent possible deadlocks when
21
22
  multiple semaphores are required to be locked at once)
22
- * best served with EM-Synchrony (uses EM::Synchrony::ConnectionPool internally)
23
23
  * fiber-safe
24
24
  * deadlock detection (only trivial cases: locking twice the same resource from the same owner)
25
25
  * mandatory lock expiration (with refreshing)
@@ -39,7 +39,7 @@ REQUIREMENTS
39
39
  ------------
40
40
 
41
41
  * ruby >= 1.9 (tested: ruby 1.9.3p374, 1.9.3-p194, 1.9.2-p320, 1.9.1-p378)
42
- * http://github.com/redis/redis-rb ~> 3.0.1
42
+ * http://github.com/redis/redis-rb ~> 3.0.2
43
43
  * http://rubyeventmachine.com ~> 1.0.0
44
44
  * (optional) http://github.com/igrigorik/em-synchrony
45
45
 
@@ -53,7 +53,7 @@ $ [sudo] gem install redis-em-mutex
53
53
  #### Gemfile
54
54
 
55
55
  ```ruby
56
- gem "redis-em-mutex", "~> 0.2.3"
56
+ gem "redis-em-mutex", "~> 0.3.0"
57
57
  ```
58
58
 
59
59
  #### Github
@@ -62,6 +62,27 @@ gem "redis-em-mutex", "~> 0.2.3"
62
62
  git clone git://github.com/royaltm/redis-em-mutex.git
63
63
  ```
64
64
 
65
+ UPGRADING
66
+ ---------
67
+
68
+ 0.2.x -> 0.3.x
69
+
70
+ To upgrade redis-em-mutex on production from 0.2.x to 0.3.x you must make sure the correct handler has been
71
+ selected. See more on HANDLERS below.
72
+
73
+ The "pure" and "script" handlers are not compatible. Two different handlers must not utilize the same semaphore-key space.
74
+
75
+ Because only the "pure" handler is compatible with redis-em-mutex <= 0.2.x, when upgrading live production make sure to add
76
+ `handler: :pure` option to `Redis::EM::Mutex.setup` or set the environment variable on production app servers:
77
+
78
+ ```sh
79
+ REDIS_EM_MUTEX_HANDLER=pure
80
+ export REDIS_EM_MUTEX_HANDLER
81
+ ```
82
+ Upgrading from "pure" to "script" handler requires that all "pure" handler locks __MUST BE DELETED__ from redis-server beforehand.
83
+ Neglecting that will result in possible deadlocks. The "script" handler assumes that the lock expiration process is handled
84
+ by redis-server's PEXPIREAT feature. The "pure" handler does not set timeouts on keys. It handles expiration differently.
85
+
65
86
  USAGE
66
87
  -----
67
88
 
@@ -107,6 +128,58 @@ USAGE
107
128
  end
108
129
  ```
109
130
 
131
+ ### Handlers
132
+
133
+ There are 2 different mutex implementations since version 0.3.0.
134
+
135
+ * The "pure" classic handler utilizes redis optimistic transaction commands (watch/multi).
136
+ This handler works with redis-server 2.4.x and later.
137
+ * The new "script" handler takes advantage of fast atomic server side operations written in LUA.
138
+ Therefore the "script" handler is compatible only with redis-server 2.6.x and later.
139
+
140
+ __IMPORTANT__: The "pure" and "script" implementations are not compatible. The values that each handler stores in semaphore keys have different meaning to them.
141
+ You can not operate on the same set of keys using both handlers from e.g. different applications or application versions.
142
+ See UPGRADING for more info on this.
143
+
144
+ You choose your preferred implementation with `handler` option:
145
+
146
+ ```ruby
147
+ Redis::EM::Mutex.setup(handler: :script)
148
+ Redis::EM::Mutex.handler # "Redis::EM::Mutex::ScriptHandlerMixin"
149
+
150
+ # or
151
+
152
+ Redis::EM::Mutex.setup do |opts|
153
+ opts.handler = :pure
154
+ end
155
+ Redis::EM::Mutex.handler # "Redis::EM::Mutex::PureHandlerMixin"
156
+ ```
157
+
158
+ You may also setup `REDIS_EM_MUTEX_HANDLER` environment variable to preferred implementation name.
159
+ Passing `handler` option to {Redis::EM::Mutex.setup} overrides environment variable.
160
+
161
+ The default handler option is `auto` which selects best handler available for your redis-server.
162
+ It's good for quick sandbox setup, however you should set explicitly which handler you require on production.
163
+
164
+ The differences:
165
+
166
+ * Performance. The "script" handler is faster then the "pure" handler.
167
+ The "pure" handler generates twice as much CPU load as "script" handler.
168
+ See {file:BENCHMARK.md BENCHMARK}.
169
+
170
+ * Expiration. The "script" implementation handler uses PEXPIREAT to mark semaphore life-time.
171
+ The "pure" handler stores semaphore expiry timestamp in key value.
172
+ Therefore the "script" handler can't refresh semaphores once they expire.
173
+ The "pure" handler on the other hand could refresh expired semaphore but only
174
+ if nothing else has locked on that expired key.
175
+
176
+ To detect feature of the current handler:
177
+
178
+ ```ruby
179
+ Redis::EM::Mutex.can_refresh_expired? # true / false
180
+ Redis::EM::Mutex.new(:foo).can_refresh_expired? # true / false
181
+ ```
182
+
110
183
  ### Namespaces
111
184
 
112
185
  ```ruby
@@ -128,6 +201,17 @@ USAGE
128
201
 
129
202
  ### Multi-locks
130
203
 
204
+ This feature enables you to lock more then one key at the same time.
205
+ The multi key semaphores are deadlock-safe.
206
+
207
+ The classic deadlock example scenario with multiple resources:
208
+
209
+ * A acquires lock on resource :foo
210
+ * B acquires lock on resource :bar
211
+ * A tries to lock on resource :bar still keeping the :foo
212
+ * but at the same time B tries to acquire :foo while keeping the :bar.
213
+ * The deadlock occurs.
214
+
131
215
  ```ruby
132
216
  EM.synchrony do
133
217
  Redis::EM::Mutex.synchronize('foo', 'bar', 'baz') do
@@ -218,8 +302,8 @@ The locking scope will be Mutex global namespace + class name + method name.
218
302
 
219
303
  ### ConditionVariable
220
304
 
221
- Redis::EM::Mutex may be used with EventMachine::Synchrony::Thread::ConditionVariable
222
- in place of EventMachine::Synchrony::Thread::Mutex.
305
+ `Redis::EM::Mutex` may be used with `EventMachine::Synchrony::Thread::ConditionVariable`
306
+ in place of `EventMachine::Synchrony::Thread::Mutex`.
223
307
 
224
308
  ```ruby
225
309
  mutex = Redis::EM::Mutex.new('resource')
data/Rakefile CHANGED
@@ -5,12 +5,31 @@ task :default => [:test]
5
5
  $gem_name = "redis-em-mutex"
6
6
 
7
7
  desc "Run spec tests"
8
- task :test do
9
- Dir["spec/#{$gem_name}-*.rb"].each do |spec|
10
- sh "rspec #{spec}"
8
+ namespace :test do
9
+
10
+ task :all => [:auto, :pure, :script]
11
+
12
+ task :auto do
13
+ Dir["spec/#{$gem_name}-*.rb"].each do |spec|
14
+ sh({'REDIS_EM_MUTEX_HANDLER' => nil}, "rspec #{spec}")
15
+ end
16
+ end
17
+
18
+ task :pure do
19
+ Dir["spec/#{$gem_name}-*.rb"].each do |spec|
20
+ sh({'REDIS_EM_MUTEX_HANDLER' => 'pure'}, "rspec #{spec}")
21
+ end
22
+ end
23
+
24
+ task :script do
25
+ Dir["spec/#{$gem_name}-*.rb"].each do |spec|
26
+ sh({'REDIS_EM_MUTEX_HANDLER' => 'script'}, "rspec #{spec}")
27
+ end
11
28
  end
12
29
  end
13
30
 
31
+ task :test => [:'test:all']
32
+
14
33
  desc "Build the gem"
15
34
  task :gem do
16
35
  sh "gem build #$gem_name.gemspec"
@@ -33,5 +52,5 @@ end
33
52
 
34
53
  desc "Documentation"
35
54
  task :doc do
36
- sh "yardoc"
55
+ sh "yardoc - README.md BENCHMARK.md"
37
56
  end
@@ -0,0 +1,99 @@
1
+ $:.unshift "lib"
2
+ gem 'redis', '~>3.0.2'
3
+ require 'securerandom'
4
+ require 'benchmark'
5
+ require 'em-synchrony'
6
+ require 'em-synchrony/fiber_iterator'
7
+ require 'redis-em-mutex'
8
+
9
+ RMutex = Redis::EM::Mutex
10
+ include Benchmark
11
+
12
+ MUTEX_OPTIONS = {
13
+ expire: 10000,
14
+ ns: '__Benchmark',
15
+ }
16
+
17
+ TEST_KEY = '__TEST__'
18
+
19
+ def assert(condition)
20
+ raise "Assertion failed: #{__FILE__}:#{__LINE__}" unless condition
21
+ end
22
+
23
+ # lock and unlock 1000 times
24
+ def test1(keys, concurrency = 10)
25
+ count = 0
26
+ mutex = RMutex.new(*keys)
27
+ EM::Synchrony::FiberIterator.new((1..1000).to_a, concurrency).each do |i|
28
+ mutex.synchronize { count+=1 }
29
+ end
30
+ assert(count == 1000)
31
+ end
32
+
33
+ # lock, set, incr, read, del, unlock, sleep as many times as possible in 5 seconds
34
+ # the cooldown period will be included in total time
35
+ def test2(keys, redis)
36
+ running = true
37
+ count = 0
38
+ playing = 0
39
+ mutex = RMutex.new(*keys)
40
+ f = Fiber.current
41
+ (1..100).map {|i| i/100000.0+0.001}.shuffle.each do |i|
42
+ EM::Synchrony.next_tick do
43
+ while running
44
+ playing+=1
45
+ EM::Synchrony.sleep(i)
46
+ mutex.synchronize do
47
+ # print "."
48
+ value = rand(1000000000000000000)
49
+ redis.set(TEST_KEY, value)
50
+ redis.incr(TEST_KEY)
51
+ assert redis.get(TEST_KEY).to_i == value+1
52
+ redis.del(TEST_KEY)
53
+ count += 1
54
+ end
55
+ playing-=1
56
+ end
57
+ end
58
+ end
59
+ EM::Synchrony.add_timer(5) do
60
+ running = false
61
+ # print "0"
62
+ EM::Synchrony.sleep(0.001) while playing > 0
63
+ EM.next_tick { f.resume }
64
+ end
65
+ Fiber.yield
66
+ print '%5d' % count
67
+ end
68
+
69
+ EM.synchrony do
70
+ concurrency = 10
71
+ RMutex.setup(MUTEX_OPTIONS) {|opts| opts.size = concurrency}
72
+ if RMutex.respond_to? :handler
73
+ puts "Version: #{RMutex::VERSION}, handler: #{RMutex.handler}"
74
+ else
75
+ puts "Version: #{RMutex::VERSION}, handler: N/A"
76
+ end
77
+
78
+ puts "lock/unlock 1000 times with concurrency: #{concurrency}"
79
+ Benchmark.benchmark(CAPTION, 7, FORMAT) do |x|
80
+ [1,2,3,5,10].each do |n|
81
+ keys = n.times.map { SecureRandom.random_bytes + '.lck' }
82
+ x.report("keys:%2d " % n) { test1(keys, concurrency) }
83
+ EM::Synchrony.sleep(1)
84
+ end
85
+ end
86
+
87
+ puts
88
+ puts "lock/write/incr/read/del/unlock in 5 seconds + cooldown period:"
89
+ Benchmark.benchmark(CAPTION, 8, FORMAT) do |x|
90
+ redis = Redis.new
91
+ [1,2,3,5,10].each do |n|
92
+ keys = n.times.map { SecureRandom.random_bytes + '.lck' }
93
+ x.report("keys:%2d " % n) { test2(keys, redis) }
94
+ EM::Synchrony.sleep(1)
95
+ end
96
+ end
97
+ RMutex.stop_watcher(true)
98
+ EM.stop
99
+ end
@@ -0,0 +1,88 @@
1
+ require 'redis'
2
+ class Redis
3
+ module EM
4
+ class ConnectionPool
5
+ def initialize(opts)
6
+ @pool = []
7
+ @queue = []
8
+ @acquired = {}
9
+
10
+ opts[:size].times { @pool << yield }
11
+ end
12
+
13
+ %w[
14
+ exists
15
+ setnx
16
+ publish
17
+ script
18
+ msetnx
19
+ eval
20
+ evalsha
21
+ ].each do |name|
22
+ class_eval <<-EOD, __FILE__, __LINE__
23
+ def #{name}(*args)
24
+ execute do |redis|
25
+ redis.#{name}(*args)
26
+ end
27
+ end
28
+ EOD
29
+ end
30
+
31
+ %w[
32
+ watch
33
+ mget
34
+ ].each do |name|
35
+ class_eval <<-EOD, __FILE__, __LINE__
36
+ def #{name}(*args, &blk)
37
+ execute do |redis|
38
+ redis.#{name}(*args, &blk)
39
+ end
40
+ end
41
+ EOD
42
+ end
43
+
44
+ def multi(&blk)
45
+ execute do |redis|
46
+ redis.multi(&blk)
47
+ end
48
+ end
49
+
50
+ def execute
51
+ f = Fiber.current
52
+ begin
53
+ until (conn = acquire f)
54
+ @queue << f
55
+ Fiber.yield
56
+ end
57
+ yield conn
58
+ ensure
59
+ release(f)
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def acquire(fiber)
66
+ if conn = @pool.pop
67
+ @acquired[fiber.__id__] = conn
68
+ conn
69
+ end
70
+ end
71
+
72
+ def release(fiber)
73
+ @pool.push(@acquired.delete(fiber.__id__))
74
+
75
+ if queue = @queue.shift
76
+ queue.resume
77
+ end
78
+ end
79
+
80
+ def method_missing(method, *args, &blk)
81
+ execute do |conn|
82
+ conn.__send__(method, *args, &blk)
83
+ end
84
+ end
85
+ end
86
+
87
+ end
88
+ end
@@ -27,7 +27,7 @@ class Redis
27
27
  Redis::EM::Mutex.new(*args)
28
28
  end
29
29
 
30
- # Attempts to grab the lock and waits if it isnt available.
30
+ # Attempts to grab the lock and waits if it isn't available.
31
31
  #
32
32
  # See: Redis::EM::Mutex.lock
33
33
  def lock(*args)