officer 0.0.1 → 0.1.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.rdoc +0 -10
- data/VERSION +1 -1
- data/lib/officer/client.rb +73 -0
- data/lib/officer/commands.rb +85 -0
- data/lib/officer/connection.rb +60 -0
- data/lib/officer/lock_store.rb +100 -0
- data/lib/officer/log.rb +21 -0
- data/lib/officer/server.rb +26 -0
- data/lib/officer.rb +1 -1
- data/officer.gemspec +69 -0
- metadata +9 -1
data/README.rdoc
CHANGED
@@ -3,16 +3,6 @@
|
|
3
3
|
This project is a work in progress and shouldn't be considered production ready at this time.
|
4
4
|
It is implemented using Ruby and Eventmachine. Inspiration comes from elock.
|
5
5
|
|
6
|
-
== Note on Patches/Pull Requests
|
7
|
-
|
8
|
-
* Fork the project.
|
9
|
-
* Make your feature addition or bug fix.
|
10
|
-
* Add tests for it. This is important so I don't break it in a
|
11
|
-
future version unintentionally.
|
12
|
-
* Commit, do not mess with rakefile, version, or history.
|
13
|
-
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
14
|
-
* Send me a pull request. Bonus points for topic branches.
|
15
|
-
|
16
6
|
== Copyright
|
17
7
|
|
18
8
|
Copyright (c) 2010 Chad Remesch. See LICENSE for details.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.1.1
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'fcntl'
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'active_support'
|
6
|
+
require 'json'
|
7
|
+
|
8
|
+
module Officer
|
9
|
+
|
10
|
+
class GenericError < RuntimeError; end
|
11
|
+
class AlreadyConnectedError < GenericError; end
|
12
|
+
class NotConnectedError < GenericError; end
|
13
|
+
class LockError < GenericError; end
|
14
|
+
class UnlockError < GenericError; end
|
15
|
+
|
16
|
+
class Client
|
17
|
+
def initialize options={}
|
18
|
+
options.reverse_merge! :host => 'localhost', :port => 11500
|
19
|
+
|
20
|
+
@host = options[:host]
|
21
|
+
@port = options[:port]
|
22
|
+
|
23
|
+
connect
|
24
|
+
end
|
25
|
+
|
26
|
+
def reconnect
|
27
|
+
disconnect
|
28
|
+
connect
|
29
|
+
end
|
30
|
+
|
31
|
+
def lock name
|
32
|
+
execute({:command => 'lock', :name => name}.to_json)
|
33
|
+
end
|
34
|
+
|
35
|
+
def unlock name
|
36
|
+
execute({:command => 'unlock', :name => name}.to_json)
|
37
|
+
end
|
38
|
+
|
39
|
+
def with_lock name
|
40
|
+
response = lock name
|
41
|
+
raise LockError unless response['result'] == 'acquired'
|
42
|
+
|
43
|
+
begin
|
44
|
+
yield
|
45
|
+
ensure
|
46
|
+
response = unlock name
|
47
|
+
raise UnlockError unless response['result'] == 'released'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
def connect
|
53
|
+
raise AlreadyConnectedError if @socket
|
54
|
+
|
55
|
+
@socket = TCPSocket.new @host, @port.to_i
|
56
|
+
@socket.fcntl Fcntl::F_SETFD, Fcntl::FD_CLOEXEC
|
57
|
+
end
|
58
|
+
|
59
|
+
def disconnect
|
60
|
+
raise NotConnectedError unless @socket
|
61
|
+
|
62
|
+
@socket.close
|
63
|
+
@socket = nil
|
64
|
+
end
|
65
|
+
|
66
|
+
def execute command
|
67
|
+
@socket.write command + "\n"
|
68
|
+
result = @socket.gets "\n"
|
69
|
+
JSON.parse result.chomp
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Officer
|
2
|
+
module Command
|
3
|
+
|
4
|
+
class Factory
|
5
|
+
@@commands = {} # command_name => klass
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def create line, connection
|
9
|
+
request = JSON.parse line
|
10
|
+
@@commands[request['command']].new connection, request
|
11
|
+
end
|
12
|
+
|
13
|
+
def register command_name, klass
|
14
|
+
@@commands[command_name] = klass
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Base
|
20
|
+
class << self
|
21
|
+
def register
|
22
|
+
Factory.register to_s.split('::').last.underscore, self
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize connection, request
|
27
|
+
@connection = connection
|
28
|
+
@request = request
|
29
|
+
|
30
|
+
setup
|
31
|
+
|
32
|
+
raise('Invalid request') unless valid?
|
33
|
+
end
|
34
|
+
|
35
|
+
def execute
|
36
|
+
raise 'Must override.'
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def valid?
|
41
|
+
raise 'Must override.'
|
42
|
+
end
|
43
|
+
|
44
|
+
def require_string arg
|
45
|
+
arg.class == String && arg.length > 0
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class Lock < Base
|
50
|
+
register
|
51
|
+
|
52
|
+
def execute
|
53
|
+
L.debug 'executing lock command.'
|
54
|
+
Officer::LockStore.instance.acquire @name, @connection
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
def setup
|
59
|
+
@name = @request['name']
|
60
|
+
end
|
61
|
+
|
62
|
+
def valid?
|
63
|
+
require_string @request['name']
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class Unlock < Base
|
68
|
+
register
|
69
|
+
|
70
|
+
def execute
|
71
|
+
L.debug 'executing unlock command.'
|
72
|
+
Officer::LockStore.instance.release @name, @connection
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
def setup
|
77
|
+
@name = @request['name']
|
78
|
+
end
|
79
|
+
|
80
|
+
def valid?
|
81
|
+
require_string @request['name']
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Officer
|
2
|
+
module Connection
|
3
|
+
|
4
|
+
module EmCallbacks
|
5
|
+
def post_init
|
6
|
+
L.debug "Client connected."
|
7
|
+
@connected = true
|
8
|
+
end
|
9
|
+
|
10
|
+
def receive_line line
|
11
|
+
line.chomp!
|
12
|
+
|
13
|
+
L.debug "Received line: #{line}"
|
14
|
+
|
15
|
+
command = Officer::Command::Factory.create line, self
|
16
|
+
command.execute
|
17
|
+
|
18
|
+
rescue Exception => e
|
19
|
+
L.debug_exception e
|
20
|
+
raise
|
21
|
+
end
|
22
|
+
|
23
|
+
def unbind
|
24
|
+
@connected = false
|
25
|
+
|
26
|
+
L.debug "client disconnected."
|
27
|
+
Officer::LockStore.instance.unbind self
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module LockCallbacks
|
32
|
+
def acquired name
|
33
|
+
L.debug "acquired lock: #{name}"
|
34
|
+
send_line({:result => 'acquired', :name => name}.to_json)
|
35
|
+
end
|
36
|
+
|
37
|
+
def released name
|
38
|
+
L.debug "released lock: #{name}"
|
39
|
+
send_line({:result => 'released', :name => name}.to_json)
|
40
|
+
end
|
41
|
+
|
42
|
+
def release_failed name
|
43
|
+
L.debug "release lock failed: #{name}"
|
44
|
+
send_line({:result => 'release_failed', :name => name}.to_json)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class Connection < EventMachine::Protocols::LineAndTextProtocol
|
49
|
+
include EmCallbacks
|
50
|
+
include LockCallbacks
|
51
|
+
|
52
|
+
attr_reader :connected
|
53
|
+
|
54
|
+
def send_line line
|
55
|
+
send_data "#{line}\n" if @connected
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module Officer
|
2
|
+
|
3
|
+
class Lock
|
4
|
+
attr_reader :name
|
5
|
+
attr_reader :queue
|
6
|
+
|
7
|
+
def initialize name
|
8
|
+
@name = name
|
9
|
+
@queue = []
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class LockStore
|
14
|
+
include Singleton
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@locks = {} # name => Lock
|
18
|
+
@connections = {} # Connection => [name, ...]
|
19
|
+
end
|
20
|
+
|
21
|
+
def log_state
|
22
|
+
L.debug '-----'
|
23
|
+
|
24
|
+
L.debug 'LOCK STORE:'
|
25
|
+
puts
|
26
|
+
|
27
|
+
puts "locks:"
|
28
|
+
@locks.each do |name, lock|
|
29
|
+
puts "#{name}: connections=[#{lock.queue.map{|c| c.object_id}.join(', ')}]"
|
30
|
+
end
|
31
|
+
puts
|
32
|
+
|
33
|
+
puts "Connections:"
|
34
|
+
@connections.each do |connection, names|
|
35
|
+
puts "#{connection.object_id}: names=[#{names.join(', ')}]"
|
36
|
+
end
|
37
|
+
puts
|
38
|
+
|
39
|
+
L.debug '-----'
|
40
|
+
end
|
41
|
+
|
42
|
+
def acquire name, connection
|
43
|
+
lock = @locks[name] ||= Lock.new(name)
|
44
|
+
|
45
|
+
# Queue the lock request unless this connection already has the lock.
|
46
|
+
unless lock.queue.include? connection
|
47
|
+
lock.queue << connection
|
48
|
+
(@connections[connection] ||= []) << name
|
49
|
+
end
|
50
|
+
|
51
|
+
# Tell the client to block unless it has acquired the lock.
|
52
|
+
if lock.queue.first == connection
|
53
|
+
connection.acquired name
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def release name, connection
|
58
|
+
lock = @locks[name]
|
59
|
+
names = @connections[connection]
|
60
|
+
|
61
|
+
# Client should only be able to release a lock that
|
62
|
+
# exists and that it has previously queued.
|
63
|
+
if lock.nil? || !names.include?(name)
|
64
|
+
connection.release_failed(name) and return
|
65
|
+
end
|
66
|
+
|
67
|
+
# If connecton has the lock, release it and let the next
|
68
|
+
# connection know that it has acquired the lock.
|
69
|
+
if lock.queue.first == connection
|
70
|
+
lock.queue.shift
|
71
|
+
connection.released name
|
72
|
+
|
73
|
+
if next_connection = lock.queue.first
|
74
|
+
next_connection.acquired name
|
75
|
+
else
|
76
|
+
@locks.delete name
|
77
|
+
end
|
78
|
+
|
79
|
+
# If the connection is queued, but doesn't have the lock,
|
80
|
+
# release it and leave the other connections alone.
|
81
|
+
else
|
82
|
+
lock.queue.delete connection
|
83
|
+
connection.released name
|
84
|
+
end
|
85
|
+
|
86
|
+
@connections[connection].delete name
|
87
|
+
end
|
88
|
+
|
89
|
+
def unbind connection
|
90
|
+
names = @connections[connection] || []
|
91
|
+
|
92
|
+
names.each do |name|
|
93
|
+
release name, connection
|
94
|
+
end
|
95
|
+
|
96
|
+
@connections.delete connection
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
data/lib/officer/log.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module Officer
|
2
|
+
|
3
|
+
class Log
|
4
|
+
include Singleton
|
5
|
+
|
6
|
+
def debug message
|
7
|
+
puts message
|
8
|
+
end
|
9
|
+
|
10
|
+
def debug_exception e
|
11
|
+
debug '-----'
|
12
|
+
debug "EXCEPTION: "
|
13
|
+
debug e
|
14
|
+
debug e.backtrace.join "\n "
|
15
|
+
debug '-----'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
L = Officer::Log.instance
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Officer
|
2
|
+
|
3
|
+
class Server
|
4
|
+
def initialize options={}
|
5
|
+
options.reverse_merge! :port => 11500, :host => '0.0.0.0'
|
6
|
+
|
7
|
+
@port = options[:port]
|
8
|
+
@host = options[:host]
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
EM.error_handler do |e|
|
13
|
+
L.debug_exception e
|
14
|
+
end
|
15
|
+
|
16
|
+
EM::run do
|
17
|
+
EM::PeriodicTimer.new(5) do
|
18
|
+
Officer::LockStore.instance.log_state
|
19
|
+
end
|
20
|
+
|
21
|
+
EM::start_server @host, @port, Connection::Connection
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
data/lib/officer.rb
CHANGED
@@ -7,7 +7,6 @@ require 'rubygems'
|
|
7
7
|
require 'active_support'
|
8
8
|
require 'eventmachine'
|
9
9
|
require 'json'
|
10
|
-
require 'ruby-debug'
|
11
10
|
|
12
11
|
# Application.
|
13
12
|
require 'officer/log'
|
@@ -15,3 +14,4 @@ require 'officer/commands'
|
|
15
14
|
require 'officer/connection'
|
16
15
|
require 'officer/lock_store'
|
17
16
|
require 'officer/server'
|
17
|
+
require 'officer/client'
|
data/officer.gemspec
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{officer}
|
8
|
+
s.version = "0.1.1"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Chad Remesch"]
|
12
|
+
s.date = %q{2010-02-02}
|
13
|
+
s.default_executable = %q{officer}
|
14
|
+
s.description = %q{Distributed lock server and client written in Ruby and EventMachine}
|
15
|
+
s.email = %q{chad@remesch.com}
|
16
|
+
s.executables = ["officer"]
|
17
|
+
s.extra_rdoc_files = [
|
18
|
+
"LICENSE",
|
19
|
+
"README.rdoc"
|
20
|
+
]
|
21
|
+
s.files = [
|
22
|
+
".document",
|
23
|
+
".gitignore",
|
24
|
+
"LICENSE",
|
25
|
+
"README.rdoc",
|
26
|
+
"Rakefile",
|
27
|
+
"VERSION",
|
28
|
+
"bin/officer",
|
29
|
+
"lib/officer.rb",
|
30
|
+
"lib/officer/client.rb",
|
31
|
+
"lib/officer/commands.rb",
|
32
|
+
"lib/officer/connection.rb",
|
33
|
+
"lib/officer/lock_store.rb",
|
34
|
+
"lib/officer/log.rb",
|
35
|
+
"lib/officer/server.rb",
|
36
|
+
"officer.gemspec",
|
37
|
+
"test/helper.rb",
|
38
|
+
"test/test_officer.rb"
|
39
|
+
]
|
40
|
+
s.homepage = %q{http://github.com/chadrem/officer}
|
41
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
42
|
+
s.require_paths = ["lib"]
|
43
|
+
s.rubygems_version = %q{1.3.5}
|
44
|
+
s.summary = %q{Distributed lock server and client}
|
45
|
+
s.test_files = [
|
46
|
+
"test/helper.rb",
|
47
|
+
"test/test_officer.rb"
|
48
|
+
]
|
49
|
+
|
50
|
+
if s.respond_to? :specification_version then
|
51
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
52
|
+
s.specification_version = 3
|
53
|
+
|
54
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
55
|
+
s.add_development_dependency(%q<eventmachine>, [">= 0"])
|
56
|
+
s.add_development_dependency(%q<json>, [">= 0"])
|
57
|
+
s.add_development_dependency(%q<active_support>, [">= 0"])
|
58
|
+
else
|
59
|
+
s.add_dependency(%q<eventmachine>, [">= 0"])
|
60
|
+
s.add_dependency(%q<json>, [">= 0"])
|
61
|
+
s.add_dependency(%q<active_support>, [">= 0"])
|
62
|
+
end
|
63
|
+
else
|
64
|
+
s.add_dependency(%q<eventmachine>, [">= 0"])
|
65
|
+
s.add_dependency(%q<json>, [">= 0"])
|
66
|
+
s.add_dependency(%q<active_support>, [">= 0"])
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
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.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chad Remesch
|
@@ -58,7 +58,15 @@ files:
|
|
58
58
|
- README.rdoc
|
59
59
|
- Rakefile
|
60
60
|
- VERSION
|
61
|
+
- bin/officer
|
61
62
|
- lib/officer.rb
|
63
|
+
- lib/officer/client.rb
|
64
|
+
- lib/officer/commands.rb
|
65
|
+
- lib/officer/connection.rb
|
66
|
+
- lib/officer/lock_store.rb
|
67
|
+
- lib/officer/log.rb
|
68
|
+
- lib/officer/server.rb
|
69
|
+
- officer.gemspec
|
62
70
|
- test/helper.rb
|
63
71
|
- test/test_officer.rb
|
64
72
|
has_rdoc: true
|