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.
@@ -0,0 +1,396 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Please note that this code is heavily AI-assisted.
4
+
5
+ require "resolv"
6
+ require "socket"
7
+ require "timeout"
8
+ require_relative "native"
9
+
10
+ # High-performance Ruby Fiber Scheduler backed by Zig and libxev.
11
+ #
12
+ # Carbon Fiber implements the Ruby Fiber Scheduler protocol with a native
13
+ # event loop: io_uring on Linux, kqueue on macOS. Install it as the thread's
14
+ # scheduler and existing blocking I/O code becomes concurrent automatically.
15
+ #
16
+ # @example Basic usage
17
+ # require "carbon_fiber"
18
+ #
19
+ # Fiber.set_scheduler(CarbonFiber::Scheduler.new)
20
+ # Fiber.schedule { Net::HTTP.get(URI(url)) }
21
+ #
22
+ # @example With the Async framework
23
+ # require "carbon_fiber/async"
24
+ # CarbonFiber::Async.default!
25
+ #
26
+ # Async { |task| task.sleep(1) }
27
+ #
28
+ # @see CarbonFiber::Scheduler
29
+ # @see CarbonFiber::Async
30
+ module CarbonFiber
31
+ # Implements the Ruby Fiber Scheduler interface.
32
+ #
33
+ # Delegates I/O and timer operations to a native Zig selector (io_uring on
34
+ # Linux, kqueue on macOS). Operations the native layer doesn't cover
35
+ # (DNS, process_wait) run on background threads.
36
+ #
37
+ # @example
38
+ # scheduler = CarbonFiber::Scheduler.new
39
+ # Fiber.set_scheduler(scheduler)
40
+ # Fiber.schedule { sleep 1; puts "done" }
41
+ # scheduler.run
42
+ # Fiber.set_scheduler(nil)
43
+ class Scheduler
44
+ # @param root_fiber [Fiber] the event loop fiber (defaults to current)
45
+ # @param selector [Class] native selector class to instantiate
46
+ def initialize(root_fiber = Fiber.current, selector: CarbonFiber::Native::Selector)
47
+ @root_fiber = root_fiber
48
+ @scheduler_thread = Thread.current
49
+ @selector = selector.new(root_fiber)
50
+ @active_fibers = 0
51
+ @background_count = 0
52
+ @closed = false
53
+ @closing = false
54
+ end
55
+
56
+ # Called by Ruby when +Fiber.set_scheduler(nil)+ is invoked.
57
+ def scheduler_close
58
+ close(true)
59
+ end
60
+
61
+ # Drain pending work and release the native selector.
62
+ def close(internal = false)
63
+ return true if @closed || @closing
64
+
65
+ unless internal
66
+ return Fiber.set_scheduler(nil) if Fiber.scheduler == self
67
+ end
68
+
69
+ @closing = true
70
+ run
71
+ true
72
+ ensure
73
+ unless @closed
74
+ @selector&.destroy
75
+ @closed = true
76
+ @closing = false
77
+ freeze
78
+ end
79
+ end
80
+
81
+ # @return [Boolean] whether the scheduler has been closed
82
+ def closed?
83
+ @closed
84
+ end
85
+
86
+ # Monotonic clock used by the scheduler for timers.
87
+ # @return [Float] seconds since an arbitrary epoch
88
+ def current_time
89
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
90
+ end
91
+
92
+ # Create and schedule a non-blocking fiber.
93
+ # @yield the block to run inside the fiber
94
+ # @return [Fiber]
95
+ def fiber(&block)
96
+ fiber = Fiber.new(blocking: false) do
97
+ block.call
98
+ ensure
99
+ fiber_done
100
+ end
101
+
102
+ @active_fibers += 1
103
+ @selector.push(fiber)
104
+ @selector.wakeup unless Thread.current.equal?(@scheduler_thread)
105
+
106
+ fiber
107
+ end
108
+
109
+ # Transfer control to the next ready fiber or the event loop.
110
+ def transfer
111
+ @selector.transfer
112
+ end
113
+
114
+ # Re-enqueue the current fiber and transfer to the event loop.
115
+ def yield
116
+ @selector.yield
117
+ end
118
+
119
+ # Enqueue a fiber into the ready queue.
120
+ # @param fiber [Fiber]
121
+ def push(fiber)
122
+ @selector.push(fiber)
123
+ end
124
+
125
+ # Resume a fiber, optionally passing a value.
126
+ # @param fiber [Fiber]
127
+ # @param arguments [Array] at most one value to pass to the fiber
128
+ def resume(fiber, *arguments)
129
+ if arguments.empty?
130
+ @selector.push(fiber)
131
+ else
132
+ @selector.resume(fiber, arguments.first)
133
+ end
134
+ end
135
+
136
+ # Deliver an exception to a suspended fiber.
137
+ # @param fiber [Fiber]
138
+ # @param exception [Exception]
139
+ def raise(fiber, exception)
140
+ @selector.raise(fiber, exception)
141
+ end
142
+
143
+ # Wake the event loop (thread-safe).
144
+ def wakeup
145
+ @selector.wakeup
146
+ end
147
+
148
+ # Run one iteration of the event loop.
149
+ # @param timeout [Float, nil] maximum seconds to wait
150
+ def select(timeout = nil)
151
+ @selector.select(timeout)
152
+ end
153
+
154
+ # Suspend the current fiber until unblocked or timed out.
155
+ # @param _blocker [Object] unused, required by the protocol
156
+ # @param timeout [Float, nil] seconds before automatic resume
157
+ def block(_blocker, timeout = nil)
158
+ @selector.block(Fiber.current, timeout)
159
+ end
160
+
161
+ # Resume a fiber previously suspended by {#block}.
162
+ # @param _blocker [Object] unused, required by the protocol
163
+ # @param fiber [Fiber]
164
+ def unblock(_blocker, fiber)
165
+ @selector.unblock(fiber)
166
+ true
167
+ end
168
+
169
+ # Intercept +Kernel#sleep+. Parks the fiber on a native timer.
170
+ # @param duration [Float, nil] seconds to sleep; nil sleeps forever
171
+ def kernel_sleep(duration = nil)
172
+ if duration.nil?
173
+ transfer
174
+ elsif duration <= 0
175
+ self.yield
176
+ else
177
+ block(nil, duration)
178
+ end
179
+
180
+ true
181
+ end
182
+
183
+ # Wait for I/O readiness on a file descriptor.
184
+ # @param io [IO]
185
+ # @param events [Integer] bitmask of +IO::READABLE+, +IO::WRITABLE+
186
+ # @param timeout [Float, nil]
187
+ # @return [Integer, false] readiness bitmask, or false on timeout
188
+ def io_wait(io, events, timeout = nil)
189
+ return poll_io_now(io, events) if timeout == 0
190
+
191
+ # Native io_wait_object handles fileno extraction, Fiber.current,
192
+ # and nil/numeric timeout in Zig — skipping a Ruby frame + branch
193
+ # per call on Net::HTTP's hot read/write loop.
194
+ result = @selector.io_wait_object(io, events, timeout)
195
+ result.nil? ? await_background_operation { io_select_readiness(io, events, timeout) } : result
196
+ rescue NoMethodError, TypeError
197
+ await_background_operation { io_select_readiness(io, events, timeout) }
198
+ end
199
+
200
+ # Read from an IO into a buffer via the native selector.
201
+ # Falls back to a background thread for non-socket descriptors.
202
+ # @param io [IO]
203
+ # @param buffer [IO::Buffer]
204
+ # @param length [Integer]
205
+ # @param offset [Integer]
206
+ # @return [Integer] bytes read, or negative errno
207
+ def io_read(io, buffer, length, offset = 0)
208
+ # Native io_read_object extracts the descriptor in Zig, skipping a
209
+ # `respond_to?(:fileno)` + `io.fileno` method-send pair per call.
210
+ native_result = @selector.io_read_object(io, buffer, length, offset)
211
+ return native_result unless native_result.nil?
212
+
213
+ await_background_operation do
214
+ Fiber.blocking { buffer.read(io, length, offset) }
215
+ end
216
+ rescue NoMethodError, TypeError
217
+ await_background_operation do
218
+ Fiber.blocking { buffer.read(io, length, offset) }
219
+ end
220
+ end
221
+
222
+ # Write from a buffer to an IO via the native selector.
223
+ # Falls back to a background thread for non-socket descriptors.
224
+ # @param io [IO]
225
+ # @param buffer [IO::Buffer]
226
+ # @param length [Integer]
227
+ # @param offset [Integer]
228
+ # @return [Integer] bytes written, or negative errno
229
+ def io_write(io, buffer, length, offset = 0)
230
+ native_result = @selector.io_write_object(io, buffer, length, offset)
231
+ return native_result unless native_result.nil?
232
+
233
+ await_background_operation do
234
+ Fiber.blocking { buffer.write(io, length, offset) }
235
+ end
236
+ rescue NoMethodError, TypeError
237
+ await_background_operation do
238
+ Fiber.blocking { buffer.write(io, length, offset) }
239
+ end
240
+ end
241
+
242
+ # Blocking IO.select on a background thread.
243
+ def io_select(...)
244
+ await_background_operation do
245
+ Fiber.blocking { IO.select(...) }
246
+ end
247
+ end
248
+
249
+ # Cancel pending waiters on an IO and close the descriptor.
250
+ # @param io [IO]
251
+ def io_close(io)
252
+ descriptor = io.respond_to?(:to_i) ? io.to_i : io
253
+ @selector.io_close(descriptor, IOError.new("stream closed while waiting"))
254
+
255
+ Fiber.blocking do
256
+ target = io.is_a?(IO) ? io : IO.for_fd(descriptor.to_i)
257
+ target.close unless target.closed?
258
+ end
259
+
260
+ true
261
+ end
262
+
263
+ # Wait for a child process on a background thread.
264
+ # @param pid [Integer]
265
+ # @param flags [Integer] waitpid flags
266
+ # @return [Process::Status]
267
+ def process_wait(pid, flags)
268
+ # Ruby 4.0 bug: rb_process_status_wait re-enters the scheduler hook,
269
+ # so native process_wait produces an incorrect status. Background-thread
270
+ # waitpid avoids this because new threads have no scheduler installed.
271
+ await_background_operation do
272
+ if flags.zero?
273
+ Process::Status.wait(pid, flags)
274
+ else
275
+ _waited_pid, status = Process.waitpid2(pid, flags)
276
+ status
277
+ end
278
+ end
279
+ end
280
+
281
+ # Resolve a hostname to addresses via Resolv.
282
+ # @param hostname [String]
283
+ # @return [Array<String>]
284
+ def address_resolve(hostname)
285
+ if hostname.include?("%")
286
+ hostname = hostname.split("%", 2).first
287
+ end
288
+ Resolv.getaddresses(hostname)
289
+ end
290
+
291
+ # Run an arbitrary callable on a background thread.
292
+ # @param work [#call]
293
+ def blocking_operation_wait(work)
294
+ await_background_operation do
295
+ work.call
296
+ end
297
+ end
298
+
299
+ # Deliver an exception to a fiber from another fiber.
300
+ # @param fiber [Fiber]
301
+ # @param exception [Exception]
302
+ def fiber_interrupt(fiber, exception)
303
+ @selector.raise(fiber, exception)
304
+ @selector.wakeup
305
+ true
306
+ end
307
+
308
+ # Run a block with a timeout, raising an exception if it expires.
309
+ # @param duration [Float] seconds
310
+ # @param klass [Class, Exception] exception class or instance
311
+ # @param message [String]
312
+ def timeout_after(duration, klass = Timeout::Error, message = "execution expired", &block)
313
+ exc = klass.is_a?(Class) ? klass.new(message) : klass
314
+ token = @selector.raise_after(Fiber.current, exc, duration)
315
+ block.call(duration)
316
+ ensure
317
+ @selector.cancel_timer(token) if token
318
+ end
319
+
320
+ # Run one event loop iteration. Alias for {#select}.
321
+ def run_once(timeout = nil)
322
+ @selector.select(timeout)
323
+ end
324
+
325
+ # Run the event loop until all fibers and background operations complete.
326
+ def run
327
+ Kernel.raise RuntimeError, "Scheduler has been closed" if closed?
328
+
329
+ run_once until idle?
330
+ true
331
+ end
332
+
333
+ private
334
+
335
+ def idle?
336
+ @active_fibers.zero? && @background_count.zero? && !@selector.pending?
337
+ end
338
+
339
+ def fiber_done
340
+ @selector.cancel_block_timer(Fiber.current)
341
+ @active_fibers -= 1 if @active_fibers.positive?
342
+ end
343
+
344
+ def await_background_operation(&block)
345
+ fiber = Fiber.current
346
+ box = {}
347
+
348
+ Thread.new do
349
+ Thread.current.report_on_exception = false
350
+
351
+ begin
352
+ box[:result] = block.call
353
+ @selector.resume(fiber, true)
354
+ rescue => e
355
+ box[:error] = e
356
+ @selector.resume(fiber, true)
357
+ ensure
358
+ @selector.wakeup
359
+ end
360
+ end
361
+
362
+ @background_count += 1
363
+ @selector.block(fiber, nil)
364
+
365
+ Kernel.raise box[:error] if box[:error]
366
+
367
+ box[:result]
368
+ ensure
369
+ @background_count -= 1 if @background_count.positive?
370
+ end
371
+
372
+ def poll_io_now(io, events)
373
+ # Net::HTTP#begin_transport calls `wait_readable(0)` before every
374
+ # keep-alive request to probe for a closed connection. On a healthy
375
+ # connection this is always "not readable", so returning false
376
+ # directly saves one MSG_PEEK recvfrom per request. On a genuinely
377
+ # closed connection Net::HTTP will detect EOF on the next real read
378
+ # and reconnect — one extra request's worth of latency, at most.
379
+ return false if events == IO::READABLE && io.is_a?(BasicSocket)
380
+
381
+ Fiber.blocking { io_select_readiness(io, events, 0) }
382
+ end
383
+
384
+ def io_select_readiness(io, events, timeout)
385
+ readers = (events & IO::READABLE).zero? ? nil : [io]
386
+ writers = (events & IO::WRITABLE).zero? ? nil : [io]
387
+ ready = IO.select(readers, writers, nil, timeout)
388
+ return false unless ready
389
+
390
+ readiness = 0
391
+ readiness |= IO::READABLE if ready[0]&.include?(io)
392
+ readiness |= IO::WRITABLE if ready[1]&.include?(io)
393
+ readiness.zero? ? false : readiness
394
+ end
395
+ end
396
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CarbonFiber
4
+ # @return [String] the current gem version
5
+ VERSION = "0.1.0"
6
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "carbon_fiber/native"
4
+ require_relative "carbon_fiber/scheduler"
metadata ADDED
@@ -0,0 +1,155 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: carbon_fiber
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: aarch64-linux
6
+ authors:
7
+ - Yaroslav Markin
8
+ bindir: bin
9
+ cert_chain: []
10
+ dependencies:
11
+ - !ruby/object:Gem::Dependency
12
+ name: async
13
+ requirement: !ruby/object:Gem::Requirement
14
+ requirements:
15
+ - - "~>"
16
+ - !ruby/object:Gem::Version
17
+ version: '2.0'
18
+ type: :development
19
+ prerelease: false
20
+ version_requirements: !ruby/object:Gem::Requirement
21
+ requirements:
22
+ - - "~>"
23
+ - !ruby/object:Gem::Version
24
+ version: '2.0'
25
+ - !ruby/object:Gem::Dependency
26
+ name: rake
27
+ requirement: !ruby/object:Gem::Requirement
28
+ requirements:
29
+ - - "~>"
30
+ - !ruby/object:Gem::Version
31
+ version: '13.0'
32
+ type: :development
33
+ prerelease: false
34
+ version_requirements: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - "~>"
37
+ - !ruby/object:Gem::Version
38
+ version: '13.0'
39
+ - !ruby/object:Gem::Dependency
40
+ name: rake-compiler-dock
41
+ requirement: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - "~>"
44
+ - !ruby/object:Gem::Version
45
+ version: '1.11'
46
+ type: :development
47
+ prerelease: false
48
+ version_requirements: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - "~>"
51
+ - !ruby/object:Gem::Version
52
+ version: '1.11'
53
+ - !ruby/object:Gem::Dependency
54
+ name: rspec
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '3.13'
60
+ type: :development
61
+ prerelease: false
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '3.13'
67
+ - !ruby/object:Gem::Dependency
68
+ name: standard
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '1.0'
74
+ type: :development
75
+ prerelease: false
76
+ version_requirements: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: '1.0'
81
+ - !ruby/object:Gem::Dependency
82
+ name: yard
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: '0.9'
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - "~>"
93
+ - !ruby/object:Gem::Version
94
+ version: '0.9'
95
+ - !ruby/object:Gem::Dependency
96
+ name: lefthook
97
+ requirement: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - "~>"
100
+ - !ruby/object:Gem::Version
101
+ version: 2.1.5
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - "~>"
107
+ - !ruby/object:Gem::Version
108
+ version: 2.1.5
109
+ description: A high-performance Ruby Fiber Scheduler using a Zig native extension
110
+ with libxev (io_uring on Linux, kqueue on macOS). Works as a pure Ruby Fiber Scheduler,
111
+ as well as with the async gem.
112
+ email:
113
+ - yaroslav@markin.net
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - lib/carbon_fiber/async.rb
119
+ - lib/carbon_fiber/native/fallback.rb
120
+ - lib/carbon_fiber/native.rb
121
+ - lib/carbon_fiber/scheduler.rb
122
+ - lib/carbon_fiber/version.rb
123
+ - lib/carbon_fiber.rb
124
+ - lib/carbon_fiber/3.4.0/carbon_fiber_native.so
125
+ - lib/carbon_fiber/4.0.0/carbon_fiber_native.so
126
+ - README.md
127
+ - LICENSE
128
+ homepage: https://github.com/yaroslav/carbon_fiber
129
+ licenses:
130
+ - MIT
131
+ metadata:
132
+ source_code_uri: https://github.com/yaroslav/carbon_fiber
133
+ homepage_uri: https://github.com/yaroslav/carbon_fiber
134
+ changelog_uri: https://github.com/yaroslav/carbon_fiber/blob/main/CHANGELOG.md
135
+ bug_tracker_uri: https://github.com/yaroslav/carbon_fiber/issues
136
+ documentation_uri: https://rubydoc.info/gems/carbon_fiber
137
+ rdoc_options: []
138
+ require_paths:
139
+ - lib
140
+ required_ruby_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '3.4'
145
+ required_rubygems_version: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ requirements: []
151
+ rubygems_version: 4.0.6
152
+ specification_version: 4
153
+ summary: High-performance Ruby Fiber Scheduler backed by Zig with libxev. Pure Ruby
154
+ and gem async.
155
+ test_files: []