zizq 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +94 -0
- data/bin/profile-worker +145 -0
- data/bin/zizq-worker +174 -0
- data/lib/active_job/queue_adapters/zizq_adapter.rb +109 -0
- data/lib/zizq/ack_processor.rb +132 -0
- data/lib/zizq/active_job_config.rb +122 -0
- data/lib/zizq/backoff.rb +50 -0
- data/lib/zizq/bulk_enqueue.rb +87 -0
- data/lib/zizq/client.rb +982 -0
- data/lib/zizq/configuration.rb +164 -0
- data/lib/zizq/enqueue_request.rb +178 -0
- data/lib/zizq/enqueue_with.rb +109 -0
- data/lib/zizq/error.rb +43 -0
- data/lib/zizq/job.rb +188 -0
- data/lib/zizq/job_config.rb +244 -0
- data/lib/zizq/lifecycle.rb +58 -0
- data/lib/zizq/middleware.rb +79 -0
- data/lib/zizq/query.rb +566 -0
- data/lib/zizq/resources/error_enumerator.rb +241 -0
- data/lib/zizq/resources/error_page.rb +19 -0
- data/lib/zizq/resources/error_record.rb +19 -0
- data/lib/zizq/resources/job.rb +124 -0
- data/lib/zizq/resources/job_page.rb +57 -0
- data/lib/zizq/resources/page.rb +77 -0
- data/lib/zizq/resources/resource.rb +45 -0
- data/lib/zizq/resources.rb +16 -0
- data/lib/zizq/version.rb +9 -0
- data/lib/zizq/worker.rb +467 -0
- data/lib/zizq.rb +269 -0
- data/sig/generated/zizq/ack_processor.rbs +73 -0
- data/sig/generated/zizq/active_job_config.rbs +74 -0
- data/sig/generated/zizq/backoff.rbs +34 -0
- data/sig/generated/zizq/bulk_enqueue.rbs +72 -0
- data/sig/generated/zizq/client.rbs +419 -0
- data/sig/generated/zizq/configuration.rbs +95 -0
- data/sig/generated/zizq/enqueue_request.rbs +94 -0
- data/sig/generated/zizq/enqueue_with.rbs +88 -0
- data/sig/generated/zizq/error.rbs +41 -0
- data/sig/generated/zizq/job.rbs +136 -0
- data/sig/generated/zizq/job_config.rbs +150 -0
- data/sig/generated/zizq/lifecycle.rbs +34 -0
- data/sig/generated/zizq/middleware.rbs +50 -0
- data/sig/generated/zizq/query.rbs +327 -0
- data/sig/generated/zizq/resources/error_enumerator.rbs +148 -0
- data/sig/generated/zizq/resources/error_page.rbs +13 -0
- data/sig/generated/zizq/resources/error_record.rbs +20 -0
- data/sig/generated/zizq/resources/job.rbs +89 -0
- data/sig/generated/zizq/resources/job_page.rbs +33 -0
- data/sig/generated/zizq/resources/page.rbs +47 -0
- data/sig/generated/zizq/resources/resource.rbs +26 -0
- data/sig/generated/zizq/version.rbs +5 -0
- data/sig/generated/zizq/worker.rbs +152 -0
- data/sig/generated/zizq.rbs +180 -0
- data/sig/zizq.rbs +111 -0
- metadata +134 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Copyright (c) 2026 Chris Corbyn <chris@zizq.io>
|
|
2
|
+
# Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
# rbs_inline: enabled
|
|
5
|
+
# frozen_string_literal: true
|
|
6
|
+
|
|
7
|
+
require "digest"
|
|
8
|
+
require "json"
|
|
9
|
+
|
|
10
|
+
module Zizq
|
|
11
|
+
# Shared class-level configuration DSL for Zizq job classes.
|
|
12
|
+
#
|
|
13
|
+
# This module provides the queue, priority, retry, backoff, retention,
|
|
14
|
+
# and uniqueness configuration methods. It is extended onto job classes
|
|
15
|
+
# by `Zizq::Job` and can also be used with ActiveJob via
|
|
16
|
+
# `Zizq::ActiveJobConfig`.
|
|
17
|
+
#
|
|
18
|
+
# Modules including this module must implement `zizq_serialize` and
|
|
19
|
+
# `zizq_deserialize` to define how job arguments are serialized for the API.
|
|
20
|
+
module JobConfig
|
|
21
|
+
# @rbs!
|
|
22
|
+
# # The class name where this is included (invisible to steep without this).
|
|
23
|
+
# def name: () -> String?
|
|
24
|
+
|
|
25
|
+
# Serialize positional and keyword arguments into a JSON-serializable
|
|
26
|
+
# payload.
|
|
27
|
+
#
|
|
28
|
+
# Implemented by the including module.
|
|
29
|
+
def zizq_serialize(*args, **kwargs) #: (*untyped, **untyped) -> untyped
|
|
30
|
+
raise NotImplementedError, "#{self} must implement zizq_serialize"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Deserialize positional and keyword arguments from the serialized payload.
|
|
34
|
+
#
|
|
35
|
+
# Implemented by the including module.
|
|
36
|
+
def zizq_deserialize(payload) #: untyped -> [Array[untyped], Hash[Symbol, untyped]]
|
|
37
|
+
raise NotImplementedError, "#{self} must implement zizq_deserialize"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Generate a jq expression that exactly matches payloads with the given
|
|
41
|
+
# arguments.
|
|
42
|
+
#
|
|
43
|
+
# Implemented by the including module.
|
|
44
|
+
def zizq_payload_filter(*args, **kwargs) #: (*untyped, **untyped) -> String
|
|
45
|
+
raise NotImplementedError, "#{self} must implement zizq_payload_filter"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Generate a jq expression that matches a subset of the given arguments.
|
|
49
|
+
#
|
|
50
|
+
# Implemented by the including module.
|
|
51
|
+
def zizq_payload_subset_filter(*args, **kwargs) #: (*untyped, **untyped) -> String
|
|
52
|
+
raise NotImplementedError, "#{self} must implement zizq_payload_subset_filter"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Declare the default queue for this job class.
|
|
56
|
+
#
|
|
57
|
+
# If not called, defaults to "default". Jobs enqueued for this class will
|
|
58
|
+
# use the specified queue unless explicitly overridden during
|
|
59
|
+
# [`Zizq::enqueue`] or by overriding `::zizq_enqueue_options` on the job
|
|
60
|
+
# class.
|
|
61
|
+
def zizq_queue(name = nil) #: (?String?) -> String
|
|
62
|
+
if name
|
|
63
|
+
@zizq_queue = name
|
|
64
|
+
else
|
|
65
|
+
@zizq_queue || "default"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Declare the default priority for this job class.
|
|
70
|
+
#
|
|
71
|
+
# If not called, defaults to the default priority on the Zizq server.
|
|
72
|
+
# Jobs enqueued for this class will use the specified priority unless
|
|
73
|
+
# explicitly overridden during [`Zizq::enqueue`] or by overriding
|
|
74
|
+
# `::zizq_enqueue_options` on the job class.
|
|
75
|
+
def zizq_priority(priority = nil) #: (?Integer?) -> Integer?
|
|
76
|
+
if priority
|
|
77
|
+
@zizq_priority = priority
|
|
78
|
+
else
|
|
79
|
+
@zizq_priority
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Declare the default retry limit for this job class.
|
|
84
|
+
#
|
|
85
|
+
# The job may fail up to the number of times specified by the retry limit
|
|
86
|
+
# and will exponentially backoff. Once the retry limit is reached, the
|
|
87
|
+
# job is killed and becomes part of the dead set.
|
|
88
|
+
#
|
|
89
|
+
# If not configured, the server's default is used.
|
|
90
|
+
def zizq_retry_limit(limit = nil) #: (?Integer?) -> Integer?
|
|
91
|
+
if limit
|
|
92
|
+
@zizq_retry_limit = limit
|
|
93
|
+
else
|
|
94
|
+
@zizq_retry_limit
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Declare the default backoff configuration for this job class.
|
|
99
|
+
#
|
|
100
|
+
# Times are specified in seconds (optionally fractional).
|
|
101
|
+
# In a Rails app `ActiveSupport::Duration` is supported too.
|
|
102
|
+
#
|
|
103
|
+
# All three parameters must be specified together and are used in the
|
|
104
|
+
# following exponential backoff formula:
|
|
105
|
+
#
|
|
106
|
+
# delay = base + attempts**exponent + rand(0.0..jitter)*attempts
|
|
107
|
+
#
|
|
108
|
+
# Example:
|
|
109
|
+
#
|
|
110
|
+
# zizq_backoff exponent: 4.0, base: 15, jitter: 30
|
|
111
|
+
#
|
|
112
|
+
# If not configured, the server's default backoff policy is used.
|
|
113
|
+
def zizq_backoff(exponent: nil, base: nil, jitter: nil) #: (?exponent: Numeric?, ?base: Numeric?, ?jitter: Numeric?) -> Zizq::backoff?
|
|
114
|
+
if exponent || base || jitter
|
|
115
|
+
unless exponent && base && jitter
|
|
116
|
+
raise ArgumentError, "all of exponent:, base:, jitter: are required"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
@zizq_backoff = { exponent: exponent.to_f, base: base.to_f, jitter: jitter.to_f }
|
|
120
|
+
else
|
|
121
|
+
@zizq_backoff
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Declare the default retention configuration for this job class.
|
|
126
|
+
#
|
|
127
|
+
# Times are specified in seconds (optionally fractional).
|
|
128
|
+
# In a Rails app `ActiveSupport::Duration` is supported too.
|
|
129
|
+
#
|
|
130
|
+
# Both parameters are optional — only the ones provided will be sent
|
|
131
|
+
# to the server. Omitted values use the server's defaults.
|
|
132
|
+
#
|
|
133
|
+
# Example:
|
|
134
|
+
#
|
|
135
|
+
# zizq_retention completed: 0, dead: 7 * 86_400
|
|
136
|
+
#
|
|
137
|
+
# If not configured, the server's default is used.
|
|
138
|
+
def zizq_retention(completed: nil, dead: nil) #: (?completed: Numeric?, ?dead: Numeric?) -> Zizq::retention?
|
|
139
|
+
if completed || dead
|
|
140
|
+
result = {} #: Hash[Symbol, Float]
|
|
141
|
+
|
|
142
|
+
result[:completed] = completed.to_f if completed
|
|
143
|
+
result[:dead] = dead.to_f if dead
|
|
144
|
+
|
|
145
|
+
@zizq_retention = result
|
|
146
|
+
else
|
|
147
|
+
@zizq_retention
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Declare uniqueness for this job class.
|
|
152
|
+
#
|
|
153
|
+
# Requires a pro license.
|
|
154
|
+
#
|
|
155
|
+
# When enabled, duplicate jobs with the same unique key are rejected
|
|
156
|
+
# at enqueue time. The optional scope controls how long the
|
|
157
|
+
# uniqueness guard lasts:
|
|
158
|
+
#
|
|
159
|
+
# :queued — unique while "scheduled" or "ready" (server default)
|
|
160
|
+
# :active — unique while "scheduled", "ready", or "in_flight"
|
|
161
|
+
# :exists — unique until the job is reaped by the server
|
|
162
|
+
#
|
|
163
|
+
# Examples:
|
|
164
|
+
#
|
|
165
|
+
# zizq_unique true # unique, server default scope
|
|
166
|
+
# zizq_unique true, scope: :active # unique while active
|
|
167
|
+
# zizq_unique false # disable (e.g. in a subclass)
|
|
168
|
+
#
|
|
169
|
+
def zizq_unique(unique = nil, scope: nil) #: (?bool?, ?scope: Zizq::unique_scope?) -> bool
|
|
170
|
+
if unique.nil?
|
|
171
|
+
@zizq_unique || false
|
|
172
|
+
else
|
|
173
|
+
@zizq_unique = !!unique
|
|
174
|
+
@zizq_unique_scope = scope
|
|
175
|
+
@zizq_unique
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Declare or read the uniqueness scope for this job class.
|
|
180
|
+
#
|
|
181
|
+
# Usually set via `zizq_unique true, scope: :active` but can also
|
|
182
|
+
# be set independently.
|
|
183
|
+
def zizq_unique_scope(scope = nil) #: (?Zizq::unique_scope?) -> Zizq::unique_scope?
|
|
184
|
+
if scope
|
|
185
|
+
@zizq_unique_scope = scope
|
|
186
|
+
else
|
|
187
|
+
@zizq_unique_scope
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Compute the unique key for a job with the given arguments.
|
|
192
|
+
#
|
|
193
|
+
# The default implementation uses the class name and hashes the
|
|
194
|
+
# normalized serialized payload. Override this method to customize
|
|
195
|
+
# uniqueness — for example, to ignore certain arguments:
|
|
196
|
+
#
|
|
197
|
+
# def self.zizq_unique_key(user_id, template:)
|
|
198
|
+
# super(user_id) # unique per user, ignoring template
|
|
199
|
+
# end
|
|
200
|
+
def zizq_unique_key(*args, **kwargs) #: (*untyped, **untyped) -> String
|
|
201
|
+
payload = normalize_payload(zizq_serialize(*args, **kwargs))
|
|
202
|
+
"#{name}:#{Digest::SHA256.hexdigest(JSON.generate(payload))}"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Build a `Zizq::EnqueueRequest` from the class-level job config.
|
|
206
|
+
#
|
|
207
|
+
# Subclasses can override this to implement dynamic logic such as
|
|
208
|
+
# priority based on arguments:
|
|
209
|
+
#
|
|
210
|
+
# def self.zizq_enqueue_request(user_id, template:)
|
|
211
|
+
# req = super
|
|
212
|
+
# req.priority = 0 if template == "urgent"
|
|
213
|
+
# req
|
|
214
|
+
# end
|
|
215
|
+
def zizq_enqueue_request(*args, **kwargs) #: (*untyped, **untyped) -> EnqueueRequest
|
|
216
|
+
EnqueueRequest.new(
|
|
217
|
+
type: name || raise(ArgumentError, "Cannot enqueue anonymous class"),
|
|
218
|
+
queue: zizq_queue,
|
|
219
|
+
payload: zizq_serialize(*args, **kwargs),
|
|
220
|
+
priority: zizq_priority,
|
|
221
|
+
retry_limit: zizq_retry_limit,
|
|
222
|
+
backoff: zizq_backoff,
|
|
223
|
+
retention: zizq_retention,
|
|
224
|
+
unique_while: zizq_unique ? zizq_unique_scope : nil,
|
|
225
|
+
unique_key: zizq_unique ? zizq_unique_key(*args, **kwargs) : nil
|
|
226
|
+
)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
private
|
|
230
|
+
|
|
231
|
+
# Deep-sort all Hash keys so that serialization is deterministic
|
|
232
|
+
# regardless of insertion order or JSON library.
|
|
233
|
+
def normalize_payload(obj) #: (untyped) -> untyped
|
|
234
|
+
case obj
|
|
235
|
+
when Hash
|
|
236
|
+
obj.sort_by { |k, _| k.to_s }.map { |k, v| [k, normalize_payload(v)] }.to_h
|
|
237
|
+
when Array
|
|
238
|
+
obj.map { |v| normalize_payload(v) }
|
|
239
|
+
else
|
|
240
|
+
obj
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Copyright (c) 2026 Chris Corbyn <chris@zizq.io>
|
|
2
|
+
# Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
# rbs_inline: enabled
|
|
5
|
+
# frozen_string_literal: true
|
|
6
|
+
|
|
7
|
+
module Zizq
|
|
8
|
+
# Thread-safe state machine for coordinating worker shutdown.
|
|
9
|
+
#
|
|
10
|
+
# States:
|
|
11
|
+
# :running → normal operation
|
|
12
|
+
# :draining → stop accepting work, finish in-progress jobs
|
|
13
|
+
# :stopped → all work drained, safe to disconnect
|
|
14
|
+
#
|
|
15
|
+
# Transitions: running -> draining -> stopped (forward only).
|
|
16
|
+
#
|
|
17
|
+
# All transitions are signal-trap safe — they use only atomic symbol
|
|
18
|
+
# assignment and Queue#close for wakeups.
|
|
19
|
+
class Lifecycle
|
|
20
|
+
# @rbs return: void
|
|
21
|
+
def initialize
|
|
22
|
+
@state = :running #: :running | :draining | :stopped
|
|
23
|
+
@drain_latch = Thread::Queue.new
|
|
24
|
+
@stop_latch = Thread::Queue.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Non-blocking, lock-free check.
|
|
28
|
+
def running? #: () -> bool
|
|
29
|
+
@state == :running
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Transition to :draining.
|
|
33
|
+
def drain! #: () -> void
|
|
34
|
+
return unless @state == :running
|
|
35
|
+
|
|
36
|
+
@state = :draining
|
|
37
|
+
@drain_latch.close rescue nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Transition to :stopped.
|
|
41
|
+
def stop! #: () -> void
|
|
42
|
+
return if @state == :stopped
|
|
43
|
+
|
|
44
|
+
@state = :stopped
|
|
45
|
+
@stop_latch.close rescue nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Block until the state is no longer :running.
|
|
49
|
+
def wait_while_running #: () -> void
|
|
50
|
+
@drain_latch.pop
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Block until the state is :stopped.
|
|
54
|
+
def wait_until_stopped #: () -> void
|
|
55
|
+
@stop_latch.pop
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Copyright (c) 2026 Chris Corbyn <chris@zizq.io>
|
|
2
|
+
# Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
# rbs_inline: enabled
|
|
5
|
+
# frozen_string_literal: true
|
|
6
|
+
|
|
7
|
+
module Zizq
|
|
8
|
+
module Middleware
|
|
9
|
+
# A linked chain of middleware ending with a terminal.
|
|
10
|
+
#
|
|
11
|
+
# Each middleware must implement `#call(arg, chain)` where `chain` is
|
|
12
|
+
# the next link. The terminal implements `#call(arg)`.
|
|
13
|
+
#
|
|
14
|
+
# When no middleware is registered, `#call` delegates directly to the
|
|
15
|
+
# terminal with zero overhead.
|
|
16
|
+
#
|
|
17
|
+
# chain = Zizq::Middleware::Chain.new(dispatcher)
|
|
18
|
+
# chain.use(LoggingMiddleware.new)
|
|
19
|
+
# chain.use(MetricsMiddleware.new)
|
|
20
|
+
# chain.call(job)
|
|
21
|
+
# # MetricsMiddleware -> LoggingMiddleware -> dispatcher
|
|
22
|
+
#
|
|
23
|
+
# @rbs generic Arg -- the type flowing through the chain
|
|
24
|
+
# @rbs generic Ret -- the return type of the terminal
|
|
25
|
+
class Chain
|
|
26
|
+
# The terminal callable at the end of the chain.
|
|
27
|
+
attr_reader :terminal #: untyped
|
|
28
|
+
|
|
29
|
+
def initialize(terminal) #: (untyped) -> void
|
|
30
|
+
@terminal = terminal
|
|
31
|
+
@entries = [] #: Array[untyped]
|
|
32
|
+
@built = nil #: untyped?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Replace the terminal, invalidating any built chain.
|
|
36
|
+
def terminal=(terminal) #: (untyped) -> void
|
|
37
|
+
@terminal = terminal
|
|
38
|
+
@built = nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Append a middleware to the chain.
|
|
42
|
+
def use(middleware) #: (untyped) -> void
|
|
43
|
+
@entries << middleware
|
|
44
|
+
@built = nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Execute the chain with the given argument.
|
|
48
|
+
def call(arg) #: (Arg) -> Ret
|
|
49
|
+
build.call(arg)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def build #: () -> untyped
|
|
55
|
+
@built ||= if @entries.empty?
|
|
56
|
+
@terminal
|
|
57
|
+
else
|
|
58
|
+
@entries.reverse.reduce(@terminal) do |next_link, mw|
|
|
59
|
+
Link.new(mw, next_link)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# A single link in the middleware chain, connecting a middleware to
|
|
66
|
+
# the next link (or terminal).
|
|
67
|
+
class Link
|
|
68
|
+
def initialize(middleware, next_link) #: (untyped, untyped) -> void
|
|
69
|
+
@middleware = middleware
|
|
70
|
+
@next_link = next_link
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Invoke this middleware, passing the next link for continuation.
|
|
74
|
+
def call(arg) #: (untyped) -> untyped
|
|
75
|
+
@middleware.call(arg, @next_link)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|