Class: CarbonFiber::Async::Selector

Inherits:
Native::Selector
  • Object
show all
Defined in:
lib/carbon_fiber/async.rb

Overview

IO::Event::Selector-compatible adapter backed by our native Zig selector. Subclasses Native::Selector so hot-path methods (transfer, yield, wakeup) dispatch directly to native code without an extra Ruby method frame.

Registration:

require "async"
require "carbon_fiber/async"
CarbonFiber::Async.default!

Or via environment:

IO_EVENT_SELECTOR=CarbonFiberSelector ruby app.rb

Constant Summary collapse

EAGAIN =

Native Zig io_read/io_write use recv/send with kernel buffer draining. Falls back to Ruby-level nonblock+io_wait for non-socket fds (pipes, files) where native returns nil.

-Errno::EAGAIN::Errno
EWOULDBLOCK =
-Errno::EWOULDBLOCK::Errno

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(loop, io_contract_v4: CarbonFiber.io_contract_v4?) ⇒ Selector

Returns a new instance of Selector.

Parameters:

  • loop (Fiber)

    the Async event loop fiber

  • io_contract_v4 (Boolean) (defaults to: CarbonFiber.io_contract_v4?)

    use the Ruby 4.1+ single-transfer contract in the native I/O paths (defaults to what the running Ruby speaks; override only for testing)



33
34
35
36
37
38
39
40
41
42
43
# File 'lib/carbon_fiber/async.rb', line 33

def initialize(loop, io_contract_v4: CarbonFiber.io_contract_v4?)
  super(loop)
  self.io_contract_v4 = io_contract_v4
  @loop = loop
  @idle_duration = 0.0

  # Auxiliary ready queue for non-Fiber pushables (e.g. FiberInterrupt)
  # and cross-thread pushes of non-Fiber objects. Thread-safe via mutex.
  @auxiliary = []
  @auxiliary_mutex = Mutex.new
end

Instance Attribute Details

#idle_durationFloat (readonly)

Returns seconds spent idle in the last #select call.

Returns:

  • (Float)

    seconds spent idle in the last #select call



24
25
26
# File 'lib/carbon_fiber/async.rb', line 24

def idle_duration
  @idle_duration
end

#loopFiber (readonly)

Returns the event loop fiber.

Returns:

  • (Fiber)

    the event loop fiber



27
28
29
# File 'lib/carbon_fiber/async.rb', line 27

def loop
  @loop
end

Instance Method Details

#closeObject

Release native resources.



50
51
52
# File 'lib/carbon_fiber/async.rb', line 50

def close
  destroy
end

#io_close(descriptor) ⇒ Object

Cancel pending waiters on the descriptor.

Parameters:

  • descriptor (Integer)


201
202
203
# File 'lib/carbon_fiber/async.rb', line 201

def io_close(descriptor)
  super(descriptor, IOError.new("stream closed while waiting"))
end

#io_read_v3(fiber, io, buffer, length, offset = 0) ⇒ Integer Also known as: io_read

Legacy contract (Ruby 3.4 through 4.0): length before offset, minimum-progress semantics.

Parameters:

  • fiber (Fiber)
  • io (IO)
  • buffer (IO::Buffer)
  • length (Integer)
  • offset (Integer) (defaults to: 0)

Returns:

  • (Integer)

    bytes read, or negative errno



133
134
135
136
137
138
# File 'lib/carbon_fiber/async.rb', line 133

def io_read_v3(fiber, io, buffer, length, offset = 0)
  result = native_io_read(io.fileno, buffer, length, offset)
  return result unless result.nil?

  ruby_io_read(fiber, io, buffer, length, offset)
end

#io_read_v4(fiber, io, buffer, offset, length) ⇒ Integer

Ruby 4.1+ contract: offset before length, single transfer. One nonblocking attempt; short results and -EAGAIN are returned directly, and the caller composes retries via io_wait.

Parameters:

  • fiber (Fiber)
  • io (IO)
  • buffer (IO::Buffer)
  • offset (Integer)
  • length (Integer)

    maximum bytes for this transfer

Returns:

  • (Integer)

    bytes read, or negative errno



149
150
151
152
153
154
155
156
# File 'lib/carbon_fiber/async.rb', line 149

def io_read_v4(fiber, io, buffer, offset, length)
  return 0 if length.zero?

  result = native_io_read(io.fileno, buffer, length, offset)
  return result unless result.nil?

  ruby_io_read_v4(io, buffer, offset, length)
end

#io_wait(fiber, io, events) ⇒ Integer, false

Wait for I/O readiness. Falls back to IO.select on a background thread when the native path returns nil (kqueue WRITE bypass, closed fd, duplicate waiter).

Parameters:

  • fiber (Fiber)
  • io (IO)
  • events (Integer)

    bitmask of IO::READABLE, IO::WRITABLE

Returns:

  • (Integer, false)

    readiness bitmask, or false on timeout



111
112
113
114
115
116
# File 'lib/carbon_fiber/async.rb', line 111

def io_wait(fiber, io, events)
  result = native_io_wait(fiber, io.fileno, events)
  return result unless result.nil?

  fallback_io_wait(io, events)
end

#io_write_v3(fiber, io, buffer, length, offset = 0) ⇒ Integer Also known as: io_write

Legacy contract (Ruby 3.4 through 4.0): length before offset, minimum-progress semantics.

Parameters:

  • fiber (Fiber)
  • io (IO)
  • buffer (IO::Buffer)
  • length (Integer)
  • offset (Integer) (defaults to: 0)

Returns:

  • (Integer)

    bytes written, or negative errno



166
167
168
169
170
171
# File 'lib/carbon_fiber/async.rb', line 166

def io_write_v3(fiber, io, buffer, length, offset = 0)
  result = native_io_write(io.fileno, buffer, length, offset)
  return result unless result.nil?

  ruby_io_write(fiber, io, buffer, length, offset)
end

#io_write_v4(fiber, io, buffer, offset, length) ⇒ Integer

Ruby 4.1+ contract: offset before length, single transfer.

Parameters:

  • fiber (Fiber)
  • io (IO)
  • buffer (IO::Buffer)
  • offset (Integer)
  • length (Integer)

    maximum bytes for this transfer

Returns:

  • (Integer)

    bytes written, or negative errno



180
181
182
183
184
185
186
187
# File 'lib/carbon_fiber/async.rb', line 180

def io_write_v4(fiber, io, buffer, offset, length)
  return 0 if length.zero?

  result = native_io_write(io.fileno, buffer, length, offset)
  return result unless result.nil?

  ruby_io_write_v4(io, buffer, offset, length)
end

#process_wait(fiber, pid, flags) ⇒ Process::Status

Wait for a child process on a background thread.

Parameters:

  • fiber (Fiber)
  • pid (Integer)
  • flags (Integer)

Returns:

  • (Process::Status)


210
211
212
213
214
215
# File 'lib/carbon_fiber/async.rb', line 210

def process_wait(fiber, pid, flags)
  Thread.new do
    Thread.current.report_on_exception = false
    Process::Status.wait(pid, flags)
  end.value
end

#push(fiber) ⇒ Object

Enqueue a fiber or fiber-like object into the ready queue.

Parameters:

  • fiber (Fiber, Object)


56
57
58
59
60
61
62
# File 'lib/carbon_fiber/async.rb', line 56

def push(fiber)
  if fiber.is_a?(Fiber)
    super
  else
    @auxiliary_mutex.synchronize { @auxiliary << fiber }
  end
end

#raise(fiber, *arguments, **options) ⇒ Object

Re-enqueue the current fiber and raise on fiber.

Parameters:

  • fiber (Fiber)


75
76
77
78
79
# File 'lib/carbon_fiber/async.rb', line 75

def raise(fiber, *arguments, **options)
  current = Fiber.current
  native_push(current) unless current.equal?(@loop)
  fiber.raise(*arguments, **options)
end

#ready?Boolean

Returns whether there is pending work.

Returns:

  • (Boolean)

    whether there is pending work



82
83
84
# File 'lib/carbon_fiber/async.rb', line 82

def ready?
  !@auxiliary.empty? || pending?
end

#resume(fiber, *arguments) ⇒ Object

Re-enqueue the current fiber and transfer to fiber with arguments.

Parameters:

  • fiber (Fiber)
  • arguments (Array)


67
68
69
70
71
# File 'lib/carbon_fiber/async.rb', line 67

def resume(fiber, *arguments)
  current = Fiber.current
  native_push(current) unless current.equal?(@loop)
  fiber.transfer(*arguments)
end

#select(duration = nil) ⇒ Object

Run one event loop iteration, draining the auxiliary queue before and after the native select.

Note: idle_duration is not actually measured—it stays at 0.0. Async uses it for load stats only (not correctness), and the two Process.clock_gettime calls plus Float allocation cost ~1-2% on select-heavy workloads.

Parameters:

  • duration (Float, nil) (defaults to: nil)

    maximum seconds to wait



96
97
98
99
100
# File 'lib/carbon_fiber/async.rb', line 96

def select(duration = nil)
  drain_auxiliary
  super
  drain_auxiliary
end