libuv 2.0.12 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/README.md +67 -34
  4. data/lib/libuv.rb +30 -5
  5. data/lib/libuv/async.rb +16 -10
  6. data/lib/libuv/check.rb +19 -12
  7. data/lib/libuv/coroutines.rb +39 -12
  8. data/lib/libuv/dns.rb +25 -18
  9. data/lib/libuv/error.rb +2 -0
  10. data/lib/libuv/ext/ext.rb +28 -36
  11. data/lib/libuv/ext/platform/darwin_x64.rb +2 -0
  12. data/lib/libuv/ext/platform/unix.rb +2 -0
  13. data/lib/libuv/ext/platform/windows.rb +2 -0
  14. data/lib/libuv/ext/tasks.rb +2 -0
  15. data/lib/libuv/ext/tasks/mac.rb +2 -0
  16. data/lib/libuv/ext/tasks/unix.rb +2 -0
  17. data/lib/libuv/ext/tasks/win.rb +2 -0
  18. data/lib/libuv/ext/types.rb +2 -1
  19. data/lib/libuv/file.rb +67 -50
  20. data/lib/libuv/filesystem.rb +63 -61
  21. data/lib/libuv/fs_event.rb +7 -4
  22. data/lib/libuv/handle.rb +30 -14
  23. data/lib/libuv/idle.rb +17 -10
  24. data/lib/libuv/mixins/accessors.rb +41 -0
  25. data/lib/libuv/mixins/assertions.rb +3 -1
  26. data/lib/libuv/mixins/fs_checks.rb +29 -6
  27. data/lib/libuv/mixins/listener.rb +4 -2
  28. data/lib/libuv/mixins/net.rb +4 -2
  29. data/lib/libuv/mixins/resource.rb +5 -3
  30. data/lib/libuv/mixins/stream.rb +128 -35
  31. data/lib/libuv/pipe.rb +54 -27
  32. data/lib/libuv/prepare.rb +19 -12
  33. data/lib/libuv/q.rb +109 -101
  34. data/lib/libuv/{loop.rb → reactor.rb} +163 -85
  35. data/lib/libuv/signal.rb +13 -5
  36. data/lib/libuv/tcp.rb +109 -63
  37. data/lib/libuv/timer.rb +44 -24
  38. data/lib/libuv/tty.rb +8 -3
  39. data/lib/libuv/udp.rb +49 -22
  40. data/lib/libuv/version.rb +3 -1
  41. data/lib/libuv/work.rb +14 -10
  42. data/libuv.gemspec +11 -9
  43. data/spec/async_spec.rb +13 -13
  44. data/spec/coroutines_spec.rb +20 -50
  45. data/spec/defer_spec.rb +182 -311
  46. data/spec/dns_spec.rb +51 -41
  47. data/spec/dsl_spec.rb +43 -0
  48. data/spec/filesystem_spec.rb +65 -87
  49. data/spec/idle_spec.rb +19 -33
  50. data/spec/pipe_spec.rb +25 -32
  51. data/spec/tcp_spec.rb +116 -53
  52. data/spec/timer_spec.rb +3 -3
  53. data/spec/udp_spec.rb +16 -17
  54. data/spec/zen_spec.rb +2 -3
  55. metadata +37 -30
@@ -1,19 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thread'
2
4
 
3
5
  module Libuv
4
- class Loop
6
+ class Reactor
5
7
  include Resource, Assertions
8
+ extend Accessors
6
9
 
7
10
 
8
- LOOPS = ThreadSafe::Cache.new
9
- CRITICAL = Mutex.new
10
- @@use_fibers = false
11
+ REACTORS = ::Concurrent::Map.new
12
+ CRITICAL = ::Mutex.new
11
13
 
12
14
 
13
15
  module ClassMethods
14
- # Get default loop
16
+ # Get default reactor
15
17
  #
16
- # @return [::Libuv::Loop]
18
+ # @return [::Libuv::Reactor]
17
19
  def default
18
20
  return @default unless @default.nil?
19
21
  CRITICAL.synchronize {
@@ -21,55 +23,79 @@ module Libuv
21
23
  }
22
24
  end
23
25
 
24
- # Create new Libuv loop
26
+ # Create new Libuv reactor
25
27
  #
26
- # @return [::Libuv::Loop]
27
- def new
28
- return create(::Libuv::Ext.loop_new)
28
+ # @return [::Libuv::Reactor]
29
+ def new(&blk)
30
+ thread = create(::Libuv::Ext.loop_new)
31
+ if block_given?
32
+ ::Thread.new do
33
+ thread.run &blk
34
+ end
35
+ end
36
+ thread
29
37
  end
30
38
 
31
- # Build a Ruby Libuv loop from an existing loop pointer
39
+ # Build a Ruby Libuv reactor from an existing reactor pointer
32
40
  #
33
- # @return [::Libuv::Loop]
41
+ # @return [::Libuv::Reactor]
34
42
  def create(pointer)
35
43
  allocate.tap { |i| i.send(:initialize, FFI::AutoPointer.new(pointer, ::Libuv::Ext.method(:loop_delete))) }
36
44
  end
37
45
 
38
- # Checks for the existence of a loop on the current thread
46
+ # Checks for the existence of a reactor on the current thread
39
47
  #
40
- # @return [::Libuv::Loop | nil]
48
+ # @return [::Libuv::Reactor | nil]
41
49
  def current
42
- LOOPS[Thread.current]
50
+ REACTORS[Thread.current]
43
51
  end
44
52
  end
45
53
  extend ClassMethods
46
54
 
47
55
 
48
- # Initialize a loop using an FFI::Pointer to a libuv loop
56
+ # Initialize a reactor using an FFI::Pointer to a libuv reactor
49
57
  def initialize(pointer) # :notnew:
50
58
  @pointer = pointer
51
- @loop = self
59
+ @reactor = self
60
+ @run_count = 0
61
+ @ref_count = 0
52
62
 
53
63
  # Create an async call for scheduling work from other threads
54
64
  @run_queue = Queue.new
55
- @process_queue = @loop.async method(:process_queue_cb)
65
+ @process_queue = @reactor.async method(:process_queue_cb)
56
66
  @process_queue.unref
57
67
 
58
68
  # Create a next tick timer
59
- @next_tick = @loop.timer method(:next_tick_cb)
69
+ @next_tick = @reactor.timer method(:next_tick_cb)
60
70
  @next_tick.unref
61
71
 
62
- # Create an async call for ending the loop
63
- @stop_loop = @loop.async method(:stop_cb)
64
- @stop_loop.unref
72
+ # Create an async call for ending the reactor
73
+ @stop_reactor = @reactor.async method(:stop_cb)
74
+ @stop_reactor.unref
65
75
 
66
76
  # Libuv can prevent the application shutting down once the main thread has ended
67
77
  # The addition of a prepare function prevents this from happening.
68
- @loop_prep = Libuv::Prepare.new(@loop, method(:noop))
69
- @loop_prep.unref
70
- @loop_prep.start
78
+ @reactor_prep = Libuv::Prepare.new(@reactor, method(:noop))
79
+ @reactor_prep.unref
80
+ @reactor_prep.start
81
+
82
+ # LibUV ingnores program interrupt by default.
83
+ # We provide normal behaviour and allow this to be overriden
84
+ @on_signal = proc { stop_cb }
85
+ sig_callback = method(:signal_cb)
86
+ self.signal(:INT, sig_callback).unref
87
+ self.signal(:HUP, sig_callback).unref
88
+ self.signal(:TERM, sig_callback).unref
89
+
90
+ # Notify of errors
91
+ @throw_on_exit = nil
92
+ @reactor_notify = proc { |error|
93
+ @throw_on_exit = error
94
+ }
71
95
  end
72
96
 
97
+ attr_reader :run_count
98
+
73
99
 
74
100
  protected
75
101
 
@@ -77,12 +103,16 @@ module Libuv
77
103
  def noop; end
78
104
 
79
105
  def stop_cb
80
- LOOPS.delete(@reactor_thread)
106
+ REACTORS.delete(@reactor_thread)
81
107
  @reactor_thread = nil
82
108
 
83
109
  ::Libuv::Ext.stop(@pointer)
84
110
  end
85
111
 
112
+ def signal_cb(_)
113
+ @on_signal.call
114
+ end
115
+
86
116
  def next_tick_cb
87
117
  @next_tick_scheduled = false
88
118
  @next_tick.unref
@@ -94,7 +124,8 @@ module Libuv
94
124
  length = @run_queue.length
95
125
  update_time
96
126
  length.times do
97
- process_item
127
+ # This allows any item to pause its execution without effecting this loop
128
+ ::Fiber.new { process_item }.resume
98
129
  end
99
130
  end
100
131
 
@@ -103,7 +134,7 @@ module Libuv
103
134
  run = @run_queue.pop true # pop non-block
104
135
  run.call
105
136
  rescue Exception => e
106
- @loop.log :error, :next_tick_cb, e
137
+ @reactor.log e, 'performing next tick callback'
107
138
  end
108
139
  end
109
140
 
@@ -120,47 +151,76 @@ module Libuv
120
151
 
121
152
  def handle; @pointer; end
122
153
 
123
- # Run the actual event loop. This method will block until the loop is stopped.
154
+ # Run the actual event reactor. This method will block until the reactor is stopped.
124
155
  #
125
156
  # @param run_type [:UV_RUN_DEFAULT, :UV_RUN_ONCE, :UV_RUN_NOWAIT]
126
157
  # @yieldparam promise [::Libuv::Q::Promise] Yields a promise that can be used for logging unhandled
127
- # exceptions on the loop.
158
+ # exceptions on the reactor.
128
159
  def run(run_type = :UV_RUN_DEFAULT)
129
160
  if @reactor_thread.nil?
130
- @loop_notify = @loop.defer
131
-
132
161
  begin
133
- @reactor_thread = Thread.current
134
- LOOPS[@reactor_thread] = @loop
162
+ @reactor_thread = ::Thread.current
163
+ raise 'only one reactor allowed per-thread' if REACTORS[@reactor_thread]
164
+
165
+ REACTORS[@reactor_thread] = @reactor
166
+ @throw_on_exit = nil
167
+
135
168
  if block_given?
136
169
  update_time
137
- if @@use_fibers
138
- Fiber.new { yield @loop_notify.promise }.resume
139
- else
140
- yield @loop_notify.promise
141
- end
170
+ ::Fiber.new {
171
+ begin
172
+ yield @reactor
173
+ rescue Exception => e
174
+ log(e, 'in reactor run block')
175
+ end
176
+ }.resume
142
177
  end
178
+ @run_count += 1
143
179
  ::Libuv::Ext.run(@pointer, run_type) # This is blocking
144
180
  ensure
181
+ REACTORS.delete(@reactor_thread)
145
182
  @reactor_thread = nil
146
183
  @run_queue.clear
147
184
  end
185
+
186
+ # Raise the last unhandled error to occur on the reactor thread
187
+ raise @throw_on_exit if @throw_on_exit
188
+
148
189
  elsif block_given?
149
- if @@use_fibers
150
- schedule { Fiber.new { yield @loop_notify.promise }.resume }
190
+ if reactor_thread?
191
+ update_time
192
+ yield @reactor
151
193
  else
152
- schedule { yield @loop_notify.promise }
194
+ raise 'reactor already running on another thread'
153
195
  end
154
196
  end
155
- @loop
197
+
198
+ @reactor
199
+ end
200
+
201
+ # Prevents the reactor loop from stopping
202
+ def ref
203
+ if reactor_thread? && reactor_running?
204
+ @process_queue.ref if @ref_count == 0
205
+ @ref_count += 1
206
+ end
207
+ end
208
+
209
+ # Allows the reactor loop to stop
210
+ def unref
211
+ if reactor_thread? && reactor_running? && @ref_count > 0
212
+ @ref_count -= 1
213
+ @process_queue.unref if @ref_count == 0
214
+ end
156
215
  end
157
216
 
158
217
 
159
218
  # Provides a promise notifier for receiving un-handled exceptions
160
219
  #
161
220
  # @return [::Libuv::Q::Promise]
162
- def notifier
163
- @loop_notify.promise
221
+ def notifier(callback = nil, &blk)
222
+ @reactor_notify = callback || blk
223
+ self
164
224
  end
165
225
 
166
226
  # Creates a deferred result object for where the result of an operation may only be returned
@@ -168,7 +228,7 @@ module Libuv
168
228
  #
169
229
  # @return [::Libuv::Q::Deferred]
170
230
  def defer
171
- Q.defer(@loop)
231
+ Q.defer(@reactor)
172
232
  end
173
233
 
174
234
  # Combines multiple promises into a single promise that is resolved when all of the input
@@ -180,7 +240,7 @@ module Libuv
180
240
  # the promises is resolved with a rejection, this resulting promise will be resolved with the
181
241
  # same rejection.
182
242
  def all(*promises)
183
- Q.all(@loop, *promises)
243
+ Q.all(@reactor, *promises)
184
244
  end
185
245
 
186
246
  #
@@ -190,7 +250,7 @@ module Libuv
190
250
  # @param *promises [::Libuv::Q::Promise] a number of promises that will be combined into a single promise
191
251
  # @return [::Libuv::Q::Promise] Returns a single promise
192
252
  def any(*promises)
193
- Q.any(@loop, *promises)
253
+ Q.any(@reactor, *promises)
194
254
  end
195
255
 
196
256
  #
@@ -201,15 +261,16 @@ module Libuv
201
261
  # @return [::Libuv::Q::Promise] Returns a single promise that will be resolved with an array of values,
202
262
  # each [result, wasResolved] value pair corresponding to a at the same index in the `promises` array.
203
263
  def finally(*promises)
204
- Q.finally(@loop, *promises)
264
+ Q.finally(@reactor, *promises)
205
265
  end
206
266
 
207
267
 
208
- # forces loop time update, useful for getting more granular times
268
+ # forces reactor time update, useful for getting more granular times
209
269
  #
210
270
  # @return nil
211
271
  def update_time
212
272
  ::Libuv::Ext.update_time(@pointer)
273
+ self
213
274
  end
214
275
 
215
276
  # Get current time in milliseconds
@@ -228,28 +289,30 @@ module Libuv
228
289
 
229
290
  if name
230
291
  msg = ::Libuv::Ext.strerror(err)
231
- ::Libuv::Error.const_get(name.to_sym).new(msg)
292
+ ::Libuv::Error.const_get(name.to_sym).new("#{msg}, #{name}:#{err}")
232
293
  else
233
294
  # We want a back-trace in this case
234
295
  raise "error lookup failed for code #{err}"
235
296
  end
236
297
  rescue Exception => e
237
- @loop.log :warn, :error_lookup_failed, e
298
+ @reactor.log e, 'performing error lookup'
238
299
  e
239
300
  end
240
301
 
241
302
  # Get a new TCP instance
242
303
  #
243
304
  # @return [::Libuv::TCP]
244
- def tcp
245
- TCP.new(@loop)
305
+ def tcp(callback = nil, &blk)
306
+ callback ||= blk
307
+ TCP.new(@reactor, progress: callback)
246
308
  end
247
309
 
248
310
  # Get a new UDP instance
249
311
  #
250
312
  # @return [::Libuv::UDP]
251
- def udp
252
- UDP.new(@loop)
313
+ def udp(callback = nil, &blk)
314
+ callback ||= blk
315
+ UDP.new(@reactor, progress: callback)
253
316
  end
254
317
 
255
318
  # Get a new TTY instance
@@ -260,7 +323,7 @@ module Libuv
260
323
  def tty(fileno, readable = false)
261
324
  assert_type(Integer, fileno, "io#fileno must return an integer file descriptor, #{fileno.inspect} given")
262
325
 
263
- TTY.new(@loop, fileno, readable)
326
+ TTY.new(@reactor, fileno, readable)
264
327
  end
265
328
 
266
329
  # Get a new Pipe instance
@@ -268,7 +331,7 @@ module Libuv
268
331
  # @param ipc [true, false] indicate if a handle will be used for ipc, useful for sharing tcp socket between processes
269
332
  # @return [::Libuv::Pipe]
270
333
  def pipe(ipc = false)
271
- Pipe.new(@loop, ipc)
334
+ Pipe.new(@reactor, ipc)
272
335
  end
273
336
 
274
337
  # Get a new timer instance
@@ -276,21 +339,21 @@ module Libuv
276
339
  # @param callback [Proc] the callback to be called on timer trigger
277
340
  # @return [::Libuv::Timer]
278
341
  def timer(callback = nil, &blk)
279
- Timer.new(@loop, callback || blk)
342
+ Timer.new(@reactor, callback || blk)
280
343
  end
281
344
 
282
345
  # Get a new Prepare handle
283
346
  #
284
347
  # @return [::Libuv::Prepare]
285
348
  def prepare(callback = nil, &blk)
286
- Prepare.new(@loop, callback || blk)
349
+ Prepare.new(@reactor, callback || blk)
287
350
  end
288
351
 
289
352
  # Get a new Check handle
290
353
  #
291
354
  # @return [::Libuv::Check]
292
355
  def check(callback = nil, &blk)
293
- Check.new(@loop, callback || blk)
356
+ Check.new(@reactor, callback || blk)
294
357
  end
295
358
 
296
359
  # Get a new Idle handle
@@ -298,7 +361,7 @@ module Libuv
298
361
  # @param callback [Proc] the callback to be called on idle trigger
299
362
  # @return [::Libuv::Idle]
300
363
  def idle(callback = nil, &block)
301
- Idle.new(@loop, callback || block)
364
+ Idle.new(@reactor, callback || block)
302
365
  end
303
366
 
304
367
  # Get a new Async handle
@@ -306,7 +369,7 @@ module Libuv
306
369
  # @return [::Libuv::Async]
307
370
  def async(callback = nil, &block)
308
371
  callback ||= block
309
- handle = Async.new(@loop)
372
+ handle = Async.new(@reactor)
310
373
  handle.progress callback if callback
311
374
  handle
312
375
  end
@@ -316,12 +379,18 @@ module Libuv
316
379
  # @return [::Libuv::Signal]
317
380
  def signal(signum = nil, callback = nil, &block)
318
381
  callback ||= block
319
- handle = Signal.new(@loop)
382
+ handle = Signal.new(@reactor)
320
383
  handle.progress callback if callback
321
384
  handle.start(signum) if signum
322
385
  handle
323
386
  end
324
387
 
388
+ # Allows user defined behaviour when sig int is received
389
+ def on_program_interrupt(callback = nil, &block)
390
+ @on_signal = callback || block
391
+ self
392
+ end
393
+
325
394
  # Queue some work for processing in the libuv thread pool
326
395
  #
327
396
  # @param callback [Proc] the callback to be called in the thread pool
@@ -330,7 +399,7 @@ module Libuv
330
399
  def work(callback = nil, &block)
331
400
  callback ||= block
332
401
  assert_block(callback)
333
- Work.new(@loop, callback) # Work is a promise object
402
+ Work.new(@reactor, callback) # Work is a promise object
334
403
  end
335
404
 
336
405
  # Lookup a hostname
@@ -339,10 +408,14 @@ module Libuv
339
408
  # @param port [Integer, String] the service being connected too
340
409
  # @param callback [Proc] the callback to be called on success
341
410
  # @return [::Libuv::Dns]
342
- def lookup(hostname, hint = :IPv4, port = 9, &block)
343
- dns = Dns.new(@loop, hostname, port, hint) # Work is a promise object
344
- dns.then block if block_given?
345
- dns
411
+ def lookup(hostname, hint = :IPv4, port = 9, wait: true, &block)
412
+ dns = Dns.new(@reactor, hostname, port, hint, wait: wait) # Work is a promise object
413
+ if block_given? || !wait
414
+ dns.then block
415
+ dns
416
+ else
417
+ dns.results
418
+ end
346
419
  end
347
420
 
348
421
  # Get a new FSEvent instance
@@ -352,7 +425,7 @@ module Libuv
352
425
  # @raise [ArgumentError] if path is not a string
353
426
  def fs_event(path)
354
427
  assert_type(String, path)
355
- FSEvent.new(@loop, path)
428
+ FSEvent.new(@reactor, path)
356
429
  end
357
430
 
358
431
  # Opens a file and returns an object that can be used to manipulate it
@@ -361,21 +434,21 @@ module Libuv
361
434
  # @param flags [Integer] see ruby File::Constants
362
435
  # @param mode [Integer]
363
436
  # @return [::Libuv::File]
364
- def file(path, flags = 0, mode = 0)
437
+ def file(path, flags = 0, mode: 0, **opts, &blk)
365
438
  assert_type(String, path, "path must be a String")
366
439
  assert_type(Integer, flags, "flags must be an Integer")
367
440
  assert_type(Integer, mode, "mode must be an Integer")
368
- File.new(@loop, path, flags, mode)
441
+ File.new(@reactor, path, flags, mode: mode, **opts, &blk)
369
442
  end
370
443
 
371
444
  # Returns an object for manipulating the filesystem
372
445
  #
373
446
  # @return [::Libuv::Filesystem]
374
447
  def filesystem
375
- Filesystem.new(@loop)
448
+ Filesystem.new(@reactor)
376
449
  end
377
450
 
378
- # Schedule some work to be processed on the event loop as soon as possible (thread safe)
451
+ # Schedule some work to be processed on the event reactor as soon as possible (thread safe)
379
452
  #
380
453
  # @param callback [Proc] the callback to be called on the reactor thread
381
454
  # @raise [ArgumentError] if block is not given
@@ -389,9 +462,11 @@ module Libuv
389
462
  @run_queue << callback
390
463
  @process_queue.call
391
464
  end
465
+
466
+ self
392
467
  end
393
468
 
394
- # Queue some work to be processed in the next iteration of the event loop (thread safe)
469
+ # Queue some work to be processed in the next iteration of the event reactor (thread safe)
395
470
  #
396
471
  # @param callback [Proc] the callback to be called on the reactor thread
397
472
  # @raise [ArgumentError] if block is not given
@@ -410,27 +485,29 @@ module Libuv
410
485
  else
411
486
  @process_queue.call
412
487
  end
488
+
489
+ self
413
490
  end
414
491
 
415
- # Notifies the loop there was an event that should be logged
492
+ # Notifies the reactor there was an event that should be logged
416
493
  #
417
- # @param level [Symbol] the error level (info, warn, error etc)
418
- # @param id [Object] some kind of identifying information
419
- # @param *args [*args] any additional information
420
- def log(level, id, *args)
421
- @loop_notify.notify(level, id, *args)
494
+ # @param error [Exception] the error
495
+ # @param msg [String|nil] optional context on the error
496
+ # @param trace [Array<String>] optional additional trace of caller if async
497
+ def log(error, msg = nil, trace = nil)
498
+ @reactor_notify.call(error, msg, trace)
422
499
  end
423
500
 
424
- # Closes handles opened by the loop class and completes the current loop iteration (thread safe)
501
+ # Closes handles opened by the reactor class and completes the current reactor iteration (thread safe)
425
502
  def stop
426
- @stop_loop.call
503
+ @stop_reactor.call
427
504
  end
428
505
 
429
506
  # True if the calling thread is the same thread as the reactor.
430
507
  #
431
508
  # @return [Boolean]
432
509
  def reactor_thread?
433
- @reactor_thread == Thread.current
510
+ @reactor_thread == ::Thread.current
434
511
  end
435
512
 
436
513
  # Exposed to allow joining on the thread, when run in a multithreaded environment. Performing other actions on the thread has undefined semantics (read: a dangerous endevor).
@@ -438,11 +515,12 @@ module Libuv
438
515
  # @return [Thread]
439
516
  attr_reader :reactor_thread
440
517
 
441
- # Tells you whether the Libuv reactor loop is currently running.
518
+ # Tells you whether the Libuv reactor reactor is currently running.
442
519
  #
443
520
  # @return [Boolean]
444
521
  def reactor_running?
445
522
  !@reactor_thread.nil?
446
523
  end
524
+ alias_method :running?, :reactor_running?
447
525
  end
448
526
  end