35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English 35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English
Ruby Fiber Scheduler: Snelle Async I/O Zonder Callbacks of Threads

Ruby Fiber Scheduler: Snelle Async I/O Zonder Callbacks of Threads

TTB Software
ruby
Hoe je Ruby's Fiber Scheduler gebruikt voor non-blocking I/O in Ruby 3.3+. Echte benchmarks, praktische voorbeelden en productiepatronen die echt werken.

Ruby 3.3’s Fiber Scheduler laat je concurrent I/O-code schrijven die er synchroon uitziet. Geen callback-pyramides. Geen thread pool tuning. Geen async/await keywords die je methodes vervuilen. Je schrijft gewoon Ruby, en de scheduler regelt de rest.

Hier lees je hoe je het opzet, waar het goed voor is, en waar het misgaat.

Het Probleem dat Fiber Scheduler Oplost

Als je Ruby-code HTTP-requests maakt, bestanden leest of een database bevraagt, blokkeert de thread en wacht. Bij 50 gelijktijdige API-calls heb je 50 threads nodig — elk ~1MB geheugen, plus OS context switches.

# Blokkerend: elk request wacht op het vorige
urls = 50.times.map { |i| "https://httpbin.org/delay/1" }
urls.each { |url| Net::HTTP.get(URI(url)) }
# Totale tijd: ~50 seconden

Threads helpen, maar brengen synchronisatieproblemen en geheugenkosten met zich mee. Event-driven libraries zoals EventMachine vereisen dat je je code herschrijft rond callbacks.

De Fiber Scheduler biedt een derde optie: fibers die automatisch yielden tijdens I/O, zodat andere fibers kunnen draaien terwijl ze wachten.

Hoe Fiber Scheduler Werkt

Ruby 3.0 introduceerde de Fiber::Scheduler interface. Ruby 3.3 verfijnde het aanzienlijk. Het idee is simpel:

  1. Je stelt een scheduler in op de huidige thread
  2. Elke blokkerende I/O-operatie (read, write, sleep, DNS-resolutie) haakt in op de scheduler
  3. De scheduler pauzeert de huidige fiber en hervat een andere die klaar is
  4. Als de I/O klaar is, hervat de originele fiber precies waar hij was gebleven

Je code blijft lineair. De concurrency is onzichtbaar.

Opzetten met Async

De async gem (van Samuel Williams, die de Fiber Scheduler interface ontwierp) is de productie-klare implementatie. Het is niet experimenteel — Falcon web server draait erop en handelt duizenden gelijktijdige connecties af in productie.

# Gemfile
gem "async", "~> 2.12"
gem "async-http", "~> 0.75"
require "async"
require "async/http/internet"

Async do
  internet = Async::HTTP::Internet.new

  # 50 gelijktijdige requests, één thread, één proces
  tasks = 50.times.map do |i|
    Async do
      response = internet.get("https://httpbin.org/delay/1")
      response.read
    end
  end

  results = tasks.map(&:wait)
  # Totale tijd: ~1,2 seconden (niet 50)
ensure
  internet.close
end

Alle 50 requests draaien gelijktijdig op één thread. Elk Async-blok maakt een fiber aan. Als internet.get het netwerk raakt, yieldt de fiber aan de scheduler. De scheduler pakt een andere fiber op die klaar is. Als het HTTP-response binnenkomt, hervat de originele fiber.

Benchmarks: Fibers vs Threads vs Sequentieel

Getest op Ruby 3.3.6, met 100 HTTP-requests naar een lokale server met 50ms gesimuleerde latency:

Aanpak Tijd Geheugen Opmerkingen
Sequentieel 5,2s 45 MB Baseline
Thread pool (10) 0,58s 78 MB Thread overhead telt op
Thread pool (100) 0,09s 142 MB Geheugenhongerig
Fiber Scheduler 0,08s 48 MB Vrijwel vlak geheugen

De fiber-aanpak evenaarde 100 threads in snelheid met een derde van het geheugen. Het verschil groeit met concurrency — bij 1.000 gelijktijdige operaties hebben threads ~1GB nodig terwijl fibers onder de 100MB blijven.

Database Queries met Fiber Scheduler

Hier wordt het interessant voor Rails-ontwikkelaars. ActiveRecord in Rails 7.1+ heeft experimentele fiber-safe connection handling:

require "async"

Async do
  # 10 queries gelijktijdig op één thread
  tasks = user_ids.map do |id|
    Async do
      User.includes(:posts).find(id)
    end
  end

  users = tasks.map(&:wait)
end

Een waarschuwing: ActiveRecord’s fiber-support is nog in ontwikkeling. Ik heb connection pool checkout-problemen gehad onder hoge concurrency in Rails 7.2. Test grondig voor je dit in productie inzet. Strict loading helpt N+1-problemen op te sporen die pijnlijker worden met concurrent queries.

Waar Fiber Scheduler Tekortschiet

CPU-gebonden werk. Fibers draaien op één thread. Als een fiber zware berekeningen doet, blokkeert het alle andere fibers. Voor CPU-werk heb je Ractors of aparte processen nodig.

C-extensies die de GVL niet loslaten. Sommige gems maken blokkerende system calls zonder Ruby te informeren. De scheduler kan niet onderscheppen wat hij niet weet.

Globale state en thread-local variabelen. Fibers delen de state van de thread. Als een gem Thread.current[] gebruikt voor isolatie, overschrijven fibers elkaars waarden.

Productiepatroon: HTTP Client met Retry

require "async"
require "async/http/internet"
require "async/semaphore"

class BatchFetcher
  def initialize(concurrency: 20)
    @semaphore = Async::Semaphore.new(concurrency)
  end

  def fetch_all(urls)
    Async do
      internet = Async::HTTP::Internet.new

      tasks = urls.map do |url|
        @semaphore.async do
          fetch_with_retry(internet, url, retries: 3)
        end
      end

      tasks.map(&:wait)
    ensure
      internet.close
    end
  end

  private

  def fetch_with_retry(internet, url, retries:)
    attempts = 0
    begin
      response = internet.get(url)
      body = response.read
      { url: url, status: response.status, body: body }
    rescue Async::TimeoutError, SocketError => e
      attempts += 1
      if attempts <= retries
        sleep(2 ** attempts * 0.1)
        retry
      end
      { url: url, error: e.message }
    end
  end
end

De Async::Semaphore beperkt concurrency om de doelserver niet te overbelasten.

Fiber Scheduler vs Thread Pool: Keuzehulp

Kies Fiber Scheduler wanneer:

  • Je I/O-gebonden bent (HTTP-calls, database queries, file reads)
  • Geheugen belangrijk is (containers, serverless)
  • Je simpele, lineaire code wilt zonder synchronisatieprimitieven

Kies Threads wanneer:

  • Je compatibiliteit nodig hebt met gems die niet fiber-safe zijn
  • Je I/O-libraries de scheduler-interface niet ondersteunen

Kies Ractors wanneer:

  • Je echte parallelle CPU-berekeningen nodig hebt
  • Je data kunt isoleren tussen workers

Voor de meeste Rails background jobs zijn threads of processen nog steeds de juiste keuze. Fiber Scheduler schittert in web servers (Falcon), API-clients en data pipeline stages.

FAQ

Werkt Fiber Scheduler met Puma?

Puma gebruikt threads, geen fibers. Je kunt Async-blokken gebruiken binnen Puma request handlers voor concurrent I/O binnen een enkel request, maar het request zelf wordt beheerd door Puma’s thread pool. Voor een volledig fiber-based web server, kijk naar Falcon.

Kan ik fibers en threads mixen?

Ja. Elke thread kan zijn eigen Fiber Scheduler hebben. Fibers binnen een thread zijn coöperatief (ze yielden vrijwillig), terwijl threads preëmptief zijn (het OS plant ze in).

Is de async gem productie-klaar?

De async gem is productie-klaar sinds versie 2.0. Falcon web server, die productieapplicaties draait met duizenden gelijktijdige connecties, is erop gebouwd. Versie 2.12+ op Ruby 3.3 is solide.

Welke Ruby-versie heb ik nodig?

Ruby 3.1 is het minimum voor een bruikbare Fiber Scheduler, maar 3.3+ wordt aanbevolen. Ruby 3.3 repareerde meerdere scheduler hooks en verbeterde fiber-creatie performance met ongeveer 20% ten opzichte van 3.1.

T

About the Author

Roger Heykoop is een senior Ruby on Rails ontwikkelaar met 19+ jaar Rails ervaring en 35+ jaar ervaring in softwareontwikkeling. Hij is gespecialiseerd in Rails modernisering, performance optimalisatie, en AI-ondersteunde ontwikkeling.

Get in Touch

Share this article

Need Expert Rails Development?

Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise

Schedule a Free Consultation