slyphon-zookeeper 0.3.0 → 0.8.0.rc.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,15 +4,18 @@ module ZookeeperCommon
4
4
  # sigh, i guess define this here?
5
5
  ZKRB_GLOBAL_CB_REQ = -1
6
6
 
7
+ protected
7
8
  def get_next_event(blocking=true)
8
- return nil if closed? # protect against this happening in a callback after close
9
- super(blocking)
9
+ @event_queue.pop(!blocking).tap do |event|
10
+ logger.debug { "#{self.class}##{__method__} delivering event #{event.inspect}" }
11
+ end
12
+ rescue ThreadError
13
+ nil
10
14
  end
11
15
 
12
- protected
13
16
  def setup_call(meth_name, opts)
14
17
  req_id = nil
15
- @req_mutex.synchronize {
18
+ @mutex.synchronize {
16
19
  req_id = @current_req_id
17
20
  @current_req_id += 1
18
21
  setup_completion(req_id, meth_name, opts) if opts[:callback]
@@ -38,20 +41,65 @@ protected
38
41
  end
39
42
 
40
43
  def get_watcher(req_id)
41
- @req_mutex.synchronize {
44
+ @mutex.synchronize {
42
45
  (req_id == ZKRB_GLOBAL_CB_REQ) ? @watcher_reqs[req_id] : @watcher_reqs.delete(req_id)
43
46
  }
44
47
  end
45
48
 
46
49
  def get_completion(req_id)
47
- @req_mutex.synchronize { @completion_reqs.delete(req_id) }
50
+ @mutex.synchronize { @completion_reqs.delete(req_id) }
51
+ end
52
+
53
+ def setup_dispatch_thread!
54
+ logger.debug { "starting dispatch thread" }
55
+ @dispatcher ||= Thread.new do
56
+ while true
57
+ begin
58
+ dispatch_next_callback(get_next_event(true))
59
+ rescue QueueWithPipe::ShutdownException
60
+ logger.info { "dispatch thread exiting, got shutdown exception" }
61
+ break
62
+ rescue Exception => e
63
+ $stderr.puts ["#{e.class}: #{e.message}", e.backtrace.map { |n| "\t#{n}" }.join("\n")].join("\n")
64
+ end
65
+ end
66
+ signal_dispatch_thread_exit!
67
+ end
48
68
  end
69
+
70
+ # this method is part of the reopen/close code, and is responsible for
71
+ # shutting down the dispatch thread.
72
+ #
73
+ # @dispatcher will be nil when this method exits
74
+ #
75
+ def stop_dispatch_thread!
76
+ logger.debug { "#{self.class}##{__method__}" }
77
+
78
+ if @dispatcher
79
+ @mutex.synchronize do
80
+ event_queue.graceful_close!
49
81
 
50
- def dispatch_next_callback(blocking=true)
51
- hash = get_next_event(blocking)
52
- # Zookeeper.logger.debug { "get_next_event returned: #{hash.inspect}" }
82
+ # we now release the mutex so that dispatch_next_callback can grab it
83
+ # to do what it needs to do while delivering events
84
+ @dispatch_shutdown_cond.wait
53
85
 
86
+ @dispatcher.join
87
+ @dispatcher = nil
88
+ end
89
+ end
90
+ end
91
+
92
+ def signal_dispatch_thread_exit!
93
+ @mutex.synchronize do
94
+ logger.debug { "dispatch thread exiting!" }
95
+ @dispatch_shutdown_cond.broadcast
96
+ end
97
+ end
98
+
99
+ def dispatch_next_callback(hash)
54
100
  return nil unless hash
101
+
102
+ Zookeeper.logger.debug { "get_next_event returned: #{prettify_event(hash).inspect}" }
55
103
 
56
104
  is_completion = hash.has_key?(:rc)
57
105
 
@@ -96,4 +144,16 @@ protected
96
144
  "Required arguments are: #{required.inspect}, but only the arguments #{args.keys.inspect} were supplied."
97
145
  end
98
146
  end
147
+
148
+ private
149
+ def prettify_event(hash)
150
+ hash.dup.tap do |h|
151
+ # pretty up the event display
152
+ h[:type] = ZookeeperConstants::EVENT_TYPE_NAMES.fetch(h[:type]) if h[:type]
153
+ h[:state] = ZookeeperConstants::STATE_NAMES.fetch(h[:state]) if h[:state]
154
+ h[:req_id] = :global_session if h[:req_id] == -1
155
+ end
156
+ end
99
157
  end
158
+
159
+ require 'zookeeper/common/queue_with_pipe'
@@ -0,0 +1,78 @@
1
+ module ZookeeperCommon
2
+ # Ceci n'est pas une pipe
3
+ class QueueWithPipe
4
+ extend Forwardable
5
+
6
+ def_delegators :@queue, :clear
7
+
8
+ # raised when close has been called, and pop() is performed
9
+ #
10
+ class ShutdownException < StandardError; end
11
+
12
+ # @private
13
+ KILL_TOKEN = Object.new unless defined?(KILL_TOKEN)
14
+
15
+ def initialize
16
+ # r, w = IO.pipe
17
+ # @pipe = { :read => r, :write => w }
18
+ @queue = Queue.new
19
+
20
+ # with the EventMachine client, we want to let EM handle clearing the
21
+ # event pipe, so we set this to false
22
+ # @clear_reads_on_pop = true
23
+
24
+ @mutex = Mutex.new
25
+ @closed = false
26
+ @graceful = false
27
+ end
28
+
29
+ def push(obj)
30
+ logger.debug { "#{self.class}##{__method__} obj: #{obj.inspect}, kill_token? #{obj == KILL_TOKEN}" }
31
+ @queue.push(obj)
32
+ end
33
+
34
+ def pop(non_blocking=false)
35
+ raise ShutdownException if closed? # this may get us in trouble
36
+
37
+ rv = @queue.pop(non_blocking)
38
+
39
+ if rv == KILL_TOKEN
40
+ close
41
+ raise ShutdownException
42
+ end
43
+
44
+ rv
45
+ end
46
+
47
+ # close the queue and causes ShutdownException to be raised on waiting threads
48
+ def graceful_close!
49
+ @mutex.synchronize do
50
+ return if @graceful or @closed
51
+ logger.debug { "#{self.class}##{__method__} gracefully closing" }
52
+ @graceful = true
53
+ push(KILL_TOKEN)
54
+ end
55
+ nil
56
+ end
57
+
58
+ def close
59
+ @mutex.synchronize do
60
+ return if @closed
61
+ @closed = true
62
+ end
63
+ end
64
+
65
+ def closed?
66
+ @mutex.synchronize { !!@closed }
67
+ end
68
+
69
+ private
70
+ def clear_reads_on_pop?
71
+ @clear_reads_on_pop
72
+ end
73
+
74
+ def logger
75
+ Zookeeper.logger
76
+ end
77
+ end
78
+ end
@@ -18,6 +18,34 @@ module ZookeeperConstants
18
18
  ZOO_CHILD_EVENT = 4
19
19
  ZOO_SESSION_EVENT = -1
20
20
  ZOO_NOTWATCHING_EVENT = -2
21
+
22
+ # only used by the C extension
23
+ ZOO_LOG_LEVEL_ERROR = 1
24
+ ZOO_LOG_LEVEL_WARN = 2
25
+ ZOO_LOG_LEVEL_INFO = 3
26
+ ZOO_LOG_LEVEL_DEBUG = 4
27
+
28
+ # used to find the name for a numeric event
29
+ # @private
30
+ EVENT_TYPE_NAMES = {
31
+ 1 => :created,
32
+ 2 => :deleted,
33
+ 3 => :changed,
34
+ 4 => :child,
35
+ -1 => :session,
36
+ -2 => :notwatching,
37
+ }
38
+
39
+ # used to pretty print the state name
40
+ # @private
41
+ STATE_NAMES = {
42
+ -112 => :expired_session,
43
+ -113 => :auth_failed,
44
+ 0 => :closed,
45
+ 1 => :connecting,
46
+ 2 => :associating,
47
+ 3 => :connected,
48
+ }
21
49
 
22
50
  def print_events
23
51
  puts "ZK events:"
@@ -13,6 +13,7 @@ module ZookeeperEM
13
13
  @em_connection = nil
14
14
  logger.debug { "ZookeeperEM::Client obj_id %x: init" % [object_id] }
15
15
  super(*a, &b)
16
+ on_attached.succeed
16
17
  end
17
18
 
18
19
  # EM::DefaultDeferrable that will be called back when our em_connection has been detached
@@ -29,152 +30,18 @@ module ZookeeperEM
29
30
  @on_attached
30
31
  end
31
32
 
32
- # returns a Deferrable that will be called when the Zookeeper C event loop
33
- # has been shut down
34
- #
35
- # if a block is given, it will be registered as a callback when the
36
- # connection has been closed
37
- #
38
- def close(&block)
39
- on_close(&block)
40
-
41
- logger.debug { "#{self.class.name}: close called, closed? #{closed?} running? #{running?}" }
42
-
43
- if @_running
44
- @start_stop_mutex.synchronize do
45
- @_running = false
46
- end
47
-
48
- if @em_connection
49
- EM.next_tick do
50
- @em_connection.detach do
51
- logger.debug { "#{self.class.name}: connection unbound, continuing with shutdown" }
52
- finish_closing
53
- end
54
- end
55
- else
56
- logger.debug { "#{self.class.name}: em_connection was never set up, finish closing" }
57
- finish_closing
58
- end
59
- else
60
- logger.debug { "#{self.class.name}: we are not running, so returning on_close deferred" }
61
- end
62
-
63
- on_close
64
- end
65
-
66
- # make this public as the ZKConnection object needs to call it
67
- public :dispatch_next_callback
68
-
69
- protected
70
- # instead of setting up a dispatch thread here, we instead attach
71
- # the #selectable_io to the event loop
72
- def setup_dispatch_thread!
73
- EM.schedule do
74
- if running? and not closed?
75
- begin
76
- logger.debug { "adding EM.watch(#{selectable_io.inspect})" }
77
- @em_connection = EM.watch(selectable_io, ZKConnection, self) { |cnx| cnx.notify_readable = true }
78
- rescue Exception => e
79
- $stderr.puts "caught exception from EM.watch(): #{e.inspect}"
80
- end
81
- end
82
- end
33
+ def dispatch_next_callback(hash)
34
+ EM.schedule { super(hash) }
83
35
  end
84
36
 
85
- def finish_closing
86
- unless @_closed
87
- @start_stop_mutex.synchronize do
88
- logger.debug { "closing handle" }
89
- close_handle
90
- end
91
-
92
- unless selectable_io.closed?
93
- logger.debug { "calling close on selectable_io: #{selectable_io.inspect}" }
94
- selectable_io.close
95
- end
96
- end
97
-
98
- on_close.succeed
99
- end
100
- end
101
-
102
- # this class is handed to EventMachine.watch to handle event dispatching
103
- # when the queue has a message waiting. There's a pipe shared between
104
- # the event thread managed by the queue implementation in C. It's made
105
- # available to the ruby-space through the Zookeeper#selectable_io method.
106
- # When the pipe is readable, that means there's an event waiting. We call
107
- # dispatch_next_event and read a single byte off the pipe.
108
- #
109
- class ZKConnection < EM::Connection
110
-
111
- def initialize(zk_client)
112
- @zk_client = zk_client
113
- end
114
-
115
- def post_init
116
- logger.debug { "post_init called" }
117
- @attached = true
118
-
119
- @on_unbind = EM::DefaultDeferrable.new.tap do |d|
120
- d.callback do
121
- logger.debug { "on_unbind deferred fired" }
122
- end
123
- end
124
-
125
- # probably because of the way post_init works, unless we fire this
126
- # callback in next_tick @em_connection in the client may not be set
127
- # (which on_attached callbacks may be relying on)
128
- EM.next_tick do
129
- logger.debug { "firing on_attached callback" }
130
- @zk_client.on_attached.succeed
131
- end
132
- end
133
-
134
- # EM::DefaultDeferrable that will be called back when our em_connection has been detached
135
- # and we've completed the close operation
136
- def on_unbind(&block)
137
- @on_unbind.callback(&block) if block
138
- @on_unbind
139
- end
140
-
141
- def attached?
142
- @attached
143
- end
144
-
145
- def unbind
146
- on_unbind.succeed
147
- end
148
-
149
- def detach(&blk)
150
- on_unbind(&blk)
151
- return unless @attached
152
- @attached = false
153
- rval = super()
154
- logger.debug { "#{self.class.name}: detached, rval: #{rval.inspect}" }
155
- end
156
-
157
- # we have an event waiting
158
- def notify_readable
159
- if @zk_client.running?
160
-
161
- read_io_nb if @zk_client.dispatch_next_callback(false)
162
-
163
- elsif attached?
164
- logger.debug { "#{self.class.name}: @zk_client was not running? and attached? #{attached?}, detaching!" }
165
- detach
37
+ # this is synchronous, but since the API still allows attaching to on_close,
38
+ # we just fake it here
39
+ def close(&block)
40
+ on_close(&block).tap do |d|
41
+ super()
42
+ d.succeed
166
43
  end
167
44
  end
168
-
169
- private
170
- def read_io_nb(size=1)
171
- @io.read_nonblock(1)
172
- rescue Errno::EWOULDBLOCK, Errno::EAGAIN, IOError
173
- end
174
-
175
- def logger
176
- Zookeeper.logger
177
- end
178
- end
179
- end
45
+ end # Client
46
+ end # ZookeeperEM
180
47
 
@@ -27,7 +27,7 @@ module ZookeeperExceptions
27
27
  ZNOTHING = -117
28
28
  ZSESSIONMOVED = -118
29
29
 
30
- class ZookeeperException < Exception
30
+ class ZookeeperException < StandardError
31
31
  class EverythingOk < ZookeeperException; end
32
32
  class SystemError < ZookeeperException; end
33
33
  class RunTimeInconsistency < ZookeeperException; end
@@ -58,6 +58,9 @@ module ZookeeperExceptions
58
58
  class NotConnected < ZookeeperException; end
59
59
  class ShuttingDownException < ZookeeperException; end
60
60
  class DataTooLargeException < ZookeeperException; end
61
+
62
+ # yes, make an alias, this is the way zookeeper refers to it
63
+ ExpiredSession = SessionExpired
61
64
 
62
65
  def self.by_code(code)
63
66
  case code
@@ -3,7 +3,7 @@ $:.push File.expand_path("../lib", __FILE__)
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "slyphon-zookeeper"
6
- s.version = '0.3.0'
6
+ s.version = '0.8.0.rc.1'
7
7
 
8
8
  s.authors = ["Phillip Pearson", "Eric Maland", "Evan Weaver", "Brian Wickman", "Neil Conway", "Jonathan D. Simms"]
9
9
  s.email = ["slyphon@gmail.com"]
@@ -0,0 +1,50 @@
1
+ # tests the CZookeeper, obviously only available when running under MRI
2
+ require 'spec_helper'
3
+
4
+ if Module.const_defined?(:CZookeeper)
5
+ describe CZookeeper do
6
+ def pop_all_events
7
+ [].tap do |rv|
8
+ begin
9
+ rv << @event_queue.pop(non_blocking=true)
10
+ rescue ThreadError
11
+ end
12
+ end
13
+ end
14
+
15
+ def wait_until_connected(timeout=2)
16
+ wait_until(timeout) { @czk.state == ZookeeperConstants::ZOO_CONNECTED_STATE }
17
+ end
18
+
19
+ describe do
20
+ before do
21
+ @event_queue = ZookeeperCommon::QueueWithPipe.new
22
+ @czk = CZookeeper.new('localhost:2181', @event_queue)
23
+ end
24
+
25
+ after do
26
+ @czk.close rescue Exception
27
+ @event_queue.close rescue Exception
28
+ end
29
+
30
+ it %[should be in connected state within a reasonable amount of time] do
31
+ wait_until_connected.should be_true
32
+ end
33
+
34
+ describe :after_connected do
35
+ before do
36
+ wait_until_connected.should be_true
37
+ end
38
+
39
+ it %[should have a connection event after being connected] do
40
+ event = wait_until(2) { @event_queue.pop }
41
+ event.should be
42
+ event[:req_id].should == ZookeeperCommon::ZKRB_GLOBAL_CB_REQ
43
+ event[:type].should == ZookeeperConstants::ZOO_SESSION_EVENT
44
+ event[:state].should == ZookeeperConstants::ZOO_CONNECTED_STATE
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+