carbon_fiber 0.1.2-aarch64-linux → 0.2.0-aarch64-linux
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/README.md +63 -22
- data/lib/carbon_fiber/3.4.0/carbon_fiber_native.so +0 -0
- data/lib/carbon_fiber/4.0.0/carbon_fiber_native.so +0 -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 +34 -1
- data/lib/carbon_fiber/scheduler.rb +96 -23
- 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: 9a6f29b6c07a95c995006af75fe1d796a8a9c48f10146a44ff6beaa14640a3d2
|
|
4
|
+
data.tar.gz: cb3462f4c3da1d019c56962fd3189cd175afcd1b24299b8ee808899132303c68
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4786a5e168cfa36669d9a64f099e3ac48048a833b0ec58a7dde2b1c865dc323d63ab979b4c373fb376ca3df89598ab5da13c89c2b2f10289a165b6d24d1954f9
|
|
7
|
+
data.tar.gz: 9888e5f0fb3ecec49f5de0e411cc0bd334ecf28a36e5db1da8a7410a2cc9fa80f0beba22479a88e2f23b875388db19b4c6775b5a69f9f788333552d54f5ab323
|
data/README.md
CHANGED
|
@@ -50,18 +50,18 @@ end
|
|
|
50
50
|
|
|
51
51
|
## Performance
|
|
52
52
|
|
|
53
|
-
AWS EC2 c7a.2xlarge, 8 dedicated vCPUs,
|
|
53
|
+
AWS EC2 c7a.2xlarge, 8 dedicated vCPUs, Amazon Linux 2023, kernel 6.18.20, Ruby 4.0.2 + YJIT, io_uring. 5-run median.
|
|
54
54
|
|
|
55
55
|
Some benchmarks:
|
|
56
56
|
|
|
57
57
|
| Workload | Carbon Fiber | Async | Itsi | Carbon Fiber vs. Async |
|
|
58
58
|
|---|---|---|---|---|
|
|
59
|
-
| `http_server` | **
|
|
60
|
-
| `http_client_api` | **
|
|
61
|
-
| `http_client_download` | **
|
|
62
|
-
| `tcp_echo` | **
|
|
63
|
-
| `cascading_timeout` | **4.
|
|
64
|
-
| `connection_pool` | **4.
|
|
59
|
+
| `http_server` | **49.380k req/s** | 30.823k req/s | 30.864k req/s | +60% |
|
|
60
|
+
| `http_client_api` | **19.500k req/s** | 16.721k req/s | timeout | +17% |
|
|
61
|
+
| `http_client_download` | **8.426k dl/s** | 7.062k dl/s | timeout | +19% |
|
|
62
|
+
| `tcp_echo` | **52.973k ops/s** | 32.330k ops/s | 32.046k ops/s | +64% |
|
|
63
|
+
| `cascading_timeout` | **4.668k ops/s** | 4.414k ops/s | error | +6% |
|
|
64
|
+
| `connection_pool` | **4.967k co/s** | 4.612k co/s | 4.954k co/s | +8% |
|
|
65
65
|
|
|
66
66
|
Wins on most workloads against Async, Itsi, fiber_scheduler, io-event, and libev. [See detailed benchmarks →](#benchmarks)
|
|
67
67
|
|
|
@@ -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:
|
|
@@ -256,7 +297,7 @@ If the native extension can't be loaded (on Windows, for example), a pure-Ruby f
|
|
|
256
297
|
|
|
257
298
|
## Benchmarks
|
|
258
299
|
|
|
259
|
-
AWS EC2 c7a.2xlarge, 8 dedicated vCPUs,
|
|
300
|
+
AWS EC2 c7a.2xlarge, 8 dedicated vCPUs, Amazon Linux 2023, kernel 6.18.20, Ruby 4.0.2 + YJIT, io_uring. 5-run median.
|
|
260
301
|
|
|
261
302
|
### Ruby Fiber Schedulers (leading ones): Carbon Fiber vs. Async vs. Itsi
|
|
262
303
|
|
|
@@ -264,14 +305,14 @@ Measuring pure Ruby Fiber Scheduler performance (`Fiber.set_scheduler`).
|
|
|
264
305
|
|
|
265
306
|
| Workload | Unit | Carbon Fiber | Async | Itsi | Carbon Fiber vs. Async |
|
|
266
307
|
|---|---|---|---|---|---|
|
|
267
|
-
| `http_client_api` | req/s | **
|
|
268
|
-
| `http_client_download` | dl/s | **
|
|
269
|
-
| `http_server` | req/s | **
|
|
270
|
-
| `tcp_echo` | ops/s | **
|
|
271
|
-
| `connection_pool` | co/s | **4,
|
|
272
|
-
| `fan_out_gather` | cyc/s | 2,
|
|
273
|
-
| `db_query_mix` | qry/s | 1,660 | 1,
|
|
274
|
-
| `cascading_timeout` | ops/s | **4,
|
|
308
|
+
| `http_client_api` | req/s | **19,500** | 16,721 | timeout | +17% |
|
|
309
|
+
| `http_client_download` | dl/s | **8,426** | 7,062 | timeout | +19% |
|
|
310
|
+
| `http_server` | req/s | **49,380** | 30,823 | 30,864 | +60% |
|
|
311
|
+
| `tcp_echo` | ops/s | **52,973** | 32,330 | 32,046 | +64% |
|
|
312
|
+
| `connection_pool` | co/s | **4,967** | 4,612 | 4,954 | +8% |
|
|
313
|
+
| `fan_out_gather` | cyc/s | 2,022 | 1,923 | **2,094** | +5% |
|
|
314
|
+
| `db_query_mix` | qry/s | 1,660 | 1,623 | **1,662** | +2% |
|
|
315
|
+
| `cascading_timeout` | ops/s | **4,668** | 4,414 | error | +6% |
|
|
275
316
|
|
|
276
317
|
Enabling YJIT turned out to be very beneficial for Async as well—numbers here are with `--yjit` on both sides.
|
|
277
318
|
|
|
@@ -281,12 +322,12 @@ Swapped the io-event selector for Carbon Fiber's native backend. Same Async code
|
|
|
281
322
|
|
|
282
323
|
| Workload | Unit | Stock Async | Carbon Fiber | Delta |
|
|
283
324
|
|---|---|---|---|---|
|
|
284
|
-
| `http_client_api` | req/s |
|
|
285
|
-
| `http_client_download` | dl/s |
|
|
286
|
-
| `task_churn` | task/s |
|
|
287
|
-
| `condition_signal` | sig/s |
|
|
288
|
-
| `cascading_timeout` | ops/s | 4,
|
|
289
|
-
| `tcp_throughput` | ops/s |
|
|
325
|
+
| `http_client_api` | req/s | 17,119 | **17,323** | +1.2% |
|
|
326
|
+
| `http_client_download` | dl/s | 2,263 | **2,474** | +9.3% |
|
|
327
|
+
| `task_churn` | task/s | 119,477 | **120,985** | +1.3% |
|
|
328
|
+
| `condition_signal` | sig/s | 44,887 | **46,921** | +4.5% |
|
|
329
|
+
| `cascading_timeout` | ops/s | 4,414 | **4,472** | +1.3% |
|
|
330
|
+
| `tcp_throughput` | ops/s | 32,606 | **48,214** | +47.9% |
|
|
290
331
|
|
|
291
332
|
### Examples of how to run benchmarks
|
|
292
333
|
|
|
Binary file
|
|
Binary file
|
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,35 @@ 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
|
+
|
|
144
|
+
# Mirrors `Selector#kernel_sleep` on the native side so
|
|
145
|
+
# `Scheduler#kernel_sleep` can delegate to `@selector.kernel_sleep`
|
|
146
|
+
# in both paths. Branches on the duration: nil parks the fiber on
|
|
147
|
+
# the loop without a timer, non-positive yields, positive parks on
|
|
148
|
+
# a native timer for `duration` seconds.
|
|
149
|
+
def kernel_sleep(duration = nil)
|
|
150
|
+
if duration.nil?
|
|
151
|
+
transfer
|
|
152
|
+
elsif duration <= 0
|
|
153
|
+
self.yield
|
|
154
|
+
else
|
|
155
|
+
block(Fiber.current, duration)
|
|
156
|
+
end
|
|
157
|
+
true
|
|
158
|
+
end
|
|
159
|
+
|
|
127
160
|
# Suspend the current fiber until unblocked or timed out.
|
|
128
161
|
def block(fiber, timeout = nil)
|
|
129
162
|
token = nil
|
|
@@ -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
|
|
@@ -169,15 +179,7 @@ module CarbonFiber
|
|
|
169
179
|
# Intercept +Kernel#sleep+. Parks the fiber on a native timer.
|
|
170
180
|
# @param duration [Float, nil] seconds to sleep; nil sleeps forever
|
|
171
181
|
def kernel_sleep(duration = nil)
|
|
172
|
-
|
|
173
|
-
transfer
|
|
174
|
-
elsif duration <= 0
|
|
175
|
-
self.yield
|
|
176
|
-
else
|
|
177
|
-
block(nil, duration)
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
true
|
|
182
|
+
@selector.kernel_sleep(duration)
|
|
181
183
|
end
|
|
182
184
|
|
|
183
185
|
# Wait for I/O readiness on a file descriptor.
|
|
@@ -197,14 +199,15 @@ module CarbonFiber
|
|
|
197
199
|
await_background_operation { io_select_readiness(io, events, timeout) }
|
|
198
200
|
end
|
|
199
201
|
|
|
200
|
-
# 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).
|
|
201
204
|
# Falls back to a background thread for non-socket descriptors.
|
|
202
205
|
# @param io [IO]
|
|
203
206
|
# @param buffer [IO::Buffer]
|
|
204
207
|
# @param length [Integer]
|
|
205
208
|
# @param offset [Integer]
|
|
206
209
|
# @return [Integer] bytes read, or negative errno
|
|
207
|
-
def
|
|
210
|
+
def io_read_v3(io, buffer, length, offset = 0)
|
|
208
211
|
# Native io_read_object extracts the descriptor in Zig, skipping a
|
|
209
212
|
# `respond_to?(:fileno)` + `io.fileno` method-send pair per call.
|
|
210
213
|
native_result = @selector.io_read_object(io, buffer, length, offset)
|
|
@@ -219,14 +222,39 @@ module CarbonFiber
|
|
|
219
222
|
end
|
|
220
223
|
end
|
|
221
224
|
|
|
222
|
-
#
|
|
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).
|
|
223
251
|
# Falls back to a background thread for non-socket descriptors.
|
|
224
252
|
# @param io [IO]
|
|
225
253
|
# @param buffer [IO::Buffer]
|
|
226
254
|
# @param length [Integer]
|
|
227
255
|
# @param offset [Integer]
|
|
228
256
|
# @return [Integer] bytes written, or negative errno
|
|
229
|
-
def
|
|
257
|
+
def io_write_v3(io, buffer, length, offset = 0)
|
|
230
258
|
native_result = @selector.io_write_object(io, buffer, length, offset)
|
|
231
259
|
return native_result unless native_result.nil?
|
|
232
260
|
|
|
@@ -239,6 +267,38 @@ module CarbonFiber
|
|
|
239
267
|
end
|
|
240
268
|
end
|
|
241
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
|
+
|
|
242
302
|
# Blocking IO.select on a background thread.
|
|
243
303
|
def io_select(...)
|
|
244
304
|
await_background_operation do
|
|
@@ -246,15 +306,14 @@ module CarbonFiber
|
|
|
246
306
|
end
|
|
247
307
|
end
|
|
248
308
|
|
|
249
|
-
# Cancel pending waiters on
|
|
250
|
-
# @param
|
|
251
|
-
def io_close(
|
|
252
|
-
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)
|
|
253
312
|
@selector.io_close(descriptor, IOError.new("stream closed while waiting"))
|
|
254
313
|
|
|
255
314
|
Fiber.blocking do
|
|
256
|
-
|
|
257
|
-
|
|
315
|
+
io = IO.for_fd(descriptor)
|
|
316
|
+
io.close unless io.closed?
|
|
258
317
|
end
|
|
259
318
|
|
|
260
319
|
true
|
|
@@ -278,14 +337,28 @@ module CarbonFiber
|
|
|
278
337
|
end
|
|
279
338
|
end
|
|
280
339
|
|
|
281
|
-
# 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.
|
|
282
349
|
# @param hostname [String]
|
|
283
350
|
# @return [Array<String>]
|
|
284
351
|
def address_resolve(hostname)
|
|
285
352
|
if hostname.include?("%")
|
|
286
353
|
hostname = hostname.split("%", 2).first
|
|
287
354
|
end
|
|
288
|
-
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
|
|
289
362
|
end
|
|
290
363
|
|
|
291
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: aarch64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Yaroslav Markin
|
|
@@ -116,6 +116,7 @@ extensions: []
|
|
|
116
116
|
extra_rdoc_files: []
|
|
117
117
|
files:
|
|
118
118
|
- lib/carbon_fiber/async.rb
|
|
119
|
+
- lib/carbon_fiber/io_contract.rb
|
|
119
120
|
- lib/carbon_fiber/native/fallback.rb
|
|
120
121
|
- lib/carbon_fiber/native.rb
|
|
121
122
|
- 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.
|