raktr 0.0.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +1 -0
- data/LICENSE.md +29 -0
- data/README.md +77 -0
- data/Rakefile +53 -0
- data/lib/raktr/connection/callbacks.rb +71 -0
- data/lib/raktr/connection/error.rb +120 -0
- data/lib/raktr/connection/peer_info.rb +90 -0
- data/lib/raktr/connection/tls.rb +164 -0
- data/lib/raktr/connection.rb +339 -0
- data/lib/raktr/global.rb +24 -0
- data/lib/raktr/iterator.rb +249 -0
- data/lib/raktr/queue.rb +89 -0
- data/lib/raktr/tasks/base.rb +57 -0
- data/lib/raktr/tasks/delayed.rb +33 -0
- data/lib/raktr/tasks/one_off.rb +30 -0
- data/lib/raktr/tasks/periodic.rb +58 -0
- data/lib/raktr/tasks/persistent.rb +29 -0
- data/lib/raktr/tasks.rb +105 -0
- data/lib/raktr/version.rb +13 -0
- data/lib/raktr.rb +707 -0
- data/spec/raktr/connection/tls_spec.rb +348 -0
- data/spec/raktr/connection_spec.rb +74 -0
- data/spec/raktr/iterator_spec.rb +203 -0
- data/spec/raktr/queue_spec.rb +91 -0
- data/spec/raktr/tasks/base.rb +8 -0
- data/spec/raktr/tasks/delayed_spec.rb +71 -0
- data/spec/raktr/tasks/one_off_spec.rb +66 -0
- data/spec/raktr/tasks/periodic_spec.rb +57 -0
- data/spec/raktr/tasks/persistent_spec.rb +54 -0
- data/spec/raktr/tasks_spec.rb +155 -0
- data/spec/raktr_spec.rb +20 -0
- data/spec/raktr_tls_spec.rb +20 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/support/fixtures/handlers/echo_client.rb +34 -0
- data/spec/support/fixtures/handlers/echo_client_tls.rb +10 -0
- data/spec/support/fixtures/handlers/echo_server.rb +12 -0
- data/spec/support/fixtures/handlers/echo_server_tls.rb +8 -0
- data/spec/support/fixtures/pems/cacert.pem +37 -0
- data/spec/support/fixtures/pems/client/cert.pem +37 -0
- data/spec/support/fixtures/pems/client/foo-cert.pem +39 -0
- data/spec/support/fixtures/pems/client/foo-key.pem +51 -0
- data/spec/support/fixtures/pems/client/key.pem +51 -0
- data/spec/support/fixtures/pems/server/cert.pem +37 -0
- data/spec/support/fixtures/pems/server/key.pem +51 -0
- data/spec/support/helpers/paths.rb +23 -0
- data/spec/support/helpers/utilities.rb +135 -0
- data/spec/support/lib/server_option_parser.rb +29 -0
- data/spec/support/lib/servers/runner.rb +13 -0
- data/spec/support/lib/servers.rb +133 -0
- data/spec/support/servers/echo.rb +14 -0
- data/spec/support/servers/echo_tls.rb +22 -0
- data/spec/support/servers/echo_unix.rb +14 -0
- data/spec/support/servers/echo_unix_tls.rb +22 -0
- data/spec/support/shared/connection.rb +696 -0
- data/spec/support/shared/raktr.rb +834 -0
- data/spec/support/shared/task.rb +21 -0
- metadata +140 -0
data/lib/raktr.rb
ADDED
@@ -0,0 +1,707 @@
|
|
1
|
+
=begin
|
2
|
+
|
3
|
+
This file is part of the Raktr project and may be subject to
|
4
|
+
redistribution and commercial restrictions. Please see the Raktr
|
5
|
+
web site for more information on licensing and terms of use.
|
6
|
+
|
7
|
+
=end
|
8
|
+
|
9
|
+
require 'socket'
|
10
|
+
require 'openssl'
|
11
|
+
|
12
|
+
# Reactor scheduler and and resource factory.
|
13
|
+
#
|
14
|
+
# You're probably interested in:
|
15
|
+
#
|
16
|
+
# * Getting access to a shared and {.global globally accessible Reactor} --
|
17
|
+
# that's probably what you want.
|
18
|
+
# * Rest of the class methods can be used to manage it.
|
19
|
+
# * Creating resources like:
|
20
|
+
# * Cross-thread, non-blocking {#create_queue Queues}.
|
21
|
+
# * Asynchronous, concurrent {#create_iterator Iterators}.
|
22
|
+
# * Network connections to:
|
23
|
+
# * {#connect Connect} to a server.
|
24
|
+
# * {#listen Listen} for clients.
|
25
|
+
# * Tasks to be scheduled:
|
26
|
+
# * {#schedule As soon as possible}.
|
27
|
+
# * {#on_tick On every loop iteration}.
|
28
|
+
# * {#delay After a configured delay}.
|
29
|
+
# * {#at_interval Every few seconds}.
|
30
|
+
# * {#on_shutdown During shutdown}.
|
31
|
+
#
|
32
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
33
|
+
class Raktr
|
34
|
+
|
35
|
+
# {Reactor} error namespace.
|
36
|
+
#
|
37
|
+
# All {Reactor} errors inherit from and live under it.
|
38
|
+
#
|
39
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
40
|
+
class Error < StandardError
|
41
|
+
|
42
|
+
# Raised when trying to perform an operation that requires the Reactor
|
43
|
+
# to be running when it is not.
|
44
|
+
#
|
45
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
46
|
+
class NotRunning < Error
|
47
|
+
end
|
48
|
+
|
49
|
+
# Raised when trying to run an already running loop.
|
50
|
+
#
|
51
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
52
|
+
class AlreadyRunning < Error
|
53
|
+
end
|
54
|
+
|
55
|
+
# Raised when trying to use UNIX-domain sockets on a host OS that
|
56
|
+
# does not support them.
|
57
|
+
#
|
58
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
59
|
+
class UNIXSocketsNotSupported < Error
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
%w(connection tasks queue iterator global).each do |f|
|
65
|
+
require_relative "raktr/#{f}"
|
66
|
+
end
|
67
|
+
|
68
|
+
# @return [Integer,nil]
|
69
|
+
# Amount of time to wait for a connection.
|
70
|
+
attr_accessor :max_tick_interval
|
71
|
+
|
72
|
+
# @return [Array<Connection>]
|
73
|
+
# {#attach Attached} connections.
|
74
|
+
attr_reader :connections
|
75
|
+
|
76
|
+
# @return [Integer]
|
77
|
+
# Amount of ticks.
|
78
|
+
attr_reader :ticks
|
79
|
+
|
80
|
+
DEFAULT_OPTIONS = {
|
81
|
+
select_timeout: 0.02,
|
82
|
+
max_tick_interval: 0.02
|
83
|
+
}
|
84
|
+
|
85
|
+
class <<self
|
86
|
+
|
87
|
+
# @return [Reactor]
|
88
|
+
# Lazy-loaded, globally accessible Reactor.
|
89
|
+
def global
|
90
|
+
@raktr ||= Global.instance
|
91
|
+
end
|
92
|
+
|
93
|
+
# Stops the {.global global Reactor} instance and destroys it. The next
|
94
|
+
# call to {.global} will return a new instance.
|
95
|
+
def stop
|
96
|
+
return if !@raktr
|
97
|
+
|
98
|
+
global.stop rescue Error::NotRunning
|
99
|
+
|
100
|
+
# Admittedly not the cleanest solution, but that's the only way to
|
101
|
+
# force a Singleton to re-initialize -- and we want the Singleton to
|
102
|
+
# cleanly implement the pattern in a Thread-safe way.
|
103
|
+
global.class.instance_variable_set(:@singleton__instance__, nil)
|
104
|
+
|
105
|
+
@raktr = nil
|
106
|
+
end
|
107
|
+
|
108
|
+
def supports_unix_sockets?
|
109
|
+
return false if jruby?
|
110
|
+
|
111
|
+
!!UNIXSocket
|
112
|
+
rescue NameError
|
113
|
+
false
|
114
|
+
end
|
115
|
+
|
116
|
+
def jruby?
|
117
|
+
RUBY_PLATFORM == 'java'
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# @param [Hash] options
|
122
|
+
# @option options [Integer,nil] :max_tick_interval (0.02)
|
123
|
+
# How long to wait for each tick when no connections are available for
|
124
|
+
# processing.
|
125
|
+
# @option options [Integer] :select_timeout (0.02)
|
126
|
+
# How long to wait for connection activity before continuing to the next
|
127
|
+
# tick.
|
128
|
+
def initialize( options = {} )
|
129
|
+
options = DEFAULT_OPTIONS.merge( options )
|
130
|
+
|
131
|
+
@max_tick_interval = options[:max_tick_interval]
|
132
|
+
@select_timeout = options[:select_timeout]
|
133
|
+
|
134
|
+
# Socket => Connection
|
135
|
+
@connections = {}
|
136
|
+
@stop = false
|
137
|
+
@ticks = 0
|
138
|
+
@thread = nil
|
139
|
+
@tasks = Tasks.new
|
140
|
+
|
141
|
+
@error_handlers = Tasks.new
|
142
|
+
@shutdown_tasks = Tasks.new
|
143
|
+
@done_signal = ::Queue.new
|
144
|
+
end
|
145
|
+
|
146
|
+
# @return [Reactor::Iterator]
|
147
|
+
# New {Reactor::Iterator} with `self` as the scheduler.
|
148
|
+
# @param [#to_a] list
|
149
|
+
# List to iterate.
|
150
|
+
# @param [Integer] concurrency
|
151
|
+
# Parallel workers to spawn.
|
152
|
+
def create_iterator( list, concurrency = 1 )
|
153
|
+
Raktr::Iterator.new( self, list, concurrency )
|
154
|
+
end
|
155
|
+
|
156
|
+
# @return [Reactor::Queue]
|
157
|
+
# New {Reactor::Queue} with `self` as the scheduler.
|
158
|
+
def create_queue
|
159
|
+
Raktr::Queue.new self
|
160
|
+
end
|
161
|
+
|
162
|
+
# @note {Connection::Error Connection errors} will be passed to the `handler`'s
|
163
|
+
# {Connection::Callbacks#on_close} method as a `reason` argument.
|
164
|
+
#
|
165
|
+
# Connects to a peer.
|
166
|
+
#
|
167
|
+
# @overload connect( host, port, handler = Connection, *handler_options )
|
168
|
+
# @param [String] host
|
169
|
+
# @param [Integer] port
|
170
|
+
# @param [Connection] handler
|
171
|
+
# Connection handler, should be a subclass of {Connection}.
|
172
|
+
# @param [Hash] handler_options
|
173
|
+
# Options to pass to the `#initialize` method of the `handler`.
|
174
|
+
#
|
175
|
+
# @overload connect( unix_socket, handler = Connection, *handler_options )
|
176
|
+
# @param [String] unix_socket
|
177
|
+
# Path to the UNIX socket to connect.
|
178
|
+
# @param [Connection] handler
|
179
|
+
# Connection handler, should be a subclass of {Connection}.
|
180
|
+
# @param [Hash] handler_options
|
181
|
+
# Options to pass to the `#initialize` method of the `handler`.
|
182
|
+
#
|
183
|
+
# @return [Connection]
|
184
|
+
# Connected instance of `handler`.
|
185
|
+
#
|
186
|
+
# @raise (see #fail_if_not_running)
|
187
|
+
# @raise (see #fail_if_non_unix)
|
188
|
+
def connect( *args, &block )
|
189
|
+
fail_if_not_running
|
190
|
+
|
191
|
+
options = determine_connection_options( *args )
|
192
|
+
|
193
|
+
connection = options[:handler].new( *options[:handler_options] )
|
194
|
+
connection.raktr = self
|
195
|
+
block.call connection if block_given?
|
196
|
+
|
197
|
+
begin
|
198
|
+
Connection::Error.translate do
|
199
|
+
socket = options[:unix_socket] ?
|
200
|
+
connect_unix( options[:unix_socket] ) : connect_tcp
|
201
|
+
|
202
|
+
connection.configure options.merge( socket: socket, role: :client )
|
203
|
+
attach connection
|
204
|
+
end
|
205
|
+
rescue Connection::Error => e
|
206
|
+
connection.close e
|
207
|
+
end
|
208
|
+
|
209
|
+
connection
|
210
|
+
end
|
211
|
+
|
212
|
+
# @note {Connection::Error Connection errors} will be passed to the `handler`'s
|
213
|
+
# {Connection::Callbacks#on_close} method as a `reason` argument.
|
214
|
+
#
|
215
|
+
# Listens for incoming connections.
|
216
|
+
#
|
217
|
+
# @overload listen( host, port, handler = Connection, *handler_options )
|
218
|
+
# @param [String] host
|
219
|
+
# @param [Integer] port
|
220
|
+
# @param [Connection] handler
|
221
|
+
# Connection handler, should be a subclass of {Connection}.
|
222
|
+
# @param [Hash] handler_options
|
223
|
+
# Options to pass to the `#initialize` method of the `handler`.
|
224
|
+
#
|
225
|
+
# @raise [Connection::Error::HostNotFound]
|
226
|
+
# If the `host` is invalid.
|
227
|
+
# @raise [Connection::Error::Permission]
|
228
|
+
# If the `port` could not be opened due to a permission error.
|
229
|
+
#
|
230
|
+
# @overload listen( unix_socket, handler = Connection, *handler_options )
|
231
|
+
# @param [String] unix_socket
|
232
|
+
# Path to the UNIX socket to create.
|
233
|
+
# @param [Connection] handler
|
234
|
+
# Connection handler, should be a subclass of {Connection}.
|
235
|
+
# @param [Hash] handler_options
|
236
|
+
# Options to pass to the `#initialize` method of the `handler`.
|
237
|
+
#
|
238
|
+
# @raise [Connection::Error::Permission]
|
239
|
+
# If the `unix_socket` file could not be created due to a permission error.
|
240
|
+
#
|
241
|
+
# @return [Connection]
|
242
|
+
# Listening instance of `handler`.
|
243
|
+
#
|
244
|
+
# @raise (see #fail_if_not_running)
|
245
|
+
# @raise (see #fail_if_non_unix)
|
246
|
+
def listen( *args, &block )
|
247
|
+
fail_if_not_running
|
248
|
+
|
249
|
+
options = determine_connection_options( *args )
|
250
|
+
|
251
|
+
server_handler = proc do
|
252
|
+
c = options[:handler].new( *options[:handler_options] )
|
253
|
+
c.raktr = self
|
254
|
+
block.call c if block_given?
|
255
|
+
c
|
256
|
+
end
|
257
|
+
|
258
|
+
server = server_handler.call
|
259
|
+
|
260
|
+
begin
|
261
|
+
Connection::Error.translate do
|
262
|
+
socket = options[:unix_socket] ?
|
263
|
+
listen_unix( options[:unix_socket] ) :
|
264
|
+
listen_tcp( options[:host], options[:port] )
|
265
|
+
|
266
|
+
server.configure options.merge( socket: socket, role: :server, server_handler: server_handler )
|
267
|
+
attach server
|
268
|
+
end
|
269
|
+
rescue Connection::Error => e
|
270
|
+
server.close e
|
271
|
+
end
|
272
|
+
|
273
|
+
server
|
274
|
+
end
|
275
|
+
|
276
|
+
# @return [Bool]
|
277
|
+
# `true` if the {Reactor} is {#run running}, `false` otherwise.
|
278
|
+
def running?
|
279
|
+
thread && thread.alive?
|
280
|
+
end
|
281
|
+
|
282
|
+
# Stops the {Reactor} {#run loop} {#schedule as soon as possible}.
|
283
|
+
#
|
284
|
+
# @raise (see #fail_if_not_running)
|
285
|
+
def stop
|
286
|
+
schedule { @stop = true }
|
287
|
+
end
|
288
|
+
|
289
|
+
# Starts the {Reactor} loop and blocks the current {#thread} until {#stop}
|
290
|
+
# is called.
|
291
|
+
#
|
292
|
+
# @param [Block] block
|
293
|
+
# Block to call right before initializing the loop.
|
294
|
+
#
|
295
|
+
# @raise (see #fail_if_running)
|
296
|
+
def run( &block )
|
297
|
+
fail_if_running
|
298
|
+
|
299
|
+
@done_signal.clear
|
300
|
+
|
301
|
+
@thread = Thread.current
|
302
|
+
|
303
|
+
block.call( self ) if block_given?
|
304
|
+
|
305
|
+
loop do
|
306
|
+
begin
|
307
|
+
@tasks.call
|
308
|
+
rescue => e
|
309
|
+
@error_handlers.call( e )
|
310
|
+
end
|
311
|
+
break if @stop
|
312
|
+
|
313
|
+
begin
|
314
|
+
process_connections
|
315
|
+
rescue => e
|
316
|
+
@error_handlers.call( e )
|
317
|
+
end
|
318
|
+
break if @stop
|
319
|
+
|
320
|
+
@ticks += 1
|
321
|
+
end
|
322
|
+
|
323
|
+
@tasks.clear
|
324
|
+
close_connections
|
325
|
+
|
326
|
+
@shutdown_tasks.call
|
327
|
+
|
328
|
+
@ticks = 0
|
329
|
+
@thread = nil
|
330
|
+
|
331
|
+
@done_signal << nil
|
332
|
+
end
|
333
|
+
|
334
|
+
# {#run Runs} the Reactor in a thread and blocks until it is {#running?}.
|
335
|
+
#
|
336
|
+
# @param (see #run)
|
337
|
+
#
|
338
|
+
# @return [Thread]
|
339
|
+
# {Reactor#thread}
|
340
|
+
#
|
341
|
+
# @raise (see #fail_if_running)
|
342
|
+
def run_in_thread( &block )
|
343
|
+
fail_if_running
|
344
|
+
|
345
|
+
Thread.new do
|
346
|
+
begin
|
347
|
+
run(&block)
|
348
|
+
rescue => e
|
349
|
+
@error_handlers.call( e )
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
sleep 0.1 while !running?
|
354
|
+
|
355
|
+
thread
|
356
|
+
end
|
357
|
+
|
358
|
+
# Waits for the Reactor to stop {#running?}.
|
359
|
+
#
|
360
|
+
# @raise (see #fail_if_not_running)
|
361
|
+
def wait
|
362
|
+
fail_if_not_running
|
363
|
+
|
364
|
+
@done_signal.pop
|
365
|
+
true
|
366
|
+
end
|
367
|
+
|
368
|
+
# Starts the {#run Reactor loop}, blocks the current {#thread} while the
|
369
|
+
# given `block` executes and then {#stop}s it.
|
370
|
+
#
|
371
|
+
# @param [Block] block
|
372
|
+
# Block to call.
|
373
|
+
#
|
374
|
+
# @raise (see #fail_if_running)
|
375
|
+
def run_block( &block )
|
376
|
+
fail ArgumentError, 'Missing block.' if !block_given?
|
377
|
+
fail_if_running
|
378
|
+
|
379
|
+
run do
|
380
|
+
block.call
|
381
|
+
next_tick { stop }
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
# @param [Block] block
|
386
|
+
# Passes exceptions raised in the Reactor {#thread} to a
|
387
|
+
# {Tasks::Persistent task}.
|
388
|
+
#
|
389
|
+
# @raise (see #fail_if_not_running)
|
390
|
+
def on_error( &block )
|
391
|
+
fail_if_not_running
|
392
|
+
@error_handlers << Tasks::Persistent.new( &block )
|
393
|
+
nil
|
394
|
+
end
|
395
|
+
|
396
|
+
# @param [Block] block
|
397
|
+
# Schedules a {Tasks::Persistent task} to be run at each tick.
|
398
|
+
#
|
399
|
+
# @raise (see #fail_if_not_running)
|
400
|
+
def on_tick( &block )
|
401
|
+
fail_if_not_running
|
402
|
+
@tasks << Tasks::Persistent.new( &block )
|
403
|
+
nil
|
404
|
+
end
|
405
|
+
|
406
|
+
# @param [Block] block
|
407
|
+
# Schedules a task to be run as soon as possible, either immediately if
|
408
|
+
# the caller is {#in_same_thread? in the same thread}, or at the
|
409
|
+
# {#next_tick} otherwise.
|
410
|
+
#
|
411
|
+
# @raise (see #fail_if_not_running)
|
412
|
+
def schedule( &block )
|
413
|
+
fail_if_not_running
|
414
|
+
|
415
|
+
if in_same_thread?
|
416
|
+
block.call self
|
417
|
+
else
|
418
|
+
next_tick(&block)
|
419
|
+
end
|
420
|
+
|
421
|
+
nil
|
422
|
+
end
|
423
|
+
|
424
|
+
# @param [Block] block
|
425
|
+
# Schedules a {Tasks::OneOff task} to be run at {#stop shutdown}.
|
426
|
+
#
|
427
|
+
# @raise (see #fail_if_not_running)
|
428
|
+
def on_shutdown( &block )
|
429
|
+
fail_if_not_running
|
430
|
+
@shutdown_tasks << Tasks::OneOff.new( &block )
|
431
|
+
nil
|
432
|
+
end
|
433
|
+
|
434
|
+
# @param [Block] block
|
435
|
+
# Schedules a {Tasks::OneOff task} to be run at the next tick.
|
436
|
+
#
|
437
|
+
# @raise (see #fail_if_not_running)
|
438
|
+
def next_tick( &block )
|
439
|
+
fail_if_not_running
|
440
|
+
@tasks << Tasks::OneOff.new( &block )
|
441
|
+
nil
|
442
|
+
end
|
443
|
+
|
444
|
+
# @note Time accuracy cannot be guaranteed.
|
445
|
+
#
|
446
|
+
# @param [Float] interval
|
447
|
+
# Time in seconds.
|
448
|
+
# @param [Block] block
|
449
|
+
# Schedules a {Tasks::Periodic task} to be run at every `interval` seconds.
|
450
|
+
#
|
451
|
+
# @raise (see #fail_if_not_running)
|
452
|
+
def at_interval( interval, &block )
|
453
|
+
fail_if_not_running
|
454
|
+
@tasks << Tasks::Periodic.new( interval, &block )
|
455
|
+
nil
|
456
|
+
end
|
457
|
+
|
458
|
+
# @note Time accuracy cannot be guaranteed.
|
459
|
+
#
|
460
|
+
# @param [Float] time
|
461
|
+
# Time in seconds.
|
462
|
+
# @param [Block] block
|
463
|
+
# Schedules a {Tasks::Delayed task} to be run in `time` seconds.
|
464
|
+
#
|
465
|
+
# @raise (see #fail_if_not_running)
|
466
|
+
def delay( time, &block )
|
467
|
+
fail_if_not_running
|
468
|
+
@tasks << Tasks::Delayed.new( time, &block )
|
469
|
+
nil
|
470
|
+
end
|
471
|
+
|
472
|
+
# @return [Thread, nil]
|
473
|
+
# Thread of the {#run loop}, `nil` if not running.
|
474
|
+
def thread
|
475
|
+
@thread
|
476
|
+
end
|
477
|
+
|
478
|
+
# @return [Bool]
|
479
|
+
# `true` if the caller is in the same {#thread} as the {#run reactor loop},
|
480
|
+
# `false` otherwise.
|
481
|
+
#
|
482
|
+
# @raise (see #fail_if_not_running)
|
483
|
+
def in_same_thread?
|
484
|
+
fail_if_not_running
|
485
|
+
Thread.current == thread
|
486
|
+
end
|
487
|
+
|
488
|
+
# @note Will call {Connection::Callbacks#on_attach}.
|
489
|
+
#
|
490
|
+
# {Connection#attach Attaches} a connection to the {Reactor} loop.
|
491
|
+
#
|
492
|
+
# @param [Connection] connection
|
493
|
+
#
|
494
|
+
# @raise (see #fail_if_not_running)
|
495
|
+
def attach( connection )
|
496
|
+
return if attached? connection
|
497
|
+
|
498
|
+
schedule do
|
499
|
+
connection.raktr = self
|
500
|
+
@connections[connection.to_io] = connection
|
501
|
+
connection.on_attach
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
# @note Will call {Connection::Callbacks#on_detach}.
|
506
|
+
#
|
507
|
+
# {Connection#detach Detaches} a connection from the {Reactor} loop.
|
508
|
+
#
|
509
|
+
# @param [Connection] connection
|
510
|
+
#
|
511
|
+
# @raise (see #fail_if_not_running)
|
512
|
+
def detach( connection )
|
513
|
+
return if !attached?( connection )
|
514
|
+
|
515
|
+
schedule do
|
516
|
+
connection.on_detach
|
517
|
+
@connections.delete connection.to_io
|
518
|
+
connection.raktr = nil
|
519
|
+
end
|
520
|
+
end
|
521
|
+
|
522
|
+
# @return [Bool]
|
523
|
+
# `true` if the connection is attached, `false` otherwise.
|
524
|
+
def attached?( connection )
|
525
|
+
@connections.include? connection.to_io
|
526
|
+
end
|
527
|
+
|
528
|
+
private
|
529
|
+
|
530
|
+
# @raise [Error::NotRunning]
|
531
|
+
# If the Reactor is not {#running?}.
|
532
|
+
def fail_if_not_running
|
533
|
+
fail Error::NotRunning, 'Reactor is not running.' if !running?
|
534
|
+
end
|
535
|
+
|
536
|
+
# @raise [Error::NotRunning]
|
537
|
+
# If the Reactor is already {#running?}.
|
538
|
+
def fail_if_running
|
539
|
+
fail Error::AlreadyRunning, 'Reactor is already running.' if running?
|
540
|
+
end
|
541
|
+
|
542
|
+
# @raise [Error::UNIXSocketsNotSupported]
|
543
|
+
# If trying to use UNIX-domain sockets on a host OS that does not
|
544
|
+
# support them.
|
545
|
+
def fail_if_non_unix
|
546
|
+
return if self.class.supports_unix_sockets?
|
547
|
+
|
548
|
+
fail Error::UNIXSocketsNotSupported,
|
549
|
+
'The host OS does not support UNIX-domain sockets.'
|
550
|
+
end
|
551
|
+
|
552
|
+
def determine_connection_options( *args )
|
553
|
+
options = {}
|
554
|
+
|
555
|
+
if args[1].is_a? Integer
|
556
|
+
options[:host], options[:port], options[:handler], *handler_options = *args
|
557
|
+
else
|
558
|
+
options[:unix_socket], options[:handler], *handler_options = *args
|
559
|
+
end
|
560
|
+
|
561
|
+
if !options[:unix_socket].is_a?( String ) &&
|
562
|
+
(!options[:host].is_a?( String ) || !options[:port].is_a?( Integer ))
|
563
|
+
fail ArgumentError,
|
564
|
+
'Either a UNIX socket path or a host and port combination are required.'
|
565
|
+
end
|
566
|
+
|
567
|
+
options[:handler] ||= Connection
|
568
|
+
options[:handler_options] = handler_options
|
569
|
+
options
|
570
|
+
end
|
571
|
+
|
572
|
+
# @return [UNIXSocket]
|
573
|
+
# Connected socket.
|
574
|
+
def connect_unix( unix_socket )
|
575
|
+
fail_if_non_unix
|
576
|
+
|
577
|
+
UNIXSocket.new( unix_socket )
|
578
|
+
end
|
579
|
+
|
580
|
+
# @return [Socket]
|
581
|
+
# Connected socket.
|
582
|
+
def connect_tcp
|
583
|
+
socket = Socket.new(
|
584
|
+
Socket::Constants::AF_INET,
|
585
|
+
Socket::Constants::SOCK_STREAM,
|
586
|
+
Socket::Constants::IPPROTO_IP
|
587
|
+
)
|
588
|
+
socket.do_not_reverse_lookup = true
|
589
|
+
socket
|
590
|
+
end
|
591
|
+
|
592
|
+
# @return [TCPServer]
|
593
|
+
# Listening server socket.
|
594
|
+
def listen_tcp( host, port )
|
595
|
+
server = TCPServer.new( host, port )
|
596
|
+
server.do_not_reverse_lookup = true
|
597
|
+
server
|
598
|
+
end
|
599
|
+
|
600
|
+
# @return [UNIXServer]
|
601
|
+
# Listening server socket.
|
602
|
+
def listen_unix( unix_socket )
|
603
|
+
UNIXServer.new( unix_socket )
|
604
|
+
end
|
605
|
+
|
606
|
+
# Closes all client connections, both ingress and egress.
|
607
|
+
def close_connections
|
608
|
+
@connections.values.each(&:close)
|
609
|
+
end
|
610
|
+
|
611
|
+
def process_connections
|
612
|
+
if @connections.empty?
|
613
|
+
sleep @max_tick_interval
|
614
|
+
return
|
615
|
+
end
|
616
|
+
|
617
|
+
# Get connections with available events - :read, :write, :error.
|
618
|
+
selected = select_connections
|
619
|
+
|
620
|
+
# Close connections that have errors.
|
621
|
+
selected.delete(:error)&.each(&:close)
|
622
|
+
|
623
|
+
# Call the corresponding event on the connections.
|
624
|
+
selected.each { |event, connections| connections.each(&"_#{event}".to_sym) }
|
625
|
+
end
|
626
|
+
|
627
|
+
# @return [Hash]
|
628
|
+
#
|
629
|
+
# Connections grouped by their available events:
|
630
|
+
#
|
631
|
+
# * `:read` -- Ready for reading (i.e. with data in their incoming buffer).
|
632
|
+
# * `:write` -- Ready for writing (i.e. with data in their
|
633
|
+
# {Connection#has_outgoing_data? outgoing buffer).
|
634
|
+
# * `:error`
|
635
|
+
def select_connections
|
636
|
+
r = []
|
637
|
+
w = []
|
638
|
+
e = []
|
639
|
+
|
640
|
+
@connections.values.each do |connection|
|
641
|
+
|
642
|
+
# Required for OSX as it connects immediately and then #select returns
|
643
|
+
# nothing as there's no activity, given that, OpenSSL doesn't get a chance
|
644
|
+
# to do its handshake so explicitly connect pending sockets, bypassing #select.
|
645
|
+
connection._connect if !connection.connected?
|
646
|
+
|
647
|
+
socket = connection.socket
|
648
|
+
|
649
|
+
e << socket
|
650
|
+
|
651
|
+
if connection.listener? || connection.connected?
|
652
|
+
r << socket
|
653
|
+
end
|
654
|
+
|
655
|
+
if connection.connected? && connection.has_outgoing_data?
|
656
|
+
w << socket
|
657
|
+
end
|
658
|
+
end
|
659
|
+
|
660
|
+
selected_sockets =
|
661
|
+
begin
|
662
|
+
Connection::Error.translate do
|
663
|
+
select( r, w, e, @select_timeout )
|
664
|
+
end
|
665
|
+
rescue Connection::Error => e
|
666
|
+
nil
|
667
|
+
end
|
668
|
+
|
669
|
+
selected_sockets ||= [[],[],[]]
|
670
|
+
|
671
|
+
# SSL sockets maintain their own buffer whose state can't be checked by
|
672
|
+
# Kernel.select, leading to cases where the SSL buffer isn't empty,
|
673
|
+
# even though Kernel.select says that there's nothing to read.
|
674
|
+
#
|
675
|
+
# So force a read for SSL sockets to cover all our bases.
|
676
|
+
#
|
677
|
+
# This is apparent especially on JRuby.
|
678
|
+
if r.size != selected_sockets[0].size
|
679
|
+
(r - selected_sockets[0]).each do |socket|
|
680
|
+
next if !socket.is_a?( OpenSSL::SSL::SSLSocket )
|
681
|
+
selected_sockets[0] << socket
|
682
|
+
end
|
683
|
+
end
|
684
|
+
|
685
|
+
if selected_sockets[0].empty? && selected_sockets[1].empty? &&
|
686
|
+
selected_sockets[2].empty?
|
687
|
+
return {}
|
688
|
+
end
|
689
|
+
|
690
|
+
{
|
691
|
+
# Since these will be processed in order, it's better have the write
|
692
|
+
# ones first to flush the buffers ASAP.
|
693
|
+
write: connections_from_sockets( selected_sockets[1] ),
|
694
|
+
read: connections_from_sockets( selected_sockets[0] ),
|
695
|
+
error: connections_from_sockets( selected_sockets[2] )
|
696
|
+
}
|
697
|
+
end
|
698
|
+
|
699
|
+
def connections_from_sockets( sockets )
|
700
|
+
sockets.map { |s| @connections[s.to_io] }
|
701
|
+
end
|
702
|
+
|
703
|
+
end
|
704
|
+
|
705
|
+
def Raktr( &block )
|
706
|
+
Raktr.global.run( &block )
|
707
|
+
end
|