redis-em-mutex 0.2.3 → 0.3.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.
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)