officer 0.0.1 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|