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.
203 204 205 206 207 208 209 210 |
# File 'lib/tina4/service_runner.rb', line 203 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.
197 198 199 200 |
# File 'lib/tina4/service_runner.rb', line 197 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.
183 184 185 186 187 188 189 190 191 192 193 194 |
# File 'lib/tina4/service_runner.rb', line 183 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
216 217 218 219 220 221 222 223 224 225 226 227 |
# File 'lib/tina4/service_runner.rb', line 216 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 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 |
# 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 # Cooperative shutdown of class-based Service instances — parity with # tina4-python #118 and tina4-nodejs #58 (ead390c76). Must happen # BEFORE thread.join(5) so a subclass's `until should_stop?` loop # observes the flip and returns before the join times out. When # invoked with a name, also fires for a registered-but-not-yet- # started service — `targets` above is @contexts-derived and empty # in that case, so without this the docstring's promise # (register_service wires up #stop) would be honoured only for # started services. Wrapped so one bad user-override #stop cannot # strand siblings. cooperative_names = if name @registry.key?(name.to_s) ? [name.to_s] : [] else targets.keys end cooperative_names.each do |svc_name| entry = @registry[svc_name] next unless entry && entry[:instance].respond_to?(:stop) begin entry[:instance].stop rescue StandardError => e Tina4::Log.error("ServiceRunner: #{svc_name}#stop raised: #{e.}") end 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 |