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 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.0.1
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
@@ -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.0.1
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