Tina4

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#

ruby
Tina4::ServiceRunner.register("heartbeat") do  loop do    Tina4::Log.info("Heartbeat", status: "alive", time: Time.now.utc.iso8601)    sleep 30  endend

The block runs in a background thread. The loop keeps it alive. sleep 30 yields control between iterations.


3. Starting All Services#

ruby
Tina4::ServiceRunner.start

Call this once, typically in your app startup file after route definitions. Called with no argument, start launches every registered service in a background thread.

ruby
# config/app.rbโ€‹require_relative "../src/routes/api"require_relative "../src/services/background"โ€‹Tina4::ServiceRunner.start

4. Starting a Single Service#

ruby
Tina4::ServiceRunner.start("heartbeat")

Useful for starting services conditionally based on environment or configuration.


5. Stopping Services#

ruby
# Stop a specific serviceTina4::ServiceRunner.stop("heartbeat")โ€‹# Stop all running services (no argument)Tina4::ServiceRunner.stop

stop signals the thread to terminate. The runner waits for the thread to finish its current iteration before stopping.


6. Checking Service Status#

ruby
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.

ruby
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  endend

If 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:

ruby
Tina4::ServiceRunner.register("payment_sync", max_restarts: 5) do  # ...end

After 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.

ruby
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  endend

9. Scheduled Task Service#

Run a task on a schedule using a service with sleep-based timing.

ruby
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  endend

10. Metrics Collector Service#

ruby
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  endend

11. Listing Registered Services#

ruby
Tina4::ServiceRunner.list.each do |info|  puts "#{info[:name]}: #{info[:running] ? 'running' : 'stopped'}"end

Output:

heartbeat: runningemail_consumer: runningdaily_report: runningmetrics_collector: running

12. Shutdown Hooks#

Register cleanup logic that runs when a service is stopped.

ruby
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  endend

13. 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.

ruby
COUNTER_MUTEX = Mutex.newCOUNTER = { value: 0 }โ€‹Tina4::ServiceRunner.register("counter_service") do  loop do    COUNTER_MUTEX.synchronize { COUNTER[:value] += 1 }    sleep 1  endend

4. 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.

ruby
trap("SIGTERM") do  Tina4::ServiceRunner.stop  exit 0end