zizq 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  3. data/lib/active_job/queue_adapters/zizq_adapter.rb +42 -20
  4. data/lib/zizq/ack_processor.rb +17 -11
  5. data/lib/zizq/active_job_config.rb +30 -10
  6. data/lib/zizq/backoff.rb +5 -1
  7. data/lib/zizq/bulk_enqueue.rb +7 -1
  8. data/lib/zizq/client.rb +363 -167
  9. data/lib/zizq/configuration.rb +15 -14
  10. data/lib/zizq/crontab.rb +35 -33
  11. data/lib/zizq/crontab_builder.rb +1 -7
  12. data/lib/zizq/crontab_entry.rb +36 -35
  13. data/lib/zizq/crontab_entry_builder.rb +15 -18
  14. data/lib/zizq/enqueue_request.rb +51 -38
  15. data/lib/zizq/enqueue_with.rb +1 -3
  16. data/lib/zizq/error.rb +12 -6
  17. data/lib/zizq/job.rb +31 -13
  18. data/lib/zizq/job_config.rb +217 -13
  19. data/lib/zizq/lifecycle.rb +10 -2
  20. data/lib/zizq/middleware.rb +7 -6
  21. data/lib/zizq/query.rb +154 -53
  22. data/lib/zizq/resources/cron_entry.rb +7 -7
  23. data/lib/zizq/resources/cron_group.rb +5 -5
  24. data/lib/zizq/resources/error_enumerator.rb +11 -9
  25. data/lib/zizq/resources/error_page.rb +2 -1
  26. data/lib/zizq/resources/error_record.rb +5 -5
  27. data/lib/zizq/resources/job.rb +41 -24
  28. data/lib/zizq/resources/job_page.rb +19 -8
  29. data/lib/zizq/resources/job_template.rb +16 -8
  30. data/lib/zizq/resources/page.rb +2 -1
  31. data/lib/zizq/resources/resource.rb +5 -4
  32. data/lib/zizq/resources.rb +9 -9
  33. data/lib/zizq/router.rb +3 -2
  34. data/lib/zizq/test/client.rb +79 -58
  35. data/lib/zizq/test.rb +8 -8
  36. data/lib/zizq/tls_configuration.rb +7 -6
  37. data/lib/zizq/version.rb +1 -1
  38. data/lib/zizq/worker.rb +68 -50
  39. data/lib/zizq/worker_configuration.rb +11 -10
  40. data/lib/zizq.rb +48 -41
  41. data/sig/generated/zizq/active_job_config.rbs +16 -5
  42. data/sig/generated/zizq/bulk_enqueue.rbs +1 -0
  43. data/sig/generated/zizq/client.rbs +36 -4
  44. data/sig/generated/zizq/crontab_entry_builder.rbs +1 -0
  45. data/sig/generated/zizq/enqueue_request.rbs +7 -2
  46. data/sig/generated/zizq/job.rbs +6 -0
  47. data/sig/generated/zizq/job_config.rbs +98 -0
  48. data/sig/generated/zizq/query.rbs +55 -2
  49. data/sig/generated/zizq/resources/job.rbs +2 -0
  50. data/sig/generated/zizq/resources/job_template.rbs +5 -2
  51. data/sig/generated/zizq/resources/page.rbs +1 -1
  52. data/sig/generated/zizq.rbs +1 -0
  53. data/sig/zizq.rbs +16 -2
  54. metadata +2 -2
data/lib/zizq/client.rb CHANGED
@@ -62,24 +62,23 @@ module Zizq
62
62
  # @rbs read_timeout: Numeric
63
63
  # @rbs stream_idle_timeout: Numeric
64
64
  # @rbs return: void
65
- def initialize(url:,
66
- format: :msgpack,
67
- ssl_context: nil,
68
- read_timeout: 30,
69
- stream_idle_timeout: 30)
65
+ def initialize(
66
+ url:,
67
+ format: :msgpack,
68
+ ssl_context: nil,
69
+ read_timeout: 30,
70
+ stream_idle_timeout: 30
71
+ )
70
72
  @url = url.chomp("/")
71
73
  @format = format
72
74
 
73
75
  endpoint_options = {
74
76
  protocol: Async::HTTP::Protocol::HTTP2,
75
- timeout: read_timeout,
77
+ timeout: read_timeout
76
78
  } #: Hash[Symbol, untyped]
77
79
  endpoint_options[:ssl_context] = ssl_context if ssl_context
78
80
 
79
- @endpoint = Async::HTTP::Endpoint.parse(
80
- @url,
81
- **endpoint_options,
82
- )
81
+ @endpoint = Async::HTTP::Endpoint.parse(@url, **endpoint_options)
83
82
 
84
83
  # Streaming take uses a dedicated HTTP/1.1 endpoint. The take
85
84
  # connection is long-lived and carries only one request, so HTTP/2's
@@ -93,14 +92,13 @@ module Zizq
93
92
  # timeout so server heartbeats (~3s) keep it alive while only
94
93
  # genuinely dead connections (no data for the full window)
95
94
  # trigger a reconnect.
96
- stream_endpoint_options = endpoint_options.merge(
97
- protocol: Async::HTTP::Protocol::HTTP11,
98
- timeout: stream_idle_timeout,
99
- )
100
- @stream_endpoint = Async::HTTP::Endpoint.parse(
101
- @url,
102
- **stream_endpoint_options,
103
- )
95
+ stream_endpoint_options =
96
+ endpoint_options.merge(
97
+ protocol: Async::HTTP::Protocol::HTTP11,
98
+ timeout: stream_idle_timeout
99
+ )
100
+ @stream_endpoint =
101
+ Async::HTTP::Endpoint.parse(@url, **stream_endpoint_options)
104
102
 
105
103
  @mutex = Mutex.new
106
104
 
@@ -166,17 +164,21 @@ module Zizq
166
164
  # @rbs retention: Zizq::retention?
167
165
  # @rbs unique_key: String?
168
166
  # @rbs unique_while: Zizq::unique_scope?
167
+ # @rbs batch: Zizq::batch?
169
168
  # @rbs return: Resources::Job
170
- def enqueue(queue:,
171
- type:,
172
- payload:,
173
- priority: nil,
174
- ready_at: nil,
175
- retry_limit: nil,
176
- backoff: nil,
177
- retention: nil,
178
- unique_key: nil,
179
- unique_while: nil)
169
+ def enqueue(
170
+ queue:,
171
+ type:,
172
+ payload:,
173
+ priority: nil,
174
+ ready_at: nil,
175
+ retry_limit: nil,
176
+ backoff: nil,
177
+ retention: nil,
178
+ unique_key: nil,
179
+ unique_while: nil,
180
+ batch: nil
181
+ )
180
182
  body = { queue:, type:, payload: } #: Hash[Symbol, untyped]
181
183
  body[:priority] = priority if priority
182
184
  # ready_at is fractional seconds in Ruby; the server expects ms.
@@ -186,6 +188,7 @@ module Zizq
186
188
  body[:retention] = retention if retention
187
189
  body[:unique_key] = unique_key if unique_key
188
190
  body[:unique_while] = unique_while.to_s if unique_while
191
+ body[:batch] = batch if batch
189
192
 
190
193
  response = post("/jobs", body)
191
194
  data = handle_response!(response, expected: [200, 201])
@@ -204,18 +207,26 @@ module Zizq
204
207
  # @rbs return: Array[Resources::Job]
205
208
  def enqueue_bulk(jobs:)
206
209
  body = {
207
- jobs: jobs.map do |job|
208
- wire = { type: job[:type], queue: job[:queue], payload: job[:payload] } #: Hash[Symbol, untyped]
209
- wire[:priority] = job[:priority] if job[:priority]
210
- # ready_at is fractional seconds in Ruby; the server expects ms.
211
- wire[:ready_at] = (job[:ready_at].to_f * 1000).to_i if job[:ready_at]
212
- wire[:retry_limit] = job[:retry_limit] if job[:retry_limit]
213
- wire[:backoff] = job[:backoff] if job[:backoff]
214
- wire[:retention] = job[:retention] if job[:retention]
215
- wire[:unique_key] = job[:unique_key] if job[:unique_key]
216
- wire[:unique_while] = job[:unique_while].to_s if job[:unique_while]
217
- wire
218
- end
210
+ jobs:
211
+ jobs.map do |job|
212
+ wire = {
213
+ type: job[:type],
214
+ queue: job[:queue],
215
+ payload: job[:payload]
216
+ } #: Hash[Symbol, untyped]
217
+ wire[:priority] = job[:priority] if job[:priority]
218
+ # ready_at is fractional seconds in Ruby; the server expects ms.
219
+ wire[:ready_at] = (job[:ready_at].to_f * 1000).to_i if job[
220
+ :ready_at
221
+ ]
222
+ wire[:retry_limit] = job[:retry_limit] if job[:retry_limit]
223
+ wire[:backoff] = job[:backoff] if job[:backoff]
224
+ wire[:retention] = job[:retention] if job[:retention]
225
+ wire[:unique_key] = job[:unique_key] if job[:unique_key]
226
+ wire[:unique_while] = job[:unique_while].to_s if job[:unique_while]
227
+ wire[:batch] = job[:batch] if job[:batch]
228
+ wire
229
+ end
219
230
  }
220
231
 
221
232
  response = post("/jobs/bulk", body)
@@ -238,31 +249,56 @@ module Zizq
238
249
  # The `filter` parameter accepts a jq expression for filtering jobs by
239
250
  # payload content (e.g. `.user_id == 42`).
240
251
  #
252
+ # Range filters (`priority`, `attempts`, `ready_at`) accept exact values or
253
+ # inclusive ranges.
254
+ #
241
255
  # @rbs id: (String | Array[String])?
242
256
  # @rbs status: (String | Array[String])?
243
257
  # @rbs queue: (String | Array[String])?
244
258
  # @rbs type: (String | Array[String])?
245
259
  # @rbs filter: String?
260
+ # @rbs priority: (Integer | Range[Integer?])?
261
+ # @rbs ready_at: (Zizq::to_f | Range[Zizq::to_f?])?
262
+ # @rbs attempts: (Integer | Range[Integer?])?
246
263
  # @rbs from: String?
247
264
  # @rbs order: Zizq::sort_direction?
248
265
  # @rbs limit: Integer?
249
266
  # @rbs return: Resources::JobPage
250
- def list_jobs(id: nil,
251
- status: nil,
252
- queue: nil,
253
- type: nil,
254
- filter: nil,
255
- from: nil,
256
- order: nil,
257
- limit: nil)
258
- options = { id:, status:, queue:, type:, filter:, from:, order:, limit: }.compact #: Hash[Symbol, untyped]
267
+ def list_jobs(
268
+ id: nil,
269
+ status: nil,
270
+ queue: nil,
271
+ type: nil,
272
+ filter: nil,
273
+ priority: nil,
274
+ ready_at: nil,
275
+ attempts: nil,
276
+ from: nil,
277
+ order: nil,
278
+ limit: nil
279
+ )
280
+ options = {
281
+ id:,
282
+ status:,
283
+ queue:,
284
+ type:,
285
+ filter:,
286
+ priority: encode_range(priority),
287
+ ready_at: encode_range(ready_at) { |v| (v.to_f * 1000).to_i },
288
+ attempts: encode_range(attempts),
289
+ from:,
290
+ order:,
291
+ limit:
292
+ }.compact #: Hash[Symbol, untyped]
259
293
 
260
294
  multi_keys = %i[id status queue type]
261
295
  params = build_where_params(options, multi_keys:)
262
296
 
263
297
  # An empty filter ([] or "") matches nothing — short-circuit.
264
298
  multi_keys.each do |key|
265
- return Resources::JobPage.new(self, { "jobs" => [], "pages" => {} }) if params[key] == ""
299
+ if params[key] == ""
300
+ return Resources::JobPage.new(self, { "jobs" => [], "pages" => {} })
301
+ end
266
302
  end
267
303
 
268
304
  response = get("/jobs", params:)
@@ -280,21 +316,36 @@ module Zizq
280
316
  # @rbs queue: (String | Array[String])?
281
317
  # @rbs type: (String | Array[String])?
282
318
  # @rbs filter: String?
319
+ # @rbs priority: (Integer | Range[Integer?])?
320
+ # @rbs ready_at: (Zizq::to_f | Range[Zizq::to_f?])?
321
+ # @rbs attempts: (Integer | Range[Integer?])?
283
322
  # @rbs return: Integer
284
- def count_jobs(id: nil,
285
- status: nil,
286
- queue: nil,
287
- type: nil,
288
- filter: nil)
289
- options = { id:, status:, queue:, type:, filter: }.compact #: Hash[Symbol, untyped]
323
+ def count_jobs(
324
+ id: nil,
325
+ status: nil,
326
+ queue: nil,
327
+ type: nil,
328
+ filter: nil,
329
+ priority: nil,
330
+ ready_at: nil,
331
+ attempts: nil
332
+ )
333
+ options = {
334
+ id:,
335
+ status:,
336
+ queue:,
337
+ type:,
338
+ filter:,
339
+ priority: encode_range(priority),
340
+ ready_at: encode_range(ready_at) { |v| (v.to_f * 1000).to_i },
341
+ attempts: encode_range(attempts)
342
+ }.compact #: Hash[Symbol, untyped]
290
343
 
291
344
  multi_keys = %i[id status queue type]
292
345
  params = build_where_params(options, multi_keys:)
293
346
 
294
347
  # An empty filter ([] or "") matches nothing — short-circuit.
295
- multi_keys.each do |key|
296
- return 0 if params[key] == ""
297
- end
348
+ multi_keys.each { |key| return 0 if params[key] == "" }
298
349
 
299
350
  response = get("/jobs/count", params:)
300
351
  data = handle_response!(response, expected: 200)
@@ -327,9 +378,7 @@ module Zizq
327
378
  params = build_where_params(filter_params, multi_keys:)
328
379
 
329
380
  # An empty multi-value filter matches nothing — short-circuit.
330
- multi_keys.each do |key|
331
- return 0 if params[key] == ""
332
- end
381
+ multi_keys.each { |key| return 0 if params[key] == "" }
333
382
 
334
383
  response = delete("/jobs", params:)
335
384
  data = handle_response!(response, expected: 200)
@@ -372,17 +421,24 @@ module Zizq
372
421
  # @rbs backoff: (Zizq::backoff | singleton(Zizq::RESET) | singleton(Zizq::UNCHANGED))?
373
422
  # @rbs retention: (Zizq::retention | singleton(Zizq::RESET) | singleton(Zizq::UNCHANGED))?
374
423
  # @rbs return: Resources::Job
375
- def update_job(id,
376
- queue: UNCHANGED,
377
- priority: UNCHANGED,
378
- ready_at: UNCHANGED,
379
- retry_limit: UNCHANGED,
380
- backoff: UNCHANGED,
381
- retention: UNCHANGED)
382
- body = build_set_body(
383
- queue:, priority:, ready_at:,
384
- retry_limit:, backoff:, retention:
385
- )
424
+ def update_job(
425
+ id,
426
+ queue: UNCHANGED,
427
+ priority: UNCHANGED,
428
+ ready_at: UNCHANGED,
429
+ retry_limit: UNCHANGED,
430
+ backoff: UNCHANGED,
431
+ retention: UNCHANGED
432
+ )
433
+ body =
434
+ build_set_body(
435
+ queue:,
436
+ priority:,
437
+ ready_at:,
438
+ retry_limit:,
439
+ backoff:,
440
+ retention:
441
+ )
386
442
  response = patch("/jobs/#{enc(id)}", body)
387
443
  data = handle_response!(response, expected: 200)
388
444
  Resources::Job.new(self, data)
@@ -408,9 +464,7 @@ module Zizq
408
464
  params = build_where_params(filter_params, multi_keys:)
409
465
 
410
466
  # An empty multi-value filter matches nothing — short-circuit.
411
- multi_keys.each do |key|
412
- return 0 if params[key] == ""
413
- end
467
+ multi_keys.each { |key| return 0 if params[key] == "" }
414
468
 
415
469
  body = validate_and_build_set(**apply)
416
470
  response = patch("/jobs", body, params:)
@@ -562,7 +616,14 @@ module Zizq
562
616
  # @rbs timezone: String?
563
617
  # @rbs paused: bool?
564
618
  # @rbs return: Resources::CronEntry
565
- def add_cron_group_entry(group, name:, expression:, job:, timezone: nil, paused: nil)
619
+ def add_cron_group_entry(
620
+ group,
621
+ name:,
622
+ expression:,
623
+ job:,
624
+ timezone: nil,
625
+ paused: nil
626
+ )
566
627
  body = build_cron_entry(name:, expression:, job:, timezone:, paused:)
567
628
  response = post("/crons/#{enc(group)}/entries", body)
568
629
  data = handle_response!(response, expected: 201)
@@ -580,8 +641,16 @@ module Zizq
580
641
  # @rbs timezone: String?
581
642
  # @rbs paused: bool?
582
643
  # @rbs return: Resources::CronEntry
583
- def replace_cron_group_entry(group, entry, expression:, job:, timezone: nil, paused: nil)
584
- body = build_cron_entry(name: entry, expression:, job:, timezone:, paused:)
644
+ def replace_cron_group_entry(
645
+ group,
646
+ entry,
647
+ expression:,
648
+ job:,
649
+ timezone: nil,
650
+ paused: nil
651
+ )
652
+ body =
653
+ build_cron_entry(name: entry, expression:, job:, timezone:, paused:)
585
654
  response = put("/crons/#{enc(group)}/entries/#{enc(entry)}", body)
586
655
  data = handle_response!(response, expected: 200)
587
656
  Resources::CronEntry.new(self, data)
@@ -594,7 +663,8 @@ module Zizq
594
663
  # @rbs paused: bool
595
664
  # @rbs return: Resources::CronEntry
596
665
  def update_cron_group_entry(group, entry, paused:)
597
- response = patch("/crons/#{enc(group)}/entries/#{enc(entry)}", { paused: })
666
+ response =
667
+ patch("/crons/#{enc(group)}/entries/#{enc(entry)}", { paused: })
598
668
  data = handle_response!(response, expected: 200)
599
669
  Resources::CronEntry.new(self, data)
600
670
  end
@@ -683,7 +753,14 @@ module Zizq
683
753
  # @rbs retry_at: Float?
684
754
  # @rbs kill: bool
685
755
  # @rbs return: Resources::Job
686
- def report_failure(id, message:, error_type: nil, backtrace: nil, retry_at: nil, kill: false)
756
+ def report_failure(
757
+ id,
758
+ message:,
759
+ error_type: nil,
760
+ backtrace: nil,
761
+ retry_at: nil,
762
+ kill: false
763
+ )
687
764
  body = { message: } #: Hash[Symbol, untyped]
688
765
  body[:error_type] = error_type if error_type
689
766
  body[:backtrace] = backtrace if backtrace
@@ -732,7 +809,14 @@ module Zizq
732
809
  # @rbs worker_id: String?
733
810
  # @rbs &block: (Resources::Job) -> void
734
811
  # @rbs return: void
735
- def take_jobs(prefetch: 1, queues: [], worker_id: nil, on_connect: nil, on_response: nil, &block)
812
+ def take_jobs(
813
+ prefetch: 1,
814
+ queues: [],
815
+ worker_id: nil,
816
+ on_connect: nil,
817
+ on_response: nil,
818
+ &block
819
+ )
736
820
  raise ArgumentError, "take_jobs requires a block" unless block
737
821
 
738
822
  params = { prefetch: } #: Hash[Symbol, untyped]
@@ -746,7 +830,10 @@ module Zizq
746
830
  response = stream_http.get(path, headers)
747
831
 
748
832
  begin
749
- raise StreamError, "take jobs stream returned HTTP #{response.status}" unless response.status == 200
833
+ unless response.status == 200
834
+ raise StreamError,
835
+ "take jobs stream returned HTTP #{response.status}"
836
+ end
750
837
  on_connect&.call
751
838
  on_response&.call(response)
752
839
 
@@ -760,11 +847,17 @@ module Zizq
760
847
  body = response.body || []
761
848
 
762
849
  case @format
763
- when :json then self.class.parse_ndjson(body, &wrapper)
764
- when :msgpack then self.class.parse_msgpack_stream(body, &wrapper)
850
+ when :json
851
+ self.class.parse_ndjson(body, &wrapper)
852
+ when :msgpack
853
+ self.class.parse_msgpack_stream(body, &wrapper)
765
854
  end
766
855
  ensure
767
- response.close rescue nil
856
+ begin
857
+ response.close
858
+ rescue StandardError
859
+ nil
860
+ end
768
861
  end
769
862
  end
770
863
  rescue SocketError,
@@ -847,7 +940,10 @@ module Zizq
847
940
  # so that resource objects like Page can follow links without resorting
848
941
  # to `.send`.
849
942
  def get_path(path) #: (String) -> Hash[String, untyped]
850
- response = request { |http| consume_response(http.get(path, {"accept" => @content_type})) }
943
+ response =
944
+ request do |http|
945
+ consume_response(http.get(path, { "accept" => @content_type }))
946
+ end
851
947
  handle_response!(response, expected: 200)
852
948
  end
853
949
 
@@ -860,9 +956,7 @@ module Zizq
860
956
 
861
957
  # Build a relative path with optional query parameters.
862
958
  def build_path(path, params: {}) #: (String, ?params: Hash[Symbol, untyped]) -> String
863
- unless params.empty?
864
- path = "#{path}?#{URI.encode_www_form(params)}"
865
- end
959
+ path = "#{path}?#{URI.encode_www_form(params)}" unless params.empty?
866
960
  path
867
961
  end
868
962
 
@@ -874,13 +968,19 @@ module Zizq
874
968
  # @rbs timezone: String?
875
969
  # @rbs paused: bool?
876
970
  # @rbs return: Hash[Symbol, untyped]
877
- def build_cron_entry(name: nil, expression: nil, job: nil, timezone: nil, paused: nil)
971
+ def build_cron_entry(
972
+ name: nil,
973
+ expression: nil,
974
+ job: nil,
975
+ timezone: nil,
976
+ paused: nil
977
+ )
878
978
  {
879
979
  name:,
880
980
  expression:,
881
981
  timezone:,
882
982
  paused:,
883
- job: build_cron_job(**job),
983
+ job: build_cron_job(**job)
884
984
  }.compact
885
985
  end
886
986
 
@@ -898,15 +998,17 @@ module Zizq
898
998
  # @rbs unique_key: String?
899
999
  # @rbs unique_while: Zizq::unique_scope?
900
1000
  # @rbs return: Hash[Symbol, untyped]
901
- def build_cron_job(type: nil,
902
- queue: nil,
903
- payload: nil,
904
- priority: nil,
905
- retry_limit: nil,
906
- backoff: nil,
907
- retention: nil,
908
- unique_key: nil,
909
- unique_while: nil)
1001
+ def build_cron_job(
1002
+ type: nil,
1003
+ queue: nil,
1004
+ payload: nil,
1005
+ priority: nil,
1006
+ retry_limit: nil,
1007
+ backoff: nil,
1008
+ retention: nil,
1009
+ unique_key: nil,
1010
+ unique_while: nil
1011
+ )
910
1012
  job = { type:, queue:, payload: } #: Hash[Symbol, untyped]
911
1013
  job[:priority] = priority if priority
912
1014
  job[:retry_limit] = retry_limit if retry_limit
@@ -926,9 +1028,64 @@ module Zizq
926
1028
  # @rbs queue: (String | Array[String])?
927
1029
  # @rbs type: (String | Array[String])?
928
1030
  # @rbs filter: String?
1031
+ # @rbs priority: (Integer | Range[Integer?])?
1032
+ # @rbs ready_at: (Zizq::to_f | Range[Zizq::to_f?])?
1033
+ # @rbs attempts: (Integer | Range[Integer?])?
929
1034
  # @rbs return: Hash[Symbol, untyped]
930
- def validate_where(id: nil, status: nil, queue: nil, type: nil, filter: nil)
931
- { id:, status:, queue:, type:, filter: }.compact
1035
+ def validate_where(
1036
+ id: nil,
1037
+ status: nil,
1038
+ queue: nil,
1039
+ type: nil,
1040
+ filter: nil,
1041
+ priority: nil,
1042
+ ready_at: nil,
1043
+ attempts: nil
1044
+ )
1045
+ {
1046
+ id:,
1047
+ status:,
1048
+ queue:,
1049
+ type:,
1050
+ filter:,
1051
+ priority: encode_range(priority),
1052
+ ready_at: encode_range(ready_at) { |v| (v.to_f * 1000).to_i },
1053
+ attempts: encode_range(attempts)
1054
+ }.compact
1055
+ end
1056
+
1057
+ # Encode an Integer or Range filter into the server's query format.
1058
+ #
1059
+ # Accepted shapes:
1060
+ #
1061
+ # - `nil` -> `nil` (no filter sent)
1062
+ # - `Integer` -> `"N"` (single value)
1063
+ # - `(a..b)` -> `"A..B"` (inclusive both ends)
1064
+ # - `(a..)` -> `"A.."` (lower bound only)
1065
+ # - `(..b)` -> `"..B"` (upper bound only)
1066
+ #
1067
+ # Exclusive ranges (`a...b`, `a...`, `...b`) raise `ArgumentError` — the
1068
+ # server only supports inclusive bounds. A bound transformation block can
1069
+ # convert values before formatting (e.g. seconds -> ms for `ready_at`).
1070
+ #
1071
+ # @rbs value: untyped
1072
+ # @rbs &block: ?(untyped) -> Integer
1073
+ # @rbs return: String?
1074
+ def encode_range(value, &block)
1075
+ block ||= ->(v) { v.to_i }
1076
+
1077
+ case value
1078
+ when nil
1079
+ nil
1080
+ when Range
1081
+ if value.exclude_end?
1082
+ raise ArgumentError,
1083
+ "exclusive ranges are not supported by the server; use an inclusive range (a..b) instead"
1084
+ end
1085
+ "#{value.begin&.then(&block)}..#{value.end&.then(&block)}"
1086
+ else
1087
+ block.call(value).to_s
1088
+ end
932
1089
  end
933
1090
 
934
1091
  # Validate set parameters via keyword args (rejects unknown keys) and
@@ -941,13 +1098,22 @@ module Zizq
941
1098
  # @rbs backoff: (Zizq::backoff | singleton(Zizq::RESET) | singleton(Zizq::UNCHANGED))?
942
1099
  # @rbs retention: (Zizq::retention | singleton(Zizq::RESET) | singleton(Zizq::UNCHANGED))?
943
1100
  # @rbs return: Hash[Symbol, untyped]
944
- def validate_and_build_set(queue: UNCHANGED,
945
- priority: UNCHANGED,
946
- ready_at: UNCHANGED,
947
- retry_limit: UNCHANGED,
948
- backoff: UNCHANGED,
949
- retention: UNCHANGED)
950
- build_set_body(queue:, priority:, ready_at:, retry_limit:, backoff:, retention:)
1101
+ def validate_and_build_set(
1102
+ queue: UNCHANGED,
1103
+ priority: UNCHANGED,
1104
+ ready_at: UNCHANGED,
1105
+ retry_limit: UNCHANGED,
1106
+ backoff: UNCHANGED,
1107
+ retention: UNCHANGED
1108
+ )
1109
+ build_set_body(
1110
+ queue:,
1111
+ priority:,
1112
+ ready_at:,
1113
+ retry_limit:,
1114
+ backoff:,
1115
+ retention:
1116
+ )
951
1117
  end
952
1118
 
953
1119
  # Build the JSON body hash for a PATCH request from set parameters.
@@ -958,26 +1124,40 @@ module Zizq
958
1124
  # - Other values are converted to their wire format.
959
1125
  #
960
1126
  # @rbs return: Hash[Symbol, untyped]
961
- def build_set_body(queue: UNCHANGED,
962
- priority: UNCHANGED,
963
- ready_at: UNCHANGED,
964
- retry_limit: UNCHANGED,
965
- backoff: UNCHANGED,
966
- retention: UNCHANGED)
1127
+ def build_set_body(
1128
+ queue: UNCHANGED,
1129
+ priority: UNCHANGED,
1130
+ ready_at: UNCHANGED,
1131
+ retry_limit: UNCHANGED,
1132
+ backoff: UNCHANGED,
1133
+ retention: UNCHANGED
1134
+ )
967
1135
  body = {} #: Hash[Symbol, untyped]
968
1136
 
969
1137
  unless queue.equal?(UNCHANGED)
970
- raise ArgumentError, "queue cannot be nil; use Zizq::RESET to clear or Zizq::UNCHANGED to leave as-is" if queue.nil?
1138
+ if queue.nil?
1139
+ raise ArgumentError,
1140
+ "queue cannot be nil; use Zizq::RESET to clear or Zizq::UNCHANGED to leave as-is"
1141
+ end
971
1142
  body[:queue] = queue
972
1143
  end
973
1144
 
974
1145
  unless priority.equal?(UNCHANGED)
975
- raise ArgumentError, "priority cannot be nil; use Zizq::RESET to clear or Zizq::UNCHANGED to leave as-is" if priority.nil?
1146
+ if priority.nil?
1147
+ raise ArgumentError,
1148
+ "priority cannot be nil; use Zizq::RESET to clear or Zizq::UNCHANGED to leave as-is"
1149
+ end
976
1150
  body[:priority] = priority
977
1151
  end
978
1152
 
979
1153
  unless ready_at.equal?(UNCHANGED)
980
- body[:ready_at] = ready_at.equal?(RESET) ? nil : (ready_at.to_f * 1000).to_i
1154
+ body[:ready_at] = (
1155
+ if ready_at.equal?(RESET)
1156
+ nil
1157
+ else
1158
+ (ready_at.to_f * 1000).to_i
1159
+ end
1160
+ )
981
1161
  end
982
1162
 
983
1163
  unless retry_limit.equal?(UNCHANGED)
@@ -1001,8 +1181,12 @@ module Zizq
1001
1181
  nil
1002
1182
  else
1003
1183
  ret = {} #: Hash[Symbol, Integer]
1004
- ret[:completed_ms] = (retention[:completed].to_f * 1000).to_i if retention[:completed]
1005
- ret[:dead_ms] = (retention[:dead].to_f * 1000).to_i if retention[:dead]
1184
+ ret[:completed_ms] = (
1185
+ retention[:completed].to_f * 1000
1186
+ ).to_i if retention[:completed]
1187
+ ret[:dead_ms] = (retention[:dead].to_f * 1000).to_i if retention[
1188
+ :dead
1189
+ ]
1006
1190
  ret
1007
1191
  end
1008
1192
  end
@@ -1025,22 +1209,32 @@ module Zizq
1025
1209
 
1026
1210
  def encode_body(body) #: (Hash[Symbol, untyped]) -> String
1027
1211
  case @format
1028
- when :msgpack then MessagePack.pack(body)
1029
- when :json then JSON.generate(body)
1030
- else raise ArgumentError, "Unknown format: #{@format}"
1212
+ when :msgpack
1213
+ MessagePack.pack(body)
1214
+ when :json
1215
+ JSON.generate(body)
1216
+ else
1217
+ raise ArgumentError, "Unknown format: #{@format}"
1031
1218
  end
1032
1219
  end
1033
1220
 
1034
1221
  def decode_body(data, content_type: nil) #: (String, ?content_type: String?) -> Hash[String, untyped]
1035
- format = case content_type
1036
- when /msgpack/ then :msgpack
1037
- when /json/ then :json
1038
- else @format
1039
- end
1222
+ format =
1223
+ case content_type
1224
+ when /msgpack/
1225
+ :msgpack
1226
+ when /json/
1227
+ :json
1228
+ else
1229
+ @format
1230
+ end
1040
1231
  case format
1041
- when :msgpack then MessagePack.unpack(data)
1042
- when :json then JSON.parse(data)
1043
- else raise ArgumentError, "Unknown format: #{format}"
1232
+ when :msgpack
1233
+ MessagePack.unpack(data)
1234
+ when :json
1235
+ JSON.parse(data)
1236
+ else
1237
+ raise ArgumentError, "Unknown format: #{format}"
1044
1238
  end
1045
1239
  end
1046
1240
 
@@ -1060,7 +1254,11 @@ module Zizq
1060
1254
  # Read the response body and close it, returning a RawResponse that is
1061
1255
  # safe to use outside the reactor.
1062
1256
  def consume_response(response) #: (untyped) -> RawResponse
1063
- RawResponse.new(status: response.status, body: response.read, content_type: response.headers["content-type"])
1257
+ RawResponse.new(
1258
+ status: response.status,
1259
+ body: response.read,
1260
+ content_type: response.headers["content-type"]
1261
+ )
1064
1262
  ensure
1065
1263
  response.close
1066
1264
  end
@@ -1146,24 +1344,22 @@ module Zizq
1146
1344
  end
1147
1345
 
1148
1346
  def thread_local_http(key, endpoint) #: (Symbol, Async::HTTP::Endpoint) -> Async::HTTP::Client
1149
- Thread.current.thread_variable_get(key) || begin
1150
- client = Async::HTTP::Client.new(endpoint)
1151
- @mutex.synchronize do
1152
- @http_clients.reject! { |ref| !ref.weakref_alive? }
1153
- @http_clients << WeakRef.new(client)
1347
+ Thread.current.thread_variable_get(key) ||
1348
+ begin
1349
+ client = Async::HTTP::Client.new(endpoint)
1350
+ @mutex.synchronize do
1351
+ @http_clients.reject! { |ref| !ref.weakref_alive? }
1352
+ @http_clients << WeakRef.new(client)
1353
+ end
1354
+ Thread.current.thread_variable_set(key, client)
1355
+ client
1154
1356
  end
1155
- Thread.current.thread_variable_set(key, client)
1156
- client
1157
- end
1158
1357
  end
1159
1358
 
1160
1359
  def get(path, params: {}) #: (String, ?params: Hash[Symbol, untyped]) -> RawResponse
1161
1360
  request do |http|
1162
1361
  consume_response(
1163
- http.get(
1164
- build_path(path, params:),
1165
- {"accept" => @content_type}
1166
- )
1362
+ http.get(build_path(path, params:), { "accept" => @content_type })
1167
1363
  )
1168
1364
  end
1169
1365
  end
@@ -1173,7 +1369,7 @@ module Zizq
1173
1369
  consume_response(
1174
1370
  http.post(
1175
1371
  build_path(path),
1176
- {"content-type" => @content_type, "accept" => @content_type},
1372
+ { "content-type" => @content_type, "accept" => @content_type },
1177
1373
  Protocol::HTTP::Body::Buffered.wrap(encode_body(body))
1178
1374
  )
1179
1375
  )
@@ -1183,10 +1379,7 @@ module Zizq
1183
1379
  def raw_post(path) #: (String) -> RawResponse
1184
1380
  request do |http|
1185
1381
  consume_response(
1186
- http.post(
1187
- build_path(path),
1188
- {"accept" => @content_type}
1189
- )
1382
+ http.post(build_path(path), { "accept" => @content_type })
1190
1383
  )
1191
1384
  end
1192
1385
  end
@@ -1196,7 +1389,7 @@ module Zizq
1196
1389
  consume_response(
1197
1390
  http.put(
1198
1391
  build_path(path),
1199
- {"content-type" => @content_type, "accept" => @content_type},
1392
+ { "content-type" => @content_type, "accept" => @content_type },
1200
1393
  Protocol::HTTP::Body::Buffered.wrap(encode_body(body))
1201
1394
  )
1202
1395
  )
@@ -1206,10 +1399,7 @@ module Zizq
1206
1399
  def delete(path, params: {}) #: (String, ?params: Hash[Symbol, untyped]) -> RawResponse
1207
1400
  request do |http|
1208
1401
  consume_response(
1209
- http.delete(
1210
- build_path(path, params:),
1211
- {"accept" => @content_type}
1212
- )
1402
+ http.delete(build_path(path, params:), { "accept" => @content_type })
1213
1403
  )
1214
1404
  end
1215
1405
  end
@@ -1219,7 +1409,7 @@ module Zizq
1219
1409
  consume_response(
1220
1410
  http.patch(
1221
1411
  build_path(path, params:),
1222
- {"content-type" => @content_type, "accept" => @content_type},
1412
+ { "content-type" => @content_type, "accept" => @content_type },
1223
1413
  Protocol::HTTP::Body::Buffered.wrap(encode_body(body))
1224
1414
  )
1225
1415
  )
@@ -1237,18 +1427,24 @@ module Zizq
1237
1427
  return nil if status == 204
1238
1428
  decode_body(response.body, content_type: ct)
1239
1429
  else
1240
- body = begin
1241
- decode_body(response.body, content_type: ct)
1242
- rescue
1243
- nil
1244
- end
1430
+ body =
1431
+ begin
1432
+ decode_body(response.body, content_type: ct)
1433
+ rescue StandardError
1434
+ nil
1435
+ end
1245
1436
  message = body&.fetch("error", nil) || "HTTP #{status}"
1246
- error_class = case status
1247
- when 404 then NotFoundError
1248
- when 400..499 then ClientError
1249
- when 500..599 then ServerError
1250
- else ResponseError
1251
- end
1437
+ error_class =
1438
+ case status
1439
+ when 404
1440
+ NotFoundError
1441
+ when 400..499
1442
+ ClientError
1443
+ when 500..599
1444
+ ServerError
1445
+ else
1446
+ ResponseError
1447
+ end
1252
1448
  raise error_class.new(message, status: status, body: body)
1253
1449
  end
1254
1450
  end