io-event 1.9.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2021-2024, by Samuel Williams.
4
+ # Copyright, 2021-2025, by Samuel Williams.
5
5
  # Copyright, 2023, by Math Ieu.
6
6
 
7
7
  require_relative "../interrupt"
@@ -17,6 +17,9 @@ module IO::Event
17
17
 
18
18
  @waiting = Hash.new.compare_by_identity
19
19
 
20
+ # Flag indicating whether the selector is currently blocked in a system call.
21
+ # Set to true when blocked in ::IO.select, false otherwise.
22
+ # Used by wakeup() to determine if an interrupt signal is needed.
20
23
  @blocked = false
21
24
 
22
25
  @ready = Queue.new
@@ -196,7 +199,7 @@ module IO::Event
196
199
  result = Fiber.blocking{buffer.read(io, 0, offset)}
197
200
 
198
201
  if result < 0
199
- if again?(result)
202
+ if length > 0 and again?(result)
200
203
  self.io_wait(fiber, io, IO::READABLE)
201
204
  else
202
205
  return result
@@ -226,7 +229,7 @@ module IO::Event
226
229
  result = Fiber.blocking{buffer.write(io, 0, offset)}
227
230
 
228
231
  if result < 0
229
- if again?(result)
232
+ if length > 0 and again?(result)
230
233
  self.io_wait(fiber, io, IO::READABLE)
231
234
  else
232
235
  return result
@@ -302,96 +305,14 @@ module IO::Event
302
305
 
303
306
  return total
304
307
  end
305
- elsif Support.fiber_scheduler_v1?
306
- # Ruby <= 3.1, limited IO::Buffer support.
307
- def io_read(fiber, _io, buffer, length, offset = 0)
308
- # We need to avoid any internal buffering, so we use a duplicated IO object:
309
- io = IO.for_fd(_io.fileno, autoclose: false)
310
-
311
- total = 0
312
-
313
- maximum_size = buffer.size - offset
314
- while maximum_size > 0
315
- case result = blocking{io.read_nonblock(maximum_size, exception: false)}
316
- when :wait_readable
317
- if length > 0
318
- self.io_wait(fiber, io, IO::READABLE)
319
- else
320
- return EWOULDBLOCK
321
- end
322
- when :wait_writable
323
- if length > 0
324
- self.io_wait(fiber, io, IO::WRITABLE)
325
- else
326
- return EWOULDBLOCK
327
- end
328
- when nil
329
- break
330
- else
331
- buffer.set_string(result, offset)
332
-
333
- size = result.bytesize
334
- total += size
335
- offset += size
336
- break if size >= length
337
- length -= size
338
- end
339
-
340
- maximum_size = buffer.size - offset
341
- end
342
-
343
- return total
344
- rescue IOError => error
345
- return -Errno::EBADF::Errno
346
- rescue SystemCallError => error
347
- return -error.errno
348
- end
349
-
350
- def io_write(fiber, _io, buffer, length, offset = 0)
351
- # We need to avoid any internal buffering, so we use a duplicated IO object:
352
- io = IO.for_fd(_io.fileno, autoclose: false)
353
-
354
- total = 0
355
-
356
- maximum_size = buffer.size - offset
357
- while maximum_size > 0
358
- chunk = buffer.get_string(offset, maximum_size)
359
- case result = blocking{io.write_nonblock(chunk, exception: false)}
360
- when :wait_readable
361
- if length > 0
362
- self.io_wait(fiber, io, IO::READABLE)
363
- else
364
- return EWOULDBLOCK
365
- end
366
- when :wait_writable
367
- if length > 0
368
- self.io_wait(fiber, io, IO::WRITABLE)
369
- else
370
- return EWOULDBLOCK
371
- end
372
- else
373
- total += result
374
- offset += result
375
- break if result >= length
376
- length -= result
377
- end
378
-
379
- maximum_size = buffer.size - offset
380
- end
381
-
382
- return total
383
- rescue IOError => error
384
- return -Errno::EBADF::Errno
385
- rescue SystemCallError => error
386
- return -error.errno
387
- end
388
-
389
- def blocking(&block)
390
- fiber = Fiber.new(blocking: true, &block)
391
- return fiber.resume(fiber)
392
- end
393
308
  end
394
309
 
310
+ # Wait for a process to change state.
311
+ #
312
+ # @parameter fiber [Fiber] The fiber to resume after waiting.
313
+ # @parameter pid [Integer] The process ID to wait for.
314
+ # @parameter flags [Integer] Flags to pass to Process::Status.wait.
315
+ # @returns [Process::Status] The status of the waited process.
395
316
  def process_wait(fiber, pid, flags)
396
317
  Thread.new do
397
318
  Process::Status.wait(pid, flags)
@@ -411,6 +332,10 @@ module IO::Event
411
332
  end
412
333
  end
413
334
 
335
+ # Wait for IO events or a timeout.
336
+ #
337
+ # @parameter duration [Numeric | Nil] The maximum time to wait, or nil for no timeout.
338
+ # @returns [Integer] The number of ready IO objects.
414
339
  def select(duration = nil)
415
340
  if pop_ready
416
341
  # If we have popped items from the ready list, they may influence the duration calculation, so we don't delay the event loop:
@@ -421,19 +346,25 @@ module IO::Event
421
346
  writable = Array.new
422
347
  priority = Array.new
423
348
 
424
- @waiting.each do |io, waiter|
425
- waiter.each do |fiber, events|
426
- if (events & IO::READABLE) > 0
427
- readable << io
428
- end
429
-
430
- if (events & IO::WRITABLE) > 0
431
- writable << io
432
- end
433
-
434
- if (events & IO::PRIORITY) > 0
435
- priority << io
349
+ @waiting.delete_if do |io, waiter|
350
+ if io.closed?
351
+ true
352
+ else
353
+ waiter.each do |fiber, events|
354
+ if (events & IO::READABLE) > 0
355
+ readable << io
356
+ end
357
+
358
+ if (events & IO::WRITABLE) > 0
359
+ writable << io
360
+ end
361
+
362
+ if (events & IO::PRIORITY) > 0
363
+ priority << io
364
+ end
436
365
  end
366
+
367
+ false
437
368
  end
438
369
  end
439
370
 
@@ -14,35 +14,28 @@ class IO
14
14
  IO.const_defined?(:Buffer)
15
15
  end
16
16
 
17
- # The basic fiber scheduler was introduced along side the IO::Buffer class.
18
- #
19
- # @returns [Boolean] Whether the IO::Buffer class is available.
20
- #
21
- # To be removed on 31 Mar 2025.
22
- def self.fiber_scheduler_v1?
23
- IO.const_defined?(:Buffer)
24
- end
25
-
26
17
  # More advanced read/write methods and blocking controls were introduced in Ruby 3.2.
27
18
  #
28
19
  # To be removed on 31 Mar 2026.
29
20
  def self.fiber_scheduler_v2?
30
- # Some interface changes were back-ported incorrectly:
31
- # https://github.com/ruby/ruby/pull/10778
32
- # Specifically "Improvements to IO::Buffer read/write/pread/pwrite."
33
- # Missing correct size calculation.
34
- return false if RUBY_VERSION >= "3.2.5"
35
-
36
- IO.const_defined?(:Buffer) and Fiber.respond_to?(:blocking) and IO::Buffer.instance_method(:read).arity == -1
21
+ if RUBY_VERSION >= "3.2"
22
+ return true if RUBY_VERSION >= "3.2.6"
23
+
24
+ # Some interface changes were back-ported incorrectly and released in 3.2.5 <https://github.com/ruby/ruby/pull/10778> - Specifically "Improvements to IO::Buffer read/write/pread/pwrite." is missing correct size calculation.
25
+ return false if RUBY_VERSION >= "3.2.5"
26
+
27
+ # Feature detection:
28
+ IO.const_defined?(:Buffer) and Fiber.respond_to?(:blocking) and IO::Buffer.instance_method(:read).arity == -1
29
+ end
37
30
  end
38
31
 
39
32
  # Updated inferfaces for read/write and IO::Buffer were introduced in Ruby 3.3, including pread/pwrite.
40
33
  #
41
34
  # To become the default 31 Mar 2026.
42
35
  def self.fiber_scheduler_v3?
36
+ return true if RUBY_VERSION >= "3.3"
37
+
43
38
  if fiber_scheduler_v2?
44
- return true if RUBY_VERSION >= "3.3"
45
-
46
39
  # Feature detection if required:
47
40
  begin
48
41
  IO::Buffer.new.slice(0, 0).write(STDOUT)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2024, by Samuel Williams.
4
+ # Copyright, 2024-2025, by Samuel Williams.
5
5
 
6
6
  require_relative "priority_heap"
7
7
 
@@ -91,7 +91,6 @@ class IO
91
91
  schedule(self.now + offset.to_f, block)
92
92
  end
93
93
 
94
-
95
94
  # Compute the time interval until the next timer fires.
96
95
  #
97
96
  # @parameter now [Float] The current time.
@@ -7,6 +7,6 @@
7
7
  class IO
8
8
  # @namespace
9
9
  module Event
10
- VERSION = "1.9.0"
10
+ VERSION = "1.12.0"
11
11
  end
12
12
  end
data/license.md CHANGED
@@ -9,8 +9,9 @@ Copyright, 2022, by Bruno Sutic.
9
9
  Copyright, 2023, by Math Ieu.
10
10
  Copyright, 2024, by Pavel Rosický.
11
11
  Copyright, 2024, by Anthony Ross.
12
- Copyright, 2024, by Shizuo Fujita.
12
+ Copyright, 2024-2025, by Shizuo Fujita.
13
13
  Copyright, 2024, by Jean Boussier.
14
+ Copyright, 2025, by Stanislav (Stas) Katkov.
14
15
 
15
16
  Permission is hereby granted, free of charge, to any person obtaining a copy
16
17
  of this software and associated documentation files (the "Software"), to deal
data/readme.md CHANGED
@@ -1,4 +1,4 @@
1
- # ![Event](logo.svg)
1
+ # ![IO::Event](logo.svg)
2
2
 
3
3
  Provides low level cross-platform primitives for constructing event loops, with support for `select`, `kqueue`, `epoll` and `io_uring`.
4
4
 
@@ -18,13 +18,34 @@ Please see the [project documentation](https://socketry.github.io/io-event/) for
18
18
 
19
19
  Please see the [project releases](https://socketry.github.io/io-event/releases/index) for all releases.
20
20
 
21
+ ### v1.11.2
22
+
23
+ - Fix Windows build.
24
+
25
+ ### v1.11.1
26
+
27
+ - Fix `read_nonblock` when using the `URing` selector, which was not handling zero-length reads correctly. This allows reading available data without blocking.
28
+
29
+ ### v1.11.0
30
+
31
+ - [Introduce `IO::Event::WorkerPool` for off-loading blocking operations.](https://socketry.github.io/io-event/releases/index#introduce-io::event::workerpool-for-off-loading-blocking-operations.)
32
+
33
+ ### v1.10.2
34
+
35
+ - Improved consistency of handling closed IO when invoking `#select`.
36
+
37
+ ### v1.10.0
38
+
39
+ - `IO::Event::Profiler` is moved to dedicated gem: [fiber-profiler](https://github.com/socketry/fiber-profiler).
40
+ - Perform runtime checks for native selectors to ensure they are supported in the current environment. While compile-time checks determine availability, restrictions like seccomp and SELinux may still prevent them from working.
41
+
21
42
  ### v1.9.0
22
43
 
23
- - [Improved `IO::Event::Profiler` for detecting stalls.](https://socketry.github.io/io-event/releases/index#improved-io::event::profiler-for-detecting-stalls.)
44
+ - Improved `IO::Event::Profiler` for detecting stalls.
24
45
 
25
46
  ### v1.8.0
26
47
 
27
- - [Detecting fibers that are stalling the event loop.](https://socketry.github.io/io-event/releases/index#detecting-fibers-that-are-stalling-the-event-loop.)
48
+ - Detecting fibers that are stalling the event loop.
28
49
 
29
50
  ### v1.7.5
30
51
 
data/releases.md CHANGED
@@ -1,56 +1,55 @@
1
1
  # Releases
2
2
 
3
- ## v1.9.0
3
+ ## v1.11.2
4
4
 
5
- ### Improved `IO::Event::Profiler` for detecting stalls.
5
+ - Fix Windows build.
6
6
 
7
- A new `IO::Event::Profiler` class has been added to help detect stalls in the event loop. The previous approach was insufficient to detect all possible stalls. This new approach uses the `RUBY_EVENT_FIBER_SWITCH` event to track context switching by the scheduler, and can detect stalls no matter how they occur.
7
+ ## v1.11.1
8
8
 
9
- ``` ruby
10
- profiler = IO::Event::Profiler.new
9
+ - Fix `read_nonblock` when using the `URing` selector, which was not handling zero-length reads correctly. This allows reading available data without blocking.
10
+
11
+ ## v1.11.0
11
12
 
12
- profiler.start
13
-
14
- Fiber.new do
15
- sleep 1.0
16
- end.transfer
13
+ ### Introduce `IO::Event::WorkerPool` for off-loading blocking operations.
17
14
 
18
- profiler.stop
15
+ The {ruby IO::Event::WorkerPool} provides a mechanism for executing blocking operations on separate OS threads while properly integrating with Ruby's fiber scheduler and GVL (Global VM Lock) management. This enables true parallelism for CPU-intensive or blocking operations that would otherwise block the event loop.
16
+
17
+ ``` ruby
18
+ # Fiber scheduler integration via blocking_operation_wait hook
19
+ class MyScheduler
20
+ def initialize
21
+ @worker_pool = IO::Event::WorkerPool.new
22
+ end
23
+
24
+ def blocking_operation_wait(operation)
25
+ @worker_pool.call(operation)
26
+ end
27
+ end
28
+
29
+ # Usage with automatic offloading
30
+ Fiber.set_scheduler(MyScheduler.new)
31
+ # Automatically offload `rb_nogvl(..., RB_NOGVL_OFFLOAD_SAFE)` to a background thread:
32
+ result = some_blocking_operation()
19
33
  ```
20
34
 
21
- A default profiler is exposed using `IO::Event::Profiler.default` which is controlled by the following environment variables:
35
+ The implementation uses one or more background threads and a list of pending blocking operations. Those operations either execute through to completion or may be cancelled, which executes the "unblock function" provided to `rb_nogvl`.
22
36
 
23
- - `IO_EVENT_PROFILER=true` - Enable the profiler, otherwise `IO::Event::Profiler.default` will return `nil`.
24
- - `IO_EVENT_PROFILER_LOG_THRESHOLD` - Specify the threshold in seconds for logging a stall. Defaults to `0.01`.
25
- - `IO_EVENT_PROFILER_TRACK_CALLS` - Track the method call for each event, in order to log specifically which method is causing the stall. Defaults to `true`.
37
+ ## v1.10.2
38
+
39
+ - Improved consistency of handling closed IO when invoking `#select`.
40
+
41
+ ## v1.10.0
42
+
43
+ - `IO::Event::Profiler` is moved to dedicated gem: [fiber-profiler](https://github.com/socketry/fiber-profiler).
44
+ - Perform runtime checks for native selectors to ensure they are supported in the current environment. While compile-time checks determine availability, restrictions like seccomp and SELinux may still prevent them from working.
45
+
46
+ ## v1.9.0
26
47
 
27
- The previous environment variables `IO_EVENT_SELECTOR_STALL_LOG_THRESHOLD` and `IO_EVENT_SELECTOR_STALL_LOG` no longer have any effect.
48
+ - Improved `IO::Event::Profiler` for detecting stalls.
28
49
 
29
50
  ## v1.8.0
30
51
 
31
- ### Detecting fibers that are stalling the event loop.
32
-
33
- A new (experimental) feature for detecting fiber stalls has been added. This feature is disabled by default and can be enabled by setting the `IO_EVENT_SELECTOR_STALL_LOG_THRESHOLD` to `true` or a floating point number representing the threshold in seconds.
34
-
35
- When enabled, the event loop will measure and profile user code when resuming a fiber. If the fiber takes too long to return back to the event loop, the event loop will log a warning message with a profile of the fiber's execution.
36
-
37
- > cat test.rb
38
- #!/usr/bin/env ruby
39
-
40
- require_relative "lib/async"
41
-
42
- Async do
43
- Fiber.blocking do
44
- sleep 1
45
- end
46
- end
47
-
48
- > IO_EVENT_SELECTOR_STALL_LOG_THRESHOLD=true bundle exec ./test.rb
49
- Fiber stalled for 1.003 seconds
50
- /home/samuel/Developer/socketry/async/test.rb:6 in '#<Class:Fiber>#blocking' (1s)
51
- /home/samuel/Developer/socketry/async/test.rb:7 in 'Kernel#sleep' (1s)
52
-
53
- There is a performance overhead to this feature, so it is recommended to only enable it when debugging performance issues.
52
+ - Detecting fibers that are stalling the event loop.
54
53
 
55
54
  ## v1.7.5
56
55
 
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: io-event
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.0
4
+ version: 1.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -10,11 +10,12 @@ authors:
10
10
  - Jean Boussier
11
11
  - Benoit Daloze
12
12
  - Bruno Sutic
13
+ - Shizuo Fujita
13
14
  - Alex Matchneer
14
15
  - Anthony Ross
15
16
  - Delton Ding
16
17
  - Pavel Rosický
17
- - Shizuo Fujita
18
+ - Stanislav (Stas) Katkov
18
19
  bindir: bin
19
20
  cert_chain:
20
21
  - |
@@ -46,13 +47,14 @@ cert_chain:
46
47
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
47
48
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
48
49
  -----END CERTIFICATE-----
49
- date: 2025-02-10 00:00:00.000000000 Z
50
+ date: 1980-01-02 00:00:00.000000000 Z
50
51
  dependencies: []
51
52
  executables: []
52
53
  extensions:
53
54
  - ext/extconf.rb
54
55
  extra_rdoc_files: []
55
56
  files:
57
+ - agent.md
56
58
  - design.md
57
59
  - ext/extconf.rb
58
60
  - ext/io/event/array.h
@@ -63,8 +65,6 @@ files:
63
65
  - ext/io/event/interrupt.c
64
66
  - ext/io/event/interrupt.h
65
67
  - ext/io/event/list.h
66
- - ext/io/event/profiler.c
67
- - ext/io/event/profiler.h
68
68
  - ext/io/event/selector/epoll.c
69
69
  - ext/io/event/selector/epoll.h
70
70
  - ext/io/event/selector/kqueue.c
@@ -76,12 +76,15 @@ files:
76
76
  - ext/io/event/selector/uring.h
77
77
  - ext/io/event/time.c
78
78
  - ext/io/event/time.h
79
+ - ext/io/event/worker_pool.c
80
+ - ext/io/event/worker_pool.h
81
+ - ext/io/event/worker_pool_test.c
82
+ - ext/io/event/worker_pool_test.h
79
83
  - lib/io/event.rb
80
84
  - lib/io/event/debug/selector.rb
81
85
  - lib/io/event/interrupt.rb
82
86
  - lib/io/event/native.rb
83
87
  - lib/io/event/priority_heap.rb
84
- - lib/io/event/profiler.rb
85
88
  - lib/io/event/selector.rb
86
89
  - lib/io/event/selector/nonblock.rb
87
90
  - lib/io/event/selector/select.rb
@@ -104,14 +107,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
104
107
  requirements:
105
108
  - - ">="
106
109
  - !ruby/object:Gem::Version
107
- version: '3.1'
110
+ version: 3.2.6
108
111
  required_rubygems_version: !ruby/object:Gem::Requirement
109
112
  requirements:
110
113
  - - ">="
111
114
  - !ruby/object:Gem::Version
112
115
  version: '0'
113
116
  requirements: []
114
- rubygems_version: 3.6.2
117
+ rubygems_version: 3.6.7
115
118
  specification_version: 4
116
119
  summary: An event loop.
117
120
  test_files: []
metadata.gz.sig CHANGED
Binary file