carbon_fiber 0.1.3 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +41 -0
- data/lib/carbon_fiber/async.rb +76 -8
- data/lib/carbon_fiber/io_contract.rb +15 -0
- data/lib/carbon_fiber/native/fallback.rb +18 -1
- data/lib/carbon_fiber/scheduler.rb +95 -14
- data/lib/carbon_fiber/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3906713d933ac5b29349ed47160f5e2704060f0a3cdd101474eea9e59cf74dcf
|
|
4
|
+
data.tar.gz: 78da45d46027261788f4c01333ae68cb90ece1992350b7aabb52e59564b32b9b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cb0ae60a2ef8f3463d437b79cc4fd9d36f092060504eb2bcaea31e17b96cf1e137784cd3b01f1f1950d53fd7cb2737f6cb15cbc0c952fb3156578d3ac7b8b31e
|
|
7
|
+
data.tar.gz: df4857f35926d78a04e75f908f03c423d0d275a4a2bb3a4d7fd03eab3f84f46b16feafffc352b6898860aeda02a9f8f91fa51156afbf8d82ef43056899990200
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
- Fix the pure-Ruby fallback loop burning CPU while a fiber waits on a background thread; inside a non-main Ractor that spin starved the thread it was waiting for and hung the loop.
|
|
4
|
+
|
|
5
|
+
## [0.2.0] - 2026-09-05
|
|
6
|
+
|
|
7
|
+
- Work inside Ractors: the native extension is declared Ractor-safe, so each Ractor can install its own `CarbonFiber::Scheduler` (several at once need Ruby 4.0 or later). See the README for the supported messaging pattern.
|
|
8
|
+
- Resolve hostnames in non-main Ractors through the platform resolver on a background thread, since Resolv cannot run there.
|
|
9
|
+
- Fix fibers parked in `Queue#pop`, `Mutex#lock`, or any `block` without a timeout being garbage collected while waiting; once unblocked this raised `TypeError: wrong argument type false (expected fiber)` or hung the loop.
|
|
10
|
+
- Fix a race between garbage collection and I/O completions arriving while the loop waits with the GVL released.
|
|
11
|
+
- Support the Ruby 4.1 buffered I/O scheduler contract: `io_read`/`io_write` take `(io, buffer, offset, length)` and perform a single transfer; Ruby 3.4 through 4.0 keep the legacy behavior. (Thanks to [Samuel Williams](https://github.com/samuel-williams-shopify)).
|
|
12
|
+
- The native extension now loads on Ruby development builds: a C shim compiled against the target headers provides the `ruby_abi_version` their ABI check requires.
|
|
13
|
+
- Match Ruby 4.1's updated `rb_data_type_t` layout when registering the native selector, and verify the layout against the target Ruby's headers when the extension loads.
|
|
14
|
+
- Under the Ruby 4.1 contract, regular-file reads and writes run on the background I/O path instead of risking blocking the scheduler thread.
|
|
15
|
+
- `Scheduler#io_close` and `Async::Selector#io_close` now take a file descriptor (Integer), matching Ruby's Fiber::Scheduler protocol. (Thanks to [Samuel Williams](https://github.com/samuel-williams-shopify)).
|
|
16
|
+
|
|
1
17
|
## [0.1.3] - 2026-05-08
|
|
2
18
|
|
|
3
19
|
- Update to Zig 0.16.0; update libxev and zig.rb to the latest versions.
|
data/README.md
CHANGED
|
@@ -240,6 +240,47 @@ end
|
|
|
240
240
|
|
|
241
241
|
---
|
|
242
242
|
|
|
243
|
+
### Inside Ractors
|
|
244
|
+
|
|
245
|
+
A fiber scheduler belongs to a thread, and every Ractor runs its own threads, so each Ractor installs its own `CarbonFiber::Scheduler`. Construct it inside the Ractor that runs it; never share one between Ractors. Running several such Ractors at once needs Ruby 4.0 or later; Ruby 3.4 aborts or hangs under that load, with the pure-Ruby selector as well. On Windows a garbage collection inside a non-main Ractor pauses for several seconds while the main Ractor waits, so Ractor workloads there are slow. Every hook works there as on the main Ractor, including DNS: `address_resolve` uses Resolv on the main Ractor and the platform resolver on a background thread elsewhere, because Resolv keeps state that Ractor isolation forbids.
|
|
246
|
+
|
|
247
|
+
`Ractor.receive` (and `Ractor::Port#receive`) block the calling thread without going through the scheduler, so a worker must not wait for messages on the thread that runs its event loop: every fiber in the Ractor would stall until the next message. Receive on a dedicated thread instead and hand messages to a scheduled fiber through a `Thread::Queue`. A push from the receiving thread wakes the parked fiber through the scheduler's cross-thread `unblock` path, the same path background operations use.
|
|
248
|
+
|
|
249
|
+
```ruby
|
|
250
|
+
worker = Ractor.new do
|
|
251
|
+
scheduler = CarbonFiber::Scheduler.new
|
|
252
|
+
Fiber.set_scheduler(scheduler)
|
|
253
|
+
|
|
254
|
+
jobs = Thread::Queue.new
|
|
255
|
+
receiver = Thread.new do
|
|
256
|
+
while (message = Ractor.receive) != :stop
|
|
257
|
+
jobs.push(message)
|
|
258
|
+
end
|
|
259
|
+
jobs.close
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
results = []
|
|
263
|
+
Fiber.schedule do
|
|
264
|
+
loop do
|
|
265
|
+
job = jobs.pop or break # a fresh binding per job for the fiber below
|
|
266
|
+
Fiber.schedule { results << handle(job) } # any I/O the scheduler intercepts
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
Fiber.set_scheduler(nil) # runs the loop until the queue closes and every job is done
|
|
271
|
+
receiver.join
|
|
272
|
+
results
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
inputs.each { |job| worker.send(job) }
|
|
276
|
+
worker.send(:stop)
|
|
277
|
+
worker.value # Ractor#take on Ruby 3.4
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
A pipe works too: the receiving thread writes a byte per message and the dispatching fiber reads it before popping the queue, so the wake-up arrives through `io_wait`. The queue alone is simpler and is what `spec/carbon_fiber/ractor_spec.rb` exercises.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
243
284
|
## How It Works
|
|
244
285
|
|
|
245
286
|
The scheduler has two layers:
|
data/lib/carbon_fiber/async.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Please note that this code is heavily AI-assisted.
|
|
4
4
|
|
|
5
|
+
require_relative "io_contract"
|
|
5
6
|
require_relative "native"
|
|
6
7
|
|
|
7
8
|
module CarbonFiber
|
|
@@ -26,8 +27,12 @@ module CarbonFiber
|
|
|
26
27
|
attr_reader :loop
|
|
27
28
|
|
|
28
29
|
# @param loop [Fiber] the Async event loop fiber
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
# @param io_contract_v4 [Boolean] use the Ruby 4.1+ single-transfer
|
|
31
|
+
# contract in the native I/O paths (defaults to what the running
|
|
32
|
+
# Ruby speaks; override only for testing)
|
|
33
|
+
def initialize(loop, io_contract_v4: CarbonFiber.io_contract_v4?)
|
|
34
|
+
super(loop)
|
|
35
|
+
self.io_contract_v4 = io_contract_v4
|
|
31
36
|
@loop = loop
|
|
32
37
|
@idle_duration = 0.0
|
|
33
38
|
|
|
@@ -117,37 +122,84 @@ module CarbonFiber
|
|
|
117
122
|
EAGAIN = -Errno::EAGAIN::Errno
|
|
118
123
|
EWOULDBLOCK = -Errno::EWOULDBLOCK::Errno
|
|
119
124
|
|
|
125
|
+
# Legacy contract (Ruby 3.4 through 4.0): length before offset,
|
|
126
|
+
# minimum-progress semantics.
|
|
120
127
|
# @param fiber [Fiber]
|
|
121
128
|
# @param io [IO]
|
|
122
129
|
# @param buffer [IO::Buffer]
|
|
123
130
|
# @param length [Integer]
|
|
124
131
|
# @param offset [Integer]
|
|
125
132
|
# @return [Integer] bytes read, or negative errno
|
|
126
|
-
def
|
|
133
|
+
def io_read_v3(fiber, io, buffer, length, offset = 0)
|
|
127
134
|
result = native_io_read(io.fileno, buffer, length, offset)
|
|
128
135
|
return result unless result.nil?
|
|
129
136
|
|
|
130
137
|
ruby_io_read(fiber, io, buffer, length, offset)
|
|
131
138
|
end
|
|
132
139
|
|
|
140
|
+
# Ruby 4.1+ contract: offset before length, single transfer. One
|
|
141
|
+
# nonblocking attempt; short results and -EAGAIN are returned
|
|
142
|
+
# directly, and the caller composes retries via io_wait.
|
|
143
|
+
# @param fiber [Fiber]
|
|
144
|
+
# @param io [IO]
|
|
145
|
+
# @param buffer [IO::Buffer]
|
|
146
|
+
# @param offset [Integer]
|
|
147
|
+
# @param length [Integer] maximum bytes for this transfer
|
|
148
|
+
# @return [Integer] bytes read, or negative errno
|
|
149
|
+
def io_read_v4(fiber, io, buffer, offset, length)
|
|
150
|
+
return 0 if length.zero?
|
|
151
|
+
|
|
152
|
+
result = native_io_read(io.fileno, buffer, length, offset)
|
|
153
|
+
return result unless result.nil?
|
|
154
|
+
|
|
155
|
+
ruby_io_read_v4(io, buffer, offset, length)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Legacy contract (Ruby 3.4 through 4.0): length before offset,
|
|
159
|
+
# minimum-progress semantics.
|
|
133
160
|
# @param fiber [Fiber]
|
|
134
161
|
# @param io [IO]
|
|
135
162
|
# @param buffer [IO::Buffer]
|
|
136
163
|
# @param length [Integer]
|
|
137
164
|
# @param offset [Integer]
|
|
138
165
|
# @return [Integer] bytes written, or negative errno
|
|
139
|
-
def
|
|
166
|
+
def io_write_v3(fiber, io, buffer, length, offset = 0)
|
|
140
167
|
result = native_io_write(io.fileno, buffer, length, offset)
|
|
141
168
|
return result unless result.nil?
|
|
142
169
|
|
|
143
170
|
ruby_io_write(fiber, io, buffer, length, offset)
|
|
144
171
|
end
|
|
145
172
|
|
|
146
|
-
#
|
|
173
|
+
# Ruby 4.1+ contract: offset before length, single transfer.
|
|
174
|
+
# @param fiber [Fiber]
|
|
147
175
|
# @param io [IO]
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
176
|
+
# @param buffer [IO::Buffer]
|
|
177
|
+
# @param offset [Integer]
|
|
178
|
+
# @param length [Integer] maximum bytes for this transfer
|
|
179
|
+
# @return [Integer] bytes written, or negative errno
|
|
180
|
+
def io_write_v4(fiber, io, buffer, offset, length)
|
|
181
|
+
return 0 if length.zero?
|
|
182
|
+
|
|
183
|
+
result = native_io_write(io.fileno, buffer, length, offset)
|
|
184
|
+
return result unless result.nil?
|
|
185
|
+
|
|
186
|
+
ruby_io_write_v4(io, buffer, offset, length)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# The public hook names follow the contract of the running Ruby;
|
|
190
|
+
# both generations stay defined for direct testing.
|
|
191
|
+
if CarbonFiber.io_contract_v4?
|
|
192
|
+
alias_method :io_read, :io_read_v4
|
|
193
|
+
alias_method :io_write, :io_write_v4
|
|
194
|
+
else
|
|
195
|
+
alias_method :io_read, :io_read_v3
|
|
196
|
+
alias_method :io_write, :io_write_v3
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Cancel pending waiters on the descriptor.
|
|
200
|
+
# @param descriptor [Integer]
|
|
201
|
+
def io_close(descriptor)
|
|
202
|
+
super(descriptor, IOError.new("stream closed while waiting"))
|
|
151
203
|
end
|
|
152
204
|
|
|
153
205
|
# Wait for a child process on a background thread.
|
|
@@ -230,6 +282,22 @@ module CarbonFiber
|
|
|
230
282
|
total
|
|
231
283
|
end
|
|
232
284
|
|
|
285
|
+
# Ruby-level single-transfer io_read/io_write for non-socket fds under
|
|
286
|
+
# the Ruby 4.1+ contract: one nonblocking attempt, -EAGAIN and short
|
|
287
|
+
# results returned directly. No io_wait, no minimum-progress loop;
|
|
288
|
+
# Ruby's own read loop composes retries.
|
|
289
|
+
def ruby_io_read_v4(io, buffer, offset, length)
|
|
290
|
+
IO::Event::Selector.nonblock(io) do
|
|
291
|
+
Fiber.blocking { buffer.read(io, offset, length) }
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def ruby_io_write_v4(io, buffer, offset, length)
|
|
296
|
+
IO::Event::Selector.nonblock(io) do
|
|
297
|
+
Fiber.blocking { buffer.write(io, offset, length) }
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
233
301
|
def fallback_io_wait(io, events)
|
|
234
302
|
Thread.new do
|
|
235
303
|
Thread.current.report_on_exception = false
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CarbonFiber
|
|
4
|
+
# Ruby 4.1 (ruby/ruby#18483) reordered the buffered scheduler hooks to
|
|
5
|
+
# +io_read(io, buffer, offset, length)+ and made them single-transfer:
|
|
6
|
+
# one nonblocking attempt, short results returned directly, -EAGAIN
|
|
7
|
+
# instead of waiting, and zero length returns 0 without I/O.
|
|
8
|
+
# +IO::Buffer::VERSION >= 3+ signals the new contract; Ruby 3.4 through
|
|
9
|
+
# 4.0 define no such constant.
|
|
10
|
+
#
|
|
11
|
+
# @return [Boolean] whether this Ruby uses the v4 scheduler I/O contract
|
|
12
|
+
def self.io_contract_v4?
|
|
13
|
+
defined?(IO::Buffer::VERSION) ? IO::Buffer::VERSION.to_i >= 3 : false
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -33,6 +33,10 @@ module CarbonFiber
|
|
|
33
33
|
true
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
# Accepted for interface parity with the native selector; the fallback
|
|
37
|
+
# has no native I/O paths, so the contract generation changes nothing.
|
|
38
|
+
attr_writer :io_contract_v4
|
|
39
|
+
|
|
36
40
|
# @return [Boolean] whether there is pending work
|
|
37
41
|
def pending?
|
|
38
42
|
@mutex.synchronize { @ready.any? || @timers.any? || @read_waits.any? }
|
|
@@ -100,7 +104,7 @@ module CarbonFiber
|
|
|
100
104
|
# Run one event loop iteration.
|
|
101
105
|
def select(timeout = nil)
|
|
102
106
|
flush_ready
|
|
103
|
-
return 0 unless pending?
|
|
107
|
+
return 0 unless pending? || parked?
|
|
104
108
|
|
|
105
109
|
deadline = next_wait_deadline(timeout)
|
|
106
110
|
|
|
@@ -124,6 +128,19 @@ module CarbonFiber
|
|
|
124
128
|
flush_ready
|
|
125
129
|
end
|
|
126
130
|
|
|
131
|
+
# True while a fiber is parked in block() or do_io_wait. Nothing in the
|
|
132
|
+
# ready list, timers, or read waits refers to it, so pending? is false,
|
|
133
|
+
# yet select must sleep on the condition variable rather than return:
|
|
134
|
+
# the wake-up comes from another thread (a background operation's
|
|
135
|
+
# resume, an unblock, or wakeup), and returning at once makes
|
|
136
|
+
# Scheduler#run spin with the GVL held. On the main Ractor the timer
|
|
137
|
+
# thread preempts that spin; inside a non-main Ractor Ruby's M:N
|
|
138
|
+
# scheduler does not, and the thread that would wake the fiber never
|
|
139
|
+
# runs.
|
|
140
|
+
def parked?
|
|
141
|
+
@mutex.synchronize { @blocked_fibers.any? }
|
|
142
|
+
end
|
|
143
|
+
|
|
127
144
|
# Mirrors `Selector#kernel_sleep` on the native side so
|
|
128
145
|
# `Scheduler#kernel_sleep` can delegate to `@selector.kernel_sleep`
|
|
129
146
|
# in both paths. Branches on the duration: nil parks the fiber on
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
# Please note that this code is heavily AI-assisted.
|
|
4
4
|
|
|
5
|
-
require "resolv"
|
|
6
5
|
require "socket"
|
|
7
6
|
require "timeout"
|
|
7
|
+
require_relative "io_contract"
|
|
8
8
|
require_relative "native"
|
|
9
9
|
|
|
10
10
|
# High-performance Ruby Fiber Scheduler backed by Zig and libxev.
|
|
@@ -43,10 +43,20 @@ module CarbonFiber
|
|
|
43
43
|
class Scheduler
|
|
44
44
|
# @param root_fiber [Fiber] the event loop fiber (defaults to current)
|
|
45
45
|
# @param selector [Class] native selector class to instantiate
|
|
46
|
-
|
|
46
|
+
# @param io_contract_v4 [Boolean] use the Ruby 4.1+ single-transfer
|
|
47
|
+
# contract in the native I/O paths (defaults to what the running
|
|
48
|
+
# Ruby speaks; override only for testing)
|
|
49
|
+
def initialize(root_fiber = Fiber.current, selector: CarbonFiber::Native::Selector,
|
|
50
|
+
io_contract_v4: CarbonFiber.io_contract_v4?)
|
|
47
51
|
@root_fiber = root_fiber
|
|
48
52
|
@scheduler_thread = Thread.current
|
|
53
|
+
@main_ractor = !defined?(Ractor) || Ractor.current == Ractor.main
|
|
54
|
+
# Resolv keeps state Ractor isolation forbids, and only the main
|
|
55
|
+
# Ractor resolves through it (see #address_resolve), so load it here
|
|
56
|
+
# rather than at require time; a worker Ractor never loads it.
|
|
57
|
+
require "resolv" if @main_ractor
|
|
49
58
|
@selector = selector.new(root_fiber)
|
|
59
|
+
@selector.io_contract_v4 = io_contract_v4
|
|
50
60
|
@active_fibers = 0
|
|
51
61
|
@background_count = 0
|
|
52
62
|
@closed = false
|
|
@@ -189,14 +199,15 @@ module CarbonFiber
|
|
|
189
199
|
await_background_operation { io_select_readiness(io, events, timeout) }
|
|
190
200
|
end
|
|
191
201
|
|
|
192
|
-
# Read from an IO into a buffer via the native selector
|
|
202
|
+
# Read from an IO into a buffer via the native selector (legacy contract,
|
|
203
|
+
# Ruby 3.4 through 4.0: length before offset, minimum-progress semantics).
|
|
193
204
|
# Falls back to a background thread for non-socket descriptors.
|
|
194
205
|
# @param io [IO]
|
|
195
206
|
# @param buffer [IO::Buffer]
|
|
196
207
|
# @param length [Integer]
|
|
197
208
|
# @param offset [Integer]
|
|
198
209
|
# @return [Integer] bytes read, or negative errno
|
|
199
|
-
def
|
|
210
|
+
def io_read_v3(io, buffer, length, offset = 0)
|
|
200
211
|
# Native io_read_object extracts the descriptor in Zig, skipping a
|
|
201
212
|
# `respond_to?(:fileno)` + `io.fileno` method-send pair per call.
|
|
202
213
|
native_result = @selector.io_read_object(io, buffer, length, offset)
|
|
@@ -211,14 +222,39 @@ module CarbonFiber
|
|
|
211
222
|
end
|
|
212
223
|
end
|
|
213
224
|
|
|
214
|
-
#
|
|
225
|
+
# Read from an IO into a buffer via the native selector (Ruby 4.1+
|
|
226
|
+
# contract: offset before length, single transfer). One nonblocking
|
|
227
|
+
# attempt; short results and -EAGAIN are returned directly, and Ruby's
|
|
228
|
+
# own read loop composes retries via io_wait.
|
|
229
|
+
# @param io [IO]
|
|
230
|
+
# @param buffer [IO::Buffer]
|
|
231
|
+
# @param offset [Integer]
|
|
232
|
+
# @param length [Integer] maximum bytes for this transfer
|
|
233
|
+
# @return [Integer] bytes read, or negative errno
|
|
234
|
+
def io_read_v4(io, buffer, offset, length)
|
|
235
|
+
return 0 if length.zero?
|
|
236
|
+
|
|
237
|
+
native_result = @selector.io_read_object(io, buffer, length, offset)
|
|
238
|
+
return native_result unless native_result.nil?
|
|
239
|
+
|
|
240
|
+
await_background_operation do
|
|
241
|
+
Fiber.blocking { buffer.read(io, offset, length) }
|
|
242
|
+
end
|
|
243
|
+
rescue NoMethodError, TypeError
|
|
244
|
+
await_background_operation do
|
|
245
|
+
Fiber.blocking { buffer.read(io, offset, length) }
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Write from a buffer to an IO via the native selector (legacy contract,
|
|
250
|
+
# Ruby 3.4 through 4.0: length before offset, minimum-progress semantics).
|
|
215
251
|
# Falls back to a background thread for non-socket descriptors.
|
|
216
252
|
# @param io [IO]
|
|
217
253
|
# @param buffer [IO::Buffer]
|
|
218
254
|
# @param length [Integer]
|
|
219
255
|
# @param offset [Integer]
|
|
220
256
|
# @return [Integer] bytes written, or negative errno
|
|
221
|
-
def
|
|
257
|
+
def io_write_v3(io, buffer, length, offset = 0)
|
|
222
258
|
native_result = @selector.io_write_object(io, buffer, length, offset)
|
|
223
259
|
return native_result unless native_result.nil?
|
|
224
260
|
|
|
@@ -231,6 +267,38 @@ module CarbonFiber
|
|
|
231
267
|
end
|
|
232
268
|
end
|
|
233
269
|
|
|
270
|
+
# Write from a buffer to an IO via the native selector (Ruby 4.1+
|
|
271
|
+
# contract: offset before length, single transfer).
|
|
272
|
+
# @param io [IO]
|
|
273
|
+
# @param buffer [IO::Buffer]
|
|
274
|
+
# @param offset [Integer]
|
|
275
|
+
# @param length [Integer] maximum bytes for this transfer
|
|
276
|
+
# @return [Integer] bytes written, or negative errno
|
|
277
|
+
def io_write_v4(io, buffer, offset, length)
|
|
278
|
+
return 0 if length.zero?
|
|
279
|
+
|
|
280
|
+
native_result = @selector.io_write_object(io, buffer, length, offset)
|
|
281
|
+
return native_result unless native_result.nil?
|
|
282
|
+
|
|
283
|
+
await_background_operation do
|
|
284
|
+
Fiber.blocking { buffer.write(io, offset, length) }
|
|
285
|
+
end
|
|
286
|
+
rescue NoMethodError, TypeError
|
|
287
|
+
await_background_operation do
|
|
288
|
+
Fiber.blocking { buffer.write(io, offset, length) }
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# The public hook names follow the contract of the running Ruby;
|
|
293
|
+
# both generations stay defined for direct testing.
|
|
294
|
+
if CarbonFiber.io_contract_v4?
|
|
295
|
+
alias_method :io_read, :io_read_v4
|
|
296
|
+
alias_method :io_write, :io_write_v4
|
|
297
|
+
else
|
|
298
|
+
alias_method :io_read, :io_read_v3
|
|
299
|
+
alias_method :io_write, :io_write_v3
|
|
300
|
+
end
|
|
301
|
+
|
|
234
302
|
# Blocking IO.select on a background thread.
|
|
235
303
|
def io_select(...)
|
|
236
304
|
await_background_operation do
|
|
@@ -238,15 +306,14 @@ module CarbonFiber
|
|
|
238
306
|
end
|
|
239
307
|
end
|
|
240
308
|
|
|
241
|
-
# Cancel pending waiters on
|
|
242
|
-
# @param
|
|
243
|
-
def io_close(
|
|
244
|
-
descriptor = io.respond_to?(:to_i) ? io.to_i : io
|
|
309
|
+
# Cancel pending waiters on a descriptor and close it.
|
|
310
|
+
# @param descriptor [Integer]
|
|
311
|
+
def io_close(descriptor)
|
|
245
312
|
@selector.io_close(descriptor, IOError.new("stream closed while waiting"))
|
|
246
313
|
|
|
247
314
|
Fiber.blocking do
|
|
248
|
-
|
|
249
|
-
|
|
315
|
+
io = IO.for_fd(descriptor)
|
|
316
|
+
io.close unless io.closed?
|
|
250
317
|
end
|
|
251
318
|
|
|
252
319
|
true
|
|
@@ -270,14 +337,28 @@ module CarbonFiber
|
|
|
270
337
|
end
|
|
271
338
|
end
|
|
272
339
|
|
|
273
|
-
# Resolve a hostname to addresses
|
|
340
|
+
# Resolve a hostname to addresses.
|
|
341
|
+
#
|
|
342
|
+
# The main Ractor uses Resolv: it speaks DNS over sockets the scheduler
|
|
343
|
+
# already multiplexes, so lookups never block the loop. Resolv keeps
|
|
344
|
+
# class-level state that Ractor isolation forbids, so a scheduler running
|
|
345
|
+
# in another Ractor asks the platform resolver instead. That call blocks
|
|
346
|
+
# its thread, and from a scheduled fiber it would re-enter this very
|
|
347
|
+
# hook, so it runs on a background thread like process_wait does.
|
|
348
|
+
# Unknown hosts yield an empty list on both paths.
|
|
274
349
|
# @param hostname [String]
|
|
275
350
|
# @return [Array<String>]
|
|
276
351
|
def address_resolve(hostname)
|
|
277
352
|
if hostname.include?("%")
|
|
278
353
|
hostname = hostname.split("%", 2).first
|
|
279
354
|
end
|
|
280
|
-
Resolv.getaddresses(hostname)
|
|
355
|
+
return Resolv.getaddresses(hostname) if @main_ractor
|
|
356
|
+
|
|
357
|
+
await_background_operation do
|
|
358
|
+
Addrinfo.getaddrinfo(hostname, nil, nil, :STREAM).map(&:ip_address)
|
|
359
|
+
rescue SocketError
|
|
360
|
+
[]
|
|
361
|
+
end
|
|
281
362
|
end
|
|
282
363
|
|
|
283
364
|
# Run an arbitrary callable on a background thread.
|
data/lib/carbon_fiber/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: carbon_fiber
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yaroslav Markin
|
|
@@ -121,6 +121,7 @@ files:
|
|
|
121
121
|
- README.md
|
|
122
122
|
- lib/carbon_fiber.rb
|
|
123
123
|
- lib/carbon_fiber/async.rb
|
|
124
|
+
- lib/carbon_fiber/io_contract.rb
|
|
124
125
|
- lib/carbon_fiber/native.rb
|
|
125
126
|
- lib/carbon_fiber/native/fallback.rb
|
|
126
127
|
- lib/carbon_fiber/scheduler.rb
|
|
@@ -148,7 +149,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
148
149
|
- !ruby/object:Gem::Version
|
|
149
150
|
version: '0'
|
|
150
151
|
requirements: []
|
|
151
|
-
rubygems_version: 4.0.
|
|
152
|
+
rubygems_version: 4.0.16
|
|
152
153
|
specification_version: 4
|
|
153
154
|
summary: High-performance Ruby Fiber Scheduler backed by Zig with libxev. Pure Ruby
|
|
154
155
|
and gem async.
|