zizq 0.2.1 → 0.3.1
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 +164 -31
- data/lib/zizq/bulk_enqueue.rb +1 -1
- data/lib/zizq/client.rb +234 -10
- data/lib/zizq/configuration.rb +31 -0
- data/lib/zizq/crontab.rb +263 -0
- data/lib/zizq/crontab_builder.rb +52 -0
- data/lib/zizq/crontab_entry.rb +182 -0
- data/lib/zizq/crontab_entry_builder.rb +133 -0
- data/lib/zizq/enqueue_with.rb +1 -1
- data/lib/zizq/job.rb +6 -2
- data/lib/zizq/query.rb +2 -2
- data/lib/zizq/resources/cron_entry.rb +27 -0
- data/lib/zizq/resources/cron_group.rb +23 -0
- data/lib/zizq/resources/job.rb +4 -36
- data/lib/zizq/resources/job_template.rb +46 -0
- data/lib/zizq/resources.rb +3 -0
- data/lib/zizq/version.rb +1 -1
- data/lib/zizq.rb +109 -19
- data/sig/generated/zizq/bulk_enqueue.rbs +2 -2
- data/sig/generated/zizq/client.rbs +124 -1
- data/sig/generated/zizq/configuration.rbs +21 -0
- data/sig/generated/zizq/crontab.rbs +141 -0
- data/sig/generated/zizq/crontab_builder.rbs +35 -0
- data/sig/generated/zizq/crontab_entry.rbs +98 -0
- data/sig/generated/zizq/crontab_entry_builder.rbs +92 -0
- data/sig/generated/zizq/enqueue_with.rbs +2 -2
- data/sig/generated/zizq/query.rbs +4 -4
- data/sig/generated/zizq/resources/cron_entry.rbs +29 -0
- data/sig/generated/zizq/resources/cron_group.rbs +21 -0
- data/sig/generated/zizq/resources/job.rbs +4 -26
- data/sig/generated/zizq/resources/job_template.rbs +31 -0
- data/sig/generated/zizq.rbs +78 -4
- data/sig/zizq.rbs +24 -17
- metadata +34 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8f79a2e4601f42c8c9334601383a3659ffa5063a5df3ce2575b1d56b4df17a89
|
|
4
|
+
data.tar.gz: 730dbf9a98a6bb877234f3821e099e0363c5ebfad4905622106fa19b17db6610
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 49f4a260cfbc35570e10e8fa0811e922be0c826ac0126f20e530353ae4c58df13e5f1cf8637d0a2f7a799ec3a0c64d9082c1a66d859de21b748db05e2db22802
|
|
7
|
+
data.tar.gz: 0e09b0848550330989701c703c5640e3df46e9e8a4229e3e15de99f033975356e24abee2e99fe537ed5b80c8a6edbfc9e302b518844ca9a6d99ce60766af5ccb
|
data/README.md
CHANGED
|
@@ -7,6 +7,7 @@ API.
|
|
|
7
7
|
This is the official Zizq client library for Ruby.
|
|
8
8
|
|
|
9
9
|
[](https://github.com/zizq-labs/zizq-ruby/actions/workflows/ci.yml)
|
|
10
|
+
[](https://rubygems.org/gems/zizq)
|
|
10
11
|
|
|
11
12
|
## Features
|
|
12
13
|
|
|
@@ -18,17 +19,66 @@ This is the official Zizq client library for Ruby.
|
|
|
18
19
|
* Scheduled jobs
|
|
19
20
|
* Configurable backoff policies
|
|
20
21
|
* Configurable job retention policies
|
|
22
|
+
* Recurring jobs (cron)
|
|
21
23
|
* Job introspection and management APIs, with support for `jq` query filters
|
|
22
24
|
* Unique jobs
|
|
23
25
|
|
|
24
|
-
##
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
> [!NOTE]
|
|
29
|
+
> If you have not yet installed the Zizq server, follow the
|
|
30
|
+
> [Getting Started](https://zizq.io/docs/getting-started) guide first.
|
|
31
|
+
|
|
32
|
+
Add it to your application's `Gemfile`:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
gem 'zizq', '~> 0.3.1'
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Or install it manually:
|
|
39
|
+
|
|
40
|
+
```shell
|
|
41
|
+
$ gem install zizq -v 0.3.1
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Ruby **3.2.8 or newer** is required. Client and server share version
|
|
45
|
+
numbers — keep the client's major/minor at or below the server's.
|
|
46
|
+
|
|
47
|
+
## Configuration
|
|
48
|
+
|
|
49
|
+
Out of the box, the client talks to a server at `http://localhost:7890` —
|
|
50
|
+
fine for local development. For anything else, configure it with
|
|
51
|
+
`Zizq.configure` in your application's bootstrap (e.g. a Rails initializer):
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
require 'zizq'
|
|
55
|
+
|
|
56
|
+
Zizq.configure do |c|
|
|
57
|
+
c.url = 'https://zizq.your.network:7890'
|
|
58
|
+
c.tls = { ca: '/path/to/server-ca-cert.pem' }
|
|
59
|
+
c.logger = Logger.new('log/zizq.log')
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
For mutual TLS, add `client_cert:` and `client_key:` to the `tls` hash.
|
|
64
|
+
|
|
65
|
+
> [!CAUTION]
|
|
66
|
+
> If your server is exposed directly to the internet, it should require
|
|
67
|
+
> mutual TLS — otherwise anybody can talk to it.
|
|
68
|
+
|
|
69
|
+
## Usage
|
|
25
70
|
|
|
26
71
|
> [!TIP]
|
|
27
|
-
>
|
|
28
|
-
>
|
|
29
|
-
>
|
|
72
|
+
> This README is an overview. The
|
|
73
|
+
> [full documentation](https://zizq.io/docs/clients/ruby/) covers each
|
|
74
|
+
> feature in depth — middleware, custom dispatchers, Active Job, job
|
|
75
|
+
> querying, and more.
|
|
76
|
+
|
|
77
|
+
### Defining a job
|
|
30
78
|
|
|
31
|
-
|
|
79
|
+
In most Ruby applications, a job is a plain class that includes `Zizq::Job`.
|
|
80
|
+
The class declares its defaults with the `zizq_*` DSL and implements
|
|
81
|
+
`#perform`:
|
|
32
82
|
|
|
33
83
|
```ruby
|
|
34
84
|
class SendEmailJob
|
|
@@ -36,54 +86,133 @@ class SendEmailJob
|
|
|
36
86
|
|
|
37
87
|
zizq_queue 'emails'
|
|
38
88
|
zizq_priority 100
|
|
89
|
+
zizq_retry_limit 5
|
|
39
90
|
|
|
40
91
|
def perform(user_id, template:)
|
|
41
|
-
|
|
92
|
+
user = User.find(user_id)
|
|
93
|
+
Mailer.deliver(user, template)
|
|
42
94
|
end
|
|
43
95
|
end
|
|
44
96
|
```
|
|
45
97
|
|
|
46
|
-
|
|
98
|
+
Every default — `zizq_queue`, `zizq_priority`, `zizq_retry_limit`,
|
|
99
|
+
`zizq_backoff`, `zizq_retention`, `zizq_unique` — can be overridden per
|
|
100
|
+
enqueue. The job's class name (`"SendEmailJob"`) becomes the API-level job
|
|
101
|
+
type, so keep it stable once jobs are in flight.
|
|
102
|
+
|
|
103
|
+
### Enqueuing jobs
|
|
104
|
+
|
|
105
|
+
Enqueue a job by passing the class and the arguments your `#perform` method
|
|
106
|
+
expects:
|
|
47
107
|
|
|
48
108
|
```ruby
|
|
49
|
-
Zizq.enqueue(SendEmailJob, 42, template: 'welcome')
|
|
109
|
+
job = Zizq.enqueue(SendEmailJob, 42, template: 'welcome')
|
|
110
|
+
job.id # => "03fu0wm75gxgmfyfplwvazhex"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Override defaults for a single call with `Zizq.enqueue_with`, or with a block
|
|
114
|
+
that mutates the request:
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
# Don't retry this one.
|
|
118
|
+
Zizq.enqueue_with(retry_limit: 0).enqueue(SendEmailJob, 42, template: 'welcome')
|
|
119
|
+
|
|
120
|
+
# Bump the priority via the block form.
|
|
121
|
+
Zizq.enqueue(SendEmailJob, 42, template: 'welcome') do |req|
|
|
122
|
+
req.priority = 1000
|
|
123
|
+
end
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Schedule a job for later with `delay` (seconds from now) or an absolute
|
|
127
|
+
`ready_at`:
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
Zizq.enqueue_with(delay: 3600).enqueue(SendEmailJob, 42, template: 'welcome')
|
|
131
|
+
Zizq.enqueue_with(ready_at: Time.new(2027, 3, 15, 14, 30)).enqueue(SendEmailJob, 42, template: 'welcome')
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
To enqueue many jobs efficiently, `Zizq.enqueue_bulk` sends them in a single
|
|
135
|
+
atomic request — across queues and job types, and `enqueue_raw` enqueues can
|
|
136
|
+
be mixed in too:
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
Zizq.enqueue_bulk do |b|
|
|
140
|
+
signups.each { |user_id| b.enqueue(SendEmailJob, user_id, template: 'welcome') }
|
|
141
|
+
end
|
|
50
142
|
```
|
|
51
143
|
|
|
52
144
|
> [!NOTE]
|
|
53
|
-
> Jobs can also be enqueued
|
|
54
|
-
> designed
|
|
145
|
+
> Jobs can also be enqueued without `Zizq::Job` via `Zizq.enqueue_raw` —
|
|
146
|
+
> designed for cross-language workflows where, for example, a Ruby app
|
|
147
|
+
> enqueues jobs consumed by a Go service.
|
|
148
|
+
|
|
149
|
+
### Running a worker
|
|
55
150
|
|
|
56
|
-
|
|
151
|
+
Jobs are processed by a worker, typically in a separate process. The simplest
|
|
152
|
+
way is the `zizq-worker` executable bundled with the gem — pass your
|
|
153
|
+
application's entrypoint so it can load your job classes:
|
|
57
154
|
|
|
58
155
|
```shell
|
|
59
|
-
$ zizq-worker --threads 5 --fibers 2
|
|
60
|
-
I, [
|
|
61
|
-
I, [
|
|
62
|
-
I, [2026-03-24T15:25:57.739861 #1331422] INFO -- : Worker 0:0 started
|
|
63
|
-
I, [2026-03-24T15:25:57.739962 #1331422] INFO -- : Worker 0:1 started
|
|
64
|
-
I, [2026-03-24T15:25:57.740131 #1331422] INFO -- : Worker 1:0 started
|
|
65
|
-
I, [2026-03-24T15:25:57.740211 #1331422] INFO -- : Worker 1:1 started
|
|
66
|
-
I, [2026-03-24T15:25:57.740352 #1331422] INFO -- : Worker 2:0 started
|
|
67
|
-
I, [2026-03-24T15:25:57.740408 #1331422] INFO -- : Worker 2:1 started
|
|
68
|
-
I, [2026-03-24T15:25:57.740532 #1331422] INFO -- : Worker 3:0 started
|
|
69
|
-
I, [2026-03-24T15:25:57.740590 #1331422] INFO -- : Worker 3:1 started
|
|
70
|
-
I, [2026-03-24T15:25:57.740722 #1331422] INFO -- : Worker 4:0 started
|
|
71
|
-
I, [2026-03-24T15:25:57.740776 #1331422] INFO -- : Worker 4:1 started
|
|
72
|
-
I, [2026-03-24T15:25:57.740844 #1331422] INFO -- : Zizq producer thread started
|
|
73
|
-
I, [2026-03-24T15:25:57.740878 #1331422] INFO -- : Connecting to http://localhost:7890...
|
|
74
|
-
I, [2026-03-24T15:25:57.792173 #1331422] INFO -- : Connected. Listening for jobs.
|
|
156
|
+
$ bundle exec zizq-worker --threads 5 --fibers 2 config/environment.rb
|
|
157
|
+
I, [...] INFO -- : Zizq worker starting: 5 threads, 2 fibers, prefetch=20
|
|
158
|
+
I, [...] INFO -- : Connected. Listening for jobs.
|
|
75
159
|
```
|
|
76
160
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
161
|
+
A worker runs `threads × fibers` handlers concurrently. Leave `--fibers 1`
|
|
162
|
+
if your application isn't fiber-safe — no `Async` context is loaded in that
|
|
163
|
+
case. Restrict to specific queues with `--queue`. `INT` / `TERM` trigger a
|
|
164
|
+
graceful shutdown (drains in-flight jobs up to `--shutdown-timeout`, default
|
|
165
|
+
30s).
|
|
166
|
+
|
|
167
|
+
For more control — for example running the worker in-process alongside a
|
|
168
|
+
Rack app — construct `Zizq::Worker` directly:
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
require 'zizq'
|
|
172
|
+
|
|
173
|
+
worker = Zizq::Worker.new(
|
|
174
|
+
thread_count: 5,
|
|
175
|
+
fiber_count: 2,
|
|
176
|
+
queues: ['emails', 'payments'],
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
Signal.trap('INT') { worker.stop }
|
|
180
|
+
worker.run # blocks until the worker stops
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`#run` blocks until the worker terminates; `#stop` drains in-flight jobs
|
|
184
|
+
gracefully, `#kill` forces an immediate stop. On any unclean shutdown the
|
|
185
|
+
server returns unfinished jobs to the queue — no job is lost.
|
|
186
|
+
|
|
187
|
+
### Recurring jobs (cron)
|
|
188
|
+
|
|
189
|
+
Define a cron schedule in your application's startup code. Definitions are
|
|
190
|
+
idempotent — every process can safely define the same schedule, and Zizq
|
|
191
|
+
keeps the server in sync by adding, replacing, and removing entries as the
|
|
192
|
+
definition changes. Cron requires a Pro license on the server.
|
|
193
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
Zizq.define_crontab('maintenance', timezone: 'Europe/London') do |cron|
|
|
196
|
+
# Every 15 minutes.
|
|
197
|
+
cron.define_entry('refresh_warehouse', '*/15 * * * *').enqueue(
|
|
198
|
+
RefreshWarehouseJob, incremental: true
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# 9am London time, every day.
|
|
202
|
+
cron.define_entry('daily_digest', '0 9 * * *').enqueue(SendDailyDigestJob)
|
|
203
|
+
end
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Once defined, schedules can be inspected and managed via
|
|
207
|
+
`Zizq.crontab('maintenance')` — paused/resumed at the schedule level or per
|
|
208
|
+
entry, and deleted entirely when no longer needed.
|
|
80
209
|
|
|
81
210
|
## Resources
|
|
82
211
|
|
|
83
212
|
* [Ruby Client Docs](https://zizq.io/docs/clients/ruby/)
|
|
84
213
|
* [Getting Started Docs](https://zizq.io/docs/getting-started/)
|
|
85
214
|
* [Zizq Command Reference](https://zizq.io/docs/cli/)
|
|
86
|
-
* [Zizq
|
|
215
|
+
* [Zizq Ruby Client Source](https://github.com/zizq-labs/zizq-ruby)
|
|
87
216
|
* [Zizq Source](https://github.com/zizq-labs/zizq)
|
|
88
217
|
|
|
89
218
|
## Support & Feedback
|
|
@@ -92,3 +221,7 @@ If you need help using Zizq,
|
|
|
92
221
|
[create an issue](https://github.com/zizq-labs/zizq-ruby/issues) on the
|
|
93
222
|
[zizq-ruby](https://github.com/zizq-labs/zizq-ruby) repo. Feedback is very
|
|
94
223
|
welcome.
|
|
224
|
+
|
|
225
|
+
## License
|
|
226
|
+
|
|
227
|
+
MIT — see [LICENSE](LICENSE).
|
data/lib/zizq/bulk_enqueue.rb
CHANGED
|
@@ -22,7 +22,7 @@ module Zizq
|
|
|
22
22
|
# Collect a job class enqueue. Accepts the same arguments as
|
|
23
23
|
# `Zizq.enqueue`.
|
|
24
24
|
#
|
|
25
|
-
# @rbs job_class: Class & Zizq::
|
|
25
|
+
# @rbs job_class: Class & Zizq::JobConfig
|
|
26
26
|
# @rbs args: Array[untyped]
|
|
27
27
|
# @rbs kwargs: Hash[Symbol, untyped]
|
|
28
28
|
# @rbs &block: ?(EnqueueRequest) -> void
|
data/lib/zizq/client.rb
CHANGED
|
@@ -50,15 +50,30 @@ module Zizq
|
|
|
50
50
|
# Initialize a new instance of the client with the given base URL and
|
|
51
51
|
# optional format options.
|
|
52
52
|
#
|
|
53
|
+
# `read_timeout` and `stream_idle_timeout` are per-operation socket
|
|
54
|
+
# I/O timeouts (seconds). Each individual socket read/write is
|
|
55
|
+
# bounded by the timeout. The streaming `#take_jobs` endpoint uses
|
|
56
|
+
# `stream_idle_timeout` because the server sends heartbeats at
|
|
57
|
+
# periodic intervals which keeps the connection alive.
|
|
58
|
+
#
|
|
53
59
|
# @rbs url: String
|
|
54
60
|
# @rbs format: Zizq::format
|
|
55
61
|
# @rbs ssl_context: OpenSSL::SSL::SSLContext?
|
|
62
|
+
# @rbs read_timeout: Numeric
|
|
63
|
+
# @rbs stream_idle_timeout: Numeric
|
|
56
64
|
# @rbs return: void
|
|
57
|
-
def initialize(url:,
|
|
65
|
+
def initialize(url:,
|
|
66
|
+
format: :msgpack,
|
|
67
|
+
ssl_context: nil,
|
|
68
|
+
read_timeout: 30,
|
|
69
|
+
stream_idle_timeout: 30)
|
|
58
70
|
@url = url.chomp("/")
|
|
59
71
|
@format = format
|
|
60
72
|
|
|
61
|
-
endpoint_options = {
|
|
73
|
+
endpoint_options = {
|
|
74
|
+
protocol: Async::HTTP::Protocol::HTTP2,
|
|
75
|
+
timeout: read_timeout,
|
|
76
|
+
} #: Hash[Symbol, untyped]
|
|
62
77
|
endpoint_options[:ssl_context] = ssl_context if ssl_context
|
|
63
78
|
|
|
64
79
|
@endpoint = Async::HTTP::Endpoint.parse(
|
|
@@ -73,8 +88,14 @@ module Zizq
|
|
|
73
88
|
# on separate threads with their own HTTP/2 clients, so they're
|
|
74
89
|
# unaffected either way. HTTP/1.1 gives the stream a plain TCP
|
|
75
90
|
# socket with no framing tax and measurably better throughput.
|
|
91
|
+
#
|
|
92
|
+
# The stream endpoint uses `stream_idle_timeout` for its socket
|
|
93
|
+
# timeout so server heartbeats (~3s) keep it alive while only
|
|
94
|
+
# genuinely dead connections (no data for the full window)
|
|
95
|
+
# trigger a reconnect.
|
|
76
96
|
stream_endpoint_options = endpoint_options.merge(
|
|
77
97
|
protocol: Async::HTTP::Protocol::HTTP11,
|
|
98
|
+
timeout: stream_idle_timeout,
|
|
78
99
|
)
|
|
79
100
|
@stream_endpoint = Async::HTTP::Endpoint.parse(
|
|
80
101
|
@url,
|
|
@@ -204,7 +225,7 @@ module Zizq
|
|
|
204
225
|
|
|
205
226
|
# Get a single job by ID.
|
|
206
227
|
def get_job(id) #: (String) -> Resources::Job
|
|
207
|
-
response = get("/jobs/#{id}")
|
|
228
|
+
response = get("/jobs/#{enc(id)}")
|
|
208
229
|
data = handle_response!(response, expected: 200)
|
|
209
230
|
Resources::Job.new(self, data)
|
|
210
231
|
end
|
|
@@ -285,7 +306,7 @@ module Zizq
|
|
|
285
306
|
# @rbs id: String
|
|
286
307
|
# @rbs return: void
|
|
287
308
|
def delete_job(id)
|
|
288
|
-
response = delete("/jobs/#{id}")
|
|
309
|
+
response = delete("/jobs/#{enc(id)}")
|
|
289
310
|
handle_response!(response, expected: [200, 204])
|
|
290
311
|
nil
|
|
291
312
|
end
|
|
@@ -342,7 +363,7 @@ module Zizq
|
|
|
342
363
|
queue:, priority:, ready_at:,
|
|
343
364
|
retry_limit:, backoff:, retention:
|
|
344
365
|
)
|
|
345
|
-
response = patch("/jobs/#{id}", body)
|
|
366
|
+
response = patch("/jobs/#{enc(id)}", body)
|
|
346
367
|
data = handle_response!(response, expected: 200)
|
|
347
368
|
Resources::Job.new(self, data)
|
|
348
369
|
end
|
|
@@ -383,7 +404,7 @@ module Zizq
|
|
|
383
404
|
# @rbs attempt: Integer
|
|
384
405
|
# @rbs return: Resources::ErrorRecord
|
|
385
406
|
def get_error(id, attempt:)
|
|
386
|
-
response = get("/jobs/#{id}/errors/#{attempt}")
|
|
407
|
+
response = get("/jobs/#{enc(id)}/errors/#{enc(attempt.to_s)}")
|
|
387
408
|
data = handle_response!(response, expected: 200)
|
|
388
409
|
Resources::ErrorRecord.new(self, data)
|
|
389
410
|
end
|
|
@@ -397,7 +418,7 @@ module Zizq
|
|
|
397
418
|
# @rbs return: Resources::ErrorPage
|
|
398
419
|
def list_errors(id, from: nil, order: nil, limit: nil)
|
|
399
420
|
params = { from:, order:, limit: }.compact #: Hash[Symbol, untyped]
|
|
400
|
-
response = get("/jobs/#{id}/errors", params:)
|
|
421
|
+
response = get("/jobs/#{enc(id)}/errors", params:)
|
|
401
422
|
data = handle_response!(response, expected: 200)
|
|
402
423
|
Resources::ErrorPage.new(self, data)
|
|
403
424
|
end
|
|
@@ -422,6 +443,136 @@ module Zizq
|
|
|
422
443
|
data["queues"]
|
|
423
444
|
end
|
|
424
445
|
|
|
446
|
+
# List all cron group names.
|
|
447
|
+
#
|
|
448
|
+
# @rbs return: Array[String]
|
|
449
|
+
def list_cron_groups
|
|
450
|
+
response = get("/crons")
|
|
451
|
+
data = handle_response!(response, expected: 200)
|
|
452
|
+
data["crons"]
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# Fetch a cron group and all its entries.
|
|
456
|
+
#
|
|
457
|
+
# @rbs name: String
|
|
458
|
+
# @rbs return: Resources::CronGroup
|
|
459
|
+
def get_cron_group(name)
|
|
460
|
+
response = get("/crons/#{enc(name)}")
|
|
461
|
+
data = handle_response!(response, expected: 200)
|
|
462
|
+
Resources::CronGroup.new(self, data)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# Create or replace an entire cron group.
|
|
466
|
+
#
|
|
467
|
+
# Entries not present in the request are removed. Entries with unchanged
|
|
468
|
+
# expressions preserve their scheduling state.
|
|
469
|
+
#
|
|
470
|
+
# @rbs name: String
|
|
471
|
+
# @rbs paused: bool?
|
|
472
|
+
# @rbs entries: Array[Zizq::cron_entry_params]
|
|
473
|
+
# @rbs return: Resources::CronGroup
|
|
474
|
+
def replace_cron_group(name, paused: nil, entries: [])
|
|
475
|
+
body = {
|
|
476
|
+
paused:,
|
|
477
|
+
entries: entries.map { |entry| build_cron_entry(**entry) }
|
|
478
|
+
}.compact
|
|
479
|
+
response = put("/crons/#{enc(name)}", body)
|
|
480
|
+
data = handle_response!(response, expected: 200)
|
|
481
|
+
Resources::CronGroup.new(self, data)
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# Update group-level fields (currently just pause/unpause).
|
|
485
|
+
#
|
|
486
|
+
# @rbs name: String
|
|
487
|
+
# @rbs paused: bool?
|
|
488
|
+
# @rbs return: Resources::CronGroup
|
|
489
|
+
def update_cron_group(name, paused: nil)
|
|
490
|
+
response = patch("/crons/#{enc(name)}", { paused: }.compact)
|
|
491
|
+
data = handle_response!(response, expected: 200)
|
|
492
|
+
Resources::CronGroup.new(self, data)
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# Delete a cron group and all its entries.
|
|
496
|
+
#
|
|
497
|
+
# @rbs name: String
|
|
498
|
+
# @rbs return: void
|
|
499
|
+
def delete_cron_group(name)
|
|
500
|
+
response = delete("/crons/#{enc(name)}")
|
|
501
|
+
handle_response!(response, expected: 204)
|
|
502
|
+
nil
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
# Fetch a single cron entry.
|
|
506
|
+
#
|
|
507
|
+
# @rbs group: String
|
|
508
|
+
# @rbs entry: String
|
|
509
|
+
# @rbs return: Resources::CronEntry
|
|
510
|
+
def get_cron_group_entry(group, entry)
|
|
511
|
+
response = get("/crons/#{enc(group)}/entries/#{enc(entry)}")
|
|
512
|
+
data = handle_response!(response, expected: 200)
|
|
513
|
+
Resources::CronEntry.new(self, data)
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
# Add a single entry to a cron group (creates the group if needed).
|
|
517
|
+
#
|
|
518
|
+
# Raises a ClientError (409 Conflict) if an entry with the same name
|
|
519
|
+
# already exists.
|
|
520
|
+
#
|
|
521
|
+
# @rbs group: String
|
|
522
|
+
# @rbs name: String
|
|
523
|
+
# @rbs expression: String
|
|
524
|
+
# @rbs job: Zizq::cron_job_params
|
|
525
|
+
# @rbs timezone: String?
|
|
526
|
+
# @rbs paused: bool?
|
|
527
|
+
# @rbs return: Resources::CronEntry
|
|
528
|
+
def add_cron_group_entry(group, name:, expression:, job:, timezone: nil, paused: nil)
|
|
529
|
+
body = build_cron_entry(name:, expression:, job:, timezone:, paused:)
|
|
530
|
+
response = post("/crons/#{enc(group)}/entries", body)
|
|
531
|
+
data = handle_response!(response, expected: 201)
|
|
532
|
+
Resources::CronEntry.new(self, data)
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
# Create or replace a single cron entry.
|
|
536
|
+
#
|
|
537
|
+
# Preserves scheduling state if the expression is unchanged.
|
|
538
|
+
#
|
|
539
|
+
# @rbs group: String
|
|
540
|
+
# @rbs entry: String
|
|
541
|
+
# @rbs expression: String
|
|
542
|
+
# @rbs job: Zizq::cron_job_params
|
|
543
|
+
# @rbs timezone: String?
|
|
544
|
+
# @rbs paused: bool?
|
|
545
|
+
# @rbs return: Resources::CronEntry
|
|
546
|
+
def replace_cron_group_entry(group, entry, expression:, job:, timezone: nil, paused: nil)
|
|
547
|
+
body = build_cron_entry(name: entry, expression:, job:, timezone:, paused:)
|
|
548
|
+
response = put("/crons/#{enc(group)}/entries/#{enc(entry)}", body)
|
|
549
|
+
data = handle_response!(response, expected: 200)
|
|
550
|
+
Resources::CronEntry.new(self, data)
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
# Update entry-level fields (currently just pause/unpause).
|
|
554
|
+
#
|
|
555
|
+
# @rbs group: String
|
|
556
|
+
# @rbs entry: String
|
|
557
|
+
# @rbs paused: bool
|
|
558
|
+
# @rbs return: Resources::CronEntry
|
|
559
|
+
def update_cron_group_entry(group, entry, paused:)
|
|
560
|
+
response = patch("/crons/#{enc(group)}/entries/#{enc(entry)}", { paused: })
|
|
561
|
+
data = handle_response!(response, expected: 200)
|
|
562
|
+
Resources::CronEntry.new(self, data)
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
# Delete a single cron entry.
|
|
566
|
+
#
|
|
567
|
+
# @rbs group: String
|
|
568
|
+
# @rbs entry: String
|
|
569
|
+
# @rbs return: void
|
|
570
|
+
def delete_cron_group_entry(group, entry)
|
|
571
|
+
response = delete("/crons/#{enc(group)}/entries/#{enc(entry)}")
|
|
572
|
+
handle_response!(response, expected: 204)
|
|
573
|
+
nil
|
|
574
|
+
end
|
|
575
|
+
|
|
425
576
|
# Mark a job as successfully completed (ack).
|
|
426
577
|
#
|
|
427
578
|
# If this method (or [`#report_failure`]) is not called upon job
|
|
@@ -439,7 +590,7 @@ module Zizq
|
|
|
439
590
|
# The Zizq server sends heartbeat messages to connected workers so that
|
|
440
591
|
# it can quickly detect and handle disconnected clients.
|
|
441
592
|
def report_success(id) #: (String) -> nil
|
|
442
|
-
response = raw_post("/jobs/#{id}/success")
|
|
593
|
+
response = raw_post("/jobs/#{enc(id)}/success")
|
|
443
594
|
handle_response!(response, expected: 204)
|
|
444
595
|
nil
|
|
445
596
|
end
|
|
@@ -503,7 +654,7 @@ module Zizq
|
|
|
503
654
|
body[:retry_at] = (retry_at * 1000).to_i if retry_at
|
|
504
655
|
body[:kill] = kill if kill
|
|
505
656
|
|
|
506
|
-
response = post("/jobs/#{id}/failure", body)
|
|
657
|
+
response = post("/jobs/#{enc(id)}/failure", body)
|
|
507
658
|
data = handle_response!(response, expected: 200)
|
|
508
659
|
Resources::Job.new(self, data)
|
|
509
660
|
end
|
|
@@ -579,7 +730,12 @@ module Zizq
|
|
|
579
730
|
response.close rescue nil
|
|
580
731
|
end
|
|
581
732
|
end
|
|
582
|
-
rescue SocketError,
|
|
733
|
+
rescue SocketError,
|
|
734
|
+
IOError,
|
|
735
|
+
EOFError,
|
|
736
|
+
Errno::ECONNRESET,
|
|
737
|
+
Errno::EPIPE,
|
|
738
|
+
IO::TimeoutError,
|
|
583
739
|
OpenSSL::SSL::SSLError => e
|
|
584
740
|
raise ConnectionError, e.message
|
|
585
741
|
end
|
|
@@ -660,6 +816,11 @@ module Zizq
|
|
|
660
816
|
|
|
661
817
|
private
|
|
662
818
|
|
|
819
|
+
# URL-encode a single path segment.
|
|
820
|
+
def enc(value) #: (String) -> String
|
|
821
|
+
URI.encode_uri_component(value)
|
|
822
|
+
end
|
|
823
|
+
|
|
663
824
|
# Build a relative path with optional query parameters.
|
|
664
825
|
def build_path(path, params: {}) #: (String, ?params: Hash[Symbol, untyped]) -> String
|
|
665
826
|
unless params.empty?
|
|
@@ -668,6 +829,57 @@ module Zizq
|
|
|
668
829
|
path
|
|
669
830
|
end
|
|
670
831
|
|
|
832
|
+
# Validate and build a cron entry body from keyword arguments.
|
|
833
|
+
#
|
|
834
|
+
# @rbs name: String
|
|
835
|
+
# @rbs expression: String
|
|
836
|
+
# @rbs job: Zizq::cron_job_params
|
|
837
|
+
# @rbs timezone: String?
|
|
838
|
+
# @rbs paused: bool?
|
|
839
|
+
# @rbs return: Hash[Symbol, untyped]
|
|
840
|
+
def build_cron_entry(name: nil, expression: nil, job: nil, timezone: nil, paused: nil)
|
|
841
|
+
{
|
|
842
|
+
name:,
|
|
843
|
+
expression:,
|
|
844
|
+
timezone:,
|
|
845
|
+
paused:,
|
|
846
|
+
job: build_cron_job(**job),
|
|
847
|
+
}.compact
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
# Validate and build a cron job template from keyword arguments.
|
|
851
|
+
#
|
|
852
|
+
# Uses keyword args so that unknown keys raise ArgumentError.
|
|
853
|
+
#
|
|
854
|
+
# @rbs type: String
|
|
855
|
+
# @rbs queue: String
|
|
856
|
+
# @rbs payload: untyped
|
|
857
|
+
# @rbs priority: Integer?
|
|
858
|
+
# @rbs retry_limit: Integer?
|
|
859
|
+
# @rbs backoff: Zizq::backoff?
|
|
860
|
+
# @rbs retention: Zizq::retention?
|
|
861
|
+
# @rbs unique_key: String?
|
|
862
|
+
# @rbs unique_while: Zizq::unique_scope?
|
|
863
|
+
# @rbs return: Hash[Symbol, untyped]
|
|
864
|
+
def build_cron_job(type: nil,
|
|
865
|
+
queue: nil,
|
|
866
|
+
payload: nil,
|
|
867
|
+
priority: nil,
|
|
868
|
+
retry_limit: nil,
|
|
869
|
+
backoff: nil,
|
|
870
|
+
retention: nil,
|
|
871
|
+
unique_key: nil,
|
|
872
|
+
unique_while: nil)
|
|
873
|
+
job = { type:, queue:, payload: } #: Hash[Symbol, untyped]
|
|
874
|
+
job[:priority] = priority if priority
|
|
875
|
+
job[:retry_limit] = retry_limit if retry_limit
|
|
876
|
+
job[:backoff] = backoff if backoff
|
|
877
|
+
job[:retention] = retention if retention
|
|
878
|
+
job[:unique_key] = unique_key if unique_key
|
|
879
|
+
job[:unique_while] = unique_while.to_s if unique_while
|
|
880
|
+
job
|
|
881
|
+
end
|
|
882
|
+
|
|
671
883
|
# Validate and normalize filter parameters for bulk operations.
|
|
672
884
|
#
|
|
673
885
|
# Uses keyword arguments so that unknown keys raise ArgumentError.
|
|
@@ -942,6 +1154,18 @@ module Zizq
|
|
|
942
1154
|
end
|
|
943
1155
|
end
|
|
944
1156
|
|
|
1157
|
+
def put(path, body) #: (String, Hash[Symbol, untyped]) -> RawResponse
|
|
1158
|
+
request do |http|
|
|
1159
|
+
consume_response(
|
|
1160
|
+
http.put(
|
|
1161
|
+
build_path(path),
|
|
1162
|
+
{"content-type" => @content_type, "accept" => @content_type},
|
|
1163
|
+
Protocol::HTTP::Body::Buffered.wrap(encode_body(body))
|
|
1164
|
+
)
|
|
1165
|
+
)
|
|
1166
|
+
end
|
|
1167
|
+
end
|
|
1168
|
+
|
|
945
1169
|
def delete(path, params: {}) #: (String, ?params: Hash[Symbol, untyped]) -> RawResponse
|
|
946
1170
|
request do |http|
|
|
947
1171
|
consume_response(
|
data/lib/zizq/configuration.rb
CHANGED
|
@@ -42,6 +42,27 @@ module Zizq
|
|
|
42
42
|
# Note: Mutual TLS support requires a Zizq Pro license on the server.
|
|
43
43
|
attr_accessor :tls #: Zizq::tls_options?
|
|
44
44
|
|
|
45
|
+
# Per-operation socket I/O timeout (seconds) for regular API calls
|
|
46
|
+
# (enqueue, queries, mutations). Each socket read/write is bounded
|
|
47
|
+
# by this value. A request whose handshake or any single read exceeds
|
|
48
|
+
# this raises `IO::TimeoutError`.
|
|
49
|
+
#
|
|
50
|
+
# Default: 30.
|
|
51
|
+
attr_accessor :read_timeout #: Numeric
|
|
52
|
+
|
|
53
|
+
# Per-operation socket I/O timeout (seconds) for the long-lived
|
|
54
|
+
# `#take_jobs` stream. The server sends heartbeats every ~3 seconds,
|
|
55
|
+
# so each read returns within that window and keeps the connection
|
|
56
|
+
# alive; the connection only times out if the server falls silent for
|
|
57
|
+
# longer than this. The Worker catches the resulting error and
|
|
58
|
+
# reconnects with backoff.
|
|
59
|
+
#
|
|
60
|
+
# Should be comfortably above the server's heartbeat interval to
|
|
61
|
+
# avoid false-positive disconnects.
|
|
62
|
+
#
|
|
63
|
+
# Default: 30.
|
|
64
|
+
attr_accessor :stream_idle_timeout #: Numeric
|
|
65
|
+
|
|
45
66
|
# Middleware chain for enqueue. Each middleware receives an
|
|
46
67
|
# `EnqueueRequest` and a chain to continue.
|
|
47
68
|
attr_reader :enqueue_middleware #: Middleware::Chain[EnqueueRequest, EnqueueRequest]
|
|
@@ -55,6 +76,8 @@ module Zizq
|
|
|
55
76
|
@format = :msgpack
|
|
56
77
|
@logger = Logger.new($stdout, level: Logger::INFO)
|
|
57
78
|
@tls = nil
|
|
79
|
+
@read_timeout = 30
|
|
80
|
+
@stream_idle_timeout = 30
|
|
58
81
|
@enqueue_middleware = Middleware::Chain.new(Identity.new)
|
|
59
82
|
@dequeue_middleware = Middleware::Chain.new(Zizq::Job)
|
|
60
83
|
end
|
|
@@ -89,6 +112,14 @@ module Zizq
|
|
|
89
112
|
raise ArgumentError, "Zizq.configure: format must be :msgpack or :json, got #{format.inspect}"
|
|
90
113
|
end
|
|
91
114
|
|
|
115
|
+
unless read_timeout.is_a?(Numeric) && read_timeout > 0
|
|
116
|
+
raise ArgumentError, "Zizq.configure: read_timeout must be a positive number, got #{read_timeout.inspect}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
unless stream_idle_timeout.is_a?(Numeric) && stream_idle_timeout > 0
|
|
120
|
+
raise ArgumentError, "Zizq.configure: stream_idle_timeout must be a positive number, got #{stream_idle_timeout.inspect}"
|
|
121
|
+
end
|
|
122
|
+
|
|
92
123
|
tls = @tls
|
|
93
124
|
validate_tls!(tls) if tls
|
|
94
125
|
end
|