redis-em-mutex 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,20 +1,20 @@
1
1
  BENCHMARK
2
2
  =========
3
3
 
4
- To measure the performance of {Redis::EM::Mutex} I've wrote a simple script called `benchmark_mutex.rb`.
4
+ To measure the performance of {Redis::EM::Mutex} I've wrote a simple script called `test/bench.rb`.
5
5
  The script is included in respository.
6
6
 
7
7
  Below are the results of running tests against the following versions:
8
8
 
9
9
  - redis-em-mutex v0.1.2
10
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
11
+ - redis-em-mutex v0.3.1 - "pure" handler
12
+ - redis-em-mutex v0.3.1 - "script" handler
13
13
 
14
14
  To run theese tests type:
15
15
 
16
16
  ```sh
17
- cp benchmark_mutex.rb /tmp/
17
+ cp test/bench.rb /tmp/benchmark_mutex.rb
18
18
 
19
19
  git reset --hard v0.1.2
20
20
  ruby /tmp/benchmark_mutex.rb
@@ -22,9 +22,9 @@ ruby /tmp/benchmark_mutex.rb
22
22
  git reset --hard v0.2.3
23
23
  ruby /tmp/benchmark_mutex.rb
24
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
25
+ git reset --hard v0.3.1
26
+ REDIS_EM_MUTEX_HANDLER=pure ruby test/bench.rb
27
+ REDIS_EM_MUTEX_HANDLER=script ruby test/bench.rb
28
28
  ```
29
29
 
30
30
  Here are the results of running those tests on Quad Core Xeon machine
@@ -41,38 +41,38 @@ Lock/unlock 1000 times using 10 concurrent fibers.
41
41
  Version: 0.1.2, handler: N/A
42
42
  lock/unlock 1000 times with concurrency: 10
43
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)
44
+ keys: 1/ 1 0.500000 0.240000 0.740000 ( 0.987120)
45
+ keys: 2/ 3 0.700000 0.260000 0.960000 ( 1.179436)
46
+ keys: 3/ 5 0.890000 0.340000 1.230000 ( 1.336847)
47
+ keys: 5/ 9 1.010000 0.540000 1.550000 ( 1.610321)
48
+ keys:10/19 1.480000 0.520000 2.000000 ( 2.120616)
49
49
 
50
50
  Version: 0.2.3, handler: N/A
51
51
  lock/unlock 1000 times with concurrency: 10
52
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)
53
+ keys: 1/ 1 0.550000 0.270000 0.820000 ( 0.861067)
54
+ keys: 2/ 3 0.960000 0.460000 1.420000 ( 1.724032)
55
+ keys: 3/ 5 1.590000 0.510000 2.100000 ( 2.223966)
56
+ keys: 5/ 9 2.660000 0.940000 3.600000 ( 3.784084)
57
+ keys:10/19 5.430000 1.850000 7.280000 ( 8.406377)
58
58
 
59
- Version: 0.3.0, handler: Redis::EM::Mutex::PureHandlerMixin
59
+ Version: 0.3.1, handler: Redis::EM::Mutex::PureHandlerMixin
60
60
  lock/unlock 1000 times with concurrency: 10
61
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)
62
+ keys: 1/ 1 0.620000 0.210000 0.830000 ( 0.869947)
63
+ keys: 2/ 3 0.680000 0.310000 0.990000 ( 1.044803)
64
+ keys: 3/ 5 0.890000 0.340000 1.230000 ( 1.267044)
65
+ keys: 5/ 9 1.190000 0.370000 1.560000 ( 1.576557)
66
+ keys:10/19 1.580000 0.490000 2.070000 ( 2.123451)
67
67
 
68
- Version: 0.3.0, handler: Redis::EM::Mutex::ScriptHandlerMixin
68
+ Version: 0.3.1, handler: Redis::EM::Mutex::ScriptHandlerMixin
69
69
  lock/unlock 1000 times with concurrency: 10
70
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)
71
+ keys: 1/ 1 0.270000 0.060000 0.330000 ( 0.530289)
72
+ keys: 2/ 3 0.360000 0.070000 0.430000 ( 0.664696)
73
+ keys: 3/ 5 0.430000 0.070000 0.500000 ( 0.803888)
74
+ keys: 5/ 9 0.450000 0.160000 0.610000 ( 1.040182)
75
+ keys:10/19 0.710000 0.130000 0.840000 ( 1.767735)
76
76
  ```
77
77
 
78
78
  Test 2
@@ -98,37 +98,53 @@ during that period.
98
98
  ```
99
99
  Version: 0.1.2, handler: N/A
100
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)
101
+ user system total real
102
+ keys: 1/ 1 3134 2.120000 1.260000 3.380000 ( 5.123046)
103
+ keys: 2/ 3 2678 2.350000 1.050000 3.400000 ( 5.134010)
104
+ keys: 3/ 5 2566 2.410000 1.300000 3.710000 ( 5.157308)
105
+ keys: 5/ 9 2256 3.260000 1.200000 4.460000 ( 5.209614)
106
+ keys:10/19 1693 3.250000 1.050000 4.300000 ( 5.230359)
107
107
 
108
108
  Version: 0.2.3, handler: N/A
109
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
110
+ user system total real
111
+ keys: 1/ 1 3199 1.870000 1.120000 2.990000 ( 5.114284)
112
+ keys: 2/ 3 2271 2.680000 1.130000 3.810000 ( 5.221153)
113
+ keys: 3/ 5 1627 2.980000 1.120000 4.100000 ( 5.289593)
114
+ keys: 5/ 9 1094 2.980000 1.260000 4.240000 ( 5.401877)
115
+ keys:10/19 630 3.420000 1.140000 4.560000 ( 5.862919)
116
+
117
+ Version: 0.3.1, handler: Redis::EM::Mutex::PureHandlerMixin
118
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
119
+ user system total real
120
+ keys: 1/ 1 3086 2.450000 1.190000 3.640000 ( 5.128574)
121
+ keys: 2/ 3 2556 2.540000 1.100000 3.640000 ( 5.148499)
122
+ keys: 3/ 5 2423 2.490000 1.150000 3.640000 ( 5.175866)
123
+ keys: 5/ 9 1997 2.980000 1.110000 4.090000 ( 5.218399)
124
+ keys:10/19 1715 3.180000 1.130000 4.310000 ( 5.232533)
125
+
126
+ Version: 0.3.1, handler: Redis::EM::Mutex::ScriptHandlerMixin
127
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)
128
+ user system total real
129
+ keys: 1/ 1 4679 2.380000 0.850000 3.230000 ( 5.073898)
130
+ keys: 2/ 3 4410 2.250000 0.930000 3.180000 ( 5.101776)
131
+ keys: 3/ 5 3428 1.950000 0.730000 2.680000 ( 5.111283)
132
+ keys: 5/ 9 3279 2.050000 0.660000 2.710000 ( 5.203372)
133
+ keys:10/19 2285 1.690000 0.400000 2.090000 ( 5.163491)
134
+ ```
135
+
136
+ Stress test
137
+ ===========
138
+
139
+ You may also want to try this tool: `test/stress.rb`.
140
+
141
+ Written originally by [mlanett](https://github.com/mlanett/redis-lock/blob/master/test/stress.rb).
142
+
143
+ ```
144
+ Usage: test/stress.rb --forks F --tries T --sleep S
145
+ -f, --forks FORKS How many processes to fork
146
+ -t, --tries TRIES How many attempts each process should try
147
+ -s, --sleep SLEEP How long processes should sleep/work
148
+ -k, --keys KEYS How many keys a process should run through
149
+ -h, --help Display this usage summary
134
150
  ```
data/HISTORY.md CHANGED
@@ -1,3 +1,7 @@
1
+ 0.3.1
2
+ - fixed: script handler multi-lock time to wait on failure
3
+ - fixed: bench script randomizes subsets of multi key sets
4
+
1
5
  0.3.0
2
6
  - fixed: optimized pure handler
3
7
  - added redis/em-connection-pool no more em-synchrony/connection_pool dependency
data/LICENCE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2013 by Rafal Michalski (rafal@yeondir.com)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md CHANGED
@@ -8,31 +8,35 @@ Author: Rafał Michalski (mailto:rafal@yeondir.com)
8
8
  DESCRIPTION
9
9
  -----------
10
10
 
11
- __redis-em-mutex__ is the cross server-process-fiber EventMachine + Redis based semaphore.
11
+ __redis-em-mutex__ is the cross server/process/fiber|owner EventMachine + Redis based semaphore.
12
12
 
13
13
  FEATURES
14
14
  --------
15
15
 
16
- * only for EventMachine
17
- * no CPU-intensive sleep/polling while waiting for lock to become available
18
- * fibers waiting for the lock are signalled via Redis channel as soon as the lock
19
- has been released (~< 1 ms)
16
+ * EventMachine reactor based
17
+ * carefully designed, well thought out locking pattern
18
+ (NOT the flawed SETNX/GET/GETSET one from redis documentation page)
19
+ * no CPU-intensive sleep/polling while waiting for lock to become available;
20
+ fibers waiting for the lock are signalled via Redis channel as soon as the lock
21
+ is released (~< 1 ms)
20
22
  * alternative fast "script" handler (server-side LUA script based - redis-server 2.6.x)
21
23
  * multi-locks (all-or-nothing) locking (to prevent possible deadlocks when
22
24
  multiple semaphores are required to be locked at once)
23
25
  * fiber-safe
24
26
  * deadlock detection (only trivial cases: locking twice the same resource from the same owner)
25
- * mandatory lock expiration (with refreshing)
27
+ * mandatory lock lifetime expiration (with refreshing)
26
28
  * macro-style definitions (Mutex::Macro mixin)
27
29
  * compatible with Synchrony::Thread::ConditionVariable
28
30
  * extendable (beyond fibers) mutex ownership
31
+ * redis HA achievable with [redis-sentinel](http://redis.io/topics/sentinel) and [redis-sentinel](https://github.com/flyerhzm/redis-sentinel) gem.
29
32
 
30
33
  BUGS/LIMITATIONS
31
34
  ----------------
32
35
 
33
36
  * only for EventMachine
34
- * NOT thread-safe
35
- * locking order between concurrent processes is undetermined (no FIFO)
37
+ * NOT thread-safe (not meant to be)
38
+ * locking order between concurrent processes is undetermined (no FIFO between processes)
39
+ however during {file:BENCHMARK.md BENCHMARKING} no starvation effect was observed.
36
40
  * it's not nifty, rather somewhat complicated
37
41
 
38
42
  REQUIREMENTS
@@ -42,6 +46,8 @@ REQUIREMENTS
42
46
  * http://github.com/redis/redis-rb ~> 3.0.2
43
47
  * http://rubyeventmachine.com ~> 1.0.0
44
48
  * (optional) http://github.com/igrigorik/em-synchrony
49
+ But due to the redis/synchrony dependency em-synchrony will always be bundled
50
+ and required.
45
51
 
46
52
  INSTALL
47
53
  -------
@@ -53,7 +59,7 @@ $ [sudo] gem install redis-em-mutex
53
59
  #### Gemfile
54
60
 
55
61
  ```ruby
56
- gem "redis-em-mutex", "~> 0.3.0"
62
+ gem "redis-em-mutex", "~> 0.3.1"
57
63
  ```
58
64
 
59
65
  #### Github
@@ -70,7 +76,7 @@ UPGRADING
70
76
  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
77
  selected. See more on HANDLERS below.
72
78
 
73
- The "pure" and "script" handlers are not compatible. Two different handlers must not utilize the same semaphore-key space.
79
+ The "pure" and "script" handlers are incompatible. Two different handlers must not utilize the same semaphore-key space.
74
80
 
75
81
  Because only the "pure" handler is compatible with redis-em-mutex <= 0.2.x, when upgrading live production make sure to add
76
82
  `handler: :pure` option to `Redis::EM::Mutex.setup` or set the environment variable on production app servers:
@@ -79,6 +85,7 @@ Because only the "pure" handler is compatible with redis-em-mutex <= 0.2.x, when
79
85
  REDIS_EM_MUTEX_HANDLER=pure
80
86
  export REDIS_EM_MUTEX_HANDLER
81
87
  ```
88
+
82
89
  Upgrading from "pure" to "script" handler requires that all "pure" handler locks __MUST BE DELETED__ from redis-server beforehand.
83
90
  Neglecting that will result in possible deadlocks. The "script" handler assumes that the lock expiration process is handled
84
91
  by redis-server's PEXPIREAT feature. The "pure" handler does not set timeouts on keys. It handles expiration differently.
@@ -130,14 +137,14 @@ USAGE
130
137
 
131
138
  ### Handlers
132
139
 
133
- There are 2 different mutex implementations since version 0.3.0.
140
+ There are 2 different mutex implementations since version 0.3.
134
141
 
135
142
  * The "pure" classic handler utilizes redis optimistic transaction commands (watch/multi).
136
143
  This handler works with redis-server 2.4.x and later.
137
144
  * The new "script" handler takes advantage of fast atomic server side operations written in LUA.
138
145
  Therefore the "script" handler is compatible only with redis-server 2.6.x and later.
139
146
 
140
- __IMPORTANT__: The "pure" and "script" implementations are not compatible. The values that each handler stores in semaphore keys have different meaning to them.
147
+ __IMPORTANT__: The "pure" and "script" implementations are incompatible. The values that each handler stores in semaphore keys have different meaning to them.
141
148
  You can not operate on the same set of keys using both handlers from e.g. different applications or application versions.
142
149
  See UPGRADING for more info on this.
143
150
 
@@ -183,18 +190,17 @@ To detect feature of the current handler:
183
190
  ### Namespaces
184
191
 
185
192
  ```ruby
186
- Redis::EM::Mutex.setup(ns: 'my_namespace')
193
+ Redis::EM::Mutex.setup(ns: 'Tudor')
187
194
 
188
195
  # or multiple namespaces:
189
196
 
190
- ns = Redis::EM::Mutex::NS.new('my_namespace')
197
+ ns = Redis::EM::Mutex::NS.new('Tudor')
191
198
 
192
199
  EM.synchrony do
193
- ns.synchronize('foo') do
194
- # .... do something with foo
200
+ ns.synchronize('Boscogne') do
201
+ # .... do something special with Tudor:Boscogne
195
202
  end
196
203
 
197
- # ...
198
204
  EM.stop
199
205
  end
200
206
  ```
@@ -215,7 +221,7 @@ The classic deadlock example scenario with multiple resources:
215
221
  ```ruby
216
222
  EM.synchrony do
217
223
  Redis::EM::Mutex.synchronize('foo', 'bar', 'baz') do
218
- # .... do something with foo, bar and baz
224
+ # .... do something special with foo, bar and baz
219
225
  end
220
226
 
221
227
  # ...
@@ -251,7 +257,7 @@ The classic deadlock example scenario with multiple resources:
251
257
 
252
258
  ### Macro-style definition
253
259
 
254
- Borrowed from http://github.com/kenn/redis-mutex.
260
+ Idea of macro-style definition was borrowed from http://github.com/kenn/redis-mutex.
255
261
  Redis::EM::Mutex::Macro is a mixin which protects selected instance methods of a class with a mutex.
256
262
  The locking scope will be Mutex global namespace + class name + method name.
257
263
 
@@ -396,7 +402,7 @@ their locked status in parent process will be preserved.
396
402
 
397
403
 
398
404
  ```ruby
399
- mutex = Redis::EM::Mutex.new('resource1', 'resource2', expire: 60)
405
+ mutex = Redis::EM::Mutex.new('pirkaff', 'roshinu', expire: 60)
400
406
 
401
407
  EM.synchrony do
402
408
  mutex.lock
@@ -432,25 +438,64 @@ their locked status in parent process will be preserved.
432
438
 
433
439
  #### Redis factory
434
440
 
435
- Want to use some non-standard redis options or customized client for semaphore watcher and/or redis pool?
436
- Use `:redis_factory` option then.
441
+ Want to use some non-standard redis options or customized redis client?
442
+ `redis_factory` option to the rescue.
443
+
444
+ High Availability example setup with redis-sentinel:
437
445
 
438
446
  ```ruby
447
+ gem 'redis-sentinel', '~> 1.1.4'
448
+ require 'redis-em-mutex'
439
449
  require 'redis-sentinel'
450
+ Redis::Client.class_eval do
451
+ define_method(:sleep) {|n| EM::Synchrony.sleep(n) }
452
+ end
440
453
 
441
- Redis::EM::Mutex.setup do |opts|
442
- opts.size = 10
443
- opts.password = 'password'
444
- opts.db = 11
445
- opts.redis_factory = proc do |options|
446
- Redis.new options.merge(
447
- master_name: "my_master",
448
- sentinels: [{host: "redis1", port: 6379}, {host: "redis2", port: 6380}])
454
+ REDIS_OPTS = {password: 'fight or die', db: 1}
455
+ SENTINEL_OPTS = {
456
+ master_name: "femto",
457
+ sentinels: [{host: "wyald", port: 26379}, {host: "zodd", port: 26379}],
458
+ failover_reconnect_timeout: 30
459
+ }
460
+
461
+ Redis::EM::Mutex.setup(REDIS_OPTS) do |config|
462
+ config.size = 5 # redis pool size
463
+ config.reconnect_max = :forever # reconnect watcher forever
464
+ config.redis_factory = proc do |opts|
465
+ Redis.new opts.merge SENTINEL_OPTS
449
466
  end
450
467
  end
451
468
  ```
452
469
 
453
- LICENCE
454
- -------
470
+ ADVOCACY
471
+ --------
455
472
 
456
- The MIT License - Copyright (c) 2012 Rafał Michalski
473
+ Interesting (not eventmachine oriented) ruby-redis-mutex implementations:
474
+
475
+ * [mlanett/redis-lock](https://github.com/mlanett/redis-lock)
476
+ Robust, well thought out and nice to use as it simply adds lock/unlock
477
+ commands to Redis.
478
+ Similar concept of locking/unlocking pattern (compared to the "pure" handler)
479
+ though it uses two redis keys for keeping owner and lifetime expiration separately.
480
+ "pure" handler stores both in one key, so less redis operations are involved.
481
+ Blocked lock failure is handled by sleep/polling which involves more cpu load
482
+ on ruby and redis. You may actually see it by running `time test/stress.rb`
483
+ tool on both implementations and compare user/sys load.
484
+
485
+ * [dv/redis-semaphore](https://github.com/dv/redis-semaphore)
486
+ Very promising experiment. Utilizes BLPOP to provide real FIFO queue of
487
+ lock acquiring processes. In this way it doesn't need polling nor other means
488
+ of signaling that the lock is available to those in waiting queue.
489
+ This one could be used with EM straight out without any patching.
490
+
491
+ IMHO the solution has two drawbacks:
492
+
493
+ - no lifetime expiration or other means of protection from failure of a lock owner process;
494
+ still they are trying hard to implement it [now](https://github.com/dv/redis-semaphore/pull/5)
495
+ and I hope they will succeed.
496
+
497
+ - the redis keys are used in an inversed manner: the lack of a key means that the lock is gone.
498
+ On the contrary, when the lock is being released, the key is created and kept.
499
+ This is not a problem when you have some static set of keys. However it might be a problem
500
+ when you need to use lock keys based on random resources and you would need to implement
501
+ some garbage collector to prevent redis from eating to much memory.
data/Rakefile CHANGED
@@ -4,23 +4,25 @@ task :default => [:test]
4
4
 
5
5
  $gem_name = "redis-em-mutex"
6
6
 
7
- desc "Run spec tests"
8
7
  namespace :test do
9
8
 
10
- task :all => [:auto, :pure, :script]
9
+ task :all => [:pure, :script]
11
10
 
11
+ desc "Run specs against auto-detected handler"
12
12
  task :auto do
13
13
  Dir["spec/#{$gem_name}-*.rb"].each do |spec|
14
14
  sh({'REDIS_EM_MUTEX_HANDLER' => nil}, "rspec #{spec}")
15
15
  end
16
16
  end
17
17
 
18
+ desc "Run specs against pure handler"
18
19
  task :pure do
19
20
  Dir["spec/#{$gem_name}-*.rb"].each do |spec|
20
21
  sh({'REDIS_EM_MUTEX_HANDLER' => 'pure'}, "rspec #{spec}")
21
22
  end
22
23
  end
23
24
 
25
+ desc "Run specs against script handler"
24
26
  task :script do
25
27
  Dir["spec/#{$gem_name}-*.rb"].each do |spec|
26
28
  sh({'REDIS_EM_MUTEX_HANDLER' => 'script'}, "rspec #{spec}")
@@ -28,8 +30,14 @@ namespace :test do
28
30
  end
29
31
  end
30
32
 
33
+ desc "Run all specs"
31
34
  task :test => [:'test:all']
32
35
 
36
+ desc "Run stress test WARNING: flushes database on redis-server"
37
+ task :stress do
38
+ sh "test/stress.rb"
39
+ end
40
+
33
41
  desc "Build the gem"
34
42
  task :gem do
35
43
  sh "gem build #$gem_name.gemspec"
@@ -123,18 +123,16 @@ class Redis
123
123
 
124
124
  # Refreshes lock expiration timeout.
125
125
  # Returns `true` if refresh was successfull.
126
- # Returns `false` if the semaphore wasn't locked or when it was locked but it has expired
127
- # and now it's got a new owner.
126
+ # Returns `false` if the semaphore wasn't locked or when it was locked but it has expired.
128
127
  def refresh(expire_timeout=nil)
129
128
  if @lock_expire && owner_ident == (lock_full_ident = @locked_owner_id)
130
129
  lock_expire = (Time.now + (expire_timeout.to_f.nonzero? || self.expire_timeout)).to_f
131
- case keys = eval_safe(@eval_refresh, @ns_names, [lock_full_ident, (lock_expire*1000.0).to_i])
132
- when 1
130
+ !!if 1 == eval_safe(@eval_refresh, @ns_names, [lock_full_ident, (lock_expire*1000.0).to_i])
133
131
  @lock_expire = lock_expire
134
- return true
135
132
  end
133
+ else
134
+ false
136
135
  end
137
- return false
138
136
  end
139
137
 
140
138
  # Releases the lock. Returns self on success.
@@ -176,7 +174,7 @@ class Redis
176
174
  ident_match = owner_ident
177
175
  loop do
178
176
  start_time = Time.now.to_f
179
- case timeout = eval_safe(@eval_lock, @ns_names, [ident_match,
177
+ case timeout = eval_safe(@eval_lock, names, [ident_match,
180
178
  ((lock_expire = (Time.now + expire_timeout).to_f)*1000.0).to_i])
181
179
  when 'OK'
182
180
  @locked_owner_id = ident_match
@@ -256,19 +254,18 @@ class Redis
256
254
  return 'OK'
257
255
  end
258
256
  local res=redis.call('mget',unpack(KEYS))
259
- for i=1,size do
260
- if res[i]==lock then
261
- return 'DD'
262
- end
263
- end
264
257
  exp=nil
265
- for i=1,size do
266
- res=redis.call('pttl',KEYS[i])
267
- if not exp or res<exp then
268
- exp=res
258
+ for i, v in next, res do
259
+ if v==lock then
260
+ return 'DD'
261
+ elseif v then
262
+ v=redis.call('pttl',KEYS[i])
263
+ if not exp or v<exp then
264
+ exp=v
265
+ end
269
266
  end
270
267
  end
271
- return exp
268
+ return exp or -1
272
269
  EOL
273
270
 
274
271
  # * unlock multiple *keys, lock_id, pub_channel, pub_message
@@ -284,8 +281,9 @@ class Redis
284
281
  end
285
282
  end
286
283
  if #args>0 then
287
- redis.call('del',unpack(args))
288
- redis.call('publish',ARGV[2],ARGV[3])
284
+ if redis.call('del',unpack(args)) > 0 then
285
+ redis.call('publish',ARGV[2],ARGV[3])
286
+ end
289
287
  end
290
288
  return #args
291
289
  EOL
@@ -1,7 +1,7 @@
1
1
  class Redis
2
2
  module EM
3
3
  class Mutex
4
- VERSION = '0.3.0'
4
+ VERSION = '0.3.1'
5
5
  end
6
6
  end
7
7
  end
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env ruby
2
+ require 'bundler/setup'
3
+ require 'securerandom'
4
+ require 'benchmark'
5
+ require 'minitest/unit'
6
+
7
+ include Benchmark
8
+ include MiniTest::Assertions
9
+
10
+ REDIS_OPTIONS = {}
11
+
12
+ TEST_KEY = '__TEST__'
13
+
14
+ # lock and unlock 1000 times
15
+ def test1(iterator, synchronize, counter, concurrency = 10)
16
+ iterator.call((1..1000).to_a, concurrency) do
17
+ synchronize.call { counter.call }
18
+ end
19
+ assert_equal(counter.call(0), 1000)
20
+ end
21
+
22
+ # lock, set, incr, read, del, unlock, sleep as many times as possible in 5 seconds
23
+ # the cooldown period will be included in total time
24
+ def test2(iterator, synchronize, sleeper, counter, redis)
25
+ finish_at = Time.now + 5.0
26
+ iterator.call((1..100).map {|i| i/100000.0+0.001}.shuffle, 100) do |i|
27
+ while Time.now - finish_at < 0
28
+ sleeper.call(i)
29
+ synchronize.call do
30
+ # print "."
31
+ value = rand(1000000000000000000)
32
+ redis.set(TEST_KEY, value)
33
+ redis.incr(TEST_KEY)
34
+ assert_equal redis.get(TEST_KEY).to_i, value+1
35
+ redis.del(TEST_KEY)
36
+ counter.call
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+
43
+ def test_all(iterator, synchronize, sleeper, counter, concurrency = 10, keysets = [1,2,3,5,10])
44
+ puts "lock/unlock 1000 times with concurrency: #{concurrency}"
45
+ Benchmark.benchmark(CAPTION, 15, FORMAT) do |x|
46
+ keysets.each do |n|
47
+ counter.call -counter.call(0)
48
+ x.report("keys:%2d/%2d " % [n, n*2-1]) { test1(iterator, synchronize[n], counter, concurrency) }
49
+ sleeper.call 1
50
+ end
51
+ end
52
+
53
+ puts
54
+ puts "lock/write/incr/read/del/unlock in 5 seconds + cooldown period:"
55
+ Benchmark.benchmark(CAPTION, 15, FORMAT) do |x|
56
+ redis = Redis.new REDIS_OPTIONS
57
+ keysets.each do |n|
58
+ counter.call -counter.call(0)
59
+ x.report("keys:%2d/%2d" % [n, n*2-1]) {
60
+ test2(iterator, synchronize[n], sleeper, counter, redis)
61
+ print "\b\b\b\b\b%5d" % counter.call(0)
62
+ }
63
+ sleeper.call 1
64
+ end
65
+ end
66
+ end
67
+
68
+ $:.unshift "lib"
69
+ gem 'redis', '~>3.0.2'
70
+ require 'em-synchrony'
71
+ require 'em-synchrony/fiber_iterator'
72
+ require 'redis-em-mutex'
73
+
74
+ REDIS_OPTIONS.replace(driver: :synchrony)
75
+ MUTEX_OPTIONS = {
76
+ expire: 10000,
77
+ ns: '__Benchmark',
78
+ }
79
+
80
+ RMutex = Redis::EM::Mutex
81
+ EM.synchrony do
82
+ concurrency = 10
83
+ RMutex.setup(REDIS_OPTIONS.merge(MUTEX_OPTIONS)) {|opts| opts.size = concurrency}
84
+ if RMutex.respond_to? :handler
85
+ puts "Version: #{RMutex::VERSION}, handler: #{RMutex.handler}"
86
+ else
87
+ puts "Version: #{RMutex::VERSION}, handler: N/A"
88
+ end
89
+ counter = 0
90
+ test_all(
91
+ proc do |iter, concurrency, &blk|
92
+ EM::Synchrony::FiberIterator.new(iter, concurrency).each(&blk)
93
+ end,
94
+ proc do |n|
95
+ m = n*2-1
96
+ keys = m.times.map { SecureRandom.random_bytes + '.lck' }
97
+ proc do |&blk|
98
+ RMutex.synchronize(*keys.sample(n), &blk)
99
+ end
100
+ end,
101
+ EM::Synchrony.method(:sleep),
102
+ proc do |incr=1|
103
+ counter+=incr
104
+ end,
105
+ concurrency)
106
+ RMutex.stop_watcher(true)
107
+ EM.stop
108
+ end
109
+
110
+ # #gem 'mlanett-redis-lock', require: 'redis-lock'
111
+ # $:.unshift "../redis-lock/lib"
112
+ # require 'hiredis'
113
+ # require 'redis'
114
+ # require 'redis-lock'
115
+
116
+ # REDIS_OPTIONS.replace(driver: :hiredis)
117
+
118
+ # class ThreadIterator
119
+ # def initialize(iter, concurrency)
120
+ # @iter = iter
121
+ # @concurrency = concurrency
122
+ # @threads = []
123
+ # @mutex = ::Mutex.new
124
+ # end
125
+
126
+ # def each(&blk)
127
+ # @threads = @concurrency.times.map do
128
+ # Thread.new do
129
+ # while value = @mutex.synchronize { @iter.shift }
130
+ # blk.call value
131
+ # end
132
+ # end
133
+ # end
134
+ # @threads.each {|t| t.join}
135
+ # end
136
+ # end
137
+
138
+ # concurrency = 10
139
+ # RMutex = Redis
140
+ # counter = 0
141
+ # test_all(
142
+ # proc do |iter, concurrency, &blk|
143
+ # ThreadIterator.new(iter, concurrency).each(&blk)
144
+ # end,
145
+ # proc do |keys|
146
+ # mutex = RMutex.new REDIS_OPTIONS
147
+ # opts = {sleep: 100, acquire: 21, life: 1}
148
+ # proc do |&blk|
149
+ # mutex.lock(keys[0], opts, &blk)
150
+ # end
151
+ # end,
152
+ # Kernel.method(:sleep),
153
+ # proc do |incr=1|
154
+ # print "\r#{counter}"
155
+ # counter+=incr
156
+ # end,
157
+ # concurrency,
158
+ # [1])
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env ruby
2
+ # Author: Mark Lanett - https://github.com/mlanett
3
+ # Origin: https://github.com/mlanett/redis-lock/blob/master/test/stress.rb
4
+ # Adapted for redis-em-mutex by: royaltm
5
+
6
+ require "bundler/setup" # set up gem paths
7
+ require "redis"
8
+ require "redis-em-mutex" # load this gem
9
+ require "optparse"
10
+ require "ostruct"
11
+
12
+ options = OpenStruct.new({
13
+ forks: 30,
14
+ tries: 10,
15
+ sleep: 2,
16
+ keys: 5
17
+ })
18
+
19
+ TEST_REDIS = { url: "redis://127.0.0.1:6379/1", driver: :synchrony }
20
+ RMutex = ::Redis::EM::Mutex
21
+
22
+ OptionParser.new do |opts|
23
+ opts.banner = "Usage: #{__FILE__} --forks F --tries T --sleep S"
24
+ opts.on( "-f", "--forks FORKS", "How many processes to fork" ) { |i| options.forks = i.to_i }
25
+ opts.on( "-t", "--tries TRIES", "How many attempts each process should try" ) { |i| options.tries = i.to_i }
26
+ opts.on( "-s", "--sleep SLEEP", "How long processes should sleep/work" ) { |i| options.sleep = i.to_i }
27
+ opts.on( "-k", "--keys KEYS", "How many keys a process should run through" ) { |i| options.keys = i.to_i }
28
+ opts.on( "-h", "--help", "Display this usage summary" ) { puts opts; exit }
29
+ end.parse!
30
+
31
+ class Runner
32
+
33
+ attr :options
34
+
35
+ def initialize( options )
36
+ @options = options
37
+ end
38
+
39
+ def redis
40
+ @redis ||= ::Redis.connect(TEST_REDIS)
41
+ end
42
+
43
+ def test( key, time )
44
+ RMutex.synchronize( key, block: time, expire: time*2 ) do
45
+ val1 = rand(65536)
46
+ redis.set( "#{key}:widget", val1 )
47
+ ::EM::Synchrony.sleep( time )
48
+ val2 = redis.get("#{key}:widget").to_i
49
+ expect( val1, val2 )
50
+ end
51
+ true
52
+ rescue => x
53
+ # STDERR.puts "Failed due to #{x.inspect}"
54
+ false
55
+ end
56
+
57
+ def run
58
+ keys = Hash[ (0...options.keys).map { |i| [ i, "key:#{i}" ] } ] # i => key:i
59
+ fails = Hash[ (0...options.keys).map { |i| [ i, 0 ] } ] # i => 0
60
+ stats = OpenStruct.new( ok: 0, fails: 0 )
61
+ while keys.size > 0 do
62
+ i = keys.keys.sample
63
+ if test( keys[i], (options.sleep) ) then
64
+ keys.delete(i)
65
+ stats.ok += 1
66
+ else
67
+ fails[i] += 1
68
+ stats.fails += 1
69
+ if fails[i] >= options.tries then
70
+ keys.delete(i)
71
+ end
72
+ end
73
+ end
74
+ puts "[#{Process.pid}] Complete; Ok: #{stats.ok}, Failures: #{stats.fails}"
75
+ end
76
+
77
+ def launch
78
+ EM.fork_reactor do
79
+ Fiber.new do
80
+ GC.copy_on_write_friendly = true if ( GC.copy_on_write_friendly? rescue false )
81
+ run
82
+ EM.stop
83
+ end.resume
84
+ end
85
+ end
86
+
87
+ def expect( val1, val2 )
88
+ if val1 != val2 then
89
+ STDERR.puts "[#{Process.pid}] Value mismatch"
90
+ Kernel.abort
91
+ end
92
+ end
93
+
94
+ end
95
+
96
+ # main
97
+
98
+ puts "[#{Process.pid}] Starting with #{options.inspect}"
99
+
100
+ RMutex.setup(TEST_REDIS)
101
+
102
+ EM.synchrony do
103
+ redis = ::Redis.connect(TEST_REDIS)
104
+ redis.flushdb # clean before run
105
+ redis.client.disconnect # don't keep when forking
106
+
107
+ options.forks.times do
108
+ Runner.new( options ).launch
109
+ end
110
+ Process.waitall
111
+ EM.stop
112
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis-em-mutex
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-02-24 00:00:00.000000000 Z
12
+ date: 2013-02-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: redis
@@ -118,9 +118,9 @@ extra_rdoc_files:
118
118
  files:
119
119
  - BENCHMARK.md
120
120
  - HISTORY.md
121
+ - LICENCE
121
122
  - README.md
122
123
  - Rakefile
123
- - benchmark_mutex.rb
124
124
  - lib/redis-em-mutex.rb
125
125
  - lib/redis/em-connection-pool.rb
126
126
  - lib/redis/em-mutex.rb
@@ -136,6 +136,8 @@ files:
136
136
  - spec/redis-em-mutex-namespaces.rb
137
137
  - spec/redis-em-mutex-owners.rb
138
138
  - spec/redis-em-mutex-semaphores.rb
139
+ - test/bench.rb
140
+ - test/stress.rb
139
141
  homepage: http://github.com/royaltm/redis-em-mutex
140
142
  licenses: []
141
143
  post_install_message:
@@ -1,99 +0,0 @@
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