Class: Tina4::ServiceRunner

Inherits:
Object
  • Object
show all
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

Class Method Details

.clear!Object

Remove all registrations and stop all services. Useful for tests.



142
143
144
145
146
147
148
149
# File 'lib/tina4/service_runner.rb', line 142

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.



53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/tina4/service_runner.rb', line 53

def discover(service_dir = nil)
  service_dir ||= ENV["TINA4_SERVICE_DIR"] || "src/services"
  full_dir = File.expand_path(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.message}")
    end
  end
end

.is_running(name) ⇒ Object

Check if a specific service is currently running.



136
137
138
139
# File 'lib/tina4/service_runner.rb', line 136

def is_running(name)
  ctx = @contexts[name.to_s]
  ctx&.running == true && @threads[name.to_s]&.alive? == true
end

.listObject

List all registered services with their status.



122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/tina4/service_runner.rb', line 122

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

Returns:

  • (Boolean)


155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/tina4/service_runner.rb', line 155

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)

Raises:

  • (ArgumentError)


41
42
43
44
45
46
47
48
49
# File 'lib/tina4/service_runner.rb', line 41

def register(name, handler = nil, options = {}, &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: options }
  end
  Tina4::Log.debug("Service registered: #{name}")
end

.start(name = nil) ⇒ Object

Start all registered services, or a specific one by name.



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/tina4/service_runner.rb', line 71

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.



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/tina4/service_runner.rb', line 95

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