qpid_proton 0.18.1 → 0.19.0

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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/ext/cproton/cproton.c +863 -75
  3. data/lib/codec/data.rb +589 -815
  4. data/lib/codec/mapping.rb +142 -126
  5. data/lib/core/condition.rb +89 -0
  6. data/lib/core/connection.rb +188 -228
  7. data/lib/core/connection_driver.rb +202 -0
  8. data/lib/core/container.rb +366 -0
  9. data/lib/core/delivery.rb +76 -251
  10. data/lib/core/disposition.rb +21 -35
  11. data/lib/core/endpoint.rb +21 -53
  12. data/lib/core/event.rb +156 -0
  13. data/lib/core/exceptions.rb +109 -106
  14. data/lib/core/link.rb +24 -49
  15. data/lib/core/listener.rb +82 -0
  16. data/lib/core/message.rb +59 -155
  17. data/lib/core/messaging_handler.rb +190 -0
  18. data/lib/core/receiver.rb +38 -7
  19. data/lib/core/sasl.rb +43 -46
  20. data/lib/core/sender.rb +55 -32
  21. data/lib/core/session.rb +58 -58
  22. data/lib/core/ssl.rb +5 -13
  23. data/lib/core/ssl_details.rb +1 -2
  24. data/lib/core/ssl_domain.rb +5 -8
  25. data/lib/core/terminus.rb +62 -30
  26. data/lib/core/tracker.rb +45 -0
  27. data/lib/core/transfer.rb +121 -0
  28. data/lib/core/transport.rb +62 -97
  29. data/lib/core/uri.rb +73 -0
  30. data/lib/core/url.rb +11 -7
  31. data/lib/handler/adapter.rb +78 -0
  32. data/lib/handler/messaging_adapter.rb +127 -0
  33. data/lib/handler/messaging_handler.rb +128 -178
  34. data/lib/handler/reactor_messaging_adapter.rb +158 -0
  35. data/lib/messenger/messenger.rb +9 -8
  36. data/lib/messenger/subscription.rb +1 -2
  37. data/lib/messenger/tracker.rb +1 -2
  38. data/lib/messenger/tracker_status.rb +1 -2
  39. data/lib/qpid_proton.rb +36 -66
  40. data/lib/reactor/container.rb +40 -234
  41. data/lib/types/array.rb +73 -130
  42. data/lib/types/described.rb +2 -44
  43. data/lib/types/hash.rb +19 -56
  44. data/lib/types/strings.rb +1 -2
  45. data/lib/types/type.rb +68 -0
  46. data/lib/util/{handler.rb → deprecation.rb} +22 -15
  47. data/lib/util/error_handler.rb +4 -25
  48. data/lib/util/timeout.rb +1 -2
  49. data/lib/util/version.rb +1 -2
  50. data/lib/util/wrapper.rb +58 -38
  51. metadata +16 -33
  52. data/lib/core/base_handler.rb +0 -31
  53. data/lib/core/selectable.rb +0 -130
  54. data/lib/event/collector.rb +0 -148
  55. data/lib/event/event.rb +0 -318
  56. data/lib/event/event_base.rb +0 -91
  57. data/lib/event/event_type.rb +0 -71
  58. data/lib/handler/acking.rb +0 -70
  59. data/lib/handler/c_adaptor.rb +0 -47
  60. data/lib/handler/c_flow_controller.rb +0 -33
  61. data/lib/handler/endpoint_state_handler.rb +0 -217
  62. data/lib/handler/incoming_message_handler.rb +0 -74
  63. data/lib/handler/outgoing_message_handler.rb +0 -100
  64. data/lib/handler/wrapped_handler.rb +0 -76
  65. data/lib/reactor/acceptor.rb +0 -41
  66. data/lib/reactor/backoff.rb +0 -41
  67. data/lib/reactor/connector.rb +0 -115
  68. data/lib/reactor/global_overrides.rb +0 -44
  69. data/lib/reactor/link_option.rb +0 -90
  70. data/lib/reactor/reactor.rb +0 -196
  71. data/lib/reactor/session_per_connection.rb +0 -45
  72. data/lib/reactor/ssl_config.rb +0 -41
  73. data/lib/reactor/task.rb +0 -39
  74. data/lib/reactor/urls.rb +0 -45
  75. data/lib/util/class_wrapper.rb +0 -54
  76. data/lib/util/condition.rb +0 -47
  77. data/lib/util/constants.rb +0 -85
  78. data/lib/util/engine.rb +0 -82
  79. data/lib/util/reactor.rb +0 -32
  80. data/lib/util/swig_helper.rb +0 -114
  81. data/lib/util/uuid.rb +0 -32
@@ -0,0 +1,202 @@
1
+ # Licensed to the Apache Software Foundation (ASF) under one
2
+ # or more contributor license agreements. See the NOTICE file
3
+ # distributed with this work for additional information
4
+ # regarding copyright ownership. The ASF licenses this file
5
+ # to you under the Apache License, Version 2.0 (the
6
+ # "License"); you may not use this file except in compliance
7
+ # with the License. You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ require 'socket'
19
+
20
+ module Qpid::Proton
21
+
22
+ # Associate an AMQP {Connection} and {Transport} with an {IO}
23
+ #
24
+ # - {#read} reads AMQP binary data from the {IO} and generates events
25
+ # - {#tick} generates timing-related events
26
+ # - {#event} gets events to be dispatched to {Handler::MessagingHandler}s
27
+ # - {#write} writes AMQP binary data to the {IO}
28
+ #
29
+ # Thread safety: The {ConnectionDriver} is not thread safe but separate
30
+ # {ConnectionDriver} instances can be processed concurrently. The
31
+ # {Container} handles multiple connections concurrently in multiple threads.
32
+ #
33
+ class ConnectionDriver
34
+ # Create a {Connection} and {Transport} associated with +io+
35
+ # @param io [IO] An {IO} or {IO}-like object that responds
36
+ # to {IO#read_nonblock} and {IO#write_nonblock}
37
+ def initialize(io)
38
+ @impl = Cproton.pni_connection_driver or raise NoMemoryError
39
+ @io = io
40
+ @rbuf = "" # String for re-usable read buffer
41
+ end
42
+
43
+ # @return [Connection]
44
+ def connection()
45
+ @connection ||= Connection.wrap(Cproton.pni_connection_driver_connection(@impl))
46
+ end
47
+
48
+ # @return [Transport]
49
+ def transport()
50
+ @transport ||= Transport.wrap(Cproton.pni_connection_driver_transport(@impl))
51
+ end
52
+
53
+ # @return [IO] Allows ConnectionDriver to be passed directly to {IO#select}
54
+ def to_io() @io; end
55
+
56
+ # @return [Bool] True if the driver can read more data
57
+ def can_read?() Cproton.pni_connection_driver_read_size(@impl) > 0; end
58
+
59
+ # @return [Bool] True if the driver has data to write
60
+ def can_write?() Cproton.pni_connection_driver_write_size(@impl) > 0; end
61
+
62
+ # True if the ConnectionDriver has nothing left to do: both sides of the
63
+ # transport are closed and there are no events to dispatch.
64
+ def finished?() Cproton.pn_connection_driver_finished(@impl); end
65
+
66
+ # Get the next event to dispatch, nil if no events available
67
+ def event()
68
+ e = Cproton.pn_connection_driver_next_event(@impl)
69
+ Event.new(e) if e
70
+ end
71
+
72
+ # True if {#event} will return non-nil
73
+ def event?() Cproton.pn_connection_driver_has_event(@impl); end
74
+
75
+ # Iterator for all available events
76
+ def each_event()
77
+ while e = event
78
+ yield e
79
+ end
80
+ end
81
+
82
+ # Non-blocking read from {#io}, generate events for {#event}
83
+ # IO errors are returned as transport errors by {#event}, not raised
84
+ def read
85
+ size = Cproton.pni_connection_driver_read_size(@impl)
86
+ return if size <= 0
87
+ @io.read_nonblock(size, @rbuf) # Use the same string rbuf for reading each time
88
+ Cproton.pni_connection_driver_read_copy(@impl, @rbuf) unless @rbuf.empty?
89
+ rescue Errno::EWOULDBLOCK, Errno::EAGAIN, Errno::EINTR
90
+ # Try again later.
91
+ rescue EOFError # EOF is not an error
92
+ close_read
93
+ rescue IOError, SystemCallError => e
94
+ close e
95
+ end
96
+
97
+ # Non-blocking write to {#io}
98
+ # IO errors are returned as transport errors by {#event}, not raised
99
+ def write
100
+ data = Cproton.pn_connection_driver_write_buffer(@impl)
101
+ return unless data && data.size > 0
102
+ n = @io.write_nonblock(data)
103
+ Cproton.pn_connection_driver_write_done(@impl, n) if n > 0
104
+ rescue Errno::EWOULDBLOCK, Errno::EAGAIN, Errno::EINTR
105
+ # Try again later.
106
+ rescue IOError, SystemCallError => e
107
+ close e
108
+ end
109
+
110
+ # Handle time-related work, for example idle-timeout events.
111
+ # May generate events for {#event} and change {#can_read?}, {#can_write?}
112
+ #
113
+ # @param [Time] now the current time, defaults to {Time#now}.
114
+ #
115
+ # @return [Time] time of the next scheduled event, or nil if there are no
116
+ # scheduled events. If non-nil you must call {#tick} again no later than
117
+ # this time.
118
+ def tick(now=Time.now)
119
+ transport = Cproton.pni_connection_driver_transport(@impl)
120
+ ms = Cproton.pn_transport_tick(transport, (now.to_r * 1000).to_i)
121
+ return ms.zero? ? nil : Time.at(ms.to_r / 1000);
122
+ end
123
+
124
+ # Disconnect the write side of the transport, *without* sending an AMQP
125
+ # close frame. To close politely, you should use {Connection#close}, the
126
+ # transport will close itself once the protocol close is complete.
127
+ #
128
+ def close_write error=nil
129
+ set_error error
130
+ Cproton.pn_connection_driver_write_close(@impl)
131
+ @io.close_write rescue nil # Allow double-close
132
+ end
133
+
134
+ # Is the read side of the driver closed?
135
+ def read_closed?() Cproton.pn_connection_driver_read_closed(@impl); end
136
+
137
+ # Is the write side of the driver closed?
138
+ def write_closed?() Cproton.pn_connection_driver_read_closed(@impl); end
139
+
140
+ # Disconnect the read side of the transport, without waiting for an AMQP
141
+ # close frame. See comments on {#close_write}
142
+ def close_read error=nil
143
+ set_error error
144
+ Cproton.pn_connection_driver_read_close(@impl)
145
+ @io.close_read rescue nil # Allow double-close
146
+ end
147
+
148
+ # Disconnect both sides of the transport sending/waiting for AMQP close
149
+ # frames. See comments on {#close_write}
150
+ def close error=nil
151
+ close_write error
152
+ close_read
153
+ end
154
+
155
+ private
156
+
157
+ def set_error err
158
+ transport.condition ||= Condition.convert(err, "proton:io") if err
159
+ end
160
+ end
161
+
162
+ # A {ConnectionDriver} that feeds raw proton events to a handler.
163
+ class HandlerDriver < ConnectionDriver
164
+ # Combine an {IO} with a handler and provide
165
+ # a simplified way to run the driver via {#process}
166
+ #
167
+ # @param io [IO]
168
+ # @param handler [Handler::MessagingHandler] to receive raw events in {#dispatch} and {#process}
169
+ def initialize(io, handler)
170
+ super(io)
171
+ @handler = handler
172
+ @adapter = Handler::Adapter.adapt(handler)
173
+ end
174
+
175
+ # @return [MessagingHandler] The handler dispatched to by {#process}
176
+ attr_reader :handler
177
+
178
+ # Dispatch all available raw proton events from {#event} to {#handler}
179
+ def dispatch()
180
+ each_event do |e|
181
+ case e.method # Events that affect the driver
182
+ when :on_transport_tail_closed then close_read
183
+ when :on_transport_head_closed then close_write
184
+ end
185
+ e.dispatch @adapter
186
+ end
187
+ end
188
+
189
+ # Do {#read}, {#tick}, {#write} and {#dispatch} without blocking.
190
+ # @param [Time] now the current time
191
+ # @return [Time] Latest time to call {#process} again for scheduled events,
192
+ # or nil if there are no scheduled events
193
+ def process(now=Time.now)
194
+ read
195
+ next_tick = tick(now)
196
+ dispatch # Generate data for write
197
+ write
198
+ dispatch # Consume events generated by write
199
+ return next_tick
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,366 @@
1
+ # Licensed to the Apache Software Foundation (ASF) under one
2
+ # or more contributor license agreements. See the NOTICE file
3
+ # distributed with this work for additional information
4
+ # regarding copyright ownership. The ASF licenses this file
5
+ # to you under the Apache License, Version 2.0 (the
6
+ # "License"); you may not use this file except in compliance
7
+ # with the License. You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+
19
+ require 'thread'
20
+ require 'set'
21
+ require_relative 'listener'
22
+
23
+ module Qpid::Proton
24
+ # An AMQP container manages a set of {Listener}s and {Connection}s which
25
+ # contain {#Sender} and {#Receiver} links to transfer messages. Usually, each
26
+ # AMQP client or server process has a single container for all of its
27
+ # connections and links.
28
+ #
29
+ # One or more threads can call {#run}, events generated by all the listeners and
30
+ # connections will be dispatched in the {#run} threads.
31
+ class Container
32
+ private
33
+
34
+ # Container driver applies options and adds container context to events
35
+ class ConnectionTask < Qpid::Proton::HandlerDriver
36
+ def initialize container, io, opts, server=false
37
+ super io, opts[:handler]
38
+ transport.set_server if server
39
+ transport.apply opts
40
+ connection.apply opts
41
+ end
42
+ end
43
+
44
+ class ListenTask < Listener
45
+
46
+ def initialize(io, handler, container)
47
+ super
48
+ @closing = @closed = nil
49
+ env = ENV['PN_TRACE_EVT']
50
+ if env && ["true", "1", "yes", "on"].include?(env.downcase)
51
+ @log_prefix = "[0x#{object_id.to_s(16)}](PN_LISTENER_"
52
+ else
53
+ @log_prefix = nil
54
+ end
55
+ dispatch(:on_open);
56
+ end
57
+
58
+ def process
59
+ return if @closed
60
+ unless @closing
61
+ begin
62
+ return @io.accept, dispatch(:on_accept)
63
+ rescue IO::WaitReadable, Errno::EINTR
64
+ rescue IOError, SystemCallError => e
65
+ close e
66
+ end
67
+ end
68
+ ensure
69
+ if @closing
70
+ @io.close rescue nil
71
+ @closed = true
72
+ dispatch(:on_error, @condition) if @condition
73
+ dispatch(:on_close)
74
+ end
75
+ end
76
+
77
+ def can_read?() !finished?; end
78
+ def can_write?() false; end
79
+ def finished?() @closed; end
80
+
81
+ def dispatch(method, *args)
82
+ # TODO aconway 2017-11-27: better logging
83
+ STDERR.puts "#{@log_prefix}#{([method[3..-1].upcase]+args).join ', '})" if @log_prefix
84
+ @handler.__send__(method, self, *args) if @handler && @handler.respond_to?(method)
85
+ end
86
+ end
87
+
88
+ public
89
+
90
+ # Error raised if the container is used after {#stop} has been called.
91
+ class StoppedError < RuntimeError
92
+ def initialize(*args) super("container has been stopped"); end
93
+ end
94
+
95
+ # Create a new Container
96
+ # @overload initialize(id=nil)
97
+ # @param id [String,Symbol] A unique ID for this container, use random UUID if nil.
98
+ #
99
+ # @overload initialize(handler=nil, id=nil)
100
+ # @param id [String,Symbol] A unique ID for this container, use random UUID if nil.
101
+ # @param handler [MessagingHandler] Optional default handler for connections
102
+ # that do not have their own handler (see {#connect} and {#listen})
103
+ #
104
+ # *Note*: For multi-threaded code, it is recommended to use a separate
105
+ # handler instance for each connection, as a shared handler may be called
106
+ # concurrently.
107
+ #
108
+ def initialize(*args)
109
+ case args.size
110
+ when 2 then @handler, @id = args
111
+ when 1 then
112
+ @id = String.try_convert(args[0]) || (args[0].to_s if args[0].is_a? Symbol)
113
+ @handler = args[0] unless @id
114
+ else raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0..2"
115
+ end
116
+ # Use an empty messaging adapter to give default behaviour if there's no global handler.
117
+ @adapter = Handler::Adapter.adapt(@handler) || Handler::MessagingAdapter.new(nil)
118
+ @id = (@id || SecureRandom.uuid).freeze
119
+
120
+ # Implementation note:
121
+ #
122
+ # - #run threads take work from @work
123
+ # - Each driver and the Container itself is processed by at most one #run thread at a time
124
+ # - The Container thread does IO.select
125
+ # - nil on the @work queue makes a #run thread exit
126
+
127
+ @work = Queue.new
128
+ @work << :start << self # Issue start and start start selecting
129
+ @wake = IO.pipe # Wakes #run thread in IO.select
130
+ @auto_stop = true # Stop when @active drops to 0
131
+
132
+ # Following instance variables protected by lock
133
+ @lock = Mutex.new
134
+ @active = 0 # All active tasks, in @selectable, @work or being processed
135
+ @selectable = Set.new # Tasks ready to block in IO.select
136
+ @running = 0 # Count of #run threads
137
+ @stopped = false # #stop called
138
+ @stop_err = nil # Optional error to pass to tasks, from #stop
139
+ end
140
+
141
+ # @return [MessagingHandler] The container-wide handler
142
+ attr_reader :handler
143
+
144
+ # @return [String] unique identifier for this container
145
+ attr_reader :id
146
+
147
+ # Auto-stop flag.
148
+ #
149
+ # True (the default) means that the container will stop automatically, as if {#stop}
150
+ # had been called, when the last listener or connection closes.
151
+ #
152
+ # False means {#run} will not return unless {#stop} is called.
153
+ #
154
+ # @return [Bool] auto-stop state
155
+ attr_accessor :auto_stop
156
+
157
+ # True if the container has been stopped and can no longer be used.
158
+ # @return [Bool] stopped state
159
+ attr_accessor :stopped
160
+
161
+ # Number of threads in {#run}
162
+ # @return [Bool] {#run} thread count
163
+ def running; @lock.synchronize { @running }; end
164
+
165
+ # Open an AMQP connection.
166
+ #
167
+ # @param url [String, URI] Open a {TCPSocket} to url.host, url.port.
168
+ # url.scheme must be "amqp" or "amqps", url.scheme.nil? is treated as "amqp"
169
+ # url.user, url.password are used as defaults if opts[:user], opts[:password] are nil
170
+ # @option (see Connection#open)
171
+ # @return [Connection] The new AMQP connection
172
+ def connect(url, opts=nil)
173
+ not_stopped
174
+ url = Qpid::Proton::uri url
175
+ opts ||= {}
176
+ if url.user || url.password
177
+ opts[:user] ||= url.user
178
+ opts[:password] ||= url.password
179
+ end
180
+ opts[:ssl_domain] ||= SSLDomain.new(SSLDomain::MODE_CLIENT) if url.scheme == "amqps"
181
+ connect_io(TCPSocket.new(url.host, url.port), opts)
182
+ end
183
+
184
+ # Open an AMQP protocol connection on an existing {IO} object
185
+ # @param io [IO] An existing {IO} object, e.g. a {TCPSocket}
186
+ # @option (see Connection#open)
187
+ def connect_io(io, opts=nil)
188
+ not_stopped
189
+ cd = connection_driver(io, opts)
190
+ cd.connection.open()
191
+ add(cd)
192
+ cd.connection
193
+ end
194
+
195
+ # Listen for incoming AMQP connections
196
+ #
197
+ # @param url [String,URI] Listen on host:port of the AMQP URL
198
+ # @param handler [Listener::Handler] A {Listener::Handler} object that will be called
199
+ # with events for this listener and can generate a new set of options for each one.
200
+ # @return [Listener] The AMQP listener.
201
+ #
202
+ def listen(url, handler=Listener::Handler.new)
203
+ not_stopped
204
+ url = Qpid::Proton::uri url
205
+ # TODO aconway 2017-11-01: amqps, SSL
206
+ listen_io(TCPServer.new(url.host, url.port), handler)
207
+ end
208
+
209
+ # Listen for incoming AMQP connections on an existing server socket.
210
+ # @param io A server socket, for example a {TCPServer}
211
+ # @param handler [Listener::Handler] Handler for events from this listener
212
+ #
213
+ def listen_io(io, handler=Listener::Handler.new)
214
+ not_stopped
215
+ l = ListenTask.new(io, handler, self)
216
+ add(l)
217
+ l
218
+ end
219
+
220
+ # Run the container: wait for IO activity, dispatch events to handlers.
221
+ #
222
+ # More than one thread can call {#run} concurrently, the container will use
223
+ # all the {#run} threads as a thread pool. Calls to
224
+ # {Handler::MessagingHandler} methods are serialized for each connection or
225
+ # listener, even if the container has multiple threads.
226
+ #
227
+ def run
228
+ @lock.synchronize do
229
+ @running += 1 # Note: ensure clause below will decrement @running
230
+ raise StoppedError if @stopped
231
+ end
232
+ while task = @work.pop
233
+ case task
234
+
235
+ when :start
236
+ @adapter.on_container_start(self) if @adapter.respond_to? :on_container_start
237
+
238
+ when Container
239
+ r, w = [@wake[0]], []
240
+ @lock.synchronize do
241
+ @selectable.each do |s|
242
+ r << s if s.send :can_read?
243
+ w << s if s.send :can_write?
244
+ end
245
+ end
246
+ r, w = IO.select(r, w)
247
+ selected = Set.new(r).merge(w)
248
+ drain_wake if selected.delete?(@wake[0])
249
+ stop_select = nil
250
+ @lock.synchronize do
251
+ if stop_select = @stopped # close everything
252
+ selected += @selectable
253
+ selected.each { |s| s.close @stop_err }
254
+ @wake.each { |fd| fd.close() }
255
+ end
256
+ @selectable -= selected # Remove selected tasks
257
+ end
258
+ selected.each { |s| @work << s } # Queue up tasks needing #process
259
+ @work << self unless stop_select
260
+
261
+ when ConnectionTask then
262
+ task.process
263
+ rearm task
264
+
265
+ when ListenTask then
266
+ io, opts = task.process
267
+ add(connection_driver(io, opts, true)) if io
268
+ rearm task
269
+ end
270
+ # TODO aconway 2017-10-26: scheduled tasks, heartbeats
271
+ end
272
+ ensure
273
+ @lock.synchronize do
274
+ if (@running -= 1) > 0
275
+ work_wake nil # Signal the next thread
276
+ else
277
+ @adapter.on_container_stop(self) if @adapter.respond_to? :on_container_stop
278
+ end
279
+ end
280
+ end
281
+
282
+ # Stop the container.
283
+ #
284
+ # Close all listeners and abort all connections without doing AMQP protocol close.
285
+ #
286
+ # {#stop} returns immediately, calls to {#run} will return when all activity
287
+ # is finished.
288
+ #
289
+ # The container can no longer be used, using a stopped container raises
290
+ # {StoppedError} on attempting. Create a new container if you want to
291
+ # resume activity.
292
+ #
293
+ # @param error [Condition] Optional transport/listener error condition
294
+ #
295
+ def stop(error=nil)
296
+ @lock.synchronize do
297
+ raise StoppedError if @stopped
298
+ @stopped = true
299
+ @stop_err = Condition.convert(error)
300
+ check_stop_lh
301
+ # NOTE: @stopped =>
302
+ # - no new run threads can join
303
+ # - no more select calls after next wakeup
304
+ # - once @active == 0, all threads will be stopped with nil
305
+ end
306
+ wake
307
+ end
308
+
309
+ protected
310
+
311
+ def wake; @wake[1].write_nonblock('x') rescue nil; end
312
+
313
+ # Normally if we add work we need to set a wakeup to ensure a single #run
314
+ # thread doesn't get stuck in select while there is other work on the queue.
315
+ def work_wake(task) @work << task; wake; end
316
+
317
+ def drain_wake
318
+ begin
319
+ @wake[0].read_nonblock(256) while true
320
+ rescue Errno::EWOULDBLOCK, Errno::EAGAIN, Errno::EINTR
321
+ end
322
+ end
323
+
324
+ def connection_driver(io, opts=nil, server=false)
325
+ opts ||= {}
326
+ opts[:container] = self
327
+ opts[:handler] ||= @adapter
328
+ ConnectionTask.new(self, io, opts, server)
329
+ end
330
+
331
+ # All new tasks are added here
332
+ def add task
333
+ @lock.synchronize do
334
+ @active += 1
335
+ task.close @stop_err if @stopped
336
+ end
337
+ work_wake task
338
+ end
339
+
340
+ def rearm task
341
+ @lock.synchronize do
342
+ if task.finished?
343
+ @active -= 1
344
+ check_stop_lh
345
+ elsif @stopped
346
+ task.close @stop_err
347
+ work_wake task
348
+ else
349
+ @selectable << task
350
+ end
351
+ end
352
+ wake
353
+ end
354
+
355
+ def check_stop_lh
356
+ if @active.zero? && (@auto_stop || @stopped)
357
+ @stopped = true
358
+ work_wake nil # Signal threads to stop
359
+ true
360
+ end
361
+ end
362
+
363
+ def not_stopped; raise StoppedError if @lock.synchronize { @stopped }; end
364
+
365
+ end
366
+ end