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 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