zeridion-flare 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,313 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zeridion
4
+ module Flare
5
+ module Worker
6
+ # The poll loop. Runs register-once → poll → dispatch → ack, and bounds
7
+ # the poll `capacity` to true free slots clamped 1–50. Owns no threads of
8
+ # its own beyond the per-job heartbeat pumps it starts; the bounded job
9
+ # threads live in the Pool. Mirrors the reference SDK's poll-loop service.
10
+ class Runner
11
+ CAPACITY_MIN = 1
12
+ CAPACITY_MAX = 50
13
+
14
+ # @param worker_id [String]
15
+ # @param config [Configuration]
16
+ # @param registry [Registry]
17
+ # @param executor [Executor]
18
+ # @param pool [Pool]
19
+ # @param poll_client [Object] strict client; worker owns it (long read-timeout, no poll-timeout retry)
20
+ # @param heartbeat_client [Object] strict client for heartbeats
21
+ # @param ack_client [Object] strict client for acks (<= 1 retry)
22
+ # @param register_client [Object] strict client for register
23
+ # @param stop_token [CancellationToken] flips on host shutdown / #stop
24
+ # @param logger [Logger, nil]
25
+ def initialize(worker_id:, config:, registry:, executor:, pool:,
26
+ poll_client:, heartbeat_client:, ack_client:, register_client:,
27
+ stop_token:, logger: nil)
28
+ @worker_id = worker_id
29
+ @config = config
30
+ @registry = registry
31
+ @executor = executor
32
+ @pool = pool
33
+ @poll_client = poll_client
34
+ @heartbeat_client = heartbeat_client
35
+ @ack_client = ack_client
36
+ @register_client = register_client
37
+ @stop = stop_token
38
+ @logger = logger
39
+ end
40
+
41
+ # Run the loop on the CURRENT thread until the stop token trips. The
42
+ # caller (Worker) decides whether that is the main thread or a spawned
43
+ # poll thread.
44
+ def run
45
+ if @registry.empty?
46
+ @logger&.warn("worker_starting no job types discovered — worker will not poll")
47
+ return
48
+ end
49
+
50
+ queues = poll_queues
51
+ job_types = @registry.job_types
52
+
53
+ @logger&.info(
54
+ "worker_starting worker_id=#{@worker_id} job_type_count=#{job_types.length} " \
55
+ "queues=#{queues.join(',')}",
56
+ )
57
+
58
+ register(queues, job_types)
59
+ poll_loop(queues, job_types)
60
+ end
61
+
62
+ private
63
+
64
+ def poll_queues
65
+ @config.explicit_queues? ? @config.queues : @registry.queues
66
+ end
67
+
68
+ def register(queues, job_types)
69
+ body = {
70
+ "worker_id" => @worker_id,
71
+ "queues" => queues,
72
+ "job_types" => job_types,
73
+ }
74
+ hostname = @config.hostname || Identity.default_hostname
75
+ body["hostname"] = hostname unless hostname.to_s.empty?
76
+ body["sdk_version"] = ::Zeridion::Flare::VERSION
77
+
78
+ schedules = @registry.recurring_schedules.map(&:to_wire)
79
+ # OMIT recurring_schedules entirely when empty (never send []).
80
+ body["recurring_schedules"] = schedules unless schedules.empty?
81
+
82
+ # Best-effort: a strict client raises, the worker swallows (D4).
83
+ # Timeout::Error is included because Net::ReadTimeout < Timeout::Error
84
+ # (NOT IOError/SystemCallError) — a cold-start register read-timeout
85
+ # must not escape and kill the poll thread before polling begins.
86
+ begin
87
+ @register_client.register_worker(body)
88
+ @logger&.info("worker_registered worker_id=#{@worker_id}")
89
+ rescue ::Zeridion::Flare::FlareError, IOError, SystemCallError, Timeout::Error => e
90
+ @logger&.warn("registration_failed worker_id=#{@worker_id} error=#{e.class}: #{e.message}")
91
+ end
92
+ end
93
+
94
+ def poll_loop(queues, job_types)
95
+ until @stop.cancelled?
96
+ capacity = current_capacity
97
+ if capacity.nil?
98
+ # Saturated: no free slots. Idle the poll interval and re-check.
99
+ @stop.wait(@config.poll_interval_s)
100
+ next
101
+ end
102
+
103
+ jobs = poll_once(queues, job_types, capacity)
104
+ next if jobs.nil? # an error / rate-limit already slept
105
+
106
+ if jobs.empty?
107
+ @stop.wait(@config.poll_interval_s)
108
+ next
109
+ end
110
+
111
+ @logger&.debug("polled_jobs count=#{jobs.length}")
112
+ dispatch(jobs)
113
+ end
114
+
115
+ @logger&.info("worker_stopped worker_id=#{@worker_id}")
116
+ end
117
+
118
+ # @return [Integer, nil] the clamped capacity to poll with, or nil when
119
+ # the pool is fully saturated (poll should be skipped this cycle).
120
+ def current_capacity
121
+ free = @pool.free_slots
122
+ return nil if free <= 0
123
+
124
+ free.clamp(CAPACITY_MIN, CAPACITY_MAX)
125
+ end
126
+
127
+ # @return [Array<Hash>, nil] polled jobs, or nil if the cycle slept due
128
+ # to an error / rate-limit (caller should `next`).
129
+ def poll_once(queues, job_types, capacity)
130
+ body = {
131
+ "worker_id" => @worker_id,
132
+ "queues" => queues,
133
+ "capacity" => capacity,
134
+ "job_types" => job_types,
135
+ }
136
+ response = @poll_client.poll_workers(body)
137
+ jobs = response.is_a?(Hash) ? response["jobs"] : nil
138
+ jobs.is_a?(Array) ? jobs : []
139
+ rescue ::Zeridion::Flare::RateLimitError => e
140
+ sleep_for_rate_limit(e)
141
+ nil
142
+ rescue ::Zeridion::Flare::AuthError => e
143
+ # 401 is fatal-fast on poll: stop spinning (rotation = restart).
144
+ @logger&.error("poll_failed_auth worker_id=#{@worker_id} error=#{e.message} — stopping poll")
145
+ @stop.cancel!
146
+ nil
147
+ rescue StandardError => e
148
+ # Transient / network / 5xx / a long-poll read timeout: log and retry
149
+ # next cycle (the next poll IS the retry — no poll-timeout retry).
150
+ @logger&.error("poll_failed error=#{e.class}: #{e.message} retry_after=#{@config.poll_interval_s}s")
151
+ @stop.wait(@config.poll_interval_s)
152
+ nil
153
+ end
154
+
155
+ def dispatch(jobs)
156
+ jobs.each do |item|
157
+ break if @stop.cancelled?
158
+ # Reserve a slot; stop if we somehow lost the race (shouldn't, since
159
+ # capacity gated the poll, but the server may over-return).
160
+ break unless @pool.try_acquire
161
+
162
+ @pool.submit { process_job(item) }
163
+ end
164
+ end
165
+
166
+ # The full per-job lifecycle: heartbeat pump (immediate beat) → execute
167
+ # → stop pump → ack. Runs on a pool thread.
168
+ def process_job(item)
169
+ job_id = item["id"]
170
+ start_monotonic = monotonic_now
171
+
172
+ cancellation = CancellationToken.new
173
+ # If the host is already shutting down, wire shutdown → this job's
174
+ # cancellation so a freshly-claimed job unwinds cooperatively.
175
+ cancellation.cancel! if @stop.cancelled?
176
+
177
+ progress_slot = ProgressSlot.new
178
+ context = build_context(item, cancellation, progress_slot)
179
+
180
+ @logger&.info(
181
+ "job_executing job_id=#{job_id} job_type=#{item['job_type']} " \
182
+ "attempt=#{item['attempt']} max_attempts=#{item['max_attempts']}",
183
+ )
184
+
185
+ hb_stop = CancellationToken.new
186
+ pump = Heartbeat.new(
187
+ client: @heartbeat_client,
188
+ worker_id: @worker_id,
189
+ job_id: job_id,
190
+ timeout_seconds: job_timeout(item),
191
+ progress_slot: progress_slot,
192
+ cancellation: cancellation,
193
+ stop: hb_stop,
194
+ heartbeat_min_ms: @config.heartbeat_min_ms,
195
+ logger: @logger,
196
+ ).start
197
+
198
+ # Local per-job timeout watchdog: trips the cooperative token (D6 — no
199
+ # thread-kill). A non-yielding handler can't be force-stopped; this
200
+ # bounds well-behaved handlers and lets the rest land on the reaper.
201
+ watchdog = start_timeout_watchdog(item, cancellation)
202
+
203
+ outcome =
204
+ begin
205
+ @executor.execute(item, context)
206
+ ensure
207
+ watchdog&.cancel!
208
+ pump.stop_and_join(timeout: @config.rpc_timeout_s + 1)
209
+ end
210
+
211
+ duration_ms = ((monotonic_now - start_monotonic) * 1000).round
212
+ log_outcome(job_id, outcome, duration_ms)
213
+ ack(job_id, outcome, duration_ms)
214
+ rescue StandardError => e
215
+ @logger&.error("job_unexpected_error job_id=#{item['id']} error=#{e.class}: #{e.message}")
216
+ ack(item["id"], Executor::Outcome.new(succeeded: false,
217
+ error: { "type" => e.class.name, "message" => e.message }), 0)
218
+ end
219
+
220
+ def build_context(item, cancellation, progress_slot)
221
+ JobContext.new(
222
+ job_id: item["id"],
223
+ job_type: item["job_type"],
224
+ attempt: item["attempt"],
225
+ max_attempts: item["max_attempts"],
226
+ enqueued_at: JobContext.parse_timestamp(item["enqueued_at"]),
227
+ logger: @logger,
228
+ cancellation: cancellation,
229
+ progress_slot: progress_slot,
230
+ )
231
+ end
232
+
233
+ def job_timeout(item)
234
+ t = item["timeout_seconds"].to_i
235
+ t.positive? ? t : @config.default_timeout_s
236
+ end
237
+
238
+ # Spawn a watchdog thread that trips the cooperative cancellation token
239
+ # once the job's timeout elapses (unless cancelled first). Returns a
240
+ # CancellationToken used to cancel the watchdog when the job finishes.
241
+ def start_timeout_watchdog(item, cancellation)
242
+ timeout = job_timeout(item)
243
+ stop_watch = CancellationToken.new
244
+ Thread.new do
245
+ # Wake on either the timeout OR the job finishing (stop_watch).
246
+ woke_to_stop = stop_watch.wait(timeout)
247
+ cancellation.cancel! unless woke_to_stop
248
+ end
249
+ stop_watch
250
+ end
251
+
252
+ def log_outcome(job_id, outcome, duration_ms)
253
+ if outcome.succeeded?
254
+ @logger&.info("job_succeeded job_id=#{job_id} duration_ms=#{duration_ms}")
255
+ else
256
+ @logger&.warn("job_failed job_id=#{job_id} error_message=#{outcome.error&.fetch('message', 'unknown')}")
257
+ end
258
+ end
259
+
260
+ # Ack the terminal outcome. Status is "succeeded"/"failed" only (D1 — a
261
+ # cancelled/timed-out job acks "failed", never "cancelled"). At most one
262
+ # retry (D5): rely on the server's at-least-once delivery rather than
263
+ # amplifying with client backoff.
264
+ def ack(job_id, outcome, duration_ms)
265
+ status = outcome.succeeded? ? "succeeded" : "failed"
266
+ body = {
267
+ "job_id" => job_id,
268
+ "worker_id" => @worker_id,
269
+ "status" => status,
270
+ "duration_ms" => duration_ms,
271
+ }
272
+ body["error"] = outcome.error if !outcome.succeeded? && outcome.error && !outcome.error.empty?
273
+
274
+ attempts = 0
275
+ begin
276
+ @ack_client.ack_worker(body)
277
+ rescue ::Zeridion::Flare::FlareError, IOError, SystemCallError, Timeout::Error => e
278
+ attempts += 1
279
+ if attempts <= 1
280
+ retry
281
+ end
282
+ @logger&.error("ack_failed job_id=#{job_id} status=#{status} error=#{e.class}: #{e.message}")
283
+ end
284
+ end
285
+
286
+ # On 429: sleep = Retry-After (delta) if present, else
287
+ # max(0, X-RateLimit-Reset(epoch) − now), clamped to the poll read
288
+ # timeout. Keep heartbeating in-flight jobs (their pumps run on their own
289
+ # threads/clients, untouched by a poll back-off).
290
+ def sleep_for_rate_limit(error)
291
+ seconds = error.retry_after
292
+ # RateLimitError#retry_after carries X-RateLimit-Reset (an epoch). If
293
+ # it looks like an absolute epoch (large), convert to a delta.
294
+ delay =
295
+ if seconds.nil?
296
+ @config.poll_interval_s
297
+ elsif seconds > 1_000_000_000 # epoch seconds, not a delta
298
+ [seconds - Time.now.to_i, 0].max
299
+ else
300
+ [seconds, 0].max
301
+ end
302
+ delay = delay.clamp(0, @config.poll_read_timeout_s)
303
+ @logger&.warn("poll_rate_limited sleep_s=#{delay}")
304
+ @stop.wait(delay)
305
+ end
306
+
307
+ def monotonic_now
308
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
309
+ end
310
+ end
311
+ end
312
+ end
313
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zeridion
4
+ module Flare
5
+ module Worker
6
+ # The background-worker runtime — the Ruby analog of the reference SDK's
7
+ # hosted worker service. One bootstrap object:
8
+ #
9
+ # require "zeridion_flare/worker"
10
+ #
11
+ # class SendWelcomeEmail
12
+ # include Zeridion::Flare::Job
13
+ # flare_options queue: "email", max_attempts: 5, timeout: 120
14
+ # def perform(payload, ctx) = Mailer.welcome(payload["email"]).deliver_now
15
+ # end
16
+ #
17
+ # worker = Zeridion::Flare::Worker.new(concurrency: 5)
18
+ # worker.run # blocks; SIGTERM/SIGINT → graceful drain
19
+ #
20
+ # Lifecycle:
21
+ # #start — spawn the poll loop on a background thread, return immediately.
22
+ # #run — #start, install SIGTERM/SIGINT handlers, then block until a
23
+ # signal arrives, then #stop. (Signals are wired ONLY inside #run.)
24
+ # #stop — idempotent; stop polling and drain in-flight jobs on the
25
+ # CURRENT thread within the shutdown grace, then join everything.
26
+ #
27
+ # The worker OWNS its HTTP clients (decision D5): a poll client with a read
28
+ # timeout > 30 s and no poll-timeout retry, a single-shot heartbeat client,
29
+ # an ack client with at most one retry, and a register client. All four
30
+ # reuse the existing Client transport — the worker does NOT reinvent HTTP.
31
+ class Worker
32
+ # @param api_key [String, nil] falls back to FLARE_API_KEY.
33
+ # @param base_url [String, nil] default https://api.zeridion.com.
34
+ # @param registry [Registry, nil] explicit registry; default = every
35
+ # class that included Zeridion::Flare::Job / RecurringJob so far.
36
+ # @param transport [Object, nil] HTTP transport (for tests / DI); passed
37
+ # through to every Client the worker builds.
38
+ # @param max_response_bytes [Integer] body cap forwarded to each Client.
39
+ # @param config_options see Configuration#initialize (queues:,
40
+ # concurrency:, poll_interval_s:, default_timeout_s:,
41
+ # default_max_attempts:, shutdown_grace_s:, poll_read_timeout_s:,
42
+ # rpc_timeout_s:, hostname:, logger:, heartbeat_min_ms:).
43
+ def initialize(api_key: nil, base_url: nil, registry: nil, transport: nil,
44
+ max_response_bytes: 10 * 1024 * 1024, **config_options)
45
+ @config = Configuration.new(**config_options)
46
+ @registry = registry || Registry.from_declared
47
+ @worker_id = Identity.generate(hostname: @config.hostname)
48
+ @logger = @config.logger
49
+ @stop = CancellationToken.new
50
+ @lifecycle_mutex = Mutex.new
51
+ @started = false
52
+ @stopped = false
53
+ @poll_thread = nil
54
+ @run_latch = CancellationToken.new
55
+
56
+ base_url ||= ::Zeridion::Flare::Client::DEFAULT_BASE_URL
57
+ @poll_client = build_poll_client(api_key, base_url, transport, max_response_bytes)
58
+ @heartbeat_client = build_rpc_client(api_key, base_url, transport, max_response_bytes, max_retries: 0)
59
+ @ack_client = build_rpc_client(api_key, base_url, transport, max_response_bytes, max_retries: 1)
60
+ @register_client = build_rpc_client(api_key, base_url, transport, max_response_bytes, max_retries: 0)
61
+
62
+ @pool = Pool.new(concurrency: @config.concurrency)
63
+ @executor = Executor.new(registry: @registry, logger: @logger)
64
+ @runner = build_runner
65
+ end
66
+
67
+ # @return [String] this worker's id (wrk_{host}_{pid}_{rand}).
68
+ attr_reader :worker_id
69
+
70
+ # @return [Configuration]
71
+ attr_reader :config
72
+
73
+ # Start the pool + poll loop on a background thread. Non-blocking.
74
+ # Idempotent. Returns self.
75
+ def start
76
+ @lifecycle_mutex.synchronize do
77
+ return self if @started
78
+
79
+ @started = true
80
+ @pool.start
81
+ @poll_thread = Thread.new do
82
+ if Thread.current.respond_to?(:name=)
83
+ Thread.current.name = "flare-poll"
84
+ end
85
+ @runner.run
86
+ end
87
+ end
88
+ self
89
+ end
90
+
91
+ # Start, install SIGTERM/SIGINT handlers, and block until a stop signal
92
+ # (or another thread calls #stop). On wake, drain and return. Must run on
93
+ # the main thread (Signal.trap is process-global).
94
+ def run
95
+ start
96
+ install_signal_handlers
97
+ # Block the calling thread until stop is requested.
98
+ @run_latch.wait(Float::INFINITY)
99
+ stop
100
+ ensure
101
+ uninstall_signal_handlers
102
+ end
103
+
104
+ # Idempotent graceful shutdown: stop polling, then drain in-flight jobs
105
+ # on the CURRENT thread within shutdown_grace_s, then join the poll
106
+ # thread + pool. Safe to call from a signal context or another thread.
107
+ def stop
108
+ @lifecycle_mutex.synchronize do
109
+ return if @stopped
110
+
111
+ @stopped = true
112
+ end
113
+
114
+ # 1. Flip the stop token → poll loop stops claiming new jobs and
115
+ # cooperatively cancels freshly-claimed ones.
116
+ @stop.cancel!
117
+ # 2. Wake #run if it is blocking.
118
+ @run_latch.cancel!
119
+ # 3. Let the poll loop notice and exit.
120
+ @poll_thread&.join(@config.poll_read_timeout_s + 5)
121
+ # 4. Drain in-flight jobs (each acks itself) within the grace window.
122
+ @logger&.info("shutdown_waiting in_flight=#{@pool.in_flight}") if @pool.in_flight.positive?
123
+ all_done = @pool.shutdown(timeout: @config.shutdown_grace_s)
124
+ @logger&.warn("shutdown_grace_exceeded — some jobs still running") unless all_done
125
+ nil
126
+ end
127
+
128
+ # @return [Boolean] true once #stop has completed (test convenience).
129
+ def stopped?
130
+ @lifecycle_mutex.synchronize { @stopped }
131
+ end
132
+
133
+ private
134
+
135
+ def build_runner
136
+ Runner.new(
137
+ worker_id: @worker_id,
138
+ config: @config,
139
+ registry: @registry,
140
+ executor: @executor,
141
+ pool: @pool,
142
+ poll_client: @poll_client,
143
+ heartbeat_client: @heartbeat_client,
144
+ ack_client: @ack_client,
145
+ register_client: @register_client,
146
+ stop_token: @stop,
147
+ logger: @logger,
148
+ )
149
+ end
150
+
151
+ # Poll client: long read timeout (> 30 s long-poll), retries disabled so
152
+ # a long-poll read timeout is never retried inside the client (the next
153
+ # loop poll is the retry).
154
+ def build_poll_client(api_key, base_url, transport, max_response_bytes)
155
+ ::Zeridion::Flare::Client.new(
156
+ api_key: api_key,
157
+ base_url: base_url,
158
+ max_retries: 0,
159
+ timeout_seconds: @config.poll_read_timeout_s,
160
+ max_response_bytes: max_response_bytes,
161
+ transport: transport,
162
+ )
163
+ end
164
+
165
+ # Heartbeat / ack / register client: short read timeout, retry budget per
166
+ # caller (heartbeat 0, ack 1, register 0).
167
+ def build_rpc_client(api_key, base_url, transport, max_response_bytes, max_retries:)
168
+ ::Zeridion::Flare::Client.new(
169
+ api_key: api_key,
170
+ base_url: base_url,
171
+ max_retries: max_retries,
172
+ timeout_seconds: @config.rpc_timeout_s,
173
+ max_response_bytes: max_response_bytes,
174
+ transport: transport,
175
+ )
176
+ end
177
+
178
+ def install_signal_handlers
179
+ @previous_handlers = {}
180
+ %w[TERM INT].each do |sig|
181
+ @previous_handlers[sig] = Signal.trap(sig) do
182
+ # A trap handler MUST NOT acquire a Mutex (Ruby raises
183
+ # "can't be called from trap context" — both CancellationTokens use
184
+ # one internally). Hand the actual wake to a fresh thread, which
185
+ # runs OUTSIDE the trap context and may safely lock. The real drain
186
+ # then runs on the main thread once #run unblocks.
187
+ Thread.new do
188
+ @stop.cancel!
189
+ @run_latch.cancel!
190
+ end
191
+ end
192
+ rescue ArgumentError
193
+ # Signal not supported on this platform — skip it.
194
+ nil
195
+ end
196
+ end
197
+
198
+ def uninstall_signal_handlers
199
+ return unless @previous_handlers
200
+
201
+ @previous_handlers.each do |sig, handler|
202
+ Signal.trap(sig, handler) if handler
203
+ rescue ArgumentError
204
+ nil
205
+ end
206
+ @previous_handlers = nil
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Opt-in background-worker runtime for the Zeridion Flare Ruby SDK.
4
+ #
5
+ # require "zeridion_flare/worker"
6
+ #
7
+ # This pulls in the full worker surface on top of the thin client:
8
+ #
9
+ # * Zeridion::Flare::Job — mixin + `flare_options` macro for payload jobs
10
+ # * Zeridion::Flare::RecurringJob — mixin for payloadless cron jobs
11
+ # * Zeridion::Flare::Worker — the runtime (register → poll → dispatch →
12
+ # heartbeat → ack → drain)
13
+ # * Zeridion::Flare::Worker::Configuration
14
+ # * Zeridion::Flare::JobContext — per-execution context (cancellation, progress)
15
+ #
16
+ # The runtime is opt-in by import so thin-client bundles stay lean. It reads the
17
+ # single client version constant and reuses the existing HTTP transport for
18
+ # auth / retry / body-cap — it does NOT reinvent HTTP.
19
+ #
20
+ # Requires Ruby 3.2+ (uses Data.define).
21
+
22
+ require_relative "../zeridion_flare"
23
+
24
+ require_relative "worker/errors"
25
+ require_relative "worker/recurring_schedule"
26
+ require_relative "worker/progress_slot"
27
+ require_relative "worker/cancellation_token"
28
+ require_relative "worker/job_context"
29
+ require_relative "worker/job"
30
+ require_relative "worker/recurring_job"
31
+ require_relative "worker/registry"
32
+ require_relative "worker/job_definition"
33
+ require_relative "worker/identity"
34
+ require_relative "worker/configuration"
35
+ require_relative "worker/executor"
36
+ require_relative "worker/heartbeat"
37
+ require_relative "worker/pool"
38
+ require_relative "worker/runner"
39
+ require_relative "worker/worker"
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "zeridion_flare/version"
4
+ require_relative "zeridion_flare/errors"
5
+ require_relative "zeridion_flare/webhook"
6
+ require_relative "zeridion_flare/client"
7
+
8
+ # Top-level convenience: the module name is `Zeridion::Flare`. Most consumers
9
+ # will write `client = Zeridion::Flare::Client.new(api_key: "...")` or alias
10
+ # the module as needed.
11
+ module Zeridion
12
+ module Flare
13
+ end
14
+ end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: zeridion-flare
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Zeridion
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-07-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: minitest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.20'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.20'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '13.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '13.0'
41
+ description: Synchronous Ruby client for the Zeridion Flare API (background jobs,
42
+ recurring schedules, workers, webhook verification) plus an opt-in background-worker
43
+ runtime (require "zeridion_flare/worker") that polls, dispatches to your job classes,
44
+ heartbeats with progress, honors server cancellation, and drains gracefully on shutdown.
45
+ Zero runtime dependencies — uses Ruby stdlib Net::HTTP only.
46
+ email:
47
+ - support@zeridion.com
48
+ executables: []
49
+ extensions: []
50
+ extra_rdoc_files: []
51
+ files:
52
+ - LICENSE.txt
53
+ - README.md
54
+ - lib/zeridion_flare.rb
55
+ - lib/zeridion_flare/client.rb
56
+ - lib/zeridion_flare/errors.rb
57
+ - lib/zeridion_flare/version.rb
58
+ - lib/zeridion_flare/webhook.rb
59
+ - lib/zeridion_flare/worker.rb
60
+ - lib/zeridion_flare/worker/cancellation_token.rb
61
+ - lib/zeridion_flare/worker/configuration.rb
62
+ - lib/zeridion_flare/worker/errors.rb
63
+ - lib/zeridion_flare/worker/executor.rb
64
+ - lib/zeridion_flare/worker/heartbeat.rb
65
+ - lib/zeridion_flare/worker/identity.rb
66
+ - lib/zeridion_flare/worker/job.rb
67
+ - lib/zeridion_flare/worker/job_context.rb
68
+ - lib/zeridion_flare/worker/job_definition.rb
69
+ - lib/zeridion_flare/worker/pool.rb
70
+ - lib/zeridion_flare/worker/progress_slot.rb
71
+ - lib/zeridion_flare/worker/recurring_job.rb
72
+ - lib/zeridion_flare/worker/recurring_schedule.rb
73
+ - lib/zeridion_flare/worker/registry.rb
74
+ - lib/zeridion_flare/worker/runner.rb
75
+ - lib/zeridion_flare/worker/worker.rb
76
+ homepage: https://docs.zeridion.com/flare
77
+ licenses:
78
+ - MIT
79
+ metadata:
80
+ homepage_uri: https://docs.zeridion.com/flare
81
+ source_code_uri: https://github.com/zeridion/zeridion
82
+ documentation_uri: https://docs.zeridion.com/flare
83
+ rubygems_mfa_required: 'true'
84
+ post_install_message:
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: 3.2.0
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ requirements: []
99
+ rubygems_version: 3.5.22
100
+ signing_key:
101
+ specification_version: 4
102
+ summary: Official Ruby SDK for Zeridion Flare — managed background jobs + worker runtime.
103
+ test_files: []