officer 0.1.1 → 0.2.1
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/README.markdown +50 -0
- data/VERSION +1 -1
- data/lib/officer/client.rb +13 -5
- data/lib/officer/commands.rb +21 -3
- data/lib/officer/connection.rb +36 -1
- data/lib/officer/lock_store.rb +31 -18
- data/officer.gemspec +3 -3
- metadata +3 -3
- data/README.rdoc +0 -8
data/README.markdown
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# Officer - Distributed Lock Server and Client
|
2
|
+
|
3
|
+
This project is a work in progress and shouldn't be considered production ready at this time.
|
4
|
+
It is implemented using Ruby and Eventmachine. Inspiration comes from [elock](http://github.com/dustin/elock).
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
install gemcutter
|
9
|
+
gem install officer
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
Start the server using the 'officer' command in a shell.
|
14
|
+
It will listen on all interfaces on port 11500.
|
15
|
+
All debugging output goes to stdout for now.
|
16
|
+
|
17
|
+
## Ruby Client
|
18
|
+
|
19
|
+
require 'rubygems'
|
20
|
+
require 'officer'
|
21
|
+
|
22
|
+
### Create a client object (:host and :port default to localhost:11500)
|
23
|
+
|
24
|
+
client = Officer::Client.new :host => 'localhost', :port => 11500
|
25
|
+
|
26
|
+
### Lock
|
27
|
+
|
28
|
+
client.lock 'some_lock_name'
|
29
|
+
|
30
|
+
### Unlock
|
31
|
+
|
32
|
+
client.unlock 'some_lock_name
|
33
|
+
|
34
|
+
### Wrap a block of code in a lock/unlock (with optional 5 second timeout)
|
35
|
+
|
36
|
+
client.with_lock('some_lock_name', :timeout => 5) do
|
37
|
+
puts 'hello world'
|
38
|
+
end
|
39
|
+
|
40
|
+
### Release all locks for this connection
|
41
|
+
|
42
|
+
client.reset
|
43
|
+
|
44
|
+
### Reconnect (all locks will be released)
|
45
|
+
|
46
|
+
client.reconnect
|
47
|
+
|
48
|
+
## Copyright
|
49
|
+
|
50
|
+
Copyright (c) 2010 Chad Remesch. See LICENSE for details.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.2.1
|
data/lib/officer/client.rb
CHANGED
@@ -11,6 +11,7 @@ module Officer
|
|
11
11
|
class AlreadyConnectedError < GenericError; end
|
12
12
|
class NotConnectedError < GenericError; end
|
13
13
|
class LockError < GenericError; end
|
14
|
+
class LockTimeoutError < LockError; end
|
14
15
|
class UnlockError < GenericError; end
|
15
16
|
|
16
17
|
class Client
|
@@ -28,17 +29,20 @@ module Officer
|
|
28
29
|
connect
|
29
30
|
end
|
30
31
|
|
31
|
-
def lock name
|
32
|
-
execute({:command => 'lock', :name => name}.to_json)
|
32
|
+
def lock name, options={}
|
33
|
+
execute({:command => 'lock', :name => name, :timeout => options[:timeout]}.to_json)
|
33
34
|
end
|
34
35
|
|
35
36
|
def unlock name
|
36
37
|
execute({:command => 'unlock', :name => name}.to_json)
|
37
38
|
end
|
38
39
|
|
39
|
-
def with_lock name
|
40
|
-
response = lock name
|
41
|
-
|
40
|
+
def with_lock name, options={}
|
41
|
+
response = lock name, options
|
42
|
+
result = response['result']
|
43
|
+
|
44
|
+
raise LockTimeoutError if result == 'timed_out'
|
45
|
+
raise LockError if result != 'acquired'
|
42
46
|
|
43
47
|
begin
|
44
48
|
yield
|
@@ -48,6 +52,10 @@ module Officer
|
|
48
52
|
end
|
49
53
|
end
|
50
54
|
|
55
|
+
def reset
|
56
|
+
execute({:command => 'reset'}.to_json)
|
57
|
+
end
|
58
|
+
|
51
59
|
private
|
52
60
|
def connect
|
53
61
|
raise AlreadyConnectedError if @socket
|
data/lib/officer/commands.rb
CHANGED
@@ -37,13 +37,20 @@ module Officer
|
|
37
37
|
end
|
38
38
|
|
39
39
|
private
|
40
|
-
def
|
41
|
-
|
40
|
+
def setup # Override if necessary.
|
41
|
+
end
|
42
|
+
|
43
|
+
def valid? # Override if necessary.
|
44
|
+
true
|
42
45
|
end
|
43
46
|
|
44
47
|
def require_string arg
|
45
48
|
arg.class == String && arg.length > 0
|
46
49
|
end
|
50
|
+
|
51
|
+
def optional_positive_integer arg
|
52
|
+
arg.nil? || (arg.is_a?(Fixnum) && arg > 0)
|
53
|
+
end
|
47
54
|
end
|
48
55
|
|
49
56
|
class Lock < Base
|
@@ -51,16 +58,18 @@ module Officer
|
|
51
58
|
|
52
59
|
def execute
|
53
60
|
L.debug 'executing lock command.'
|
54
|
-
Officer::LockStore.instance.acquire @name, @connection
|
61
|
+
Officer::LockStore.instance.acquire @name, @connection, :timeout => @timeout
|
55
62
|
end
|
56
63
|
|
57
64
|
private
|
58
65
|
def setup
|
59
66
|
@name = @request['name']
|
67
|
+
@timeout = @request['timeout']
|
60
68
|
end
|
61
69
|
|
62
70
|
def valid?
|
63
71
|
require_string @request['name']
|
72
|
+
optional_positive_integer @request['timeout']
|
64
73
|
end
|
65
74
|
end
|
66
75
|
|
@@ -81,5 +90,14 @@ module Officer
|
|
81
90
|
require_string @request['name']
|
82
91
|
end
|
83
92
|
end
|
93
|
+
|
94
|
+
class Reset < Base
|
95
|
+
register
|
96
|
+
|
97
|
+
def execute
|
98
|
+
L.debug 'executing reset command.'
|
99
|
+
Officer::LockStore.instance.reset @connection
|
100
|
+
end
|
101
|
+
end
|
84
102
|
end
|
85
103
|
end
|
data/lib/officer/connection.rb
CHANGED
@@ -4,7 +4,9 @@ module Officer
|
|
4
4
|
module EmCallbacks
|
5
5
|
def post_init
|
6
6
|
L.debug "Client connected."
|
7
|
+
|
7
8
|
@connected = true
|
9
|
+
@timers = {} # name => Timer
|
8
10
|
end
|
9
11
|
|
10
12
|
def receive_line line
|
@@ -24,31 +26,64 @@ module Officer
|
|
24
26
|
@connected = false
|
25
27
|
|
26
28
|
L.debug "client disconnected."
|
27
|
-
|
29
|
+
|
30
|
+
Officer::LockStore.instance.reset self
|
28
31
|
end
|
29
32
|
end
|
30
33
|
|
31
34
|
module LockCallbacks
|
32
35
|
def acquired name
|
33
36
|
L.debug "acquired lock: #{name}"
|
37
|
+
|
38
|
+
@timers.delete(name).cancel if @timers[name]
|
39
|
+
|
34
40
|
send_line({:result => 'acquired', :name => name}.to_json)
|
35
41
|
end
|
36
42
|
|
37
43
|
def released name
|
38
44
|
L.debug "released lock: #{name}"
|
45
|
+
|
39
46
|
send_line({:result => 'released', :name => name}.to_json)
|
40
47
|
end
|
41
48
|
|
42
49
|
def release_failed name
|
43
50
|
L.debug "release lock failed: #{name}"
|
51
|
+
|
44
52
|
send_line({:result => 'release_failed', :name => name}.to_json)
|
45
53
|
end
|
54
|
+
|
55
|
+
def reset_succeeded
|
56
|
+
L.debug "reset"
|
57
|
+
|
58
|
+
@timers.values.each {|timer| timer.cancel}
|
59
|
+
send_line({:result => 'reset_succeeded'}.to_json)
|
60
|
+
end
|
61
|
+
|
62
|
+
def queued name, options={}
|
63
|
+
L.debug "queued. options=#{options.inspect}"
|
64
|
+
|
65
|
+
timeout = options[:timeout]
|
66
|
+
|
67
|
+
return if timeout.nil? || @timers[name]
|
68
|
+
|
69
|
+
@timers[name] = EM::Timer.new(timeout) do
|
70
|
+
Officer::LockStore.instance.timeout name, self
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def timed_out name
|
75
|
+
L.debug "Timed out connection=#{object_id}, name=#{name}"
|
76
|
+
|
77
|
+
@timers.delete name
|
78
|
+
send_line({:result => 'timed_out', :name => name}.to_json)
|
79
|
+
end
|
46
80
|
end
|
47
81
|
|
48
82
|
class Connection < EventMachine::Protocols::LineAndTextProtocol
|
49
83
|
include EmCallbacks
|
50
84
|
include LockCallbacks
|
51
85
|
|
86
|
+
private
|
52
87
|
attr_reader :connected
|
53
88
|
|
54
89
|
def send_line line
|
data/lib/officer/lock_store.rb
CHANGED
@@ -15,7 +15,7 @@ module Officer
|
|
15
15
|
|
16
16
|
def initialize
|
17
17
|
@locks = {} # name => Lock
|
18
|
-
@connections = {} # Connection =>
|
18
|
+
@connections = {} # Connection => Set(name, ...)
|
19
19
|
end
|
20
20
|
|
21
21
|
def log_state
|
@@ -32,43 +32,45 @@ module Officer
|
|
32
32
|
|
33
33
|
puts "Connections:"
|
34
34
|
@connections.each do |connection, names|
|
35
|
-
puts "#{connection.object_id}: names=[#{names.join(', ')}]"
|
35
|
+
puts "#{connection.object_id}: names=[#{names.to_a.join(', ')}]"
|
36
36
|
end
|
37
37
|
puts
|
38
38
|
|
39
39
|
L.debug '-----'
|
40
40
|
end
|
41
41
|
|
42
|
-
def acquire name, connection
|
42
|
+
def acquire name, connection, options={}
|
43
43
|
lock = @locks[name] ||= Lock.new(name)
|
44
44
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
(@connections[connection] ||= []) << name
|
49
|
-
end
|
45
|
+
lock.queue << connection unless lock.queue.include? connection
|
46
|
+
|
47
|
+
(@connections[connection] ||= Set.new) << name
|
50
48
|
|
51
|
-
# Tell the client to block unless it has acquired the lock.
|
52
49
|
if lock.queue.first == connection
|
53
50
|
connection.acquired name
|
51
|
+
else
|
52
|
+
connection.queued name, options
|
54
53
|
end
|
55
54
|
end
|
56
55
|
|
57
|
-
def release name, connection
|
56
|
+
def release name, connection, options={}
|
57
|
+
options.reverse_merge! :callback => true
|
58
|
+
|
58
59
|
lock = @locks[name]
|
59
60
|
names = @connections[connection]
|
60
61
|
|
61
62
|
# Client should only be able to release a lock that
|
62
63
|
# exists and that it has previously queued.
|
63
64
|
if lock.nil? || !names.include?(name)
|
64
|
-
connection.release_failed(name)
|
65
|
+
connection.release_failed(name) if options[:callback]
|
66
|
+
return
|
65
67
|
end
|
66
68
|
|
67
69
|
# If connecton has the lock, release it and let the next
|
68
70
|
# connection know that it has acquired the lock.
|
69
71
|
if lock.queue.first == connection
|
70
72
|
lock.queue.shift
|
71
|
-
connection.released name
|
73
|
+
connection.released name if options[:callback]
|
72
74
|
|
73
75
|
if next_connection = lock.queue.first
|
74
76
|
next_connection.acquired name
|
@@ -76,24 +78,35 @@ module Officer
|
|
76
78
|
@locks.delete name
|
77
79
|
end
|
78
80
|
|
79
|
-
# If the connection is queued
|
80
|
-
#
|
81
|
+
# If the connection is queued and doesn't have the lock,
|
82
|
+
# dequeue it and leave the other connections alone.
|
81
83
|
else
|
82
84
|
lock.queue.delete connection
|
83
85
|
connection.released name
|
84
86
|
end
|
85
87
|
|
86
|
-
|
88
|
+
names.delete name
|
87
89
|
end
|
88
90
|
|
89
|
-
def
|
90
|
-
names = @connections[connection] ||
|
91
|
+
def reset connection
|
92
|
+
names = @connections[connection] || Set.new
|
91
93
|
|
92
94
|
names.each do |name|
|
93
|
-
release name, connection
|
95
|
+
release name, connection, :callback => false
|
94
96
|
end
|
95
97
|
|
96
98
|
@connections.delete connection
|
99
|
+
connection.reset_succeeded
|
100
|
+
end
|
101
|
+
|
102
|
+
def timeout name, connection
|
103
|
+
lock = @locks[name]
|
104
|
+
names = @connections[connection]
|
105
|
+
|
106
|
+
lock.queue.delete connection
|
107
|
+
names.delete name
|
108
|
+
|
109
|
+
connection.timed_out name
|
97
110
|
end
|
98
111
|
end
|
99
112
|
|
data/officer.gemspec
CHANGED
@@ -5,7 +5,7 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{officer}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.2.1"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Chad Remesch"]
|
@@ -16,13 +16,13 @@ Gem::Specification.new do |s|
|
|
16
16
|
s.executables = ["officer"]
|
17
17
|
s.extra_rdoc_files = [
|
18
18
|
"LICENSE",
|
19
|
-
"README.
|
19
|
+
"README.markdown"
|
20
20
|
]
|
21
21
|
s.files = [
|
22
22
|
".document",
|
23
23
|
".gitignore",
|
24
24
|
"LICENSE",
|
25
|
-
"README.
|
25
|
+
"README.markdown",
|
26
26
|
"Rakefile",
|
27
27
|
"VERSION",
|
28
28
|
"bin/officer",
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: officer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chad Remesch
|
@@ -50,12 +50,12 @@ extensions: []
|
|
50
50
|
|
51
51
|
extra_rdoc_files:
|
52
52
|
- LICENSE
|
53
|
-
- README.
|
53
|
+
- README.markdown
|
54
54
|
files:
|
55
55
|
- .document
|
56
56
|
- .gitignore
|
57
57
|
- LICENSE
|
58
|
-
- README.
|
58
|
+
- README.markdown
|
59
59
|
- Rakefile
|
60
60
|
- VERSION
|
61
61
|
- bin/officer
|
data/README.rdoc
DELETED
@@ -1,8 +0,0 @@
|
|
1
|
-
= Officer - Distributed Lock Server and Client
|
2
|
-
|
3
|
-
This project is a work in progress and shouldn't be considered production ready at this time.
|
4
|
-
It is implemented using Ruby and Eventmachine. Inspiration comes from elock.
|
5
|
-
|
6
|
-
== Copyright
|
7
|
-
|
8
|
-
Copyright (c) 2010 Chad Remesch. See LICENSE for details.
|