carbon_fiber 0.1.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +320 -0
- 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 +256 -0
- data/lib/carbon_fiber/native/fallback.rb +387 -0
- data/lib/carbon_fiber/native.rb +41 -0
- data/lib/carbon_fiber/scheduler.rb +396 -0
- data/lib/carbon_fiber/version.rb +6 -0
- data/lib/carbon_fiber.rb +4 -0
- metadata +155 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b2168303a8c978a7e889d8767d9df9e1380d58777ab15e07bdb1964a9635ed09
|
|
4
|
+
data.tar.gz: f693ad61709cc53fee8f9b302eb5869d5253edd76d035457c089422e065f9b60
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f8b12b3107a2fee454ccd5d421484b28d7ee916796aab57c9428d3c4a912fc95718f83b5d3409dbdd386f0b3686900178947ce66481aaa1c8ef441d2b923aefe
|
|
7
|
+
data.tar.gz: fd9ad628d5545082aa9e2eb6700952c4fc9cd0f8a7bbc7250e83055813a0c56200a219d0d7f235f449d006e59c20a928a9a86074c1e798a943b52cf9c8bcf39a
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yaroslav Markin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# Carbon Fiber
|
|
2
|
+
|
|
3
|
+
**High performance Ruby Fiber Scheduler powered by Zig and libxev.**
|
|
4
|
+
|
|
5
|
+
<div align="center">
|
|
6
|
+
<img src="https://raw.githubusercontent.com/yaroslav/carbon_fiber/refs/heads/main/assets/images/carbon_fiber.jpg" width="600" height="400" alt="Carbon Fiber">
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
[](https://github.com/yaroslav/carbon_fiber/releases)
|
|
10
|
+
[](https://rubydoc.info/gems/carbon_fiber)
|
|
11
|
+
|
|
12
|
+
**Carbon Fiber is a Ruby Fiber Scheduler** with a native event loop (io_uring on Linux, kqueue on macOS). Install it, and your `Net::HTTP`, `TCPSocket`, `Mutex`, `Queue`, `Process.spawn` code becomes concurrent without any changes. Carbon Fiber also **supports the Async framework** ([async gem](https://github.com/socketry/async)) working as a plug and play backend.
|
|
13
|
+
|
|
14
|
+
By my benchmarks (follows), Carbon Fiber ends up being the **fastest pure Ruby Fiber Scheduler** currently available.
|
|
15
|
+
|
|
16
|
+
Carbon Fiber is implemented using the **Zig** programming language and is powered by **[libxev](https://github.com/mitchellh/libxev)** by Mitchell Hashimoto, used in his **Ghostty** terminal emulator. It is one of the first Ruby native extensions written in Zig.
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- **Very fast.** My benchmarks place it as overall the fastest pure Ruby Fiber Scheduler. Uses **io_uring on Linux** and **kqueue on macOS**.
|
|
21
|
+
- **Plug and play with plain Ruby**, thanks to the Fiber Scheduler API—`Net::HTTP`, `TCPSocket`, `Process.spawn`, `Mutex`, `Queue`, DNS—all transparently concurrent under the scheduler; no gem-specific wrappers required.
|
|
22
|
+
- **Async (gem async) support** as a swappable backend. One call swaps the [Async](https://github.com/socketry/async) event loop for ours.
|
|
23
|
+
- **Pure-Ruby fallback**—when the native extension isn't available (Windows, for instance), drops to pure Ruby 4.0+ code behind the same API.
|
|
24
|
+
|
|
25
|
+
## Example
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
require "carbon_fiber"
|
|
29
|
+
Fiber.set_scheduler(CarbonFiber::Scheduler.new)
|
|
30
|
+
|
|
31
|
+
urls.each do |url|
|
|
32
|
+
Fiber.schedule { Net::HTTP.get(URI(url)) } # plain Net::HTTP, runs concurrently
|
|
33
|
+
end
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Contents
|
|
39
|
+
|
|
40
|
+
- [Performance](#performance)
|
|
41
|
+
- [Requirements](#requirements)
|
|
42
|
+
- [Installation](#installation)
|
|
43
|
+
- [Usage](#usage)
|
|
44
|
+
- [How It Works](#how-it-works)
|
|
45
|
+
- [Benchmarks](#benchmarks)
|
|
46
|
+
- [Acknowledgements](#acknowledgements)
|
|
47
|
+
- [License](#license)
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Performance
|
|
52
|
+
|
|
53
|
+
AWS EC2 c7a.2xlarge, 8 dedicated vCPUs, Ubuntu 24.04 LTS, kernel 6.17, Ruby 4.0.2 + YJIT, io_uring. 5-run median.
|
|
54
|
+
|
|
55
|
+
Some benchmarks:
|
|
56
|
+
|
|
57
|
+
| Workload | Carbon Fiber | Async | Itsi | Carbon Fiber vs. Async |
|
|
58
|
+
|---|---|---|---|---|
|
|
59
|
+
| `http_server` | **48.175k req/s** | 37.409k req/s | 29.708k req/s | +29% |
|
|
60
|
+
| `http_client_api` | **15.528k req/s** | 13.373k req/s | timeout | +16% |
|
|
61
|
+
| `http_client_download` | **6.747k dl/s** | 5.920k dl/s | timeout | +14% |
|
|
62
|
+
| `tcp_echo` | **50.392k ops/s** | 38.907k ops/s | 30.660k ops/s | +29% |
|
|
63
|
+
| `cascading_timeout` | **4.659k ops/s** | 4.488k ops/s | error | +4% |
|
|
64
|
+
| `connection_pool` | **4.989k co/s** | 4.912k co/s | 4.968k co/s | +2% |
|
|
65
|
+
|
|
66
|
+
On my workloads, wins almost all workloads across Async, Itsi, fiber_scheduler, io-event, and libev. [See detailed benchmarks →](#benchmarks)
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Requirements
|
|
71
|
+
|
|
72
|
+
- Ruby 3.4 or 4.0.
|
|
73
|
+
- Linux (amd64 or arm64), macOS (Apple Silicon, x64 uses fallback*), Windows (fallback*).
|
|
74
|
+
|
|
75
|
+
Distributed as aleady compiled and ready to use: Zig compiler _not_ needed.
|
|
76
|
+
|
|
77
|
+
_\* Fallback means pure Ruby code, worse performance._
|
|
78
|
+
|
|
79
|
+
## Installation
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
bundle add carbon_fiber
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Usage
|
|
86
|
+
|
|
87
|
+
### As a standalone fiber scheduler
|
|
88
|
+
|
|
89
|
+
Ruby's fiber scheduler protocol is built into the standard library. Install Carbon Fiber as the thread's scheduler and every blocking I/O call (`sleep`, `Net::HTTP.get`, `TCPSocket.new`, `Process::Status.wait`) yields automatically—no callbacks, no special wrappers.
|
|
90
|
+
|
|
91
|
+
See also: [Ruby Fiber Scheduler protocol](https://ruby-doc.org/core/Fiber/Scheduler.html).
|
|
92
|
+
|
|
93
|
+
#### Example: Parallel file downloads
|
|
94
|
+
|
|
95
|
+
Fetch a batch of files concurrently. Each fiber yields to the event loop while waiting for data; the scheduler runs all of them on a single thread.
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
require "carbon_fiber"
|
|
99
|
+
|
|
100
|
+
require "net/http"
|
|
101
|
+
require "uri"
|
|
102
|
+
|
|
103
|
+
urls = %w[
|
|
104
|
+
http://files.example.net/archive-1.tar.gz
|
|
105
|
+
http://files.example.net/archive-2.tar.gz
|
|
106
|
+
http://files.example.net/archive-3.tar.gz
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
scheduler = CarbonFiber::Scheduler.new
|
|
110
|
+
Fiber.set_scheduler(scheduler)
|
|
111
|
+
|
|
112
|
+
results = {}
|
|
113
|
+
urls.each do |url|
|
|
114
|
+
Fiber.schedule do
|
|
115
|
+
results[url] = Net::HTTP.get(URI(url))
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
scheduler.run
|
|
120
|
+
Fiber.set_scheduler(nil)
|
|
121
|
+
|
|
122
|
+
puts "Downloaded #{results.size} files (#{results.values.sum(&:bytesize)} bytes total)"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The `Net::HTTP` code is unchanged from a sequential version. The scheduler intercepts every socket read and write, parks the fiber, and resumes it when the kernel signals readiness—all three downloads proceed in parallel.
|
|
126
|
+
|
|
127
|
+
#### Example: Parallel subprocess processing
|
|
128
|
+
|
|
129
|
+
Transcode a batch of video files using multiple `ffmpeg` processes, all monitored concurrently. `Process::Status.wait` yields to the scheduler while the child runs; no polling required.
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
require "carbon_fiber"
|
|
133
|
+
|
|
134
|
+
jobs = {
|
|
135
|
+
"output-1080p.mp4" => ["ffmpeg", "-i", "input.mov", "-vf", "scale=1920:1080", "output-1080p.mp4"],
|
|
136
|
+
"output-720p.mp4" => ["ffmpeg", "-i", "input.mov", "-vf", "scale=1280:720", "output-720p.mp4"],
|
|
137
|
+
"output-480p.mp4" => ["ffmpeg", "-i", "input.mov", "-vf", "scale=854:480", "output-480p.mp4"],
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
scheduler = CarbonFiber::Scheduler.new
|
|
141
|
+
Fiber.set_scheduler(scheduler)
|
|
142
|
+
|
|
143
|
+
statuses = {}
|
|
144
|
+
jobs.each do |label, cmd|
|
|
145
|
+
Fiber.schedule do
|
|
146
|
+
pid = Process.spawn(*cmd, out: File::NULL, err: File::NULL)
|
|
147
|
+
statuses[label] = Process::Status.wait(pid)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
scheduler.run
|
|
152
|
+
Fiber.set_scheduler(nil)
|
|
153
|
+
|
|
154
|
+
statuses.each do |label, status|
|
|
155
|
+
puts "#{label}: #{status.success? ? "ok" : "exit #{status.exitstatus}"}"
|
|
156
|
+
end
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
All three `ffmpeg` processes run in parallel. The scheduler uses a background thread per `process_wait` call and parks the fiber until the process exits.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
### With the Async framework
|
|
164
|
+
|
|
165
|
+
Carbon Fiber implements the `IO::Event::Selector` interface, so it can replace Async's built-in event loop. Call `CarbonFiber::Async.default!` once at startup; every subsequent `Async { }` block uses our backend.
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
require "async"
|
|
169
|
+
require "carbon_fiber/async"
|
|
170
|
+
CarbonFiber::Async.default!
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Alternatively, set the environment variable: `IO_EVENT_SELECTOR=CarbonFiberSelector ruby app.rb`.
|
|
174
|
+
|
|
175
|
+
#### Example: Fan-out API calls
|
|
176
|
+
|
|
177
|
+
Call multiple upstream endpoints in parallel using `Async::Barrier`, then wait for all results.
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
require "async"
|
|
181
|
+
require "async/barrier"
|
|
182
|
+
require "carbon_fiber/async"
|
|
183
|
+
|
|
184
|
+
require "net/http"
|
|
185
|
+
require "json"
|
|
186
|
+
|
|
187
|
+
CarbonFiber::Async.default!
|
|
188
|
+
|
|
189
|
+
ENDPOINTS = %w[/users /orders /products /inventory /settings]
|
|
190
|
+
|
|
191
|
+
Async do
|
|
192
|
+
barrier = Async::Barrier.new
|
|
193
|
+
results = {}
|
|
194
|
+
|
|
195
|
+
ENDPOINTS.each do |path|
|
|
196
|
+
barrier.async do
|
|
197
|
+
response = Net::HTTP.get_response(URI("http://api.example.com#{path}"))
|
|
198
|
+
results[path] = JSON.parse(response.body)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
barrier.wait
|
|
203
|
+
|
|
204
|
+
puts "Loaded #{results.size} resources"
|
|
205
|
+
results
|
|
206
|
+
end
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
#### Example: Rate-limited concurrent crawl
|
|
210
|
+
|
|
211
|
+
Fetch many pages concurrently while keeping no more than 10 connections open at once. `Async::Semaphore` limits in-flight requests; `Async::Barrier` tracks completion.
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
require "async"
|
|
215
|
+
require "async/barrier"
|
|
216
|
+
require "async/semaphore"
|
|
217
|
+
require "carbon_fiber/async"
|
|
218
|
+
|
|
219
|
+
require "net/http"
|
|
220
|
+
|
|
221
|
+
CarbonFiber::Async.default!
|
|
222
|
+
|
|
223
|
+
pages = (1..200).map { |i| "http://data.example.com/records?page=#{i}" }
|
|
224
|
+
|
|
225
|
+
Async do
|
|
226
|
+
barrier = Async::Barrier.new
|
|
227
|
+
semaphore = Async::Semaphore.new(10, parent: barrier)
|
|
228
|
+
results = []
|
|
229
|
+
|
|
230
|
+
pages.each do |url|
|
|
231
|
+
semaphore.async do
|
|
232
|
+
results << Net::HTTP.get(URI(url))
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
barrier.wait
|
|
237
|
+
puts "Fetched #{results.size} pages"
|
|
238
|
+
end
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## How It Works
|
|
244
|
+
|
|
245
|
+
The scheduler has two layers:
|
|
246
|
+
|
|
247
|
+
1. **Zig native core** (`ext/carbon_fiber_native/`) owns the event loop (libxev), ready queue, timer heap, and fiber chaining. Handles `io_wait`, `io_read`, `io_write`, `block`/`unblock`, timer-based sleep, and `timeout_after` directly in native code.
|
|
248
|
+
|
|
249
|
+
2. **Ruby shell** (`lib/carbon_fiber/scheduler.rb`) implements the Ruby Fiber Scheduler protocol, delegates to the native Selector, and falls back to a background thread for operations the native core doesn't cover (DNS, `process_wait`).
|
|
250
|
+
|
|
251
|
+
**Fiber chaining:** when a fiber parks, the native event loop calls `rb_fiber_transfer` directly to the next ready fiber, skipping a round-trip through the root fiber. This removes one context switch per scheduling decision on every `sleep`, `read`, and `write`.
|
|
252
|
+
|
|
253
|
+
If the native extension can't be loaded (on Windows, for example), a pure-Ruby fallback (`lib/carbon_fiber/native/fallback.rb`) provides the same Selector API using threads and condition variables.
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Benchmarks
|
|
258
|
+
|
|
259
|
+
AWS EC2 c7a.2xlarge, 8 dedicated vCPUs,Ubuntu 24.04 LTS, kernel 6.17, Ruby 4.0.2 + YJIT, io_uring. 5-run median.
|
|
260
|
+
|
|
261
|
+
### Ruby Fiber Schedulers (leading ones): Carbon Fiber vs. Async vs. Itsi
|
|
262
|
+
|
|
263
|
+
Measuring pure Ruby Fiber Scheduler performance (`Fiber.set_scheduler`).
|
|
264
|
+
|
|
265
|
+
| Workload | Unit | Carbon Fiber | Async | Itsi | Carbon Fiber vs. Async |
|
|
266
|
+
|---|---|---|---|---|---|
|
|
267
|
+
| `http_client_api` | req/s | **15,528** | 13,373 | timeout | +16% |
|
|
268
|
+
| `http_client_download` | dl/s | **6,747** | 5,920 | timeout | +14% |
|
|
269
|
+
| `http_server` | req/s | **48,175** | 37,409 | 29,708 | +29% |
|
|
270
|
+
| `tcp_echo` | ops/s | **50,392** | 38,907 | 30,660 | +29% |
|
|
271
|
+
| `connection_pool` | co/s | **4,989** | 4,912 | 4,968 | +2% |
|
|
272
|
+
| `fan_out_gather` | cyc/s | 2,024 | 2,046 | **2,104** | −1% |
|
|
273
|
+
| `db_query_mix` | qry/s | 1,660 | 1,652 | **1,662** | +0.5% |
|
|
274
|
+
| `cascading_timeout` | ops/s | **4,659** | 4,488 | error | +4% |
|
|
275
|
+
|
|
276
|
+
Enabling YJIT turned out to be very beneficial for Async as well—numbers here are with `--yjit` on both sides.
|
|
277
|
+
|
|
278
|
+
### Async framework: stock Async vs. Async + Carbon Fiber backend
|
|
279
|
+
|
|
280
|
+
Swapped the io-event selector for Carbon Fiber's native backend. Same Async code.
|
|
281
|
+
|
|
282
|
+
| Workload | Unit | Stock Async | Carbon Fiber | Delta |
|
|
283
|
+
|---|---|---|---|---|
|
|
284
|
+
| `http_client_api` | req/s | 13,375 | **14,331** | +7.1% |
|
|
285
|
+
| `http_client_download` | dl/s | 3,893 | **3,956** | +1.6% |
|
|
286
|
+
| `task_churn` | task/s | **87,883** | 85,027 | −3.3% |
|
|
287
|
+
| `condition_signal` | sig/s | 337,282 | **361,089** | +7.1% |
|
|
288
|
+
| `cascading_timeout` | ops/s | 4,497 | **4,511** | +0.3% |
|
|
289
|
+
| `tcp_throughput` | ops/s | 42,292 | **51,930** | +22.8% |
|
|
290
|
+
|
|
291
|
+
### Examples of how to run benchmarks
|
|
292
|
+
|
|
293
|
+
See [README](benchmarks/README.md) in the `benchmarks/` directory.
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
# macOS smoke test
|
|
297
|
+
benchmarks/bench -t carbon,async -w http_client_api,tcp_echo
|
|
298
|
+
|
|
299
|
+
# Linux via Docker (io_uring)
|
|
300
|
+
benchmarks/core_docker -t carbon,async,itsi -r 5
|
|
301
|
+
|
|
302
|
+
# Async framework comparison
|
|
303
|
+
benchmarks/async_docker -r 5
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Note that your results will differ depending on the operating system (io_uring or kqueue), system load and hardware, and there will be a lot of variance with certain workloads anyway. Try to benchmark on dedicated hardware, with setup (operating system, kernel) close to your production environment.
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## Acknowledgements
|
|
311
|
+
|
|
312
|
+
Thanks to Mitchell Hashimoto for [libxev](https://github.com/mitchellh/libxev) (and Ghostty).
|
|
313
|
+
|
|
314
|
+
Thanks to furunkel for [zig.rb](https://github.com/furunkel/zig.rb).
|
|
315
|
+
|
|
316
|
+
Thanks to Samuel Williams for the [Async](https://github.com/socketry/async) framework and [ecosystem](https://github.com/socketry).
|
|
317
|
+
|
|
318
|
+
## License
|
|
319
|
+
|
|
320
|
+
MIT
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Please note that this code is heavily AI-assisted.
|
|
4
|
+
|
|
5
|
+
require_relative "native"
|
|
6
|
+
|
|
7
|
+
module CarbonFiber
|
|
8
|
+
module Async
|
|
9
|
+
# IO::Event::Selector-compatible adapter backed by our native Zig selector.
|
|
10
|
+
# Subclasses Native::Selector so hot-path methods (transfer, yield, wakeup)
|
|
11
|
+
# dispatch directly to native code without an extra Ruby method frame.
|
|
12
|
+
#
|
|
13
|
+
# Registration:
|
|
14
|
+
# require "async"
|
|
15
|
+
# require "carbon_fiber/async"
|
|
16
|
+
# CarbonFiber::Async.default!
|
|
17
|
+
#
|
|
18
|
+
# Or via environment:
|
|
19
|
+
# IO_EVENT_SELECTOR=CarbonFiberSelector ruby app.rb
|
|
20
|
+
#
|
|
21
|
+
class Selector < CarbonFiber::Native::Selector
|
|
22
|
+
# @return [Float] seconds spent idle in the last {#select} call
|
|
23
|
+
attr_reader :idle_duration
|
|
24
|
+
|
|
25
|
+
# @return [Fiber] the event loop fiber
|
|
26
|
+
attr_reader :loop
|
|
27
|
+
|
|
28
|
+
# @param loop [Fiber] the Async event loop fiber
|
|
29
|
+
def initialize(loop)
|
|
30
|
+
super
|
|
31
|
+
@loop = loop
|
|
32
|
+
@idle_duration = 0.0
|
|
33
|
+
|
|
34
|
+
# Auxiliary ready queue for non-Fiber pushables (e.g. FiberInterrupt)
|
|
35
|
+
# and cross-thread pushes of non-Fiber objects. Thread-safe via mutex.
|
|
36
|
+
@auxiliary = []
|
|
37
|
+
@auxiliary_mutex = Mutex.new
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# transfer: inherited from native (no override needed)
|
|
41
|
+
# yield: inherited from native (no override needed)
|
|
42
|
+
# wakeup: inherited from native (no override needed)
|
|
43
|
+
|
|
44
|
+
# Release native resources.
|
|
45
|
+
def close
|
|
46
|
+
destroy
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Enqueue a fiber or fiber-like object into the ready queue.
|
|
50
|
+
# @param fiber [Fiber, Object]
|
|
51
|
+
def push(fiber)
|
|
52
|
+
if fiber.is_a?(Fiber)
|
|
53
|
+
super
|
|
54
|
+
else
|
|
55
|
+
@auxiliary_mutex.synchronize { @auxiliary << fiber }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Re-enqueue the current fiber and transfer to +fiber+ with arguments.
|
|
60
|
+
# @param fiber [Fiber]
|
|
61
|
+
# @param arguments [Array]
|
|
62
|
+
def resume(fiber, *arguments)
|
|
63
|
+
current = Fiber.current
|
|
64
|
+
native_push(current) unless current.equal?(@loop)
|
|
65
|
+
fiber.transfer(*arguments)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Re-enqueue the current fiber and raise on +fiber+.
|
|
69
|
+
# @param fiber [Fiber]
|
|
70
|
+
def raise(fiber, *arguments, **options)
|
|
71
|
+
current = Fiber.current
|
|
72
|
+
native_push(current) unless current.equal?(@loop)
|
|
73
|
+
fiber.raise(*arguments, **options)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @return [Boolean] whether there is pending work
|
|
77
|
+
def ready?
|
|
78
|
+
!@auxiliary.empty? || pending?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# --- Event loop ---
|
|
82
|
+
|
|
83
|
+
# Run one event loop iteration, draining the auxiliary queue before
|
|
84
|
+
# and after the native select.
|
|
85
|
+
#
|
|
86
|
+
# Note: +idle_duration+ is not actually measured—it stays at 0.0.
|
|
87
|
+
# Async uses it for load stats only (not correctness), and the two
|
|
88
|
+
# +Process.clock_gettime+ calls plus Float allocation cost ~1-2%
|
|
89
|
+
# on select-heavy workloads.
|
|
90
|
+
# @param duration [Float, nil] maximum seconds to wait
|
|
91
|
+
def select(duration = nil)
|
|
92
|
+
drain_auxiliary
|
|
93
|
+
super
|
|
94
|
+
drain_auxiliary
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# --- IO operations ---
|
|
98
|
+
|
|
99
|
+
# Wait for I/O readiness. Falls back to IO.select on a background
|
|
100
|
+
# thread when the native path returns nil (kqueue WRITE bypass,
|
|
101
|
+
# closed fd, duplicate waiter).
|
|
102
|
+
# @param fiber [Fiber]
|
|
103
|
+
# @param io [IO]
|
|
104
|
+
# @param events [Integer] bitmask of +IO::READABLE+, +IO::WRITABLE+
|
|
105
|
+
# @return [Integer, false] readiness bitmask, or false on timeout
|
|
106
|
+
def io_wait(fiber, io, events)
|
|
107
|
+
result = native_io_wait(fiber, io.fileno, events)
|
|
108
|
+
return result unless result.nil?
|
|
109
|
+
|
|
110
|
+
fallback_io_wait(io, events)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Native Zig io_read/io_write use recv/send with kernel buffer
|
|
114
|
+
# draining. Falls back to Ruby-level nonblock+io_wait for non-socket
|
|
115
|
+
# fds (pipes, files) where native returns nil.
|
|
116
|
+
|
|
117
|
+
EAGAIN = -Errno::EAGAIN::Errno
|
|
118
|
+
EWOULDBLOCK = -Errno::EWOULDBLOCK::Errno
|
|
119
|
+
|
|
120
|
+
# @param fiber [Fiber]
|
|
121
|
+
# @param io [IO]
|
|
122
|
+
# @param buffer [IO::Buffer]
|
|
123
|
+
# @param length [Integer]
|
|
124
|
+
# @param offset [Integer]
|
|
125
|
+
# @return [Integer] bytes read, or negative errno
|
|
126
|
+
def io_read(fiber, io, buffer, length, offset = 0)
|
|
127
|
+
result = native_io_read(io.fileno, buffer, length, offset)
|
|
128
|
+
return result unless result.nil?
|
|
129
|
+
|
|
130
|
+
ruby_io_read(fiber, io, buffer, length, offset)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# @param fiber [Fiber]
|
|
134
|
+
# @param io [IO]
|
|
135
|
+
# @param buffer [IO::Buffer]
|
|
136
|
+
# @param length [Integer]
|
|
137
|
+
# @param offset [Integer]
|
|
138
|
+
# @return [Integer] bytes written, or negative errno
|
|
139
|
+
def io_write(fiber, io, buffer, length, offset = 0)
|
|
140
|
+
result = native_io_write(io.fileno, buffer, length, offset)
|
|
141
|
+
return result unless result.nil?
|
|
142
|
+
|
|
143
|
+
ruby_io_write(fiber, io, buffer, length, offset)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Cancel pending waiters and close the descriptor.
|
|
147
|
+
# @param io [IO]
|
|
148
|
+
def io_close(io)
|
|
149
|
+
fd = io.respond_to?(:fileno) ? io.fileno : io.to_i
|
|
150
|
+
super(fd, IOError.new("stream closed while waiting"))
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Wait for a child process on a background thread.
|
|
154
|
+
# @param fiber [Fiber]
|
|
155
|
+
# @param pid [Integer]
|
|
156
|
+
# @param flags [Integer]
|
|
157
|
+
# @return [Process::Status]
|
|
158
|
+
def process_wait(fiber, pid, flags)
|
|
159
|
+
Thread.new do
|
|
160
|
+
Thread.current.report_on_exception = false
|
|
161
|
+
Process::Status.wait(pid, flags)
|
|
162
|
+
end.value
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
private
|
|
166
|
+
|
|
167
|
+
def drain_auxiliary
|
|
168
|
+
return if @auxiliary.empty?
|
|
169
|
+
|
|
170
|
+
items = @auxiliary_mutex.synchronize do
|
|
171
|
+
batch = @auxiliary.dup
|
|
172
|
+
@auxiliary.clear
|
|
173
|
+
batch
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
items.each { |item| item.transfer if item.alive? }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Ruby-level io_read/io_write for non-socket fds (pipes, files).
|
|
180
|
+
# Mirrors io-event's Select fallback selector.
|
|
181
|
+
def ruby_io_read(fiber, io, buffer, length, offset = 0)
|
|
182
|
+
total = 0
|
|
183
|
+
|
|
184
|
+
IO::Event::Selector.nonblock(io) do
|
|
185
|
+
while true
|
|
186
|
+
result = Fiber.blocking { buffer.read(io, 0, offset) }
|
|
187
|
+
|
|
188
|
+
if result < 0
|
|
189
|
+
if result == EAGAIN || result == EWOULDBLOCK
|
|
190
|
+
io_wait(fiber, io, IO::READABLE)
|
|
191
|
+
else
|
|
192
|
+
return result
|
|
193
|
+
end
|
|
194
|
+
elsif result == 0
|
|
195
|
+
break
|
|
196
|
+
else
|
|
197
|
+
total += result
|
|
198
|
+
break if total >= length
|
|
199
|
+
offset += result
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
total
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def ruby_io_write(fiber, io, buffer, length, offset = 0)
|
|
208
|
+
total = 0
|
|
209
|
+
|
|
210
|
+
IO::Event::Selector.nonblock(io) do
|
|
211
|
+
while true
|
|
212
|
+
result = Fiber.blocking { buffer.write(io, 0, offset) }
|
|
213
|
+
|
|
214
|
+
if result < 0
|
|
215
|
+
if result == EAGAIN || result == EWOULDBLOCK
|
|
216
|
+
io_wait(fiber, io, IO::WRITABLE)
|
|
217
|
+
else
|
|
218
|
+
return result
|
|
219
|
+
end
|
|
220
|
+
elsif result == 0
|
|
221
|
+
break
|
|
222
|
+
else
|
|
223
|
+
total += result
|
|
224
|
+
break if total >= length
|
|
225
|
+
offset += result
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
total
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def fallback_io_wait(io, events)
|
|
234
|
+
Thread.new do
|
|
235
|
+
Thread.current.report_on_exception = false
|
|
236
|
+
readers = (events & IO::READABLE).zero? ? nil : [io]
|
|
237
|
+
writers = (events & IO::WRITABLE).zero? ? nil : [io]
|
|
238
|
+
ready = ::IO.select(readers, writers, nil, nil)
|
|
239
|
+
return false unless ready
|
|
240
|
+
|
|
241
|
+
readiness = 0
|
|
242
|
+
readiness |= IO::READABLE if ready[0]&.include?(io)
|
|
243
|
+
readiness |= IO::WRITABLE if ready[1]&.include?(io)
|
|
244
|
+
readiness.zero? ? false : readiness
|
|
245
|
+
end.value
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Register as the default IO::Event selector for Async.
|
|
250
|
+
# Call after +require "async"+ so IO::Event::Selector is available.
|
|
251
|
+
def self.default!
|
|
252
|
+
IO::Event::Selector.const_set(:CarbonFiberSelector, CarbonFiber::Async::Selector) unless IO::Event::Selector.const_defined?(:CarbonFiberSelector, false)
|
|
253
|
+
ENV["IO_EVENT_SELECTOR"] = "CarbonFiberSelector"
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|