Chapter 26: Service Runner#
1. Work That Runs Forever#
Some work does not fit inside an HTTP request. A heartbeat that pings a health check endpoint every 30 seconds. A queue consumer that processes jobs continuously.
These are background services: long-running processes that start with the app and run until it stops.
Tina4's Service Runner manages background services. Register a service with a name and a block. The runner starts it in a background thread. The service runs for the lifetime of the app. If it crashes, the runner restarts it.
2. Defining a Service#
Tina4::ServiceRunner.register("heartbeat") do loop do Tina4::Log.info("Heartbeat", status: "alive", time: Time.now.utc.iso8601) sleep 30 endendThe block runs in a background thread. The loop keeps it alive. sleep 30 yields control between iterations.
3. Starting All Services#
Tina4::ServiceRunner.startCall this once, typically in your app startup file after route definitions. Called with no argument, start launches every registered service in a background thread.
# config/app.rbโrequire_relative "../src/routes/api"require_relative "../src/services/background"โTina4::ServiceRunner.start4. Starting a Single Service#
Tina4::ServiceRunner.start("heartbeat")Useful for starting services conditionally based on environment or configuration.
5. Stopping Services#
# Stop a specific serviceTina4::ServiceRunner.stop("heartbeat")โ# Stop all running services (no argument)Tina4::ServiceRunner.stopstop signals the thread to terminate. The runner waits for the thread to finish its current iteration before stopping.
6. Checking Service Status#
Tina4::ServiceRunner.is_running("heartbeat")# => true or falseโTina4::ServiceRunner.list# => [# { name: "heartbeat", running: true, last_run: <Time>, error_count: 0, options: {...} },# { name: "metrics", running: false, last_run: nil, error_count: 0, options: {...} }# ]7. Automatic Restart on Failure#
If a service crashes, the runner restarts it with an exponential backoff delay.
Tina4::ServiceRunner.register("unreliable_service") do loop do begin do_something_that_might_fail sleep 10 rescue => e Tina4::Log.error("Service error", service: "unreliable_service", error: e.message) # re-raise to trigger restart, or rescue and continue raise end endendIf the block raises, the runner logs the error, waits (1s, 2s, 4s... up to 60s), then restarts the service.
Set max_restarts to cap automatic restarts:
Tina4::ServiceRunner.register("payment_sync", max_restarts: 5) do # ...endAfter 5 restarts, the service is marked as :failed and not restarted again.
8. Queue Consumer Service#
The most common background service: processing a queue continuously.
Tina4::ServiceRunner.register("email_consumer") do queue = Tina4::Queue.new(topic: "emails")โ loop do job = queue.popโ if job.nil? sleep 2 # No jobs -- wait before polling again next endโ begin payload = job.payload send_email(payload[:to], payload[:subject], payload[:body]) job.completeโ Tina4::Log.info("Email sent", to: payload[:to], subject: payload[:subject]) rescue => e job.fail(e.message) Tina4::Log.error("Email failed", to: payload[:to], error: e.message) end endend9. Scheduled Task Service#
Run a task on a schedule using a service with sleep-based timing.
Tina4::ServiceRunner.register("daily_report") do loop do now = Time.nowโ # Run at 08:00 every day if now.hour == 8 && now.min == 0 Tina4::Log.info("Generating daily report")โ db = Tina4::Container.resolve(:database) rows = db.query("SELECT COUNT(*) as orders FROM orders WHERE DATE(created_at) = DATE('now')")โ Tina4::Log.info("Daily report complete", orders_today: rows.first["orders"])โ sleep 61 # Skip past the current minute to avoid re-running else sleep 30 # Check every 30 seconds end endend10. Metrics Collector Service#
Tina4::ServiceRunner.register("metrics_collector") do loop do mem_mb = `ps -o rss= -p #{Process.pid}`.strip.to_i / 1024โ Tina4::Log.info("Metrics", memory_mb: mem_mb, pid: Process.pid, threads: Thread.list.count )โ sleep 60 endend11. Listing Registered Services#
Tina4::ServiceRunner.list.each do |info| puts "#{info[:name]}: #{info[:running] ? 'running' : 'stopped'}"endOutput:
heartbeat: runningemail_consumer: runningdaily_report: runningmetrics_collector: running12. Shutdown Hooks#
Register cleanup logic that runs when a service is stopped.
Tina4::ServiceRunner.register("database_sync") do |service| db = Tina4::Container.resolve(:database)โ service.on_stop do Tina4::Log.info("Database sync shutting down -- flushing pending writes") db.flush endโ loop do sync_records(db) sleep 5 endend13. Gotchas#
1. Services share the main process#
Background services run in threads, not separate processes. A runaway service that consumes 100% CPU affects the HTTP server running in the same process. Keep service work lightweight or move heavy computation to separate processes via the queue system.
2. sleep is required in loops#
A loop with no sleep spins the CPU at 100%. Always sleep between iterations.
3. Thread safety#
Services share memory with the main thread. Access shared mutable state (global variables, class variables, shared caches) through a mutex.
COUNTER_MUTEX = Mutex.newCOUNTER = { value: 0 }โTina4::ServiceRunner.register("counter_service") do loop do COUNTER_MUTEX.synchronize { COUNTER[:value] += 1 } sleep 1 endend4. stop on shutdown#
In production, call Tina4::ServiceRunner.stop (no argument stops every service) in a signal trap to cleanly shut down services before the process exits.
trap("SIGTERM") do Tina4::ServiceRunner.stop exit 0end