redis-em-mutex 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/HISTORY.rdoc +9 -0
- data/README.rdoc +92 -2
- data/lib/redis/em-mutex/ns.rb +47 -0
- data/lib/redis/em-mutex/version.rb +1 -1
- data/lib/redis/em-mutex.rb +215 -132
- data/spec/redis-em-mutex-condition.rb +162 -0
- data/spec/redis-em-mutex-namespaces.rb +16 -16
- data/spec/redis-em-mutex-owners.rb +156 -0
- data/spec/redis-em-mutex-semaphores.rb +146 -76
- metadata +19 -14
data/HISTORY.rdoc
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
0.2.0
|
2
|
+
- added compatibility with EM::Synchrony::Thread::ConditionVariable
|
3
|
+
- added #sleep and #wakeup
|
4
|
+
- moved Redis::EM::Mutex::NS to autoloaded file
|
5
|
+
- added #unlock! and now mutex object stores ownership information
|
6
|
+
- added #expired? and other expiration status methods
|
7
|
+
- added watching? helper
|
8
|
+
- added customizable ownership
|
9
|
+
|
1
10
|
0.1.2
|
2
11
|
- features macro-style definitions
|
3
12
|
- fixed: setup with :redis
|
data/README.rdoc
CHANGED
@@ -18,9 +18,11 @@ Author:: Rafał Michalski (mailto:rafal@yeondir.com)
|
|
18
18
|
multiple semaphores are required to be locked at once)
|
19
19
|
* best served with EM-Synchrony (uses EM::Synchrony::ConnectionPool internally)
|
20
20
|
* fiber-safe
|
21
|
-
* deadlock detection (only trivial cases: locking twice the same resource from the same
|
21
|
+
* deadlock detection (only trivial cases: locking twice the same resource from the same owner)
|
22
22
|
* mandatory lock expiration (with refreshing)
|
23
23
|
* macro-style definitions (Mutex::Macro mixin)
|
24
|
+
* compatible with Synchrony::Thread::ConditionVariable
|
25
|
+
* extendable (beyond fibers) mutex ownership
|
24
26
|
|
25
27
|
== BUGS/LIMITATIONS
|
26
28
|
|
@@ -42,7 +44,7 @@ Author:: Rafał Michalski (mailto:rafal@yeondir.com)
|
|
42
44
|
|
43
45
|
==== Gemfile
|
44
46
|
|
45
|
-
gem "redis-em-mutex", "~> 0.
|
47
|
+
gem "redis-em-mutex", "~> 0.2.0"
|
46
48
|
|
47
49
|
==== Github
|
48
50
|
|
@@ -189,8 +191,96 @@ The locking scope will be Mutex global namespace + class name + method name.
|
|
189
191
|
|
190
192
|
end
|
191
193
|
|
194
|
+
=== ConditionVariable
|
195
|
+
|
196
|
+
Redis::EM::Mutex may be used with EventMachine::Synchrony::Thread::ConditionVariable
|
197
|
+
in place of EventMachine::Synchrony::Thread::Mutex.
|
198
|
+
|
199
|
+
mutex = Redis::EM::Mutex.new('resource')
|
200
|
+
resource = EM::Synchrony::Thread::ConditionVariable.new
|
201
|
+
EM::Synchrony.next_tick do
|
202
|
+
mutex.synchronize {
|
203
|
+
# current fiber now needs the resource
|
204
|
+
resource.wait(mutex)
|
205
|
+
# current fiber can now have the resource
|
206
|
+
}
|
207
|
+
end
|
208
|
+
EM::Synchrony.next_tick do
|
209
|
+
mutex.synchronize {
|
210
|
+
# current fiber has finished using the resource
|
211
|
+
resource.signal
|
212
|
+
}
|
213
|
+
end
|
214
|
+
|
192
215
|
=== Advanced
|
193
216
|
|
217
|
+
==== Customized owner
|
218
|
+
|
219
|
+
In some cases you might want to extend the ownership of a locked semaphore beyond the current fiber.
|
220
|
+
That is to be able to "share" a locked semaphore between group of fibers associated with some external resource.
|
221
|
+
|
222
|
+
For example we want to allow only one connection from one ip address to be made to our group of servers
|
223
|
+
at the same time.
|
224
|
+
Of course you could configure some kind of load balancer that distributes your load to do that for you,
|
225
|
+
but let's just assume that the load balancer has no such features available.
|
226
|
+
|
227
|
+
The default "owner" of a locked semaphore consists of 3 parts: "Machine uuid" + "process pid" + "fiber id".
|
228
|
+
Redis::EM::Mutex allows you to replace the last part of the "owner" with your own value.
|
229
|
+
To customize mutex's "ownership" you should add :owner option to the created mutex,
|
230
|
+
passing some well defined owner id.
|
231
|
+
In our case the "scope" of a mutex will be the client's ip address and the "owner id"
|
232
|
+
will be an __id__ of an EventMachine::Connection object created upon connection from the client.
|
233
|
+
This way we make sure that any other connection from the same ip address made to any server and process
|
234
|
+
will have different ownership defined. At the same time all the fibers working with that Connection instance
|
235
|
+
can access mutex's #refresh and #unlock methods concurrently.
|
236
|
+
|
237
|
+
module MutexedConnection
|
238
|
+
def post_init
|
239
|
+
@buffer = ""
|
240
|
+
@mutex = nil
|
241
|
+
ip = Socket.unpack_sockaddr_in(get_peername).last
|
242
|
+
Fiber.new do
|
243
|
+
@mutex = Redis::EM::Mutex.lock(ip, block: 0, owner: self.__id__)
|
244
|
+
if @mutex
|
245
|
+
send_data "Hello #{ip}!"
|
246
|
+
#... initialize stuff
|
247
|
+
else
|
248
|
+
send_data "Duplicate connection not allowed!"
|
249
|
+
close_connection_after_writing
|
250
|
+
end
|
251
|
+
end.resume
|
252
|
+
end
|
253
|
+
|
254
|
+
def unbind
|
255
|
+
if @mutex
|
256
|
+
Fiber.new { @mutex.unlock! }.resume
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
def receive_data(data)
|
261
|
+
@buffer << data
|
262
|
+
return unless @mutex
|
263
|
+
Fiber.new do
|
264
|
+
if @mutex.refresh # refresh expiration timeout
|
265
|
+
# ... handle data
|
266
|
+
else
|
267
|
+
close_connection_after_writing
|
268
|
+
end
|
269
|
+
end.resume
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
EM::Synchrony.run {
|
274
|
+
Redis::EM::Mutex.setup(size: 10, url: 'redis:///1', expire: 600)
|
275
|
+
EM.start_server "0.0.0.0", 6969, MutexedConnection
|
276
|
+
}
|
277
|
+
|
278
|
+
==== Forking live
|
279
|
+
|
280
|
+
You may safely fork process while running event reactor and having locked semaphores.
|
281
|
+
The locked semaphores in newly forked process will become unlocked while
|
282
|
+
their locked status in parent process will be preserved.
|
283
|
+
|
194
284
|
mutex = Redis::EM::Mutex.new('resource1', 'resource2', expire: 60)
|
195
285
|
|
196
286
|
EM.synchrony do
|
@@ -0,0 +1,47 @@
|
|
1
|
+
class Redis
|
2
|
+
module EM
|
3
|
+
class Mutex
|
4
|
+
class NS
|
5
|
+
attr_reader :ns
|
6
|
+
alias_method :namespace, :ns
|
7
|
+
# Creates a new namespace (Mutex factory).
|
8
|
+
#
|
9
|
+
# - ns = namespace
|
10
|
+
# - opts = options hash:
|
11
|
+
# - :block - default block timeout
|
12
|
+
# - :expire - default expire timeout
|
13
|
+
def initialize(ns, opts = {})
|
14
|
+
@ns = ns
|
15
|
+
@opts = (opts || {}).merge(:ns => ns)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Creates a namespaced cross machine/process/fiber semaphore.
|
19
|
+
#
|
20
|
+
# for arguments see: Redis::EM::Mutex.new
|
21
|
+
def new(*args)
|
22
|
+
if args.last.kind_of?(Hash)
|
23
|
+
args[-1] = @opts.merge(args.last)
|
24
|
+
else
|
25
|
+
args.push @opts
|
26
|
+
end
|
27
|
+
Redis::EM::Mutex.new(*args)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Attempts to grab the lock and waits if it isn’t available.
|
31
|
+
#
|
32
|
+
# See: Redis::EM::Mutex.lock
|
33
|
+
def lock(*args)
|
34
|
+
mutex = new(*args)
|
35
|
+
mutex if mutex.lock
|
36
|
+
end
|
37
|
+
|
38
|
+
# Executes block of code protected with namespaced semaphore.
|
39
|
+
#
|
40
|
+
# See: Redis::EM::Mutex.synchronize
|
41
|
+
def synchronize(*args, &block)
|
42
|
+
new(*args).synchronize(&block)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|