zizq 0.5.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 (52) 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 +288 -185
  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 +85 -67
  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 +2 -1
  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/resources/job.rbs +2 -0
  49. data/sig/generated/zizq/resources/job_template.rbs +5 -2
  50. data/sig/generated/zizq.rbs +1 -0
  51. data/sig/zizq.rbs +12 -1
  52. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 424b67e8dbba3c82506c24ed18cbaa9fc87af3febe9e079ef458f705e9299fdd
4
- data.tar.gz: 53bc5f8b3aefbdddc69e2471e6efbd2ba7c84016cb8eb63ff16bc463107991fe
3
+ metadata.gz: 650d5af8c85dfe12fdf6280a3cf483ba1edd7f7d3d0536965a220c9f556221ef
4
+ data.tar.gz: ec340ce8d30065b6afeb9dc9404651bd769e8a604dec326aca97c332a11bc7ba
5
5
  SHA512:
6
- metadata.gz: 10f7ab3fe215f04eff3c1753826734c609454c93f9c30c586dd0f38f49ac563d23dcb47317c12e7c00cde3dff558cb124ce0ae25a3c801b9b71a85ce806d96e9
7
- data.tar.gz: 422601a65e9862a73360e036a0e5f44ac36bdcaa04a3fe585567e4fefaf9b73e4fa4736ac3fe9d0cc552173bcd9c47fef98cbc1a6935ef3a64870a839a5a13b9
6
+ metadata.gz: a7a239bed97e29977c9863f7e56d1ef94b1a1c1cd05bd4b74dde441910bd83f480b4d0453e58b32a3d623ad59bac8b771da5397794e1a05e614e4654cb744606
7
+ data.tar.gz: c60eae8cf1142ecb8e04218d551b5595554ebf5a085076111051c4a658518608715aae729d968d98f33aa7bd3f0f2a181fecbf97a031d6c21bb6a2472d8b291c
data/README.md CHANGED
@@ -21,7 +21,8 @@ API.
21
21
  * Configurable job retention policies
22
22
  * Recurring jobs (cron)
23
23
  * Job introspection and management APIs, with support for `jq` query filters
24
- * Unique jobs
24
+ * Unique jobs (deduplicated)
25
+ * Batched jobs (folded/merged)
25
26
  * Testing helpers
26
27
 
27
28
  ## Installation
@@ -33,13 +34,13 @@ API.
33
34
  Add it to your application's `Gemfile`:
34
35
 
35
36
  ```ruby
36
- gem 'zizq', '~> 0.5.0'
37
+ gem 'zizq', '~> 0.6.0'
37
38
  ```
38
39
 
39
40
  Or install it manually:
40
41
 
41
42
  ```shell
42
- $ gem install zizq -v 0.5.0
43
+ $ gem install zizq -v 0.6.0
43
44
  ```
44
45
 
45
46
  Ruby **3.2.8 or newer** is required. Client and server share version
@@ -33,7 +33,8 @@ module ActiveJob
33
33
  class ZizqAdapter
34
34
  # Enqueue a job for immediate execution.
35
35
  def enqueue(job)
36
- result = Zizq.enqueue_raw(**build_enqueue_request(job).to_enqueue_params)
36
+ result =
37
+ Zizq.enqueue_raw(**build_enqueue_request(job).to_enqueue_params)
37
38
  job.provider_job_id = result.id
38
39
  job.successfully_enqueued = true
39
40
  result
@@ -42,7 +43,8 @@ module ActiveJob
42
43
  # Enqueue a job for execution at a specific time.
43
44
  def enqueue_at(job, timestamp)
44
45
  job.scheduled_at = timestamp
45
- result = Zizq.enqueue_raw(**build_enqueue_request(job).to_enqueue_params)
46
+ result =
47
+ Zizq.enqueue_raw(**build_enqueue_request(job).to_enqueue_params)
46
48
  job.provider_job_id = result.id
47
49
  job.successfully_enqueued = true
48
50
  result
@@ -53,16 +55,19 @@ module ActiveJob
53
55
  # Called by `ActiveJob.perform_all_later` (Rails 7.1+).
54
56
  # Returns the number of successfully enqueued jobs.
55
57
  def enqueue_all(jobs)
56
- results = Zizq.enqueue_bulk do |b|
57
- jobs.each do |job|
58
- b.enqueue_raw(**build_enqueue_request(job).to_enqueue_params)
58
+ results =
59
+ Zizq.enqueue_bulk do |b|
60
+ jobs.each do |job|
61
+ b.enqueue_raw(**build_enqueue_request(job).to_enqueue_params)
62
+ end
59
63
  end
60
- end
61
64
 
62
- jobs.zip(results).each do |job, result|
63
- job.provider_job_id = result.id
64
- job.successfully_enqueued = true
65
- end
65
+ jobs
66
+ .zip(results)
67
+ .each do |job, result|
68
+ job.provider_job_id = result.id
69
+ job.successfully_enqueued = true
70
+ end
66
71
 
67
72
  jobs.size
68
73
  rescue => e
@@ -85,22 +90,39 @@ module ActiveJob
85
90
  def build_enqueue_request(job)
86
91
  klass = job.class
87
92
 
88
- req = Zizq::EnqueueRequest.new(
89
- queue: job.queue_name,
90
- type: klass.name,
91
- payload: job.serialize,
92
- priority: job.priority,
93
- ready_at: job.scheduled_at
94
- )
93
+ req =
94
+ Zizq::EnqueueRequest.new(
95
+ queue: job.queue_name,
96
+ type: klass.name,
97
+ payload: job.serialize,
98
+ priority: job.priority,
99
+ ready_at: job.scheduled_at
100
+ )
95
101
 
96
102
  if klass.respond_to?(:zizq_unique) && klass.zizq_unique
97
103
  req.unique_key = klass.zizq_unique_key(*job.arguments)
98
104
  req.unique_while = klass.zizq_unique_scope
99
105
  end
100
106
 
101
- req.retry_limit = klass.zizq_retry_limit if klass.respond_to?(:zizq_retry_limit) && klass.zizq_retry_limit
102
- req.backoff = klass.zizq_backoff if klass.respond_to?(:zizq_backoff) && klass.zizq_backoff
103
- req.retention = klass.zizq_retention if klass.respond_to?(:zizq_retention) && klass.zizq_retention
107
+ if klass.respond_to?(:zizq_batched) && klass.zizq_batched
108
+ expr = klass.zizq_batch_expressions
109
+ if expr
110
+ req.batch = {
111
+ key: klass.zizq_batch_key(*job.arguments),
112
+ when: expr[:when],
113
+ fold: expr[:fold]
114
+ }
115
+ end
116
+ end
117
+
118
+ req.retry_limit = klass.zizq_retry_limit if klass.respond_to?(
119
+ :zizq_retry_limit
120
+ ) && klass.zizq_retry_limit
121
+ req.backoff = klass.zizq_backoff if klass.respond_to?(:zizq_backoff) &&
122
+ klass.zizq_backoff
123
+ req.retention = klass.zizq_retention if klass.respond_to?(
124
+ :zizq_retention
125
+ ) && klass.zizq_retention
104
126
 
105
127
  req
106
128
  end
@@ -79,13 +79,9 @@ module Zizq
79
79
  # Partition: acks go bulk, nacks go individually.
80
80
  acks, nacks = batch.partition { |i| i.is_a?(Ack) }
81
81
 
82
- unless acks.empty?
83
- barrier.async { process_ack_batch(acks) }
84
- end
82
+ barrier.async { process_ack_batch(acks) } unless acks.empty?
85
83
 
86
- nacks.each do |nack|
87
- barrier.async { process_nack(nack) }
88
- end
84
+ nacks.each { |nack| barrier.async { process_nack(nack) } }
89
85
  end
90
86
 
91
87
  barrier.wait
@@ -101,9 +97,13 @@ module Zizq
101
97
  begin
102
98
  @client.report_success_bulk(ids)
103
99
  rescue ClientError => e
104
- @logger.warn { "Bulk ack (#{ids.size} jobs) returned #{e.status} (dropping: #{e.message})" }
100
+ @logger.warn do
101
+ "Bulk ack (#{ids.size} jobs) returned #{e.status} (dropping: #{e.message})"
102
+ end
105
103
  rescue => e
106
- @logger.warn { "Retrying bulk ack (#{ids.size} jobs) in #{backoff.duration}s: #{e.message}" }
104
+ @logger.warn do
105
+ "Retrying bulk ack (#{ids.size} jobs) in #{backoff.duration}s: #{e.message}"
106
+ end
107
107
  backoff.wait
108
108
  retry
109
109
  end
@@ -119,11 +119,17 @@ module Zizq
119
119
  backtrace: nack.backtrace
120
120
  )
121
121
  rescue NotFoundError
122
- @logger.debug { "Nack for #{nack.job_id} returned 404 (already handled)" }
122
+ @logger.debug do
123
+ "Nack for #{nack.job_id} returned 404 (already handled)"
124
+ end
123
125
  rescue ClientError => e
124
- @logger.error { "Nack for #{nack.job_id} returned #{e.status} (dropping)" }
126
+ @logger.error do
127
+ "Nack for #{nack.job_id} returned #{e.status} (dropping)"
128
+ end
125
129
  rescue => e
126
- @logger.warn { "Retrying nack for #{nack.job_id} in #{backoff.duration}s: #{e.message}" }
130
+ @logger.warn do
131
+ "Retrying nack for #{nack.job_id} in #{backoff.duration}s: #{e.message}"
132
+ end
127
133
  backoff.wait
128
134
  retry
129
135
  end
@@ -65,16 +65,7 @@ module Zizq
65
65
  # side. This method is not used in the ActiveJob dispatch path.
66
66
  def zizq_deserialize(_payload) #: (untyped) -> [Array[untyped], Hash[Symbol, untyped]]
67
67
  raise NotImplementedError,
68
- "ActiveJob handles deserialization via ActiveJob::Base.execute"
69
- end
70
-
71
- # Override unique key generation to hash only the arguments portion
72
- # of the serialized payload. The full payload contains volatile fields
73
- # (job_id, enqueued_at, etc.) that change per instance.
74
- def zizq_unique_key(*args, **kwargs) #: (*untyped, **untyped) -> String
75
- arguments = new(*args, **kwargs).serialize["arguments"]
76
- payload = normalize_payload(arguments)
77
- "#{name}:#{Digest::SHA256.hexdigest(JSON.generate(payload))}"
68
+ "ActiveJob handles deserialization via ActiveJob::Base.execute"
78
69
  end
79
70
 
80
71
  # Generate a jq expression that exactly matches payloads with the given
@@ -139,5 +130,34 @@ module Zizq
139
130
 
140
131
  parts.join(" and ")
141
132
  end
133
+
134
+ # Static jq expressions for the batched-job configuration on this
135
+ # class. Targets `.arguments[N]` for a positional batch arg or
136
+ # `.arguments[-1].NAME` for a keyword batch arg — the latter
137
+ # assumes the class actually receives kwargs so that the last
138
+ # argument is the ActiveJob kwargs hash. Behaviour is unspecified
139
+ # if the declared `kwarg:` isn't present at enqueue time.
140
+ def zizq_batch_expressions #: () -> Zizq::batch_expressions?
141
+ return nil unless zizq_batched
142
+
143
+ target =
144
+ if (idx = zizq_batch_arg)
145
+ ".arguments[#{idx}]"
146
+ else
147
+ ".arguments[-1].#{zizq_batch_kwarg}"
148
+ end
149
+
150
+ build_batch_expressions(target)
151
+ end
152
+
153
+ private
154
+
155
+ # Hash only the arguments portion of the serialized ActiveJob
156
+ # payload. The full envelope contains volatile fields (job_id,
157
+ # enqueued_at, etc.) that change per instance and would defeat
158
+ # both `zizq_unique_key` and `zizq_batch_key`.
159
+ def hashable_args(*args, **kwargs) #: (*untyped, **untyped) -> untyped
160
+ new(*args, **kwargs).serialize["arguments"]
161
+ end
142
162
  end
143
163
  end
data/lib/zizq/backoff.rb CHANGED
@@ -44,7 +44,11 @@ module Zizq
44
44
 
45
45
  # Returns a new Backoff with the same configuration but reset state.
46
46
  def fresh #: () -> Backoff
47
- self.class.new(min_wait: @min_wait, max_wait: @max_wait, multiplier: @multiplier)
47
+ self.class.new(
48
+ min_wait: @min_wait,
49
+ max_wait: @max_wait,
50
+ multiplier: @multiplier
51
+ )
48
52
  end
49
53
  end
50
54
  end
@@ -28,7 +28,12 @@ module Zizq
28
28
  # @rbs &block: ?(EnqueueRequest) -> void
29
29
  # @rbs return: void
30
30
  def enqueue(job_class, *args, **kwargs, &block)
31
- @requests << Zizq.build_enqueue_request(job_class, *args, **kwargs, &block)
31
+ @requests << Zizq.build_enqueue_request(
32
+ job_class,
33
+ *args,
34
+ **kwargs,
35
+ &block
36
+ )
32
37
  end
33
38
 
34
39
  # Collect a raw enqueue. Accepts the same arguments as
@@ -44,6 +49,7 @@ module Zizq
44
49
  # @rbs retention: Zizq::retention?
45
50
  # @rbs unique_key: String?
46
51
  # @rbs unique_while: Zizq::unique_scope?
52
+ # @rbs batch: Zizq::batch?
47
53
  # @rbs return: void
48
54
  def enqueue_raw(queue:, type:, payload:, **opts)
49
55
  @requests << EnqueueRequest.new(queue:, type:, payload:, **opts)