agentic examples

Unix Workers

Unix Workers: I like Unix because the operating system already solved process supervision and nobody told the frameworks. A master preforks N plan workers, work arrives on a pipe, SIGTERM means "finish what you hold, then die with dignity", and the master reaps every child by PID and exit status. No supervisor gem, no thread pool config - fork(2), pipe(2), kill(2), wait(2).

Reliability & Recovery Round 14 Ryan Tomayko exit 0

source on github

bundle exec ruby examples/unix_workers.rb

a real captured run

UNIX WORKERS (master 3990, 3 preforked children: 4018, 4021, 4024)

  deploy signal: SIGTERM to all workers (finish what you hold, then exit)

  the reaping (every child accounted for, by pid and exit status):
    pid 4018    exit 0   served 7 job(s)
    pid 4021    exit 0   served 1 job(s)
    pid 4024    exit 0   served 1 job(s)
  total served: 9/9; unserved jobs stay in the pipe for the NEXT fleet

  count what's NOT here: no supervisor gem, no worker heartbeat
  table, no distributed lock. fork gave us isolation (a worker
  segfault kills ONE plan), the shared pipe gave us a work queue
  with kernel-grade load balancing, TERM-then-wait2 gave us
  deploys that finish in-flight work, and each worker's journal
  (flock'd - the process drill proved it) survives its process.
  the operating system is the best framework you already have;
  it's just that its DSL is spelled fork, pipe, kill, and wait.

source

# frozen_string_literal: true

# Unix Workers: I like Unix because the operating system already
# solved process supervision and nobody told the frameworks. A
# master preforks N plan workers, work arrives on a pipe, SIGTERM
# means "finish what you hold, then die with dignity", and the
# master reaps every child by PID and exit status. No supervisor
# gem, no thread pool config - fork(2), pipe(2), kill(2), wait(2).
#
#   bundle exec ruby examples/unix_workers.rb
#
# Runs offline; every process is real, every signal is real.

require class="s">"bundler/setup"
require class="s">"agentic"
require class="s">"json"
require class="s">"tmpdir"

Agentic.logger.level = class="y">:fatal

WORKERS = 3
JOBS = 9

# Work arrives on a shared pipe: the kernel does the load balancing
# (whichever worker reads first wins - it's a queue because Unix
# says it's a queue)
reader, writer = IO.pipe
results_reader, results_writer = IO.pipe

pids = WORKERS.times.map do |n|
  fork do
    writer.close
    results_reader.close
    draining = false
    trap(class="s">"TERM") { draining = true }

    journal = Agentic:class="y">:ExecutionJournal.new(path: File.join(Dir.tmpdir, class="s">"agentic_worker_#{n}.jsonl"), fsync_every: 10)
    served = 0
    until draining
      line = begin
        reader.read_nonblock(256)
      rescue IO:class="y">:WaitReadable
        sleep(0.005)
        next
      rescue EOFError
        break
      end
      line.split(class="s">"\n").each do |job|
        orchestrator = Agentic:class="y">:PlanOrchestrator.new(lifecycle_hooks: journal.lifecycle_hooks)
        task = Agentic:class="y">:Task.new(description: job, agent_spec: {class="s">"name" => class="s">"w", class="s">"instructions" => class="s">"w"})
        orchestrator.add_task(task, agent: ->(_t) {
          sleep(0.03)
          class="s">"#{job} done"
        })
        orchestrator.execute_plan
        served += 1
      end
    end
    journal.sync
    results_writer.puts JSON.generate({worker: n, pid: Process.pid, served: served})
    exit!(0)
  end
end
reader.close
results_writer.close

puts class="s">"UNIX WORKERS (master #{Process.pid}, #{WORKERS} preforked children: #{pids.join(", class="s">")})"
puts

# Feed the pipe as work actually arrives (paced) - a burst-written
# pipe gets drained by whoever reads first, which is a queue but not
# a fair one; arrival pacing is what lets the whole fleet lift
JOBS.times do |i|
  writer.puts class="s">"job-#{i}"
  sleep(0.025)
end
sleep(0.1) # let the fleet finish chewing

# The deploy: TERM the fleet, then REAP it - by pid, with status
puts class="s">"  deploy signal: SIGTERM to all workers (finish what you hold, then exit)"
pids.each { |pid| Process.kill(class="s">"TERM", pid) }
statuses = pids.map { |pid| Process.wait2(pid) }
writer.close

reports = results_reader.read.lines.map { |l| JSON.parse(l, symbolize_names: true) }.sort_by { |r| r[class="y">:worker] }
puts
puts class="s">"  the reaping (every child accounted for, by pid and exit status):"
statuses.each do |pid, status|
  report = reports.find { |r| r[class="y">:pid] == pid }
  puts format(class="s">"    pid %-7d exit %-3d served %d job(s)", pid, status.exitstatus, report ? report[class="y">:served] : 0)
end
puts format(class="s">"  total served: %d/%d; unserved jobs stay in the pipe for the NEXT fleet", reports.sum { |r| r[class="y">:served] }, JOBS)
puts
puts class="s">"  count what's NOT here: no supervisor gem, no worker heartbeat"
puts class="s">"  table, no distributed lock. fork gave us isolation (a worker"
puts class="s">"  segfault kills ONE plan), the shared pipe gave us a work queue"
puts class="s">"  with kernel-grade load balancing, TERM-then-wait2 gave us"
puts class="s">"  deploys that finish in-flight work, and each worker's journal"
puts class="s">"  (flock'd - the process drill proved it) survives its process."
puts class="s">"  the operating system is the best framework you already have;"
puts class="s">"  it's just that its DSL is spelled fork, pipe, kill, and wait."

clean = statuses.all? { |_, s| s.exitstatus.zero? } && reports.sum { |r| r[class="y">:served] } == JOBS
exit(clean ? 0 : 1)