raktr 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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