Class: Tina4::ServiceRunner
- Inherits:
-
Object
- Object
- Tina4::ServiceRunner
- Defined in:
- lib/tina4/service_runner.rb
Overview
In-process service runner using Ruby threads. Supports cron schedules, simple intervals, and daemon (self-looping) handlers.
Tina4::ServiceRunner.register("cleanup", timing: "*/5 * * * *") { |ctx| ... }
Tina4::ServiceRunner.register("poller", interval: 10) { |ctx| ... }
Tina4::ServiceRunner.register("worker", daemon: true) { |ctx| while ctx.running; ...; end }
Tina4::ServiceRunner.start
Class Method Summary collapse
-
.clear! ⇒ Object
Remove all registrations and stop all services.
-
.discover(service_dir = nil) ⇒ Object
Auto-discover service files from a directory.
-
.is_running(name) ⇒ Object
Check if a specific service is currently running.
-
.list ⇒ Object
List all registered services with their status.
-
.match_cron?(pattern, time = Time.now) ⇒ Boolean
Check whether a 5-field cron pattern matches a given Time.
-
.register(name, handler = nil, options = {}, &block) ⇒ Object
Register a named service with options and a handler block (or callable).
-
.register_service(name, service, options = {}) ⇒ Object
Register a class-based service (subclass of Service) by name.
-
.start(name = nil) ⇒ Object
Start all registered services, or a specific one by name.
-
.stop(name = nil) ⇒ Object
Stop all running services, or a specific one by name.
Class Method Details
.clear! ⇒ Object
Remove all registrations and stop all services. Useful for tests.
177 178 179 180 181 182 183 184 |
# File 'lib/tina4/service_runner.rb', line 177 def clear! stop @mutex.synchronize do @registry.clear @threads.clear @contexts.clear end end |
.discover(service_dir = nil) ⇒ Object
Auto-discover service files from a directory. Each file should call Tina4.service or Tina4::ServiceRunner.register.
88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/tina4/service_runner.rb', line 88 def discover(service_dir = nil) service_dir ||= ENV["TINA4_SERVICE_DIR"] || "src/services" full_dir = File.(service_dir, Tina4.root_dir || Dir.pwd) return unless Dir.exist?(full_dir) Dir.glob(File.join(full_dir, "**/*.rb")).sort.each do |file| begin load file Tina4::Log.debug("Service discovered: #{file}") rescue => e Tina4::Log.error("Failed to load service #{file}: #{e.}") end end end |
.is_running(name) ⇒ Object
Check if a specific service is currently running.
171 172 173 174 |
# File 'lib/tina4/service_runner.rb', line 171 def is_running(name) ctx = @contexts[name.to_s] ctx&.running == true && @threads[name.to_s]&.alive? == true end |
.list ⇒ Object
List all registered services with their status.
157 158 159 160 161 162 163 164 165 166 167 168 |
# File 'lib/tina4/service_runner.rb', line 157 def list @registry.map do |name, entry| ctx = @contexts[name] { name: name, options: entry[:options], running: ctx&.running == true && @threads[name]&.alive? == true, last_run: ctx&.last_run, error_count: ctx&.error_count || 0 } end end |
.match_cron?(pattern, time = Time.now) ⇒ Boolean
Check whether a 5-field cron pattern matches a given Time. Fields: minute hour day_of_month month day_of_week
190 191 192 193 194 195 196 197 198 199 200 201 |
# File 'lib/tina4/service_runner.rb', line 190 def match_cron?(pattern, time = Time.now) fields = pattern.strip.split(/\s+/) return false unless fields.length == 5 minute, hour, dom, month, dow = fields parse_cron_field(minute, time.min, 59) && parse_cron_field(hour, time.hour, 23) && parse_cron_field(dom, time.day, 31) && parse_cron_field(month, time.month, 12) && parse_cron_field(dow, time.wday, 7) end |
.register(name, handler = nil, options = {}, &block) ⇒ Object
Register a named service with options and a handler block (or callable).
Options:
timing: cron expression, e.g. "*/5 * * * *"
interval: run every N seconds
daemon: boolean — handler manages its own loop
max_retries: restart limit on crash (default 3)
41 42 43 44 45 46 47 48 49 |
# File 'lib/tina4/service_runner.rb', line 41 def register(name, handler = nil, = {}, &block) callable = handler || block raise ArgumentError, "provide a handler or block for service '#{name}'" unless callable @mutex.synchronize do @registry[name.to_s] = { handler: callable, options: } end Tina4::Log.debug("Service registered: #{name}") end |
.register_service(name, service, options = {}) ⇒ Object
Register a class-based service (subclass of Tina4::Service) by name.
Wraps the Service’s #run method as the handler callable that ServiceRunner.start invokes. The service’s #stop is also wired up so ServiceRunner.stop(name) shuts it down cleanly.
class EmailQueueWorker < Tina4::Service
def run
until should_stop?
# process work
end
end
end
Tina4::ServiceRunner.register_service("emails", EmailQueueWorker.new)
Tina4::ServiceRunner.start
Default options set daemon: true because Service subclasses manage their own loop inside #run. Override via ‘options`.
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/tina4/service_runner.rb', line 70 def register_service(name, service, = {}) raise ArgumentError, "service must be a Tina4::Service instance" unless service.is_a?(Tina4::Service) = { daemon: true }.merge() callable = service.method(:run) @mutex.synchronize do @registry[name.to_s] = { handler: callable, options: , instance: service, } end Tina4::Log.debug("Service registered (class-based): #{name}") end |
.start(name = nil) ⇒ Object
Start all registered services, or a specific one by name.
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/tina4/service_runner.rb', line 106 def start(name = nil) targets = if name entry = @registry[name.to_s] raise KeyError, "service '#{name}' not registered" unless entry { name.to_s => entry } else @registry.dup end targets.each do |svc_name, entry| next if @threads[svc_name]&.alive? ctx = ServiceContext.new(svc_name) @mutex.synchronize { @contexts[svc_name] = ctx } thread = Thread.new { run_loop(svc_name, entry[:handler], entry[:options], ctx) } thread.name = "tina4-service-#{svc_name}" if thread.respond_to?(:name=) @mutex.synchronize { @threads[svc_name] = thread } Tina4::Log.info("Service started: #{svc_name}") end end |
.stop(name = nil) ⇒ Object
Stop all running services, or a specific one by name.
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
# File 'lib/tina4/service_runner.rb', line 130 def stop(name = nil) targets = if name ctx = @contexts[name.to_s] ctx ? { name.to_s => ctx } : {} else @contexts.dup end targets.each do |svc_name, ctx| ctx.running = false Tina4::Log.info("Service stopping: #{svc_name}") end # Join threads with a timeout so we don't hang forever targets.each_key do |svc_name| thread = @threads[svc_name] next unless thread thread.join(5) @mutex.synchronize do @threads.delete(svc_name) @contexts.delete(svc_name) end end end |