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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +1 -0
  3. data/LICENSE.md +29 -0
  4. data/README.md +77 -0
  5. data/Rakefile +53 -0
  6. data/lib/raktr/connection/callbacks.rb +71 -0
  7. data/lib/raktr/connection/error.rb +120 -0
  8. data/lib/raktr/connection/peer_info.rb +90 -0
  9. data/lib/raktr/connection/tls.rb +164 -0
  10. data/lib/raktr/connection.rb +339 -0
  11. data/lib/raktr/global.rb +24 -0
  12. data/lib/raktr/iterator.rb +249 -0
  13. data/lib/raktr/queue.rb +89 -0
  14. data/lib/raktr/tasks/base.rb +57 -0
  15. data/lib/raktr/tasks/delayed.rb +33 -0
  16. data/lib/raktr/tasks/one_off.rb +30 -0
  17. data/lib/raktr/tasks/periodic.rb +58 -0
  18. data/lib/raktr/tasks/persistent.rb +29 -0
  19. data/lib/raktr/tasks.rb +105 -0
  20. data/lib/raktr/version.rb +13 -0
  21. data/lib/raktr.rb +707 -0
  22. data/spec/raktr/connection/tls_spec.rb +348 -0
  23. data/spec/raktr/connection_spec.rb +74 -0
  24. data/spec/raktr/iterator_spec.rb +203 -0
  25. data/spec/raktr/queue_spec.rb +91 -0
  26. data/spec/raktr/tasks/base.rb +8 -0
  27. data/spec/raktr/tasks/delayed_spec.rb +71 -0
  28. data/spec/raktr/tasks/one_off_spec.rb +66 -0
  29. data/spec/raktr/tasks/periodic_spec.rb +57 -0
  30. data/spec/raktr/tasks/persistent_spec.rb +54 -0
  31. data/spec/raktr/tasks_spec.rb +155 -0
  32. data/spec/raktr_spec.rb +20 -0
  33. data/spec/raktr_tls_spec.rb +20 -0
  34. data/spec/spec_helper.rb +17 -0
  35. data/spec/support/fixtures/handlers/echo_client.rb +34 -0
  36. data/spec/support/fixtures/handlers/echo_client_tls.rb +10 -0
  37. data/spec/support/fixtures/handlers/echo_server.rb +12 -0
  38. data/spec/support/fixtures/handlers/echo_server_tls.rb +8 -0
  39. data/spec/support/fixtures/pems/cacert.pem +37 -0
  40. data/spec/support/fixtures/pems/client/cert.pem +37 -0
  41. data/spec/support/fixtures/pems/client/foo-cert.pem +39 -0
  42. data/spec/support/fixtures/pems/client/foo-key.pem +51 -0
  43. data/spec/support/fixtures/pems/client/key.pem +51 -0
  44. data/spec/support/fixtures/pems/server/cert.pem +37 -0
  45. data/spec/support/fixtures/pems/server/key.pem +51 -0
  46. data/spec/support/helpers/paths.rb +23 -0
  47. data/spec/support/helpers/utilities.rb +135 -0
  48. data/spec/support/lib/server_option_parser.rb +29 -0
  49. data/spec/support/lib/servers/runner.rb +13 -0
  50. data/spec/support/lib/servers.rb +133 -0
  51. data/spec/support/servers/echo.rb +14 -0
  52. data/spec/support/servers/echo_tls.rb +22 -0
  53. data/spec/support/servers/echo_unix.rb +14 -0
  54. data/spec/support/servers/echo_unix_tls.rb +22 -0
  55. data/spec/support/shared/connection.rb +696 -0
  56. data/spec/support/shared/raktr.rb +834 -0
  57. data/spec/support/shared/task.rb +21 -0
  58. 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