slyphon-zookeeper 0.3.0 → 0.8.0.rc.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.
@@ -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
+