zizq 0.3.0 → 0.3.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a94a61282aba9442ace4ca8a3a302f75a843b14a76bd7d7748f006d5c8bf0888
4
- data.tar.gz: 202f0842b2a2b22d8fce753c197635ea456ac94955f6f266f806471d01d06a7f
3
+ metadata.gz: 5f0af940d511a8e1aa6d89df8ca13e98733430827d5f3202bcf7434a94ee02da
4
+ data.tar.gz: 9341d2815e344b3e4b8ca402185e3de01bd488cb5a3159f18e132d3c65cc18ca
5
5
  SHA512:
6
- metadata.gz: 03cb04925c7aac0aaaee30f86e27448641ec4864b903bedeee83cd61b0d0ed803655dd69c8ed586c4b83faa3d8d910c13c589398ca3157f130c569fa3edce5e9
7
- data.tar.gz: b3a2c1232195698681ae743e04db5aff883c4e82e36bab9d1735b7459c7db5fdbcb9829171fa30318cec4ac01dc34432a79c2760824f4d2db93c89fbda12da58
6
+ metadata.gz: a3e04dffd06a36db8ca940105675428390867bd8dee62b2bcf0ef7cbeef03fe080b35bf93061e9ec00b6834be0940e1d69d7bd837178718a0bd288b6895464c3
7
+ data.tar.gz: 9d5a7d77ca3e143dbcc8c5bb72a58eb1c60092335d206579646dac8df3cbf178a9627a0d8e7a68e394d2b07c32a60a82c915ab049175624760a314042d12f9b7
data/README.md CHANGED
@@ -1,12 +1,13 @@
1
1
  # Zizq — Official Ruby Client
2
2
 
3
+ This is the official Zizq client library for Ruby.
4
+
3
5
  Zizq is a simple, zero dependency, single binary job queue system that is both
4
6
  fast and durable. It is designed to work in any stack through a simple HTTP
5
7
  API.
6
8
 
7
- This is the official Zizq client library for Ruby.
8
-
9
9
  [![CI](https://github.com/zizq-labs/zizq-ruby/actions/workflows/ci.yml/badge.svg)](https://github.com/zizq-labs/zizq-ruby/actions/workflows/ci.yml)
10
+ [![Gem Version](https://img.shields.io/gem/v/zizq.svg)](https://rubygems.org/gems/zizq)
10
11
 
11
12
  ## Features
12
13
 
@@ -28,26 +29,63 @@ This is the official Zizq client library for Ruby.
28
29
  > If you have not yet installed the Zizq server, follow the
29
30
  > [Getting Started](https://zizq.io/docs/getting-started) guide first.
30
31
 
31
- Add it to your application's `Gemfile`.
32
+ Add it to your application's `Gemfile`:
32
33
 
33
- ``` ruby
34
- gem 'zizq', '~> 0.3.0'
34
+ ```ruby
35
+ gem 'zizq', '~> 0.3.2'
35
36
  ```
36
37
 
37
38
  Or install it manually:
38
39
 
39
40
  ```shell
40
- $ gem install zizq -v 0.3.0
41
+ $ gem install zizq -v 0.3.2
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.logger = Logger.new('log/zizq.log')
59
+
60
+ c.tls.ca = '/path/to/server-ca-cert.pem'
61
+
62
+ # Optional worker defaults — applied to every Zizq::Worker
63
+ # instance and to runs of the `zizq-worker` executable. Explicit
64
+ # kwargs or CLI flags override these.
65
+ c.worker.queues = ['emails', 'payments']
66
+ c.worker.fiber_count = 25
67
+ end
41
68
  ```
42
69
 
43
- ## Example
70
+ For mutual TLS, also set `c.tls.client_cert` and `c.tls.client_key`.
71
+
72
+ > [!CAUTION]
73
+ > If your server is exposed directly to the internet, it should require
74
+ > mutual TLS — otherwise anybody can talk to it.
75
+
76
+ ## Usage
44
77
 
45
78
  > [!TIP]
46
- > The client is very flexible and supports being used in a range of different
47
- > ways. Read the [full documentation](https://zizq.io/docs/clients/ruby/) on
48
- > the website for more details.
79
+ > This README is an overview. The
80
+ > [full documentation](https://zizq.io/docs/clients/ruby/) covers each
81
+ > feature in depth middleware, custom dispatchers, Active Job, job
82
+ > querying, and more.
49
83
 
50
- Mixin-based job class.
84
+ ### Defining a job
85
+
86
+ In most Ruby applications, a job is a plain class that includes `Zizq::Job`.
87
+ The class declares its defaults with the `zizq_*` DSL and implements
88
+ `#perform`:
51
89
 
52
90
  ```ruby
53
91
  class SendEmailJob
@@ -55,54 +93,140 @@ class SendEmailJob
55
93
 
56
94
  zizq_queue 'emails'
57
95
  zizq_priority 100
96
+ zizq_retry_limit 5
58
97
 
59
98
  def perform(user_id, template:)
60
- # your application logic here
99
+ user = User.find(user_id)
100
+ Mailer.deliver(user, template)
61
101
  end
62
102
  end
63
103
  ```
64
104
 
65
- Enqueueing a job.
105
+ Every default — `zizq_queue`, `zizq_priority`, `zizq_retry_limit`,
106
+ `zizq_backoff`, `zizq_retention`, `zizq_unique` — can be overridden per
107
+ enqueue. The job's class name (`"SendEmailJob"`) becomes the API-level job
108
+ type, so keep it stable once jobs are in flight.
109
+
110
+ ### Enqueuing jobs
111
+
112
+ Enqueue a job by passing the class and the arguments your `#perform` method
113
+ expects:
114
+
115
+ ```ruby
116
+ job = Zizq.enqueue(SendEmailJob, 42, template: 'welcome')
117
+ job.id # => "03fu0wm75gxgmfyfplwvazhex"
118
+ ```
119
+
120
+ Override defaults for a single call with `Zizq.enqueue_with`, or with a block
121
+ that mutates the request:
66
122
 
67
123
  ```ruby
68
- Zizq.enqueue(SendEmailJob, 42, template: 'welcome')
124
+ # Don't retry this one.
125
+ Zizq.enqueue_with(retry_limit: 0).enqueue(SendEmailJob, 42, template: 'welcome')
126
+
127
+ # Bump the priority via the block form.
128
+ Zizq.enqueue(SendEmailJob, 42, template: 'welcome') do |req|
129
+ req.priority = 1000
130
+ end
131
+ ```
132
+
133
+ Schedule a job for later with `delay` (seconds from now) or an absolute
134
+ `ready_at`:
135
+
136
+ ```ruby
137
+ Zizq.enqueue_with(delay: 3600).enqueue(SendEmailJob, 42, template: 'welcome')
138
+ Zizq.enqueue_with(ready_at: Time.new(2027, 3, 15, 14, 30)).enqueue(SendEmailJob, 42, template: 'welcome')
139
+ ```
140
+
141
+ To enqueue many jobs efficiently, `Zizq.enqueue_bulk` sends them in a single
142
+ atomic request — across queues and job types, and `enqueue_raw` enqueues can
143
+ be mixed in too:
144
+
145
+ ```ruby
146
+ Zizq.enqueue_bulk do |b|
147
+ signups.each { |user_id| b.enqueue(SendEmailJob, user_id, template: 'welcome') }
148
+ end
69
149
  ```
70
150
 
71
151
  > [!NOTE]
72
- > Jobs can also be enqueued and processed without `Zizq::Job`, which is
73
- > designed to support interoperability with any programming language.
152
+ > Jobs can also be enqueued without `Zizq::Job` via `Zizq.enqueue_raw` —
153
+ > designed for cross-language workflows where, for example, a Ruby app
154
+ > enqueues jobs consumed by a Go service.
155
+
156
+ ### Running a worker
74
157
 
75
- Using the included `zizq-worker` executable.
158
+ Jobs are processed by a worker, typically in a separate process. The simplest
159
+ way is the `zizq-worker` executable bundled with the gem. **Rails apps need
160
+ no arguments** — `zizq-worker` auto-detects `config/environment.rb` when run
161
+ from the app's root:
76
162
 
77
163
  ```shell
78
- $ zizq-worker --threads 5 --fibers 2 app.rb
79
- I, [2026-03-24T15:25:57.738131 #1331422] INFO -- : Zizq worker starting: 5 threads, 2 fibers, prefetch=20
80
- I, [2026-03-24T15:25:57.738222 #1331422] INFO -- : Queues: (all)
81
- I, [2026-03-24T15:25:57.739861 #1331422] INFO -- : Worker 0:0 started
82
- I, [2026-03-24T15:25:57.739962 #1331422] INFO -- : Worker 0:1 started
83
- I, [2026-03-24T15:25:57.740131 #1331422] INFO -- : Worker 1:0 started
84
- I, [2026-03-24T15:25:57.740211 #1331422] INFO -- : Worker 1:1 started
85
- I, [2026-03-24T15:25:57.740352 #1331422] INFO -- : Worker 2:0 started
86
- I, [2026-03-24T15:25:57.740408 #1331422] INFO -- : Worker 2:1 started
87
- I, [2026-03-24T15:25:57.740532 #1331422] INFO -- : Worker 3:0 started
88
- I, [2026-03-24T15:25:57.740590 #1331422] INFO -- : Worker 3:1 started
89
- I, [2026-03-24T15:25:57.740722 #1331422] INFO -- : Worker 4:0 started
90
- I, [2026-03-24T15:25:57.740776 #1331422] INFO -- : Worker 4:1 started
91
- I, [2026-03-24T15:25:57.740844 #1331422] INFO -- : Zizq producer thread started
92
- I, [2026-03-24T15:25:57.740878 #1331422] INFO -- : Connecting to http://localhost:7890...
93
- I, [2026-03-24T15:25:57.792173 #1331422] INFO -- : Connected. Listening for jobs.
164
+ $ bundle exec zizq-worker
165
+ I, [...] INFO -- : Zizq worker starting: 1 threads, 25 fibers, prefetch=50
166
+ I, [...] INFO -- : Connected. Listening for jobs.
94
167
  ```
95
168
 
96
- > [!NOTE]
97
- > Workers can also be created directly in code. There is no requirement to use
98
- > `zizq-worker`.
169
+ For Sinatra or other apps, pass the entrypoint explicitly:
170
+
171
+ ```shell
172
+ $ bundle exec zizq-worker app.rb
173
+ ```
174
+
175
+ Worker defaults (`queues`, `thread_count`, `fiber_count`, `prefetch`) come
176
+ from your `Zizq.configure { |c| c.worker.* }` block. CLI flags
177
+ (`--threads`, `--fibers`, `--queue`, `--all-queues`, etc.) override the
178
+ configured defaults when needed. Leave `--fibers 1` if your application
179
+ isn't fiber-safe — no `Async` context is loaded in that case. `INT` /
180
+ `TERM` trigger a graceful shutdown (drains in-flight jobs up to
181
+ `--shutdown-deadline`, default 30s).
182
+
183
+ For more control — for example running the worker in-process alongside a
184
+ Rack app — construct `Zizq::Worker` directly:
185
+
186
+ ```ruby
187
+ require 'zizq'
188
+
189
+ # Picks up queues, fiber_count, etc. from Zizq.configure { |c| c.worker.* };
190
+ # any kwarg here overrides those defaults.
191
+ worker = Zizq::Worker.new(queues: ['emails', 'payments'])
192
+
193
+ Signal.trap('INT') { worker.stop }
194
+ worker.run # blocks until the worker stops
195
+ ```
196
+
197
+ `#run` blocks until the worker terminates; `#stop` drains in-flight jobs
198
+ gracefully, `#kill` forces an immediate stop. On any unclean shutdown the
199
+ server returns unfinished jobs to the queue — no job is lost.
200
+
201
+ ### Recurring jobs (cron)
202
+
203
+ Define a cron schedule in your application's startup code. Definitions are
204
+ idempotent — every process can safely define the same schedule, and Zizq
205
+ keeps the server in sync by adding, replacing, and removing entries as the
206
+ definition changes. Cron requires a Pro license on the server.
207
+
208
+ ```ruby
209
+ Zizq.define_crontab('maintenance', timezone: 'Europe/London') do |cron|
210
+ # Every 15 minutes.
211
+ cron.define_entry('refresh_warehouse', '*/15 * * * *').enqueue(
212
+ RefreshWarehouseJob, incremental: true
213
+ )
214
+
215
+ # 9am London time, every day.
216
+ cron.define_entry('daily_digest', '0 9 * * *').enqueue(SendDailyDigestJob)
217
+ end
218
+ ```
219
+
220
+ Once defined, schedules can be inspected and managed via
221
+ `Zizq.crontab('maintenance')` — paused/resumed at the schedule level or per
222
+ entry, and deleted entirely when no longer needed.
99
223
 
100
224
  ## Resources
101
225
 
102
226
  * [Ruby Client Docs](https://zizq.io/docs/clients/ruby/)
103
227
  * [Getting Started Docs](https://zizq.io/docs/getting-started/)
104
228
  * [Zizq Command Reference](https://zizq.io/docs/cli/)
105
- * [Zizq Node.js Client Source](https://github.com/zizq-labs/zizq-node)
229
+ * [Zizq Ruby Client Source](https://github.com/zizq-labs/zizq-ruby)
106
230
  * [Zizq Source](https://github.com/zizq-labs/zizq)
107
231
 
108
232
  ## Support & Feedback
@@ -111,3 +235,7 @@ If you need help using Zizq,
111
235
  [create an issue](https://github.com/zizq-labs/zizq-ruby/issues) on the
112
236
  [zizq-ruby](https://github.com/zizq-labs/zizq-ruby) repo. Feedback is very
113
237
  welcome.
238
+
239
+ ## License
240
+
241
+ MIT — see [LICENSE](LICENSE).
data/bin/zizq-worker CHANGED
@@ -11,58 +11,96 @@ require "zizq"
11
11
  # Default deadline for a graceful `stop` before escalating to `kill`.
12
12
  DEFAULT_SHUTDOWN_DEADLINE = 30.0
13
13
 
14
- # --- Defaults from env vars, falling back to hardcoded defaults ---
15
-
16
- thread_count = Integer(ENV.fetch("ZIZQ_THREADS", Zizq::Worker::DEFAULT_THREADS))
17
- fiber_count = Integer(ENV.fetch("ZIZQ_FIBERS", Zizq::Worker::DEFAULT_FIBERS))
14
+ # --- Defaults from env vars (nil = "not set, defer to Zizq.configure then Worker default") ---
15
+ #
16
+ # Note: env-var-unset values stay as nil rather than being seeded
17
+ # with the Worker's `DEFAULT_*` constants. This lets `Zizq.configure
18
+ # { |c| c.worker.<field> = ... }` win when neither the env nor the
19
+ # CLI explicitly set the value.
20
+
21
+ thread_count = ENV.key?("ZIZQ_THREADS") ? Integer(ENV["ZIZQ_THREADS"]) : nil
22
+ fiber_count = ENV.key?("ZIZQ_FIBERS") ? Integer(ENV["ZIZQ_FIBERS"]) : nil
18
23
  prefetch = ENV.key?("ZIZQ_PREFETCH") ? Integer(ENV["ZIZQ_PREFETCH"]) : nil
19
24
  shutdown_deadline = Float(ENV.fetch("ZIZQ_SHUTDOWN_DEADLINE", DEFAULT_SHUTDOWN_DEADLINE))
20
- retry_min_wait = Float(ENV.fetch("ZIZQ_RETRY_MIN_WAIT", Zizq::Worker::DEFAULT_RETRY_MIN_WAIT))
21
- retry_max_wait = Float(ENV.fetch("ZIZQ_RETRY_MAX_WAIT", Zizq::Worker::DEFAULT_RETRY_MAX_WAIT))
22
- retry_multiplier = Float(ENV.fetch("ZIZQ_RETRY_MULTIPLIER", Zizq::Worker::DEFAULT_RETRY_MULTIPLIER))
23
-
24
- queues = if ENV.key?("ZIZQ_QUEUES")
25
- ENV["ZIZQ_QUEUES"].split(",").map(&:strip).reject(&:empty?)
26
- else
27
- []
25
+ retry_min_wait = ENV.key?("ZIZQ_RETRY_MIN_WAIT") ? Float(ENV["ZIZQ_RETRY_MIN_WAIT"]) : nil
26
+ retry_max_wait = ENV.key?("ZIZQ_RETRY_MAX_WAIT") ? Float(ENV["ZIZQ_RETRY_MAX_WAIT"]) : nil
27
+ retry_multiplier = ENV.key?("ZIZQ_RETRY_MULTIPLIER") ? Float(ENV["ZIZQ_RETRY_MULTIPLIER"]) : nil
28
+
29
+ # Queue state: explicit list vs. all-queues vs. unset (defer to config).
30
+ # `queues_set_by_user` toggles to true once any CLI/env queue flag is seen.
31
+ queues = []
32
+ queues_set_by_user = false
33
+ all_queues = false
34
+
35
+ if ENV.key?("ZIZQ_QUEUES")
36
+ parsed = ENV["ZIZQ_QUEUES"].split(",").map(&:strip).reject(&:empty?)
37
+ queues = parsed
38
+ queues_set_by_user = true
28
39
  end
29
40
 
30
41
  # --- CLI flag parsing (overrides env var defaults) ---
31
42
 
32
43
  parser = OptionParser.new do |opts|
33
- opts.banner = "Usage: zizq-worker [OPTIONS] <ENTRYPOINT>"
44
+ opts.banner = "Usage: zizq-worker [OPTIONS] [ENTRYPOINT]"
34
45
 
35
46
  opts.separator ""
36
- opts.separator "Start a Zizq worker process. The ENTRYPOINT is a Ruby file that loads your"
37
- opts.separator "application (e.g. config/environment.rb for a Rails app)."
47
+ opts.separator "Start a Zizq worker process."
48
+ opts.separator ""
49
+ opts.separator "The ENTRYPOINT is a Ruby file that loads your application before the"
50
+ opts.separator "worker starts (so Zizq.configure runs and your job classes are loaded)."
51
+ opts.separator "Resolved in this order:"
52
+ opts.separator ""
53
+ opts.separator " 1. The ENTRYPOINT positional argument, if given."
54
+ opts.separator " 2. The ZIZQ_ENTRYPOINT environment variable, if set."
55
+ opts.separator " 3. config/environment.rb in the current directory, if it exists."
56
+ opts.separator " (This is the canonical Rails boot file, so Rails apps run with"
57
+ opts.separator " no entrypoint argument at all.)"
58
+ opts.separator ""
59
+ opts.separator "Configuration:"
60
+ opts.separator ""
61
+ opts.separator " Client config (url, format, TLS, logger) and worker defaults"
62
+ opts.separator " (queues, thread/fiber count, prefetch, etc.) belong in your"
63
+ opts.separator " Zizq.configure block inside the entrypoint, e.g.:"
38
64
  opts.separator ""
39
- opts.separator "Client configuration (url, format, logger) should be set in the"
40
- opts.separator "entrypoint via Zizq.configure."
65
+ opts.separator " Zizq.configure do |c|"
66
+ opts.separator " c.url = \"https://...\""
67
+ opts.separator " c.worker.queues = [\"emails\", \"webhooks\"]"
68
+ opts.separator " c.worker.fiber_count = 25"
69
+ opts.separator " end"
70
+ opts.separator ""
71
+ opts.separator " CLI flags and env vars below override whatever is configured there."
41
72
  opts.separator ""
42
73
  opts.separator "Options:"
43
74
 
44
- opts.on("-t", "--threads N", Integer, "Number of worker threads (default: #{Zizq::Worker::DEFAULT_THREADS}, env: ZIZQ_THREADS)") do |n|
75
+ opts.on("-t", "--threads N", Integer, "Number of worker threads (fallback default: #{Zizq::Worker::DEFAULT_THREADS}, env: ZIZQ_THREADS)") do |n|
45
76
  thread_count = n
46
77
  end
47
78
 
48
- opts.on("-f", "--fibers N", Integer, "Number of fibers per thread (default: #{Zizq::Worker::DEFAULT_FIBERS}, env: ZIZQ_FIBERS)") do |n|
79
+ opts.on("-f", "--fibers N", Integer, "Number of fibers per thread (fallback default: #{Zizq::Worker::DEFAULT_FIBERS}, env: ZIZQ_FIBERS)") do |n|
49
80
  fiber_count = n
50
81
  end
51
82
 
52
- opts.on("-p", "--prefetch N", Integer, "Prefetch count (default: 2*threads*fibers, env: ZIZQ_PREFETCH)") do |n|
83
+ opts.on("-p", "--prefetch N", Integer, "Prefetch count (fallback default: 2*threads*fibers, env: ZIZQ_PREFETCH)") do |n|
53
84
  prefetch = n
54
85
  end
55
86
 
56
87
  queues_from_cli = false
57
88
  opts.on("-q", "--queue QUEUE", "Queue to process (repeatable or comma-separated, env: ZIZQ_QUEUES)") do |q|
58
- # First -q flag replaces the env var default entirely
89
+ # First CLI `-q` replaces the env var default; subsequent ones
90
+ # accumulate. `queues_from_cli` distinguishes that from
91
+ # `queues_set_by_user` (which also tracks env-var input).
59
92
  unless queues_from_cli
60
93
  queues = []
61
94
  queues_from_cli = true
62
95
  end
96
+ queues_set_by_user = true
63
97
  queues.concat(q.split(",").map(&:strip).reject(&:empty?))
64
98
  end
65
99
 
100
+ opts.on("--all-queues", "Override any configured queues to process all queues (mutually exclusive with -q)") do
101
+ all_queues = true
102
+ end
103
+
66
104
  opts.on("--shutdown-deadline N", Float, "Graceful shutdown deadline in seconds before escalating to kill (default: #{DEFAULT_SHUTDOWN_DEADLINE}, env: ZIZQ_SHUTDOWN_DEADLINE)") do |n|
67
105
  shutdown_deadline = n
68
106
  end
@@ -100,22 +138,38 @@ end
100
138
 
101
139
  # --- Validate options ---
102
140
 
103
- if thread_count < 1
141
+ if thread_count && thread_count < 1
104
142
  warn "Error: --threads must be at least 1 (got #{thread_count})"
105
143
  exit 1
106
144
  end
107
145
 
108
- if fiber_count < 1
146
+ if fiber_count && fiber_count < 1
109
147
  warn "Error: --fibers must be at least 1 (got #{fiber_count})"
110
148
  exit 1
111
149
  end
112
150
 
113
- # --- Validate and load entrypoint ---
151
+ if all_queues && queues_set_by_user
152
+ warn "Error: --all-queues and --queue (or ZIZQ_QUEUES) are mutually exclusive."
153
+ exit 1
154
+ end
155
+
156
+ # --- Resolve and load entrypoint ---
157
+ #
158
+ # Precedence:
159
+ # 1. CLI argument: `zizq-worker path/to/entrypoint.rb`
160
+ # 2. ZIZQ_ENTRYPOINT env var
161
+ # 3. Auto-detect: `config/environment.rb` (Rails apps)
114
162
 
115
- entrypoint = ARGV[0]
163
+ entrypoint = ARGV[0] || ENV["ZIZQ_ENTRYPOINT"] || (
164
+ File.file?("config/environment.rb") ? "config/environment.rb" : nil
165
+ )
116
166
 
117
167
  if entrypoint.nil?
118
- warn "Error: missing required ENTRYPOINT argument."
168
+ warn "Error: no entrypoint found."
169
+ warn ""
170
+ warn "Specify one as a CLI argument or via the ZIZQ_ENTRYPOINT env var."
171
+ warn "Rails apps are detected automatically via the presence of"
172
+ warn "config/environment.rb in the current directory."
119
173
  warn ""
120
174
  warn parser.help
121
175
  exit 1
@@ -128,17 +182,33 @@ end
128
182
 
129
183
  require File.expand_path(entrypoint)
130
184
 
131
- # --- Start the worker ---
185
+ # --- Resolve queues ---
186
+ #
187
+ # --all-queues -> [] (explicit "all queues" override)
188
+ # -q / ZIZQ_QUEUES set -> the parsed list
189
+ # neither -> nil (defer to Zizq.configure → Worker default)
190
+
191
+ queues_value =
192
+ if all_queues
193
+ []
194
+ elsif queues_set_by_user
195
+ queues
196
+ end
132
197
 
133
- worker = Zizq::Worker.new(
198
+ # --- Start the worker ---
199
+ #
200
+ # `.compact` drops any unset kwarg so `Zizq::Worker#initialize`'s
201
+ # fallback chain (kwarg -> Zizq.configuration.worker -> DEFAULT_*)
202
+ # applies for whichever values weren't given on the CLI / via env.
203
+ worker = Zizq::Worker.new(**{
134
204
  thread_count:,
135
205
  fiber_count:,
136
206
  prefetch:,
137
- queues:,
207
+ queues: queues_value,
138
208
  retry_min_wait:,
139
209
  retry_max_wait:,
140
210
  retry_multiplier:,
141
- )
211
+ }.compact)
142
212
 
143
213
  # `Zizq::Worker#stop` is patient (waits forever for in-flight jobs and
144
214
  # acks to drain). We enforce the shutdown deadline at the CLI level: the
@@ -154,7 +224,10 @@ stopping = false
154
224
  %w[INT TERM].each do |signal|
155
225
  Signal.trap(signal) do
156
226
  if stopping
157
- exit(1)
227
+ # Second signal: hard exit. `exit!` (not `exit`) so SystemExit
228
+ # terminates the process from any thread context, including
229
+ # the signal trap.
230
+ exit!(1)
158
231
  else
159
232
  worker.stop
160
233
  stopping = true
@@ -164,7 +237,10 @@ stopping = false
164
237
  worker.logger.warn do
165
238
  "Worker did not stop within #{shutdown_deadline}s, killing..."
166
239
  end
167
- exit(1)
240
+ # `exit!` rather than `exit`: this runs in a watchdog thread,
241
+ # and `exit` would only raise SystemExit in *this* thread,
242
+ # leaving main (joining the hung worker thread) untouched.
243
+ exit!(1)
168
244
  end
169
245
  end
170
246
  end
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:, format: :msgpack, ssl_context: nil)
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 = { protocol: Async::HTTP::Protocol::HTTP2 } #: Hash[Symbol, untyped]
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,
@@ -709,7 +730,12 @@ module Zizq
709
730
  response.close rescue nil
710
731
  end
711
732
  end
712
- rescue SocketError, IOError, EOFError, Errno::ECONNRESET, Errno::EPIPE,
733
+ rescue SocketError,
734
+ IOError,
735
+ EOFError,
736
+ Errno::ECONNRESET,
737
+ Errno::EPIPE,
738
+ IO::TimeoutError,
713
739
  OpenSSL::SSL::SSLError => e
714
740
  raise ConnectionError, e.message
715
741
  end
@@ -29,18 +29,26 @@ module Zizq
29
29
  # Logger instance to which to write log messages.
30
30
  attr_accessor :logger #: Logger
31
31
 
32
- # TLS options for connecting to the server over HTTPS.
32
+ # Per-operation socket I/O timeout (seconds) for regular API calls
33
+ # (enqueue, queries, mutations). Each socket read/write is bounded
34
+ # by this value. A request whose handshake or any single read exceeds
35
+ # this raises `IO::TimeoutError`.
33
36
  #
34
- # All values may be PEM-encoded strings or file paths.
37
+ # Default: 30.
38
+ attr_accessor :read_timeout #: Numeric
39
+
40
+ # Per-operation socket I/O timeout (seconds) for the long-lived
41
+ # `#take_jobs` stream. The server sends heartbeats every ~3 seconds,
42
+ # so each read returns within that window and keeps the connection
43
+ # alive; the connection only times out if the server falls silent for
44
+ # longer than this. The Worker catches the resulting error and
45
+ # reconnects with backoff.
35
46
  #
36
- # {
37
- # ca: "path/to/ca-cert.pem", # CA certificate for server verification
38
- # client_cert: "path/to/client-cert.pem", # Client certificate for mTLS
39
- # client_key: "path/to/client-key.pem", # Client private key for mTLS
40
- # }
47
+ # Should be comfortably above the server's heartbeat interval to
48
+ # avoid false-positive disconnects.
41
49
  #
42
- # Note: Mutual TLS support requires a Zizq Pro license on the server.
43
- attr_accessor :tls #: Zizq::tls_options?
50
+ # Default: 30.
51
+ attr_accessor :stream_idle_timeout #: Numeric
44
52
 
45
53
  # Middleware chain for enqueue. Each middleware receives an
46
54
  # `EnqueueRequest` and a chain to continue.
@@ -55,10 +63,79 @@ module Zizq
55
63
  @format = :msgpack
56
64
  @logger = Logger.new($stdout, level: Logger::INFO)
57
65
  @tls = nil
66
+ @worker = nil
67
+ @read_timeout = 30
68
+ @stream_idle_timeout = 30
58
69
  @enqueue_middleware = Middleware::Chain.new(Identity.new)
59
70
  @dequeue_middleware = Middleware::Chain.new(Zizq::Job)
60
71
  end
61
72
 
73
+ # TLS settings for connecting to the server over HTTPS.
74
+ #
75
+ # Configure via the `c.tls` accessors inside a `Zizq.configure`
76
+ # block:
77
+ #
78
+ # Zizq.configure do |c|
79
+ # c.tls.ca = "/path/to/server-ca-cert.pem"
80
+ # c.tls.client_cert = "/path/to/client-cert.pem"
81
+ # c.tls.client_key = "/path/to/client-key.pem"
82
+ # end
83
+ #
84
+ # All values may be PEM-encoded strings or file paths. Set
85
+ # `c.tls = nil` to explicitly disable TLS.
86
+ #
87
+ # Note: Mutual TLS support requires a Zizq Pro license on the
88
+ # server.
89
+ def tls #: () -> TlsConfiguration
90
+ @tls ||= TlsConfiguration.new
91
+ end
92
+
93
+ def tls=(value) #: ((Hash[Symbol, String?] | TlsConfiguration)?) -> void
94
+ case value
95
+ when nil
96
+ @tls = nil
97
+ when TlsConfiguration
98
+ @tls = value
99
+ when Hash
100
+ @tls = TlsConfiguration.new(**value)
101
+ else
102
+ raise ArgumentError,
103
+ "Zizq.configure: tls= expects a Hash, Zizq::TlsConfiguration, or nil " \
104
+ "(got #{value.class})"
105
+ end
106
+ end
107
+
108
+ # Defaults for `Zizq::Worker` instances. Apps populate this in
109
+ # their `Zizq.configure` block:
110
+ #
111
+ # Zizq.configure do |c|
112
+ # c.worker.queues = ["emails"]
113
+ # c.worker.fiber_count = 25
114
+ # end
115
+ #
116
+ # Anything left unset here falls through to the Worker's
117
+ # hardcoded defaults; anything explicitly passed to `Worker.new`
118
+ # (or set via `zizq-worker` CLI flags / env vars) overrides
119
+ # whatever is configured here.
120
+ def worker #: () -> WorkerConfiguration
121
+ @worker ||= WorkerConfiguration.new
122
+ end
123
+
124
+ def worker=(value) #: ((Hash[Symbol, untyped] | WorkerConfiguration)?) -> void
125
+ case value
126
+ when nil
127
+ @worker = nil
128
+ when WorkerConfiguration
129
+ @worker = value
130
+ when Hash
131
+ @worker = WorkerConfiguration.new(**value)
132
+ else
133
+ raise ArgumentError,
134
+ "Zizq.configure: worker= expects a Hash, Zizq::WorkerConfiguration, or nil " \
135
+ "(got #{value.class})"
136
+ end
137
+ end
138
+
62
139
  # The job dispatcher.
63
140
  # This is the terminal of the dequeue middleware chain.
64
141
  # Defaults to `Zizq::Job` which finds and executes jobs written by mixing
@@ -89,6 +166,14 @@ module Zizq
89
166
  raise ArgumentError, "Zizq.configure: format must be :msgpack or :json, got #{format.inspect}"
90
167
  end
91
168
 
169
+ unless read_timeout.is_a?(Numeric) && read_timeout > 0
170
+ raise ArgumentError, "Zizq.configure: read_timeout must be a positive number, got #{read_timeout.inspect}"
171
+ end
172
+
173
+ unless stream_idle_timeout.is_a?(Numeric) && stream_idle_timeout > 0
174
+ raise ArgumentError, "Zizq.configure: stream_idle_timeout must be a positive number, got #{stream_idle_timeout.inspect}"
175
+ end
176
+
92
177
  tls = @tls
93
178
  validate_tls!(tls) if tls
94
179
  end
@@ -99,21 +184,22 @@ module Zizq
99
184
  def ssl_context #: () -> OpenSSL::SSL::SSLContext?
100
185
  tls = @tls
101
186
  return nil unless tls
187
+ return nil if tls.to_h.values.all?(&:nil?)
102
188
 
103
189
  ctx = OpenSSL::SSL::SSLContext.new
104
190
 
105
- if (ca = tls[:ca])
191
+ if (ca = tls.ca)
106
192
  store = OpenSSL::X509::Store.new
107
193
  store.add_cert(load_cert(ca))
108
194
  ctx.cert_store = store
109
195
  ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
110
196
  end
111
197
 
112
- if (client_cert = tls[:client_cert])
198
+ if (client_cert = tls.client_cert)
113
199
  ctx.cert = load_cert(client_cert)
114
200
  end
115
201
 
116
- if (client_key = tls[:client_key])
202
+ if (client_key = tls.client_key)
117
203
  ctx.key = load_key(client_key)
118
204
  end
119
205
 
@@ -130,14 +216,13 @@ module Zizq
130
216
 
131
217
  private
132
218
 
133
- # @rbs tls: Zizq::tls_options
134
- def validate_tls!(tls) #: (Zizq::tls_options) -> void
135
- if tls[:client_cert] && !tls[:client_key]
136
- raise ArgumentError, "Zizq.configure: tls[:client_key] is required when tls[:client_cert] is set"
219
+ def validate_tls!(tls) #: (TlsConfiguration) -> void
220
+ if tls.client_cert && !tls.client_key
221
+ raise ArgumentError, "Zizq.configure: tls.client_key is required when tls.client_cert is set"
137
222
  end
138
223
 
139
- if tls[:client_key] && !tls[:client_cert]
140
- raise ArgumentError, "Zizq.configure: tls[:client_cert] is required when tls[:client_key] is set"
224
+ if tls.client_key && !tls.client_cert
225
+ raise ArgumentError, "Zizq.configure: tls.client_cert is required when tls.client_key is set"
141
226
  end
142
227
  end
143
228
 
@@ -0,0 +1,27 @@
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
+ # TLS settings for connecting to the Zizq server over HTTPS.
9
+ #
10
+ # Set inside a `Zizq.configure` block via the `c.tls` accessors:
11
+ #
12
+ # Zizq.configure do |c|
13
+ # c.tls.ca = "/path/to/ca-cert.pem"
14
+ # c.tls.client_cert = "/path/to/client-cert.pem"
15
+ # c.tls.client_key = "/path/to/client-key.pem"
16
+ # end
17
+ #
18
+ # All values may be PEM-encoded strings or file paths.
19
+ #
20
+ # Note: Mutual TLS support requires a Zizq Pro license on the server.
21
+ TlsConfiguration = Struct.new(
22
+ :ca, #: String?
23
+ :client_cert, #: String?
24
+ :client_key, #: String?
25
+ keyword_init: true
26
+ )
27
+ end
data/lib/zizq/version.rb CHANGED
@@ -5,5 +5,5 @@
5
5
  # frozen_string_literal: true
6
6
 
7
7
  module Zizq
8
- VERSION = "0.3.0" #: String
8
+ VERSION = "0.3.2" #: String
9
9
  end
data/lib/zizq/worker.rb CHANGED
@@ -15,7 +15,7 @@ module Zizq
15
15
  #
16
16
  # Total concurrency is calculated as `thread_count * fiber_count`.
17
17
  class Worker
18
- DEFAULT_THREADS = 5 #: Integer
18
+ DEFAULT_THREADS = 1 #: Integer
19
19
  DEFAULT_FIBERS = 1 #: Integer
20
20
  DEFAULT_RETRY_MIN_WAIT = 1
21
21
  DEFAULT_RETRY_MAX_WAIT = 30
@@ -50,11 +50,6 @@ module Zizq
50
50
  # pipeline full while ack round-trips are in flight.
51
51
  attr_reader :prefetch #: Integer
52
52
 
53
- # Proc to derive a worker ID string for each thread and fiber.
54
- #
55
- # When not present, the Zizq server assigns a random worker ID.
56
- attr_reader :worker_id_proc #: (^(Integer, Integer) -> String?)?
57
-
58
53
  # An instance of a Logger to be used for worker logging.
59
54
  attr_reader :logger #: Logger
60
55
 
@@ -66,45 +61,55 @@ module Zizq
66
61
  # their own `Zizq::Middleware::Chain` if middleware needs to be applied.
67
62
  attr_reader :dispatcher #: ^(Resources::Job) -> void
68
63
 
69
- # @rbs queues: Array[String]
70
- # @rbs thread_count: Integer
71
- # @rbs fiber_count: Integer
64
+ # All keyword arguments default to `nil` and follow a three-level
65
+ # fallback chain:
66
+ #
67
+ # 1. Explicit kwarg passed to `Worker.new`.
68
+ # 2. `Zizq.configuration.worker.<field>` set in the app's
69
+ # `Zizq.configure` block.
70
+ # 3. The Worker's hardcoded `DEFAULT_*` constants.
71
+ #
72
+ # @rbs queues: Array[String]?
73
+ # @rbs thread_count: Integer?
74
+ # @rbs fiber_count: Integer?
72
75
  # @rbs prefetch: Integer?
73
- # @rbs retry_min_wait: (Float | Integer)
74
- # @rbs retry_max_wait: (Float | Integer)
75
- # @rbs retry_multiplier: (Float | Integer)
76
- # @rbs worker_id: (^(Integer, Integer) -> String?)?
76
+ # @rbs retry_min_wait: (Float | Integer)?
77
+ # @rbs retry_max_wait: (Float | Integer)?
78
+ # @rbs retry_multiplier: (Float | Integer)?
77
79
  # @rbs logger: Logger?
78
80
  # @rbs dispatcher: (^(Resources::Job) -> void)?
79
81
  # @rbs return: void
80
82
  def initialize(
81
- queues: [],
82
- thread_count: DEFAULT_THREADS,
83
- fiber_count: DEFAULT_FIBERS,
83
+ queues: nil,
84
+ thread_count: nil,
85
+ fiber_count: nil,
84
86
  prefetch: nil,
85
- retry_min_wait: DEFAULT_RETRY_MIN_WAIT,
86
- retry_max_wait: DEFAULT_RETRY_MAX_WAIT,
87
- retry_multiplier: DEFAULT_RETRY_MULTIPLIER,
88
- worker_id: nil,
87
+ retry_min_wait: nil,
88
+ retry_max_wait: nil,
89
+ retry_multiplier: nil,
89
90
  logger: nil,
90
91
  dispatcher: nil
91
92
  )
92
- raise ArgumentError, "thread_count must be at least 1 (got #{thread_count})" if thread_count < 1
93
- raise ArgumentError, "fiber_count must be at least 1 (got #{fiber_count})" if fiber_count < 1
94
-
95
93
  Zizq.configuration.validate!
96
-
97
- @queues = queues
98
- @thread_count = thread_count
99
- @fiber_count = fiber_count
100
- @prefetch = prefetch || thread_count * fiber_count * 2
101
- @retry_min_wait = retry_min_wait
102
- @retry_max_wait = retry_max_wait
103
- @retry_multiplier = retry_multiplier
104
- @worker_id_proc = worker_id
94
+ config = Zizq.configuration.worker
95
+
96
+ @queues = queues || config.queues || []
97
+ @thread_count = thread_count || config.thread_count || DEFAULT_THREADS
98
+ @fiber_count = fiber_count || config.fiber_count || DEFAULT_FIBERS
99
+ @prefetch = prefetch || config.prefetch || @thread_count * @fiber_count * 2
100
+ @retry_min_wait = retry_min_wait || config.retry_min_wait || DEFAULT_RETRY_MIN_WAIT
101
+ @retry_max_wait = retry_max_wait || config.retry_max_wait || DEFAULT_RETRY_MAX_WAIT
102
+ @retry_multiplier = retry_multiplier || config.retry_multiplier || DEFAULT_RETRY_MULTIPLIER
105
103
  @logger = logger || Zizq.configuration.logger
106
104
  @dispatcher = dispatcher || Zizq.configuration.dequeue_middleware
107
105
 
106
+ if @thread_count < 1
107
+ raise ArgumentError, "thread_count must be at least 1 (got #{@thread_count})"
108
+ end
109
+ if @fiber_count < 1
110
+ raise ArgumentError, "fiber_count must be at least 1 (got #{@fiber_count})"
111
+ end
112
+
108
113
  reset_runtime_state
109
114
  end
110
115
 
@@ -360,14 +365,12 @@ module Zizq
360
365
  format("Worker %d:%d started", thread_idx, fiber_idx)
361
366
  end
362
367
 
363
- wid = resolve_worker_id(thread_idx, fiber_idx)
364
-
365
368
  loop do
366
369
  # pop returns nil when queue is closed and empty
367
370
  job = @dispatch_queue.pop
368
371
  break if job.nil?
369
372
 
370
- dispatch(job, wid)
373
+ dispatch(job)
371
374
  end
372
375
 
373
376
  logger.info do
@@ -392,7 +395,7 @@ module Zizq
392
395
  #
393
396
  # Delegates to the configured dispatcher (default: `Zizq::Job.dispatch`)
394
397
  # and reports success or failure.
395
- def dispatch(job, worker_id) #: (Resources::Job, String?) -> void
398
+ def dispatch(job) #: (Resources::Job) -> void
396
399
  job_id, job_type = job.id, job.type
397
400
 
398
401
  begin
@@ -460,8 +463,5 @@ module Zizq
460
463
  ))
461
464
  end
462
465
 
463
- def resolve_worker_id(thread_idx, fiber_idx) #: (Integer, Integer) -> String?
464
- worker_id_proc&.call(thread_idx, fiber_idx)
465
- end
466
466
  end
467
467
  end
@@ -0,0 +1,48 @@
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
+ # Defaults for `Zizq::Worker` instances. Accessed via
9
+ # `Zizq.configuration.worker` and typically populated inside an
10
+ # application's `Zizq.configure` block:
11
+ #
12
+ # Zizq.configure do |c|
13
+ # c.url = "https://..."
14
+ # c.worker.queues = ["emails", "webhooks"]
15
+ # c.worker.thread_count = 1
16
+ # c.worker.fiber_count = 25
17
+ # end
18
+ #
19
+ # Every field defaults to `nil`, meaning "use the Worker's own
20
+ # hardcoded default." Anything explicitly passed to `Worker.new` —
21
+ # or set via CLI flag / env var when launching `zizq-worker` —
22
+ # overrides whatever is set here.
23
+ #
24
+ # See `Zizq::Worker#initialize` for the full resolution order
25
+ # (explicit kwarg → Zizq.configuration.worker → `Worker::DEFAULT_*`).
26
+ #
27
+ # Fields:
28
+ #
29
+ # * `queues` — Queues to consume. `[]` means all queues.
30
+ # * `thread_count` — Number of worker threads.
31
+ # * `fiber_count` — Number of fibers per worker thread.
32
+ # * `prefetch` — Server-side prefetch limit. Defaults to
33
+ # `2 * threads * fibers`.
34
+ # * `retry_min_wait` — Minimum reconnect backoff in seconds.
35
+ # * `retry_max_wait` — Maximum reconnect backoff in seconds.
36
+ # * `retry_multiplier` — Multiplicative backoff factor between
37
+ # reconnect attempts.
38
+ WorkerConfiguration = Struct.new(
39
+ :queues, #: Array[String]?
40
+ :thread_count, #: Integer?
41
+ :fiber_count, #: Integer?
42
+ :prefetch, #: Integer?
43
+ :retry_min_wait, #: (Float | Integer)?
44
+ :retry_max_wait, #: (Float | Integer)?
45
+ :retry_multiplier, #: (Float | Integer)?
46
+ keyword_init: true
47
+ )
48
+ end
data/lib/zizq.rb CHANGED
@@ -29,7 +29,9 @@ module Zizq
29
29
  autoload :Lifecycle, "zizq/lifecycle"
30
30
  autoload :Query, "zizq/query"
31
31
  autoload :Resources, "zizq/resources"
32
+ autoload :TlsConfiguration, "zizq/tls_configuration"
32
33
  autoload :Worker, "zizq/worker"
34
+ autoload :WorkerConfiguration, "zizq/worker_configuration"
33
35
 
34
36
  # Sentinel indicating a field should not be included in the request.
35
37
  # Used as the default for update parameters.
@@ -81,7 +83,9 @@ module Zizq
81
83
  @client = Client.new(
82
84
  url: configuration.url,
83
85
  format: configuration.format,
84
- ssl_context: configuration.ssl_context
86
+ ssl_context: configuration.ssl_context,
87
+ read_timeout: configuration.read_timeout,
88
+ stream_idle_timeout: configuration.stream_idle_timeout
85
89
  )
86
90
  end
87
91
  end
@@ -42,11 +42,19 @@ module Zizq
42
42
  # Initialize a new instance of the client with the given base URL and
43
43
  # optional format options.
44
44
  #
45
+ # `read_timeout` and `stream_idle_timeout` are per-operation socket
46
+ # I/O timeouts (seconds). Each individual socket read/write is
47
+ # bounded by the timeout. The streaming `#take_jobs` endpoint uses
48
+ # `stream_idle_timeout` because the server sends heartbeats at
49
+ # periodic intervals which keeps the connection alive.
50
+ #
45
51
  # @rbs url: String
46
52
  # @rbs format: Zizq::format
47
53
  # @rbs ssl_context: OpenSSL::SSL::SSLContext?
54
+ # @rbs read_timeout: Numeric
55
+ # @rbs stream_idle_timeout: Numeric
48
56
  # @rbs return: void
49
- def initialize: (url: String, ?format: Zizq::format, ?ssl_context: OpenSSL::SSL::SSLContext?) -> void
57
+ def initialize: (url: String, ?format: Zizq::format, ?ssl_context: OpenSSL::SSL::SSLContext?, ?read_timeout: Numeric, ?stream_idle_timeout: Numeric) -> void
50
58
 
51
59
  # Close all thread-local HTTP clients and release connections.
52
60
  def close: () -> untyped
@@ -22,18 +22,26 @@ module Zizq
22
22
  # Logger instance to which to write log messages.
23
23
  attr_accessor logger: Logger
24
24
 
25
- # TLS options for connecting to the server over HTTPS.
25
+ # Per-operation socket I/O timeout (seconds) for regular API calls
26
+ # (enqueue, queries, mutations). Each socket read/write is bounded
27
+ # by this value. A request whose handshake or any single read exceeds
28
+ # this raises `IO::TimeoutError`.
26
29
  #
27
- # All values may be PEM-encoded strings or file paths.
30
+ # Default: 30.
31
+ attr_accessor read_timeout: Numeric
32
+
33
+ # Per-operation socket I/O timeout (seconds) for the long-lived
34
+ # `#take_jobs` stream. The server sends heartbeats every ~3 seconds,
35
+ # so each read returns within that window and keeps the connection
36
+ # alive; the connection only times out if the server falls silent for
37
+ # longer than this. The Worker catches the resulting error and
38
+ # reconnects with backoff.
28
39
  #
29
- # {
30
- # ca: "path/to/ca-cert.pem", # CA certificate for server verification
31
- # client_cert: "path/to/client-cert.pem", # Client certificate for mTLS
32
- # client_key: "path/to/client-key.pem", # Client private key for mTLS
33
- # }
40
+ # Should be comfortably above the server's heartbeat interval to
41
+ # avoid false-positive disconnects.
34
42
  #
35
- # Note: Mutual TLS support requires a Zizq Pro license on the server.
36
- attr_accessor tls: Zizq::tls_options?
43
+ # Default: 30.
44
+ attr_accessor stream_idle_timeout: Numeric
37
45
 
38
46
  # Middleware chain for enqueue. Each middleware receives an
39
47
  # `EnqueueRequest` and a chain to continue.
@@ -45,6 +53,42 @@ module Zizq
45
53
 
46
54
  def initialize: () -> untyped
47
55
 
56
+ # TLS settings for connecting to the server over HTTPS.
57
+ #
58
+ # Configure via the `c.tls` accessors inside a `Zizq.configure`
59
+ # block:
60
+ #
61
+ # Zizq.configure do |c|
62
+ # c.tls.ca = "/path/to/server-ca-cert.pem"
63
+ # c.tls.client_cert = "/path/to/client-cert.pem"
64
+ # c.tls.client_key = "/path/to/client-key.pem"
65
+ # end
66
+ #
67
+ # All values may be PEM-encoded strings or file paths. Set
68
+ # `c.tls = nil` to explicitly disable TLS.
69
+ #
70
+ # Note: Mutual TLS support requires a Zizq Pro license on the
71
+ # server.
72
+ def tls: () -> untyped
73
+
74
+ def tls=: (untyped value) -> untyped
75
+
76
+ # Defaults for `Zizq::Worker` instances. Apps populate this in
77
+ # their `Zizq.configure` block:
78
+ #
79
+ # Zizq.configure do |c|
80
+ # c.worker.queues = ["emails"]
81
+ # c.worker.fiber_count = 25
82
+ # end
83
+ #
84
+ # Anything left unset here falls through to the Worker's
85
+ # hardcoded defaults; anything explicitly passed to `Worker.new`
86
+ # (or set via `zizq-worker` CLI flags / env vars) overrides
87
+ # whatever is configured here.
88
+ def worker: () -> untyped
89
+
90
+ def worker=: (untyped value) -> untyped
91
+
48
92
  # The job dispatcher.
49
93
  # This is the terminal of the dequeue middleware chain.
50
94
  # Defaults to `Zizq::Job` which finds and executes jobs written by mixing
@@ -79,8 +123,7 @@ module Zizq
79
123
 
80
124
  private
81
125
 
82
- # @rbs tls: Zizq::tls_options
83
- def validate_tls!: (Zizq::tls_options tls) -> untyped
126
+ def validate_tls!: (untyped tls) -> untyped
84
127
 
85
128
  # Load a certificate from a PEM string or file path.
86
129
  def load_cert: (untyped pem_or_path) -> untyped
@@ -0,0 +1,27 @@
1
+ # Generated from lib/zizq/tls_configuration.rb with RBS::Inline
2
+
3
+ module Zizq
4
+ # TLS settings for connecting to the Zizq server over HTTPS.
5
+ #
6
+ # Set inside a `Zizq.configure` block via the `c.tls` accessors:
7
+ #
8
+ # Zizq.configure do |c|
9
+ # c.tls.ca = "/path/to/ca-cert.pem"
10
+ # c.tls.client_cert = "/path/to/client-cert.pem"
11
+ # c.tls.client_key = "/path/to/client-key.pem"
12
+ # end
13
+ #
14
+ # All values may be PEM-encoded strings or file paths.
15
+ #
16
+ # Note: Mutual TLS support requires a Zizq Pro license on the server.
17
+ class TlsConfiguration < Struct[String?]
18
+ attr_accessor ca(): String?
19
+
20
+ attr_accessor client_cert(): String?
21
+
22
+ attr_accessor client_key(): String?
23
+
24
+ def self.new: (?ca: String?, ?client_cert: String?, ?client_key: String?) -> instance
25
+ | ({ ?ca: String?, ?client_cert: String?, ?client_key: String? }) -> instance
26
+ end
27
+ end
@@ -46,11 +46,6 @@ module Zizq
46
46
  # pipeline full while ack round-trips are in flight.
47
47
  attr_reader prefetch: Integer
48
48
 
49
- # Proc to derive a worker ID string for each thread and fiber.
50
- #
51
- # When not present, the Zizq server assigns a random worker ID.
52
- attr_reader worker_id_proc: (^(Integer, Integer) -> String?)?
53
-
54
49
  # An instance of a Logger to be used for worker logging.
55
50
  attr_reader logger: Logger
56
51
 
@@ -62,18 +57,25 @@ module Zizq
62
57
  # their own `Zizq::Middleware::Chain` if middleware needs to be applied.
63
58
  attr_reader dispatcher: ^(Resources::Job) -> void
64
59
 
65
- # @rbs queues: Array[String]
66
- # @rbs thread_count: Integer
67
- # @rbs fiber_count: Integer
60
+ # All keyword arguments default to `nil` and follow a three-level
61
+ # fallback chain:
62
+ #
63
+ # 1. Explicit kwarg passed to `Worker.new`.
64
+ # 2. `Zizq.configuration.worker.<field>` set in the app's
65
+ # `Zizq.configure` block.
66
+ # 3. The Worker's hardcoded `DEFAULT_*` constants.
67
+ #
68
+ # @rbs queues: Array[String]?
69
+ # @rbs thread_count: Integer?
70
+ # @rbs fiber_count: Integer?
68
71
  # @rbs prefetch: Integer?
69
- # @rbs retry_min_wait: (Float | Integer)
70
- # @rbs retry_max_wait: (Float | Integer)
71
- # @rbs retry_multiplier: (Float | Integer)
72
- # @rbs worker_id: (^(Integer, Integer) -> String?)?
72
+ # @rbs retry_min_wait: (Float | Integer)?
73
+ # @rbs retry_max_wait: (Float | Integer)?
74
+ # @rbs retry_multiplier: (Float | Integer)?
73
75
  # @rbs logger: Logger?
74
76
  # @rbs dispatcher: (^(Resources::Job) -> void)?
75
77
  # @rbs return: void
76
- def initialize: (?queues: Array[String], ?thread_count: Integer, ?fiber_count: Integer, ?prefetch: Integer?, ?retry_min_wait: Float | Integer, ?retry_max_wait: Float | Integer, ?retry_multiplier: Float | Integer, ?worker_id: (^(Integer, Integer) -> String?)?, ?logger: Logger?, ?dispatcher: (^(Resources::Job) -> void)?) -> void
78
+ def initialize: (?queues: Array[String]?, ?thread_count: Integer?, ?fiber_count: Integer?, ?prefetch: Integer?, ?retry_min_wait: (Float | Integer)?, ?retry_max_wait: (Float | Integer)?, ?retry_multiplier: (Float | Integer)?, ?logger: Logger?, ?dispatcher: (^(Resources::Job) -> void)?) -> void
77
79
 
78
80
  # Request a graceful shutdown.
79
81
  #
@@ -136,7 +138,7 @@ module Zizq
136
138
  #
137
139
  # Delegates to the configured dispatcher (default: `Zizq::Job.dispatch`)
138
140
  # and reports success or failure.
139
- def dispatch: (untyped job, untyped worker_id) -> untyped
141
+ def dispatch: (untyped job) -> untyped
140
142
 
141
143
  # @rbs job_id: String
142
144
  # @rbs return: void
@@ -146,7 +148,5 @@ module Zizq
146
148
  # @rbs error: Exception
147
149
  # @rbs return: void
148
150
  def push_nack: (String job_id, Exception error) -> void
149
-
150
- def resolve_worker_id: (untyped thread_idx, untyped fiber_idx) -> untyped
151
151
  end
152
152
  end
@@ -0,0 +1,52 @@
1
+ # Generated from lib/zizq/worker_configuration.rb with RBS::Inline
2
+
3
+ module Zizq
4
+ # Defaults for `Zizq::Worker` instances. Accessed via
5
+ # `Zizq.configuration.worker` and typically populated inside an
6
+ # application's `Zizq.configure` block:
7
+ #
8
+ # Zizq.configure do |c|
9
+ # c.url = "https://..."
10
+ # c.worker.queues = ["emails", "webhooks"]
11
+ # c.worker.thread_count = 1
12
+ # c.worker.fiber_count = 25
13
+ # end
14
+ #
15
+ # Every field defaults to `nil`, meaning "use the Worker's own
16
+ # hardcoded default." Anything explicitly passed to `Worker.new` —
17
+ # or set via CLI flag / env var when launching `zizq-worker` —
18
+ # overrides whatever is set here.
19
+ #
20
+ # See `Zizq::Worker#initialize` for the full resolution order
21
+ # (explicit kwarg → Zizq.configuration.worker → `Worker::DEFAULT_*`).
22
+ #
23
+ # Fields:
24
+ #
25
+ # * `queues` — Queues to consume. `[]` means all queues.
26
+ # * `thread_count` — Number of worker threads.
27
+ # * `fiber_count` — Number of fibers per worker thread.
28
+ # * `prefetch` — Server-side prefetch limit. Defaults to
29
+ # `2 * threads * fibers`.
30
+ # * `retry_min_wait` — Minimum reconnect backoff in seconds.
31
+ # * `retry_max_wait` — Maximum reconnect backoff in seconds.
32
+ # * `retry_multiplier` — Multiplicative backoff factor between
33
+ # reconnect attempts.
34
+ class WorkerConfiguration < Struct[Array[String]? | Integer? | (Float | Integer)?]
35
+ attr_accessor queues(): Array[String]?
36
+
37
+ attr_accessor thread_count(): Integer?
38
+
39
+ attr_accessor fiber_count(): Integer?
40
+
41
+ attr_accessor prefetch(): Integer?
42
+
43
+ attr_accessor retry_min_wait(): (Float | Integer)?
44
+
45
+ attr_accessor retry_max_wait(): (Float | Integer)?
46
+
47
+ attr_accessor retry_multiplier(): (Float | Integer)?
48
+
49
+ def self.new: (?queues: Array[String]?, ?thread_count: Integer?, ?fiber_count: Integer?, ?prefetch: Integer?, ?retry_min_wait: (Float | Integer)?, ?retry_max_wait: (Float | Integer)?, ?retry_multiplier: (Float | Integer)?) -> instance
50
+ | ({ ?queues: Array[String]?, ?thread_count: Integer?, ?fiber_count: Integer?, ?prefetch: Integer?, ?retry_min_wait: (Float | Integer)?, ?retry_max_wait: (Float | Integer)?, ?retry_multiplier: (Float | Integer)? }) -> instance
51
+ end
52
+ end
data/sig/zizq.rbs CHANGED
@@ -67,7 +67,8 @@ module Zizq
67
67
  ?paused: bool?
68
68
  }
69
69
 
70
- # TLS options for connecting over HTTPS. Values are PEM strings or file paths.
70
+ # TLS options hash accepted by `Configuration#tls=` for back-compat.
71
+ # Values are PEM strings or file paths.
71
72
  type tls_options = { ?ca: String, ?client_cert: String, ?client_key: String }
72
73
 
73
74
  # Any object that can dispatch a job. Must respond to #call(job).
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.0
4
+ version: 0.3.2
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-06 00:00:00.000000000 Z
11
+ date: 2026-05-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async-http
@@ -38,11 +38,27 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.7'
41
- description: |+
41
+ description: |-
42
42
  This is the official Ruby client for the Zizq job queue server.
43
43
 
44
44
  Zizq is a simple, single binary, zero dependency, language agnostic job queue.
45
45
 
46
+ Features:
47
+
48
+ - Enqueue and process jobs across programming languages
49
+ - Persistent/journalled
50
+ - Multi-thread and/or multi-fiber
51
+ - Scheduled jobs
52
+ - Prioritized queues
53
+ - Optional ActiveJob integration
54
+ - Unique jobs
55
+ - Cron scheduling (recurring jobs)
56
+ - Job introspection and management, including `jq` filters
57
+
58
+
59
+ This client supports multi-threaded and/or multi-fiber concurrency and is very fast. The Zizq server provides everything needed. There are no separate external storage dependencies to configure such as Redis or a RDBMS.
60
+
61
+ See https://zizq.io for full details and documentation.
46
62
  email:
47
63
  executables:
48
64
  - zizq-worker
@@ -84,8 +100,10 @@ files:
84
100
  - lib/zizq/resources/job_template.rb
85
101
  - lib/zizq/resources/page.rb
86
102
  - lib/zizq/resources/resource.rb
103
+ - lib/zizq/tls_configuration.rb
87
104
  - lib/zizq/version.rb
88
105
  - lib/zizq/worker.rb
106
+ - lib/zizq/worker_configuration.rb
89
107
  - sig/generated/zizq.rbs
90
108
  - sig/generated/zizq/ack_processor.rbs
91
109
  - sig/generated/zizq/active_job_config.rbs
@@ -115,8 +133,10 @@ files:
115
133
  - sig/generated/zizq/resources/job_template.rbs
116
134
  - sig/generated/zizq/resources/page.rbs
117
135
  - sig/generated/zizq/resources/resource.rbs
136
+ - sig/generated/zizq/tls_configuration.rbs
118
137
  - sig/generated/zizq/version.rbs
119
138
  - sig/generated/zizq/worker.rbs
139
+ - sig/generated/zizq/worker_configuration.rbs
120
140
  - sig/zizq.rbs
121
141
  homepage: https://zizq.io
122
142
  licenses:
@@ -145,4 +165,3 @@ signing_key:
145
165
  specification_version: 4
146
166
  summary: The official Ruby client for the Zizq job queue
147
167
  test_files: []
148
- ...