zizq 0.3.4 → 0.3.6
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 +4 -4
- data/README.md +73 -8
- data/lib/zizq/configuration.rb +11 -0
- data/lib/zizq/router.rb +100 -0
- data/lib/zizq/test/client.rb +332 -0
- data/lib/zizq/test.rb +190 -0
- data/lib/zizq/version.rb +1 -1
- data/lib/zizq.rb +17 -7
- data/sig/generated/zizq/configuration.rbs +10 -0
- data/sig/generated/zizq/router.rbs +81 -0
- data/sig/generated/zizq/test/client.rbs +161 -0
- data/sig/generated/zizq/test.rbs +118 -0
- data/sig/generated/zizq.rbs +4 -0
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a95c96d7b844064db29aea81935d6ec6e3c6953fc47459fcfd6253b6b44aa1e7
|
|
4
|
+
data.tar.gz: 19a8e38ef04716ba02b21bb4ce1f8f2b3e597b1f9845ada71dde5ae7d31fecd3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d4158e9e2b3f58e2c1b823b26f8c81a9ee5ab347d72683377909fed7b4d0d163caf3e27708d3e1f3cbd1af3018068fda34d97405ec6095732b0cda57909a82fc
|
|
7
|
+
data.tar.gz: 95c1453a0af6a82e8ad0abd5e2a8085550ce4cf5ef6b2871cb653fdd1068994f69b36bb04a5e30d2ec76c1ccc02a1e142621a5c5bde7de9bab84517917b90fef
|
data/README.md
CHANGED
|
@@ -12,7 +12,7 @@ API.
|
|
|
12
12
|
## Features
|
|
13
13
|
|
|
14
14
|
* Multi-thread and/or multi-fiber concurrent worker (via [`async`](https://github.com/socketry/async))
|
|
15
|
-
* `Zizq::Job` based job classes, Active Job support, or
|
|
15
|
+
* `Zizq::Job` based job classes, Active Job support, or low-level/custom
|
|
16
16
|
* Enqueue and process jobs from one language to another
|
|
17
17
|
* Arbitrary named queues
|
|
18
18
|
* Granular job priorities
|
|
@@ -22,6 +22,7 @@ API.
|
|
|
22
22
|
* Recurring jobs (cron)
|
|
23
23
|
* Job introspection and management APIs, with support for `jq` query filters
|
|
24
24
|
* Unique jobs
|
|
25
|
+
* Testing helpers
|
|
25
26
|
|
|
26
27
|
## Installation
|
|
27
28
|
|
|
@@ -32,13 +33,13 @@ API.
|
|
|
32
33
|
Add it to your application's `Gemfile`:
|
|
33
34
|
|
|
34
35
|
```ruby
|
|
35
|
-
gem 'zizq', '~> 0.3.
|
|
36
|
+
gem 'zizq', '~> 0.3.6'
|
|
36
37
|
```
|
|
37
38
|
|
|
38
39
|
Or install it manually:
|
|
39
40
|
|
|
40
41
|
```shell
|
|
41
|
-
$ gem install zizq -v 0.3.
|
|
42
|
+
$ gem install zizq -v 0.3.6
|
|
42
43
|
```
|
|
43
44
|
|
|
44
45
|
Ruby **3.2.8 or newer** is required. Client and server share version
|
|
@@ -62,7 +63,7 @@ Zizq.configure do |c|
|
|
|
62
63
|
# Optional worker defaults — applied to every Zizq::Worker
|
|
63
64
|
# instance and to runs of the `zizq-worker` executable. Explicit
|
|
64
65
|
# kwargs or CLI flags override these.
|
|
65
|
-
c.worker.queues
|
|
66
|
+
c.worker.queues = ['emails', 'payments']
|
|
66
67
|
c.worker.fiber_count = 25
|
|
67
68
|
end
|
|
68
69
|
```
|
|
@@ -148,10 +149,42 @@ Zizq.enqueue_bulk do |b|
|
|
|
148
149
|
end
|
|
149
150
|
```
|
|
150
151
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
Jobs can also be enqueued without `Zizq::Job` via `Zizq.enqueue_raw` —
|
|
153
|
+
designed for lower-level code style, and for cross-language workflows where,
|
|
154
|
+
for example, a Ruby app enqueues jobs consumed by a Go service.
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
Zizq.enqueue_raw(
|
|
158
|
+
type: "send_email",
|
|
159
|
+
queue: "comms",
|
|
160
|
+
payload: { user_id: 42, template: "welcome" }
|
|
161
|
+
)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Cross-language and low-level dispatch { #router }
|
|
165
|
+
|
|
166
|
+
When a Ruby app needs to *process* jobs enqueued by another language
|
|
167
|
+
(or by `Zizq.enqueue_raw`), `Zizq::Router` maps `type` strings to
|
|
168
|
+
handler blocks operating on plain JSON payloads:
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
Zizq.configure do |c|
|
|
172
|
+
c.dispatcher = Zizq::Router.new do
|
|
173
|
+
route('send_email') do |payload|
|
|
174
|
+
Mailer.deliver(payload['user_id'], payload['template'])
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Apps that mix the two styles can fall back to Zizq::Job
|
|
178
|
+
# for anything not handled by an explicit route.
|
|
179
|
+
fallback { |job| Zizq::Job.call(job) }
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
See [Custom Dispatchers](https://zizq.io/docs/clients/ruby/dispatchers.html)
|
|
185
|
+
for full details. Dispatchers in Zizq are just objects that implement `#call`
|
|
186
|
+
with a single `Zizq::Resources::Job` argument, and `Zizq::Router` is just a
|
|
187
|
+
dispatcher itself.
|
|
155
188
|
|
|
156
189
|
### Running a worker
|
|
157
190
|
|
|
@@ -221,6 +254,38 @@ Once defined, schedules can be inspected and managed via
|
|
|
221
254
|
`Zizq.crontab('maintenance')` — paused/resumed at the schedule level or per
|
|
222
255
|
entry, and deleted entirely when no longer needed.
|
|
223
256
|
|
|
257
|
+
### Testing
|
|
258
|
+
|
|
259
|
+
Set `c.test_mode = true` in your test helper and Zizq swaps the real
|
|
260
|
+
client out for an in-memory `Zizq::Test::Client` that buffers enqueues
|
|
261
|
+
instead of dispatching them. Tests can then assert on what was
|
|
262
|
+
enqueued and drain the buffer through the configured dispatcher —
|
|
263
|
+
no running server required.
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
# test/test_helper.rb (or spec/spec_helper.rb)
|
|
267
|
+
Zizq::Test.enable!
|
|
268
|
+
|
|
269
|
+
class ActiveSupport::TestCase
|
|
270
|
+
setup { Zizq::Test.reset! }
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# In a test
|
|
274
|
+
def test_signup_fans_out
|
|
275
|
+
SignupService.new.run
|
|
276
|
+
|
|
277
|
+
assert Zizq::Test.enqueued?(SendWelcomeEmailJob, user_id: 42)
|
|
278
|
+
assert_equal 2, Zizq::Test.pending_jobs(only_queues: 'emails').size
|
|
279
|
+
|
|
280
|
+
# Drain the buffer through Zizq.configuration.dequeue_middleware
|
|
281
|
+
# (same path the real worker takes — registered middleware runs too).
|
|
282
|
+
Zizq::Test.dispatch_enqueued_jobs
|
|
283
|
+
end
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
See [Testing](https://zizq.io/docs/clients/ruby/testing.html) for
|
|
287
|
+
full details.
|
|
288
|
+
|
|
224
289
|
## Resources
|
|
225
290
|
|
|
226
291
|
* [Ruby Client Docs](https://zizq.io/docs/clients/ruby/)
|
data/lib/zizq/configuration.rb
CHANGED
|
@@ -59,6 +59,16 @@ module Zizq
|
|
|
59
59
|
# a `Resources::Job` and a chain to continue.
|
|
60
60
|
attr_reader :dequeue_middleware #: Middleware::Chain[Resources::Job, void]
|
|
61
61
|
|
|
62
|
+
# When truthy, `Zizq.client` lazily resolves to a
|
|
63
|
+
# `Zizq::Test::Client` that buffers enqueues in memory rather than
|
|
64
|
+
# dispatching to a real server. Useful inside test suites — set it
|
|
65
|
+
# once in your test helper and the rest of the app's code uses
|
|
66
|
+
# `Zizq.enqueue` / `Zizq.enqueue_bulk` unchanged. Read operations
|
|
67
|
+
# (`Zizq.query`, `Zizq.queues`, `Client#get_job`, etc.) raise
|
|
68
|
+
# rather than silently returning empty results, so missing test
|
|
69
|
+
# setup is obvious.
|
|
70
|
+
attr_accessor :test_mode #: bool
|
|
71
|
+
|
|
62
72
|
def initialize #: () -> void
|
|
63
73
|
@url = "http://localhost:7890"
|
|
64
74
|
@format = :msgpack
|
|
@@ -67,6 +77,7 @@ module Zizq
|
|
|
67
77
|
@worker = nil
|
|
68
78
|
@read_timeout = 30
|
|
69
79
|
@stream_idle_timeout = 30
|
|
80
|
+
@test_mode = false
|
|
70
81
|
@enqueue_middleware = Middleware::Chain.new(Identity.new)
|
|
71
82
|
@dequeue_middleware = Middleware::Chain.new(Zizq::Job)
|
|
72
83
|
end
|
data/lib/zizq/router.rb
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
# Dispatch jobs by `type` string, mapping each to a handler block.
|
|
9
|
+
#
|
|
10
|
+
# Designed for cross-language workflows: payloads are plain JSON
|
|
11
|
+
# values (Hashes / Arrays / strings / numbers), `type` is a String
|
|
12
|
+
# the producer agrees on with the consumer, and routes are
|
|
13
|
+
# registered explicitly — no `Zizq::Job` mixin involved.
|
|
14
|
+
#
|
|
15
|
+
# Zizq.configure do |c|
|
|
16
|
+
# c.dispatcher = Zizq::Router.new do
|
|
17
|
+
# route("send_email") do |payload|
|
|
18
|
+
# Mailer.deliver(payload["user_id"], payload["template"])
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# route("expire_tokens") do
|
|
22
|
+
# TokenSweeper.run
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# route("generate_report") do |payload, job|
|
|
26
|
+
# Reports.generate(payload["id"], attempts: job.attempts)
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# # `def` inside the block defines singleton methods on the
|
|
30
|
+
# # router. Route blocks captured *inside* the constructor
|
|
31
|
+
# # have lexical `self == router`, so they can call these
|
|
32
|
+
# # helpers; routes added outside (`router.route("…") { … }`)
|
|
33
|
+
# # keep their own lexical `self` and would need to go through
|
|
34
|
+
# # the router explicitly (`router.logger`).
|
|
35
|
+
# def logger
|
|
36
|
+
# Zizq.configuration.logger
|
|
37
|
+
# end
|
|
38
|
+
#
|
|
39
|
+
# # Anything else falls back. A common pattern is delegating
|
|
40
|
+
# # to `Zizq::Job` for the apps that mix the two styles.
|
|
41
|
+
# fallback { |job| Zizq::Job.call(job) }
|
|
42
|
+
# end
|
|
43
|
+
# end
|
|
44
|
+
#
|
|
45
|
+
# Routes can also be registered outside the constructor block:
|
|
46
|
+
#
|
|
47
|
+
# router = Zizq::Router.new
|
|
48
|
+
# router.route("send_email") { |payload| ... }
|
|
49
|
+
#
|
|
50
|
+
# Handlers are called as `handler.call(payload, job)`. Block-arity
|
|
51
|
+
# rules let `{ |payload| }` or `{ }` ignore either argument.
|
|
52
|
+
# Strict-arity lambdas need to declare both.
|
|
53
|
+
class Router
|
|
54
|
+
# Raised when a job arrives with a type that has no registered
|
|
55
|
+
# route and no fallback. Caught by Zizq's normal worker error
|
|
56
|
+
# path, which nacks the job for retry (or dead-letters it once
|
|
57
|
+
# the retry limit is hit).
|
|
58
|
+
class UnknownJobType < Zizq::Error; end
|
|
59
|
+
|
|
60
|
+
# @rbs &block: ?(self) [self: Router] -> void
|
|
61
|
+
def initialize(&block)
|
|
62
|
+
@routes = {} #: Hash[String, ^(untyped, Resources::Job) -> void]
|
|
63
|
+
@fallback = nil #: (^(Resources::Job) -> void)?
|
|
64
|
+
instance_eval(&block) if block
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Register `handler` for jobs whose `type` matches.
|
|
68
|
+
#
|
|
69
|
+
# @rbs type: String | Symbol
|
|
70
|
+
# @rbs &handler: (untyped, Resources::Job) -> void
|
|
71
|
+
def route(type, &handler)
|
|
72
|
+
@routes[type.to_s] = handler
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Register a fallback handler invoked when no route matches.
|
|
76
|
+
# Receives the full `Resources::Job` (not split into payload /
|
|
77
|
+
# job pair), since the canonical use is delegation to another
|
|
78
|
+
# dispatcher:
|
|
79
|
+
#
|
|
80
|
+
# fallback { |job| Zizq::Job.call(job) }
|
|
81
|
+
#
|
|
82
|
+
# @rbs &handler: (Resources::Job) -> void
|
|
83
|
+
def fallback(&handler)
|
|
84
|
+
@fallback = handler
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Dispatch a single job. Looks up the handler by `job.type`,
|
|
88
|
+
# falls back to the registered fallback if any, otherwise
|
|
89
|
+
# raises `UnknownJobType`.
|
|
90
|
+
def call(job) #: (Resources::Job) -> void
|
|
91
|
+
handler = @routes[job.type]
|
|
92
|
+
|
|
93
|
+
return handler.call(job.payload, job) if handler
|
|
94
|
+
return @fallback.call(job) if @fallback
|
|
95
|
+
|
|
96
|
+
raise UnknownJobType,
|
|
97
|
+
"no handler registered for job type #{job.type.inspect}"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,332 @@
|
|
|
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 Test
|
|
9
|
+
# A `Zizq::Client` stand-in for use in test suites.
|
|
10
|
+
#
|
|
11
|
+
# Buffers `enqueue` / `enqueue_bulk` calls in memory and returns
|
|
12
|
+
# synthetic `Resources::Job` instances (with generated ids) so that
|
|
13
|
+
# callers depending on the regular client's return contract don't
|
|
14
|
+
# need to special-case test mode.
|
|
15
|
+
#
|
|
16
|
+
# Read operations (`get_queues`, `list_jobs`, `count_jobs`, …) are
|
|
17
|
+
# explicitly not supported in test mode and raise `NotSupported`.
|
|
18
|
+
# Tests that need those should either run against a real server or
|
|
19
|
+
# stub at a higher level.
|
|
20
|
+
#
|
|
21
|
+
# Activated indirectly via `Zizq.configuration.test_mode = true` —
|
|
22
|
+
# `Zizq.client` then lazily builds a `Test::Client` instead of a
|
|
23
|
+
# real `Client`.
|
|
24
|
+
class Client < Zizq::Client
|
|
25
|
+
# Raised when test-mode code reaches an operation that isn't
|
|
26
|
+
# supported (queries, queue listing, worker streams, etc.).
|
|
27
|
+
class NotSupported < Zizq::Error; end
|
|
28
|
+
|
|
29
|
+
# Length of a real scru128 id in its base-32 representation.
|
|
30
|
+
# Synthetic test ids are sized to match (`test` prefix + zero
|
|
31
|
+
# padded counter) so they fit anywhere a real id would.
|
|
32
|
+
ID_LENGTH = 25
|
|
33
|
+
ID_PREFIX = "test"
|
|
34
|
+
|
|
35
|
+
# The canonical Zizq lifecycle states. We mirror these so the
|
|
36
|
+
# `status` on a buffered job reflects what the real server would
|
|
37
|
+
# report. Test mode never retries — `in_flight` only ever
|
|
38
|
+
# transitions to `completed` or `dead`.
|
|
39
|
+
STATUS_SCHEDULED = "scheduled"
|
|
40
|
+
STATUS_READY = "ready"
|
|
41
|
+
STATUS_IN_FLIGHT = "in_flight"
|
|
42
|
+
STATUS_COMPLETED = "completed"
|
|
43
|
+
STATUS_DEAD = "dead"
|
|
44
|
+
|
|
45
|
+
# Default `filter:` lambda — passes every job. Named so the
|
|
46
|
+
# filter pipeline is always callable without nil-checking.
|
|
47
|
+
PASS_ALL_FILTER = ->(_job) { true } #: ^(Resources::Job) -> bool
|
|
48
|
+
|
|
49
|
+
# Paired view of a single enqueue: the original `EnqueueRequest`
|
|
50
|
+
# (with full submission metadata — `unique_key`, `unique_while`,
|
|
51
|
+
# retry config, etc.), the synthetic `Resources::Job` returned
|
|
52
|
+
# to callers, and the data hash that backs the job (so we can
|
|
53
|
+
# mutate its status through the lifecycle).
|
|
54
|
+
Entry = Struct.new(:request, :job, :data, keyword_init: true)
|
|
55
|
+
|
|
56
|
+
def initialize #: () -> void
|
|
57
|
+
# Skip the parent's HTTP setup — we don't open connections in
|
|
58
|
+
# test mode. The parent's @http and friends stay nil; methods
|
|
59
|
+
# that would touch them are overridden below.
|
|
60
|
+
|
|
61
|
+
@entries = [] #: Array[Entry]
|
|
62
|
+
@mutex = Mutex.new
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# All buffered jobs, in submission order, optionally filtered.
|
|
66
|
+
# See `apply_filters` for the filter kwargs.
|
|
67
|
+
def enqueued_jobs(**filters) #: (**untyped) -> Array[Resources::Job]
|
|
68
|
+
@mutex.synchronize { apply_filters(@entries, **filters).map(&:job) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Original `EnqueueRequest`s in submission order. Useful when a
|
|
72
|
+
# test needs metadata that doesn't survive onto `Resources::Job`
|
|
73
|
+
# (`unique_key`, `unique_while`, `delay` before `ready_at`
|
|
74
|
+
# resolution, etc.). Same filter kwargs as `enqueued_jobs`.
|
|
75
|
+
def enqueued_requests(**filters) #: (**untyped) -> Array[EnqueueRequest]
|
|
76
|
+
@mutex.synchronize { apply_filters(@entries, **filters).map(&:request) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Jobs awaiting dispatch — `ready` and `scheduled` entries.
|
|
80
|
+
# `pending_jobs` is the set `drain` would attempt to run on its
|
|
81
|
+
# next call (modulo `ready_at` for `scheduled` entries).
|
|
82
|
+
def pending_jobs(**filters) #: (**untyped) -> Array[Resources::Job]
|
|
83
|
+
filter_by_status([STATUS_READY, STATUS_SCHEDULED], **filters)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def in_flight_jobs(**filters) #: (**untyped) -> Array[Resources::Job]
|
|
87
|
+
filter_by_status([STATUS_IN_FLIGHT], **filters)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def completed_jobs(**filters) #: (**untyped) -> Array[Resources::Job]
|
|
91
|
+
filter_by_status([STATUS_COMPLETED], **filters)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def dead_jobs(**filters) #: (**untyped) -> Array[Resources::Job]
|
|
95
|
+
filter_by_status([STATUS_DEAD], **filters)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Reset the buffer. Called between tests via `Zizq::Test.reset!`.
|
|
99
|
+
def clear! #: () -> void
|
|
100
|
+
@mutex.synchronize { @entries.clear }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def close #: () -> void
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Dispatch every runnable entry (status `ready`, or `scheduled`
|
|
107
|
+
# with `ready_at` already elapsed) through the configured
|
|
108
|
+
# dequeue middleware chain (same path the real worker uses, so
|
|
109
|
+
# any registered middlewares run in tests too), looping until no
|
|
110
|
+
# more match the filters. Re-enqueues during dispatch fall
|
|
111
|
+
# through the loop naturally — they get drained too unless they
|
|
112
|
+
# fall outside the filter set.
|
|
113
|
+
#
|
|
114
|
+
# The per-iteration snapshot is taken under the mutex and marked
|
|
115
|
+
# `in_flight` atomically. Dispatch happens outside the mutex so
|
|
116
|
+
# handlers can re-enter the client without deadlocking. On
|
|
117
|
+
# success the entry moves to `completed`; on a raised exception
|
|
118
|
+
# it moves to `dead` and the exception re-raises (matching
|
|
119
|
+
# ActiveJob's `perform_enqueued_jobs` + Sidekiq's `drain`).
|
|
120
|
+
def drain(**filters) #: (**untyped) -> Integer
|
|
121
|
+
total = 0
|
|
122
|
+
loop do
|
|
123
|
+
snapshot = take_runnable_snapshot(**filters)
|
|
124
|
+
break if snapshot.empty?
|
|
125
|
+
|
|
126
|
+
snapshot.each { |entry| dispatch_entry(entry) }
|
|
127
|
+
total += snapshot.size
|
|
128
|
+
end
|
|
129
|
+
total
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# @rbs override
|
|
133
|
+
def enqueue(queue:,
|
|
134
|
+
type:,
|
|
135
|
+
payload:,
|
|
136
|
+
priority: nil,
|
|
137
|
+
ready_at: nil,
|
|
138
|
+
retry_limit: nil,
|
|
139
|
+
backoff: nil,
|
|
140
|
+
retention: nil,
|
|
141
|
+
unique_key: nil,
|
|
142
|
+
unique_while: nil)
|
|
143
|
+
req = EnqueueRequest.new(
|
|
144
|
+
queue:,
|
|
145
|
+
type:,
|
|
146
|
+
payload:,
|
|
147
|
+
priority:,
|
|
148
|
+
ready_at:,
|
|
149
|
+
retry_limit:,
|
|
150
|
+
backoff:,
|
|
151
|
+
retention:,
|
|
152
|
+
unique_key:,
|
|
153
|
+
unique_while:,
|
|
154
|
+
)
|
|
155
|
+
@mutex.synchronize { record_unsynchronized(req) }.job
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# @rbs override
|
|
159
|
+
def enqueue_bulk(jobs:)
|
|
160
|
+
@mutex.synchronize do
|
|
161
|
+
jobs.map do |params|
|
|
162
|
+
req = EnqueueRequest.new(
|
|
163
|
+
queue: params[:queue],
|
|
164
|
+
type: params[:type],
|
|
165
|
+
payload: params[:payload],
|
|
166
|
+
priority: params[:priority],
|
|
167
|
+
ready_at: params[:ready_at],
|
|
168
|
+
retry_limit: params[:retry_limit],
|
|
169
|
+
backoff: params[:backoff],
|
|
170
|
+
retention: params[:retention],
|
|
171
|
+
unique_key: params[:unique_key],
|
|
172
|
+
unique_while: params[:unique_while],
|
|
173
|
+
)
|
|
174
|
+
record_unsynchronized(req).job
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# All read / mutation / streaming operations are deliberately
|
|
180
|
+
# unimplemented.
|
|
181
|
+
#
|
|
182
|
+
# Raising loudly beats silently returning empty results that
|
|
183
|
+
# hide missing test setup.
|
|
184
|
+
%i[
|
|
185
|
+
get_queues
|
|
186
|
+
list_jobs
|
|
187
|
+
count_jobs
|
|
188
|
+
get_job
|
|
189
|
+
delete_job
|
|
190
|
+
delete_all_jobs
|
|
191
|
+
update_job
|
|
192
|
+
update_all_jobs
|
|
193
|
+
take_jobs
|
|
194
|
+
get_error
|
|
195
|
+
list_errors
|
|
196
|
+
health
|
|
197
|
+
server_version
|
|
198
|
+
].each do |method_name|
|
|
199
|
+
define_method(method_name) do |*, **, &_|
|
|
200
|
+
Kernel.raise(
|
|
201
|
+
NotSupported,
|
|
202
|
+
"Zizq::Test::Client##{method_name} is not supported in test mode. " \
|
|
203
|
+
"Test mode buffers enqueues only — point at a real server, or stub the call."
|
|
204
|
+
)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
private
|
|
209
|
+
|
|
210
|
+
def record_unsynchronized(req) #: (EnqueueRequest) -> Entry
|
|
211
|
+
# Serialized format: ready_at is integer milliseconds.
|
|
212
|
+
# `req.ready_at` (from `to_enqueue_params`) is fractional
|
|
213
|
+
# seconds; convert here so `Resources::Job#ready_at`'s
|
|
214
|
+
# ms -> seconds round-trip produces the same value the
|
|
215
|
+
# caller passed in. When the client omits ready_at the server
|
|
216
|
+
# assigns `now`, so we do the same.
|
|
217
|
+
now_ms = (Time.now.to_f * 1000).to_i
|
|
218
|
+
ready_at_ms = req.ready_at ? (req.ready_at.to_f * 1000).to_i : now_ms
|
|
219
|
+
|
|
220
|
+
data = {
|
|
221
|
+
"id" => synthetic_id(@entries.size + 1),
|
|
222
|
+
"queue" => req.queue,
|
|
223
|
+
"type" => req.type,
|
|
224
|
+
"payload" => req.payload,
|
|
225
|
+
"priority" => req.priority,
|
|
226
|
+
"ready_at" => ready_at_ms,
|
|
227
|
+
"retry_limit" => req.retry_limit,
|
|
228
|
+
"status" => ready_at_ms > now_ms ? STATUS_SCHEDULED : STATUS_READY,
|
|
229
|
+
}
|
|
230
|
+
entry = Entry.new(request: req, job: Resources::Job.new(self, data), data: data)
|
|
231
|
+
@entries << entry
|
|
232
|
+
entry
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def synthetic_id(counter) #: (Integer) -> String
|
|
236
|
+
"#{ID_PREFIX}#{counter.to_s.rjust(ID_LENGTH - ID_PREFIX.length, '0')}"
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Returns entries matching every named filter AND the predicate.
|
|
240
|
+
# All filter kwargs are optional; unset means "don't filter on
|
|
241
|
+
# this axis." Callers must hold `@mutex` — public accessors do
|
|
242
|
+
# so via `synchronize` before calling.
|
|
243
|
+
#
|
|
244
|
+
# * `only_queues:` / `except_queues:` — String, Array of Strings.
|
|
245
|
+
# * `only_types:` / `except_types:` — String, Class, or Array
|
|
246
|
+
# of those. Class names are matched against the wire-format
|
|
247
|
+
# `type` string via `.to_s`.
|
|
248
|
+
# * `filter:` — a lambda receiving a `Resources::Job`, returning
|
|
249
|
+
# truthy to keep. Defaults to `PASS_ALL_FILTER`.
|
|
250
|
+
#
|
|
251
|
+
# `only_*` and `except_*` AND together with the predicate.
|
|
252
|
+
def apply_filters(entries,
|
|
253
|
+
only_queues: nil,
|
|
254
|
+
except_queues: nil,
|
|
255
|
+
only_types: nil,
|
|
256
|
+
except_types: nil,
|
|
257
|
+
filter: PASS_ALL_FILTER) #: (Array[Entry], **untyped) -> Array[Entry]
|
|
258
|
+
only_queues = normalize_filter(only_queues)
|
|
259
|
+
except_queues = normalize_filter(except_queues)
|
|
260
|
+
only_types = normalize_filter(only_types)
|
|
261
|
+
except_types = normalize_filter(except_types)
|
|
262
|
+
|
|
263
|
+
entries.select do |entry|
|
|
264
|
+
queue = entry.data["queue"] #: String
|
|
265
|
+
type = entry.data["type"] #: String
|
|
266
|
+
|
|
267
|
+
(only_queues.empty? || only_queues.include?(queue)) &&
|
|
268
|
+
(except_queues.empty? || !except_queues.include?(queue)) &&
|
|
269
|
+
(only_types.empty? || only_types.include?(type)) &&
|
|
270
|
+
(except_types.empty? || !except_types.include?(type)) &&
|
|
271
|
+
filter.call(entry.job)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def filter_by_status(statuses, **filters) #: (Array[String], **untyped) -> Array[Resources::Job]
|
|
276
|
+
@mutex.synchronize do
|
|
277
|
+
apply_filters(@entries, **filters)
|
|
278
|
+
.select { |e| statuses.include?(e.data["status"]) }
|
|
279
|
+
.map(&:job)
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Lock, find runnable entries matching the filters, flip each
|
|
284
|
+
# to in_flight, return the snapshot. Holding the mutex through
|
|
285
|
+
# this is fine because we don't call user code — dispatch
|
|
286
|
+
# happens outside.
|
|
287
|
+
def take_runnable_snapshot(**filters) #: (**untyped) -> Array[Entry]
|
|
288
|
+
now_ms = (Time.now.to_f * 1000).to_i
|
|
289
|
+
@mutex.synchronize do
|
|
290
|
+
apply_filters(@entries, **filters)
|
|
291
|
+
.select { |e| runnable?(e, now_ms) }
|
|
292
|
+
.each { |entry| entry.data["status"] = STATUS_IN_FLIGHT }
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def runnable?(entry, now_ms) #: (Entry, Integer) -> bool
|
|
297
|
+
case entry.data["status"]
|
|
298
|
+
when STATUS_READY then true
|
|
299
|
+
when STATUS_SCHEDULED then entry.data["ready_at"] <= now_ms
|
|
300
|
+
else false
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Accept a Class, String, or Array of those; emit an Array of
|
|
305
|
+
# Strings. Class names match the API's `type` string.
|
|
306
|
+
def normalize_filter(value) #: ((String | Class | Array[String | Class])?) -> Array[String]
|
|
307
|
+
case value
|
|
308
|
+
when nil then []
|
|
309
|
+
when Array then value.map { |x| x.to_s }
|
|
310
|
+
else [value.to_s]
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Mark the entry in_flight, dispatch through the full dequeue
|
|
315
|
+
# middleware chain (same path the real worker uses, so any
|
|
316
|
+
# registered middlewares run in tests too), settle to
|
|
317
|
+
# completed or dead. A raised exception re-raises after
|
|
318
|
+
# recording — same observable behaviour as Rails'
|
|
319
|
+
# `perform_enqueued_jobs`.
|
|
320
|
+
def dispatch_entry(entry) #: (Entry) -> void
|
|
321
|
+
entry.data["status"] = STATUS_IN_FLIGHT
|
|
322
|
+
begin
|
|
323
|
+
Zizq.configuration.dequeue_middleware.call(entry.job)
|
|
324
|
+
entry.data["status"] = STATUS_COMPLETED
|
|
325
|
+
rescue
|
|
326
|
+
entry.data["status"] = STATUS_DEAD
|
|
327
|
+
raise
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
data/lib/zizq/test.rb
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
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
|
+
# Test-mode helpers. Activated by setting `c.test_mode = true` in a
|
|
9
|
+
# `Zizq.configure` block; `Zizq.client` then lazily resolves to a
|
|
10
|
+
# `Zizq::Test::Client` that buffers enqueues instead of dispatching.
|
|
11
|
+
#
|
|
12
|
+
# Typical use in a test helper:
|
|
13
|
+
#
|
|
14
|
+
# Zizq.configure do |c|
|
|
15
|
+
# c.test_mode = true
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# class MyTestCase
|
|
19
|
+
# setup { Zizq::Test.reset! }
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# In a test:
|
|
23
|
+
#
|
|
24
|
+
# def test_signup_fans_out
|
|
25
|
+
# # Default buffered mode — assert what was enqueued.
|
|
26
|
+
# SignupService.new.run
|
|
27
|
+
# assert_equal 2, Zizq::Test.client.pending_jobs.size
|
|
28
|
+
#
|
|
29
|
+
# # Drain whatever's pending (handler re-enqueues fall through
|
|
30
|
+
# # naturally — drain loops until pending is empty).
|
|
31
|
+
# Zizq::Test.dispatch_enqueued_jobs
|
|
32
|
+
#
|
|
33
|
+
# # Block form: run the work, then drain. Matches ActiveJob's
|
|
34
|
+
# # `perform_enqueued_jobs do ... end`.
|
|
35
|
+
# Zizq::Test.dispatch_enqueued_jobs { SignupService.new.run }
|
|
36
|
+
#
|
|
37
|
+
# # Filter by queue and/or type when only a subset should fire.
|
|
38
|
+
# Zizq::Test.dispatch_enqueued_jobs(queue: "emails")
|
|
39
|
+
# Zizq::Test.dispatch_enqueued_jobs(type: SendEmailJob)
|
|
40
|
+
# end
|
|
41
|
+
module Test
|
|
42
|
+
autoload :Client, "zizq/test/client"
|
|
43
|
+
|
|
44
|
+
# Switch Zizq into test mode. After this, `Zizq.client` resolves
|
|
45
|
+
# to a `Zizq::Test::Client` that buffers enqueues in memory.
|
|
46
|
+
# Typically called once in a test helper.
|
|
47
|
+
def self.enable! #: () -> void
|
|
48
|
+
Zizq.configure { |c| c.test_mode = true }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Switch back to the real client. The buffered state is dropped
|
|
52
|
+
# along with the test client (the next `Zizq.client` access
|
|
53
|
+
# builds a fresh `Zizq::Client`).
|
|
54
|
+
def self.disable! #: () -> void
|
|
55
|
+
Zizq.configure { |c| c.test_mode = false }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The active test client. Raises if test mode is not enabled —
|
|
59
|
+
# better to fail loudly than return a stale or wrong client.
|
|
60
|
+
def self.client #: () -> Client
|
|
61
|
+
unless Zizq.configuration.test_mode
|
|
62
|
+
raise Client::NotSupported,
|
|
63
|
+
"Zizq.configuration.test_mode is not enabled; Zizq::Test.client has nothing to manage."
|
|
64
|
+
end
|
|
65
|
+
Zizq.client #: Client
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Reset buffered state between tests. Keeps the configured
|
|
69
|
+
# `test_mode` flag.
|
|
70
|
+
def self.reset! #: () -> void
|
|
71
|
+
client.clear!
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Dispatch pending jobs through the configured dequeue middleware
|
|
75
|
+
# chain (`Zizq.configuration.dequeue_middleware` — same path the
|
|
76
|
+
# real worker uses, so any registered middlewares run in tests
|
|
77
|
+
# too), looping until no more pending entries match the filters.
|
|
78
|
+
#
|
|
79
|
+
# * No block: drain whatever's pending now.
|
|
80
|
+
# * With block: yield first (test code enqueues), then drain.
|
|
81
|
+
# * `only_queues:` / `except_queues:` — String or Array of Strings.
|
|
82
|
+
# * `only_types:` / `except_types:` — String, Class, or Array of
|
|
83
|
+
# those (Class names match the serialized-format `type` via `.to_s`,
|
|
84
|
+
# so passing an ActiveJob class works directly).
|
|
85
|
+
# * `filter:` — a lambda `->(job)` returning truthy to keep an
|
|
86
|
+
# entry. Defaults to "pass all". Combines with the named
|
|
87
|
+
# filters via AND.
|
|
88
|
+
#
|
|
89
|
+
# Returns the number of jobs dispatched. A block exception
|
|
90
|
+
# propagates without draining; a handler exception during drain
|
|
91
|
+
# transitions that entry to `dead` and re-raises.
|
|
92
|
+
def self.dispatch_enqueued_jobs(**filters, &block) #: (**untyped) ?{ () -> void } -> Integer
|
|
93
|
+
yield if block
|
|
94
|
+
client.drain(**filters)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Convenience proxies onto the active test client, so test code
|
|
98
|
+
# can read `Zizq::Test.pending_jobs(only_queues: "emails")`
|
|
99
|
+
# instead of routing through `Zizq::Test.client.pending_jobs(...)`.
|
|
100
|
+
# Each forwards `**filters` to the corresponding Client accessor;
|
|
101
|
+
# see `Test::Client` for the supported filter kwargs.
|
|
102
|
+
#
|
|
103
|
+
# All accept the same filter kwargs as `#dispatch_enqueued_jobs`.
|
|
104
|
+
%i[
|
|
105
|
+
enqueued_jobs
|
|
106
|
+
enqueued_requests
|
|
107
|
+
pending_jobs
|
|
108
|
+
in_flight_jobs
|
|
109
|
+
completed_jobs
|
|
110
|
+
dead_jobs
|
|
111
|
+
].each do |name|
|
|
112
|
+
define_singleton_method(name) do |**filters|
|
|
113
|
+
client.public_send(name, **filters)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Was a job of `job_class` enqueued (optionally with matching
|
|
118
|
+
# args)?
|
|
119
|
+
#
|
|
120
|
+
# Zizq::Test.enqueued?(SendEmailJob) # any args
|
|
121
|
+
# Zizq::Test.enqueued?(SendEmailJob, 42, template: "welcome") # exact args
|
|
122
|
+
#
|
|
123
|
+
# With no args, matches by class/type only. With args/kwargs,
|
|
124
|
+
# uses the class's own `zizq_serialize` to compute the expected
|
|
125
|
+
# payload — so it works for both `Zizq::Job` and AJ classes
|
|
126
|
+
# (AJ's volatile fields like `job_id` are ignored, only the
|
|
127
|
+
# `arguments` subset is compared).
|
|
128
|
+
#
|
|
129
|
+
# For anything fuzzier (matchers, subset matching, custom
|
|
130
|
+
# predicates), drop down to
|
|
131
|
+
# `client.enqueued_jobs(only_types: ..., filter: ->(job) { ... })`.
|
|
132
|
+
def self.enqueued?(job_class, *args, **kwargs) #: (Class, *untyped, **untyped) -> bool
|
|
133
|
+
enqueued_count(job_class, *args, **kwargs) > 0
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# How many times was a job of `job_class` enqueued (optionally
|
|
137
|
+
# with matching args)? Same argument semantics as `#enqueued?`.
|
|
138
|
+
def self.enqueued_count(job_class, *args, **kwargs) #: (Class, *untyped, **untyped) -> Integer
|
|
139
|
+
type = job_class.to_s
|
|
140
|
+
return enqueued_raw_count(type: type) if args.empty? && kwargs.empty?
|
|
141
|
+
|
|
142
|
+
unless job_class.respond_to?(:zizq_serialize)
|
|
143
|
+
raise ArgumentError,
|
|
144
|
+
"#{job_class} doesn't implement zizq_serialize — " \
|
|
145
|
+
"include Zizq::Job or extend Zizq::ActiveJobConfig, " \
|
|
146
|
+
"or use Zizq::Test.enqueued_raw? for raw enqueues."
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
expected = job_class.zizq_serialize(*args, **kwargs)
|
|
150
|
+
client.enqueued_jobs(only_types: type).count do |job|
|
|
151
|
+
payloads_equivalent?(expected, job.payload)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Was a raw job (queue + type + payload) enqueued? Each kwarg is
|
|
156
|
+
# optional; unspecified means "don't filter on this axis."
|
|
157
|
+
#
|
|
158
|
+
# Zizq::Test.enqueued_raw?(type: "send_email")
|
|
159
|
+
# Zizq::Test.enqueued_raw?(type: "send_email", payload: { user_id: 42 })
|
|
160
|
+
# Zizq::Test.enqueued_raw?(queue: "emails", type: "send_email")
|
|
161
|
+
def self.enqueued_raw?(queue: nil, type: nil, payload: nil) #: (?queue: String?, ?type: String?, ?payload: untyped) -> bool
|
|
162
|
+
enqueued_raw_count(queue: queue, type: type, payload: payload) > 0
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# How many raw jobs match (queue + type + payload)? Same
|
|
166
|
+
# argument semantics as `#enqueued_raw?`.
|
|
167
|
+
def self.enqueued_raw_count(queue: nil, type: nil, payload: nil) #: (?queue: String?, ?type: String?, ?payload: untyped) -> Integer
|
|
168
|
+
filters = {}
|
|
169
|
+
filters[:only_queues] = queue if queue
|
|
170
|
+
filters[:only_types] = type if type
|
|
171
|
+
filters[:filter] = ->(job) { job.payload == payload } unless payload.nil?
|
|
172
|
+
client.enqueued_jobs(**filters).size
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Heuristic payload comparison that handles both `Zizq::Job`'s
|
|
176
|
+
# serialized format (`{"args" => [...], "kwargs" => {...}}`) and
|
|
177
|
+
# ActiveJob's (`{"job_class" => ..., "arguments" => [...],
|
|
178
|
+
# "job_id" => ..., "enqueued_at" => ..., ...}`). The AJ shape
|
|
179
|
+
# always has an `"arguments"` key while `Zizq::Job`'s doesn't, so
|
|
180
|
+
# we use that to pick whether to compare the full hash or only the
|
|
181
|
+
# `arguments` subset (dropping AJ's volatile per-enqueue fields).
|
|
182
|
+
def self.payloads_equivalent?(expected, actual) #: (untyped, untyped) -> bool
|
|
183
|
+
if expected.is_a?(Hash) && expected.key?("arguments")
|
|
184
|
+
actual.is_a?(Hash) && actual["arguments"] == expected["arguments"]
|
|
185
|
+
else
|
|
186
|
+
actual == expected
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
data/lib/zizq/version.rb
CHANGED
data/lib/zizq.rb
CHANGED
|
@@ -29,6 +29,8 @@ module Zizq
|
|
|
29
29
|
autoload :Lifecycle, "zizq/lifecycle"
|
|
30
30
|
autoload :Query, "zizq/query"
|
|
31
31
|
autoload :Resources, "zizq/resources"
|
|
32
|
+
autoload :Router, "zizq/router"
|
|
33
|
+
autoload :Test, "zizq/test"
|
|
32
34
|
autoload :TlsConfiguration, "zizq/tls_configuration"
|
|
33
35
|
autoload :Worker, "zizq/worker"
|
|
34
36
|
autoload :WorkerConfiguration, "zizq/worker_configuration"
|
|
@@ -74,19 +76,27 @@ module Zizq
|
|
|
74
76
|
#
|
|
75
77
|
# The client is memoized so that persistent HTTP connections are reused
|
|
76
78
|
# across calls, reducing TCP connection overhead.
|
|
79
|
+
#
|
|
80
|
+
# When `configuration.test_mode` is set, a `Zizq::Test::Client` is
|
|
81
|
+
# returned instead — buffering enqueues in memory rather than
|
|
82
|
+
# talking to a real server.
|
|
77
83
|
def client #: () -> Client
|
|
78
84
|
@client ||= begin
|
|
79
85
|
@client_mutex.synchronize do
|
|
80
86
|
break @client if @client
|
|
81
87
|
|
|
82
88
|
configuration.validate!
|
|
83
|
-
@client =
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
@client = if configuration.test_mode
|
|
90
|
+
Test::Client.new
|
|
91
|
+
else
|
|
92
|
+
Client.new(
|
|
93
|
+
url: configuration.url,
|
|
94
|
+
format: configuration.format,
|
|
95
|
+
ssl_context: configuration.ssl_context,
|
|
96
|
+
read_timeout: configuration.read_timeout,
|
|
97
|
+
stream_idle_timeout: configuration.stream_idle_timeout
|
|
98
|
+
)
|
|
99
|
+
end
|
|
90
100
|
end
|
|
91
101
|
end
|
|
92
102
|
end
|
|
@@ -51,6 +51,16 @@ module Zizq
|
|
|
51
51
|
# a `Resources::Job` and a chain to continue.
|
|
52
52
|
attr_reader dequeue_middleware: Middleware::Chain[Resources::Job, void]
|
|
53
53
|
|
|
54
|
+
# When truthy, `Zizq.client` lazily resolves to a
|
|
55
|
+
# `Zizq::Test::Client` that buffers enqueues in memory rather than
|
|
56
|
+
# dispatching to a real server. Useful inside test suites — set it
|
|
57
|
+
# once in your test helper and the rest of the app's code uses
|
|
58
|
+
# `Zizq.enqueue` / `Zizq.enqueue_bulk` unchanged. Read operations
|
|
59
|
+
# (`Zizq.query`, `Zizq.queues`, `Client#get_job`, etc.) raise
|
|
60
|
+
# rather than silently returning empty results, so missing test
|
|
61
|
+
# setup is obvious.
|
|
62
|
+
attr_accessor test_mode: bool
|
|
63
|
+
|
|
54
64
|
def initialize: () -> untyped
|
|
55
65
|
|
|
56
66
|
# TLS settings for connecting to the server over HTTPS.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Generated from lib/zizq/router.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module Zizq
|
|
4
|
+
# Dispatch jobs by `type` string, mapping each to a handler block.
|
|
5
|
+
#
|
|
6
|
+
# Designed for cross-language workflows: payloads are plain JSON
|
|
7
|
+
# values (Hashes / Arrays / strings / numbers), `type` is a String
|
|
8
|
+
# the producer agrees on with the consumer, and routes are
|
|
9
|
+
# registered explicitly — no `Zizq::Job` mixin involved.
|
|
10
|
+
#
|
|
11
|
+
# Zizq.configure do |c|
|
|
12
|
+
# c.dispatcher = Zizq::Router.new do
|
|
13
|
+
# route("send_email") do |payload|
|
|
14
|
+
# Mailer.deliver(payload["user_id"], payload["template"])
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# route("expire_tokens") do
|
|
18
|
+
# TokenSweeper.run
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# route("generate_report") do |payload, job|
|
|
22
|
+
# Reports.generate(payload["id"], attempts: job.attempts)
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# # `def` inside the block defines singleton methods on the
|
|
26
|
+
# # router. Route blocks captured *inside* the constructor
|
|
27
|
+
# # have lexical `self == router`, so they can call these
|
|
28
|
+
# # helpers; routes added outside (`router.route("…") { … }`)
|
|
29
|
+
# # keep their own lexical `self` and would need to go through
|
|
30
|
+
# # the router explicitly (`router.logger`).
|
|
31
|
+
# def logger
|
|
32
|
+
# Zizq.configuration.logger
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# # Anything else falls back. A common pattern is delegating
|
|
36
|
+
# # to `Zizq::Job` for the apps that mix the two styles.
|
|
37
|
+
# fallback { |job| Zizq::Job.call(job) }
|
|
38
|
+
# end
|
|
39
|
+
# end
|
|
40
|
+
#
|
|
41
|
+
# Routes can also be registered outside the constructor block:
|
|
42
|
+
#
|
|
43
|
+
# router = Zizq::Router.new
|
|
44
|
+
# router.route("send_email") { |payload| ... }
|
|
45
|
+
#
|
|
46
|
+
# Handlers are called as `handler.call(payload, job)`. Block-arity
|
|
47
|
+
# rules let `{ |payload| }` or `{ }` ignore either argument.
|
|
48
|
+
# Strict-arity lambdas need to declare both.
|
|
49
|
+
class Router
|
|
50
|
+
# Raised when a job arrives with a type that has no registered
|
|
51
|
+
# route and no fallback. Caught by Zizq's normal worker error
|
|
52
|
+
# path, which nacks the job for retry (or dead-letters it once
|
|
53
|
+
# the retry limit is hit).
|
|
54
|
+
class UnknownJobType < Zizq::Error
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @rbs &block: ?(self) [self: Router] -> void
|
|
58
|
+
def initialize: () ?{ (self) [self: Router] -> void } -> untyped
|
|
59
|
+
|
|
60
|
+
# Register `handler` for jobs whose `type` matches.
|
|
61
|
+
#
|
|
62
|
+
# @rbs type: String | Symbol
|
|
63
|
+
# @rbs &handler: (untyped, Resources::Job) -> void
|
|
64
|
+
def route: (String | Symbol type) { (untyped, Resources::Job) -> void } -> untyped
|
|
65
|
+
|
|
66
|
+
# Register a fallback handler invoked when no route matches.
|
|
67
|
+
# Receives the full `Resources::Job` (not split into payload /
|
|
68
|
+
# job pair), since the canonical use is delegation to another
|
|
69
|
+
# dispatcher:
|
|
70
|
+
#
|
|
71
|
+
# fallback { |job| Zizq::Job.call(job) }
|
|
72
|
+
#
|
|
73
|
+
# @rbs &handler: (Resources::Job) -> void
|
|
74
|
+
def fallback: () { (Resources::Job) -> void } -> untyped
|
|
75
|
+
|
|
76
|
+
# Dispatch a single job. Looks up the handler by `job.type`,
|
|
77
|
+
# falls back to the registered fallback if any, otherwise
|
|
78
|
+
# raises `UnknownJobType`.
|
|
79
|
+
def call: (untyped job) -> untyped
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Generated from lib/zizq/test/client.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module Zizq
|
|
4
|
+
module Test
|
|
5
|
+
# A `Zizq::Client` stand-in for use in test suites.
|
|
6
|
+
#
|
|
7
|
+
# Buffers `enqueue` / `enqueue_bulk` calls in memory and returns
|
|
8
|
+
# synthetic `Resources::Job` instances (with generated ids) so that
|
|
9
|
+
# callers depending on the regular client's return contract don't
|
|
10
|
+
# need to special-case test mode.
|
|
11
|
+
#
|
|
12
|
+
# Read operations (`get_queues`, `list_jobs`, `count_jobs`, …) are
|
|
13
|
+
# explicitly not supported in test mode and raise `NotSupported`.
|
|
14
|
+
# Tests that need those should either run against a real server or
|
|
15
|
+
# stub at a higher level.
|
|
16
|
+
#
|
|
17
|
+
# Activated indirectly via `Zizq.configuration.test_mode = true` —
|
|
18
|
+
# `Zizq.client` then lazily builds a `Test::Client` instead of a
|
|
19
|
+
# real `Client`.
|
|
20
|
+
class Client < Zizq::Client
|
|
21
|
+
# Raised when test-mode code reaches an operation that isn't
|
|
22
|
+
# supported (queries, queue listing, worker streams, etc.).
|
|
23
|
+
class NotSupported < Zizq::Error
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Length of a real scru128 id in its base-32 representation.
|
|
27
|
+
# Synthetic test ids are sized to match (`test` prefix + zero
|
|
28
|
+
# padded counter) so they fit anywhere a real id would.
|
|
29
|
+
ID_LENGTH: ::Integer
|
|
30
|
+
|
|
31
|
+
ID_PREFIX: ::String
|
|
32
|
+
|
|
33
|
+
# The canonical Zizq lifecycle states. We mirror these so the
|
|
34
|
+
# `status` on a buffered job reflects what the real server would
|
|
35
|
+
# report. Test mode never retries — `in_flight` only ever
|
|
36
|
+
# transitions to `completed` or `dead`.
|
|
37
|
+
STATUS_SCHEDULED: ::String
|
|
38
|
+
|
|
39
|
+
STATUS_READY: ::String
|
|
40
|
+
|
|
41
|
+
STATUS_IN_FLIGHT: ::String
|
|
42
|
+
|
|
43
|
+
STATUS_COMPLETED: ::String
|
|
44
|
+
|
|
45
|
+
STATUS_DEAD: ::String
|
|
46
|
+
|
|
47
|
+
# Default `filter:` lambda — passes every job. Named so the
|
|
48
|
+
# filter pipeline is always callable without nil-checking.
|
|
49
|
+
PASS_ALL_FILTER: ^(Resources::Job) -> bool
|
|
50
|
+
|
|
51
|
+
# Paired view of a single enqueue: the original `EnqueueRequest`
|
|
52
|
+
# (with full submission metadata — `unique_key`, `unique_while`,
|
|
53
|
+
# retry config, etc.), the synthetic `Resources::Job` returned
|
|
54
|
+
# to callers, and the data hash that backs the job (so we can
|
|
55
|
+
# mutate its status through the lifecycle).
|
|
56
|
+
class Entry < Struct[untyped]
|
|
57
|
+
attr_accessor request(): untyped
|
|
58
|
+
|
|
59
|
+
attr_accessor job(): untyped
|
|
60
|
+
|
|
61
|
+
attr_accessor data(): untyped
|
|
62
|
+
|
|
63
|
+
def self.new: (?request: untyped, ?job: untyped, ?data: untyped) -> instance
|
|
64
|
+
| ({ ?request: untyped, ?job: untyped, ?data: untyped }) -> instance
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def initialize: () -> untyped
|
|
68
|
+
|
|
69
|
+
# All buffered jobs, in submission order, optionally filtered.
|
|
70
|
+
# See `apply_filters` for the filter kwargs.
|
|
71
|
+
def enqueued_jobs: (**untyped filters) -> untyped
|
|
72
|
+
|
|
73
|
+
# Original `EnqueueRequest`s in submission order. Useful when a
|
|
74
|
+
# test needs metadata that doesn't survive onto `Resources::Job`
|
|
75
|
+
# (`unique_key`, `unique_while`, `delay` before `ready_at`
|
|
76
|
+
# resolution, etc.). Same filter kwargs as `enqueued_jobs`.
|
|
77
|
+
def enqueued_requests: (**untyped filters) -> untyped
|
|
78
|
+
|
|
79
|
+
# Jobs awaiting dispatch — `ready` and `scheduled` entries.
|
|
80
|
+
# `pending_jobs` is the set `drain` would attempt to run on its
|
|
81
|
+
# next call (modulo `ready_at` for `scheduled` entries).
|
|
82
|
+
def pending_jobs: (**untyped filters) -> untyped
|
|
83
|
+
|
|
84
|
+
def in_flight_jobs: (**untyped filters) -> untyped
|
|
85
|
+
|
|
86
|
+
def completed_jobs: (**untyped filters) -> untyped
|
|
87
|
+
|
|
88
|
+
def dead_jobs: (**untyped filters) -> untyped
|
|
89
|
+
|
|
90
|
+
# Reset the buffer. Called between tests via `Zizq::Test.reset!`.
|
|
91
|
+
def clear!: () -> untyped
|
|
92
|
+
|
|
93
|
+
def close: () -> untyped
|
|
94
|
+
|
|
95
|
+
# Dispatch every runnable entry (status `ready`, or `scheduled`
|
|
96
|
+
# with `ready_at` already elapsed) through the configured
|
|
97
|
+
# dequeue middleware chain (same path the real worker uses, so
|
|
98
|
+
# any registered middlewares run in tests too), looping until no
|
|
99
|
+
# more match the filters. Re-enqueues during dispatch fall
|
|
100
|
+
# through the loop naturally — they get drained too unless they
|
|
101
|
+
# fall outside the filter set.
|
|
102
|
+
#
|
|
103
|
+
# The per-iteration snapshot is taken under the mutex and marked
|
|
104
|
+
# `in_flight` atomically. Dispatch happens outside the mutex so
|
|
105
|
+
# handlers can re-enter the client without deadlocking. On
|
|
106
|
+
# success the entry moves to `completed`; on a raised exception
|
|
107
|
+
# it moves to `dead` and the exception re-raises (matching
|
|
108
|
+
# ActiveJob's `perform_enqueued_jobs` + Sidekiq's `drain`).
|
|
109
|
+
def drain: (**untyped filters) -> untyped
|
|
110
|
+
|
|
111
|
+
# @rbs override
|
|
112
|
+
def enqueue: ...
|
|
113
|
+
|
|
114
|
+
# @rbs override
|
|
115
|
+
def enqueue_bulk: ...
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def record_unsynchronized: (untyped req) -> untyped
|
|
120
|
+
|
|
121
|
+
def synthetic_id: (untyped counter) -> untyped
|
|
122
|
+
|
|
123
|
+
# Returns entries matching every named filter AND the predicate.
|
|
124
|
+
# All filter kwargs are optional; unset means "don't filter on
|
|
125
|
+
# this axis." Callers must hold `@mutex` — public accessors do
|
|
126
|
+
# so via `synchronize` before calling.
|
|
127
|
+
#
|
|
128
|
+
# * `only_queues:` / `except_queues:` — String, Array of Strings.
|
|
129
|
+
# * `only_types:` / `except_types:` — String, Class, or Array
|
|
130
|
+
# of those. Class names are matched against the wire-format
|
|
131
|
+
# `type` string via `.to_s`.
|
|
132
|
+
# * `filter:` — a lambda receiving a `Resources::Job`, returning
|
|
133
|
+
# truthy to keep. Defaults to `PASS_ALL_FILTER`.
|
|
134
|
+
#
|
|
135
|
+
# `only_*` and `except_*` AND together with the predicate.
|
|
136
|
+
def apply_filters: (untyped entries, ?only_queues: untyped, ?except_queues: untyped, ?only_types: untyped, ?except_types: untyped, ?filter: untyped) -> untyped
|
|
137
|
+
|
|
138
|
+
def filter_by_status: (untyped statuses, **untyped filters) -> untyped
|
|
139
|
+
|
|
140
|
+
# Lock, find runnable entries matching the filters, flip each
|
|
141
|
+
# to in_flight, return the snapshot. Holding the mutex through
|
|
142
|
+
# this is fine because we don't call user code — dispatch
|
|
143
|
+
# happens outside.
|
|
144
|
+
def take_runnable_snapshot: (**untyped filters) -> untyped
|
|
145
|
+
|
|
146
|
+
def runnable?: (untyped entry, untyped now_ms) -> untyped
|
|
147
|
+
|
|
148
|
+
# Accept a Class, String, or Array of those; emit an Array of
|
|
149
|
+
# Strings. Class names match the API's `type` string.
|
|
150
|
+
def normalize_filter: (untyped value) -> untyped
|
|
151
|
+
|
|
152
|
+
# Mark the entry in_flight, dispatch through the full dequeue
|
|
153
|
+
# middleware chain (same path the real worker uses, so any
|
|
154
|
+
# registered middlewares run in tests too), settle to
|
|
155
|
+
# completed or dead. A raised exception re-raises after
|
|
156
|
+
# recording — same observable behaviour as Rails'
|
|
157
|
+
# `perform_enqueued_jobs`.
|
|
158
|
+
def dispatch_entry: (untyped entry) -> untyped
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Generated from lib/zizq/test.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module Zizq
|
|
4
|
+
# Test-mode helpers. Activated by setting `c.test_mode = true` in a
|
|
5
|
+
# `Zizq.configure` block; `Zizq.client` then lazily resolves to a
|
|
6
|
+
# `Zizq::Test::Client` that buffers enqueues instead of dispatching.
|
|
7
|
+
#
|
|
8
|
+
# Typical use in a test helper:
|
|
9
|
+
#
|
|
10
|
+
# Zizq.configure do |c|
|
|
11
|
+
# c.test_mode = true
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# class MyTestCase
|
|
15
|
+
# setup { Zizq::Test.reset! }
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# In a test:
|
|
19
|
+
#
|
|
20
|
+
# def test_signup_fans_out
|
|
21
|
+
# # Default buffered mode — assert what was enqueued.
|
|
22
|
+
# SignupService.new.run
|
|
23
|
+
# assert_equal 2, Zizq::Test.client.pending_jobs.size
|
|
24
|
+
#
|
|
25
|
+
# # Drain whatever's pending (handler re-enqueues fall through
|
|
26
|
+
# # naturally — drain loops until pending is empty).
|
|
27
|
+
# Zizq::Test.dispatch_enqueued_jobs
|
|
28
|
+
#
|
|
29
|
+
# # Block form: run the work, then drain. Matches ActiveJob's
|
|
30
|
+
# # `perform_enqueued_jobs do ... end`.
|
|
31
|
+
# Zizq::Test.dispatch_enqueued_jobs { SignupService.new.run }
|
|
32
|
+
#
|
|
33
|
+
# # Filter by queue and/or type when only a subset should fire.
|
|
34
|
+
# Zizq::Test.dispatch_enqueued_jobs(queue: "emails")
|
|
35
|
+
# Zizq::Test.dispatch_enqueued_jobs(type: SendEmailJob)
|
|
36
|
+
# end
|
|
37
|
+
module Test
|
|
38
|
+
# Switch Zizq into test mode. After this, `Zizq.client` resolves
|
|
39
|
+
# to a `Zizq::Test::Client` that buffers enqueues in memory.
|
|
40
|
+
# Typically called once in a test helper.
|
|
41
|
+
def self.enable!: () -> untyped
|
|
42
|
+
|
|
43
|
+
# Switch back to the real client. The buffered state is dropped
|
|
44
|
+
# along with the test client (the next `Zizq.client` access
|
|
45
|
+
# builds a fresh `Zizq::Client`).
|
|
46
|
+
def self.disable!: () -> untyped
|
|
47
|
+
|
|
48
|
+
# The active test client. Raises if test mode is not enabled —
|
|
49
|
+
# better to fail loudly than return a stale or wrong client.
|
|
50
|
+
def self.client: () -> untyped
|
|
51
|
+
|
|
52
|
+
# Reset buffered state between tests. Keeps the configured
|
|
53
|
+
# `test_mode` flag.
|
|
54
|
+
def self.reset!: () -> untyped
|
|
55
|
+
|
|
56
|
+
# Dispatch pending jobs through the configured dequeue middleware
|
|
57
|
+
# chain (`Zizq.configuration.dequeue_middleware` — same path the
|
|
58
|
+
# real worker uses, so any registered middlewares run in tests
|
|
59
|
+
# too), looping until no more pending entries match the filters.
|
|
60
|
+
#
|
|
61
|
+
# * No block: drain whatever's pending now.
|
|
62
|
+
# * With block: yield first (test code enqueues), then drain.
|
|
63
|
+
# * `only_queues:` / `except_queues:` — String or Array of Strings.
|
|
64
|
+
# * `only_types:` / `except_types:` — String, Class, or Array of
|
|
65
|
+
# those (Class names match the serialized-format `type` via `.to_s`,
|
|
66
|
+
# so passing an ActiveJob class works directly).
|
|
67
|
+
# * `filter:` — a lambda `->(job)` returning truthy to keep an
|
|
68
|
+
# entry. Defaults to "pass all". Combines with the named
|
|
69
|
+
# filters via AND.
|
|
70
|
+
#
|
|
71
|
+
# Returns the number of jobs dispatched. A block exception
|
|
72
|
+
# propagates without draining; a handler exception during drain
|
|
73
|
+
# transitions that entry to `dead` and re-raises.
|
|
74
|
+
def self.dispatch_enqueued_jobs: (**untyped filters) ?{ (?) -> untyped } -> untyped
|
|
75
|
+
|
|
76
|
+
# Was a job of `job_class` enqueued (optionally with matching
|
|
77
|
+
# args)?
|
|
78
|
+
#
|
|
79
|
+
# Zizq::Test.enqueued?(SendEmailJob) # any args
|
|
80
|
+
# Zizq::Test.enqueued?(SendEmailJob, 42, template: "welcome") # exact args
|
|
81
|
+
#
|
|
82
|
+
# With no args, matches by class/type only. With args/kwargs,
|
|
83
|
+
# uses the class's own `zizq_serialize` to compute the expected
|
|
84
|
+
# payload — so it works for both `Zizq::Job` and AJ classes
|
|
85
|
+
# (AJ's volatile fields like `job_id` are ignored, only the
|
|
86
|
+
# `arguments` subset is compared).
|
|
87
|
+
#
|
|
88
|
+
# For anything fuzzier (matchers, subset matching, custom
|
|
89
|
+
# predicates), drop down to
|
|
90
|
+
# `client.enqueued_jobs(only_types: ..., filter: ->(job) { ... })`.
|
|
91
|
+
def self.enqueued?: (untyped job_class, *untyped args, **untyped kwargs) -> untyped
|
|
92
|
+
|
|
93
|
+
# How many times was a job of `job_class` enqueued (optionally
|
|
94
|
+
# with matching args)? Same argument semantics as `#enqueued?`.
|
|
95
|
+
def self.enqueued_count: (untyped job_class, *untyped args, **untyped kwargs) -> untyped
|
|
96
|
+
|
|
97
|
+
# Was a raw job (queue + type + payload) enqueued? Each kwarg is
|
|
98
|
+
# optional; unspecified means "don't filter on this axis."
|
|
99
|
+
#
|
|
100
|
+
# Zizq::Test.enqueued_raw?(type: "send_email")
|
|
101
|
+
# Zizq::Test.enqueued_raw?(type: "send_email", payload: { user_id: 42 })
|
|
102
|
+
# Zizq::Test.enqueued_raw?(queue: "emails", type: "send_email")
|
|
103
|
+
def self.enqueued_raw?: (?queue: untyped, ?type: untyped, ?payload: untyped) -> untyped
|
|
104
|
+
|
|
105
|
+
# How many raw jobs match (queue + type + payload)? Same
|
|
106
|
+
# argument semantics as `#enqueued_raw?`.
|
|
107
|
+
def self.enqueued_raw_count: (?queue: untyped, ?type: untyped, ?payload: untyped) -> untyped
|
|
108
|
+
|
|
109
|
+
# Heuristic payload comparison that handles both `Zizq::Job`'s
|
|
110
|
+
# serialized format (`{"args" => [...], "kwargs" => {...}}`) and
|
|
111
|
+
# ActiveJob's (`{"job_class" => ..., "arguments" => [...],
|
|
112
|
+
# "job_id" => ..., "enqueued_at" => ..., ...}`). The AJ shape
|
|
113
|
+
# always has an `"arguments"` key while `Zizq::Job`'s doesn't, so
|
|
114
|
+
# we use that to pick whether to compare the full hash or only the
|
|
115
|
+
# `arguments` subset (dropping AJ's volatile per-enqueue fields).
|
|
116
|
+
def self.payloads_equivalent?: (untyped expected, untyped actual) -> untyped
|
|
117
|
+
end
|
|
118
|
+
end
|
data/sig/generated/zizq.rbs
CHANGED
|
@@ -33,6 +33,10 @@ module Zizq
|
|
|
33
33
|
#
|
|
34
34
|
# The client is memoized so that persistent HTTP connections are reused
|
|
35
35
|
# across calls, reducing TCP connection overhead.
|
|
36
|
+
#
|
|
37
|
+
# When `configuration.test_mode` is set, a `Zizq::Test::Client` is
|
|
38
|
+
# returned instead — buffering enqueues in memory rather than
|
|
39
|
+
# talking to a real server.
|
|
36
40
|
def self.client: () -> untyped
|
|
37
41
|
|
|
38
42
|
# Resets all global state: configuration and shared client.
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: zizq
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chris Corbyn <chris@zizq.io>
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: async-http
|
|
@@ -100,6 +100,9 @@ files:
|
|
|
100
100
|
- lib/zizq/resources/job_template.rb
|
|
101
101
|
- lib/zizq/resources/page.rb
|
|
102
102
|
- lib/zizq/resources/resource.rb
|
|
103
|
+
- lib/zizq/router.rb
|
|
104
|
+
- lib/zizq/test.rb
|
|
105
|
+
- lib/zizq/test/client.rb
|
|
103
106
|
- lib/zizq/tls_configuration.rb
|
|
104
107
|
- lib/zizq/version.rb
|
|
105
108
|
- lib/zizq/worker.rb
|
|
@@ -133,6 +136,9 @@ files:
|
|
|
133
136
|
- sig/generated/zizq/resources/job_template.rbs
|
|
134
137
|
- sig/generated/zizq/resources/page.rbs
|
|
135
138
|
- sig/generated/zizq/resources/resource.rbs
|
|
139
|
+
- sig/generated/zizq/router.rbs
|
|
140
|
+
- sig/generated/zizq/test.rbs
|
|
141
|
+
- sig/generated/zizq/test/client.rbs
|
|
136
142
|
- sig/generated/zizq/tls_configuration.rbs
|
|
137
143
|
- sig/generated/zizq/version.rbs
|
|
138
144
|
- sig/generated/zizq/worker.rbs
|