zizq 0.3.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a94a61282aba9442ace4ca8a3a302f75a843b14a76bd7d7748f006d5c8bf0888
4
- data.tar.gz: 202f0842b2a2b22d8fce753c197635ea456ac94955f6f266f806471d01d06a7f
3
+ metadata.gz: 8f79a2e4601f42c8c9334601383a3659ffa5063a5df3ce2575b1d56b4df17a89
4
+ data.tar.gz: 730dbf9a98a6bb877234f3821e099e0363c5ebfad4905622106fa19b17db6610
5
5
  SHA512:
6
- metadata.gz: 03cb04925c7aac0aaaee30f86e27448641ec4864b903bedeee83cd61b0d0ed803655dd69c8ed586c4b83faa3d8d910c13c589398ca3157f130c569fa3edce5e9
7
- data.tar.gz: b3a2c1232195698681ae743e04db5aff883c4e82e36bab9d1735b7459c7db5fdbcb9829171fa30318cec4ac01dc34432a79c2760824f4d2db93c89fbda12da58
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
  [![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,56 @@ 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.1'
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.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
41
61
  ```
42
62
 
43
- ## Example
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
44
70
 
45
71
  > [!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.
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
49
78
 
50
- Mixin-based job class.
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`:
51
82
 
52
83
  ```ruby
53
84
  class SendEmailJob
@@ -55,54 +86,133 @@ class SendEmailJob
55
86
 
56
87
  zizq_queue 'emails'
57
88
  zizq_priority 100
89
+ zizq_retry_limit 5
58
90
 
59
91
  def perform(user_id, template:)
60
- # your application logic here
92
+ user = User.find(user_id)
93
+ Mailer.deliver(user, template)
61
94
  end
62
95
  end
63
96
  ```
64
97
 
65
- Enqueueing a job.
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:
107
+
108
+ ```ruby
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`:
66
128
 
67
129
  ```ruby
68
- Zizq.enqueue(SendEmailJob, 42, template: 'welcome')
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
69
142
  ```
70
143
 
71
144
  > [!NOTE]
72
- > Jobs can also be enqueued and processed without `Zizq::Job`, which is
73
- > designed to support interoperability with any programming language.
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
74
150
 
75
- Using the included `zizq-worker` executable.
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:
76
154
 
77
155
  ```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.
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.
94
159
  ```
95
160
 
96
- > [!NOTE]
97
- > Workers can also be created directly in code. There is no requirement to use
98
- > `zizq-worker`.
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.
99
209
 
100
210
  ## Resources
101
211
 
102
212
  * [Ruby Client Docs](https://zizq.io/docs/clients/ruby/)
103
213
  * [Getting Started Docs](https://zizq.io/docs/getting-started/)
104
214
  * [Zizq Command Reference](https://zizq.io/docs/cli/)
105
- * [Zizq Node.js Client Source](https://github.com/zizq-labs/zizq-node)
215
+ * [Zizq Ruby Client Source](https://github.com/zizq-labs/zizq-ruby)
106
216
  * [Zizq Source](https://github.com/zizq-labs/zizq)
107
217
 
108
218
  ## Support & Feedback
@@ -111,3 +221,7 @@ If you need help using Zizq,
111
221
  [create an issue](https://github.com/zizq-labs/zizq-ruby/issues) on the
112
222
  [zizq-ruby](https://github.com/zizq-labs/zizq-ruby) repo. Feedback is very
113
223
  welcome.
224
+
225
+ ## License
226
+
227
+ MIT — see [LICENSE](LICENSE).
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
@@ -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
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.1" #: String
9
9
  end
data/lib/zizq.rb CHANGED
@@ -81,7 +81,9 @@ module Zizq
81
81
  @client = Client.new(
82
82
  url: configuration.url,
83
83
  format: configuration.format,
84
- ssl_context: configuration.ssl_context
84
+ ssl_context: configuration.ssl_context,
85
+ read_timeout: configuration.read_timeout,
86
+ stream_idle_timeout: configuration.stream_idle_timeout
85
87
  )
86
88
  end
87
89
  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
@@ -35,6 +35,27 @@ module Zizq
35
35
  # Note: Mutual TLS support requires a Zizq Pro license on the server.
36
36
  attr_accessor tls: Zizq::tls_options?
37
37
 
38
+ # Per-operation socket I/O timeout (seconds) for regular API calls
39
+ # (enqueue, queries, mutations). Each socket read/write is bounded
40
+ # by this value. A request whose handshake or any single read exceeds
41
+ # this raises `IO::TimeoutError`.
42
+ #
43
+ # Default: 30.
44
+ attr_accessor read_timeout: Numeric
45
+
46
+ # Per-operation socket I/O timeout (seconds) for the long-lived
47
+ # `#take_jobs` stream. The server sends heartbeats every ~3 seconds,
48
+ # so each read returns within that window and keeps the connection
49
+ # alive; the connection only times out if the server falls silent for
50
+ # longer than this. The Worker catches the resulting error and
51
+ # reconnects with backoff.
52
+ #
53
+ # Should be comfortably above the server's heartbeat interval to
54
+ # avoid false-positive disconnects.
55
+ #
56
+ # Default: 30.
57
+ attr_accessor stream_idle_timeout: Numeric
58
+
38
59
  # Middleware chain for enqueue. Each middleware receives an
39
60
  # `EnqueueRequest` and a chain to continue.
40
61
  attr_reader enqueue_middleware: Middleware::Chain[EnqueueRequest, EnqueueRequest]
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.1
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-21 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
@@ -145,4 +161,3 @@ signing_key:
145
161
  specification_version: 4
146
162
  summary: The official Ruby client for the Zizq job queue
147
163
  test_files: []
148
- ...