zizq 0.1.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 (57) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +94 -0
  4. data/bin/profile-worker +145 -0
  5. data/bin/zizq-worker +174 -0
  6. data/lib/active_job/queue_adapters/zizq_adapter.rb +109 -0
  7. data/lib/zizq/ack_processor.rb +132 -0
  8. data/lib/zizq/active_job_config.rb +122 -0
  9. data/lib/zizq/backoff.rb +50 -0
  10. data/lib/zizq/bulk_enqueue.rb +87 -0
  11. data/lib/zizq/client.rb +982 -0
  12. data/lib/zizq/configuration.rb +164 -0
  13. data/lib/zizq/enqueue_request.rb +178 -0
  14. data/lib/zizq/enqueue_with.rb +109 -0
  15. data/lib/zizq/error.rb +43 -0
  16. data/lib/zizq/job.rb +188 -0
  17. data/lib/zizq/job_config.rb +244 -0
  18. data/lib/zizq/lifecycle.rb +58 -0
  19. data/lib/zizq/middleware.rb +79 -0
  20. data/lib/zizq/query.rb +566 -0
  21. data/lib/zizq/resources/error_enumerator.rb +241 -0
  22. data/lib/zizq/resources/error_page.rb +19 -0
  23. data/lib/zizq/resources/error_record.rb +19 -0
  24. data/lib/zizq/resources/job.rb +124 -0
  25. data/lib/zizq/resources/job_page.rb +57 -0
  26. data/lib/zizq/resources/page.rb +77 -0
  27. data/lib/zizq/resources/resource.rb +45 -0
  28. data/lib/zizq/resources.rb +16 -0
  29. data/lib/zizq/version.rb +9 -0
  30. data/lib/zizq/worker.rb +467 -0
  31. data/lib/zizq.rb +269 -0
  32. data/sig/generated/zizq/ack_processor.rbs +73 -0
  33. data/sig/generated/zizq/active_job_config.rbs +74 -0
  34. data/sig/generated/zizq/backoff.rbs +34 -0
  35. data/sig/generated/zizq/bulk_enqueue.rbs +72 -0
  36. data/sig/generated/zizq/client.rbs +419 -0
  37. data/sig/generated/zizq/configuration.rbs +95 -0
  38. data/sig/generated/zizq/enqueue_request.rbs +94 -0
  39. data/sig/generated/zizq/enqueue_with.rbs +88 -0
  40. data/sig/generated/zizq/error.rbs +41 -0
  41. data/sig/generated/zizq/job.rbs +136 -0
  42. data/sig/generated/zizq/job_config.rbs +150 -0
  43. data/sig/generated/zizq/lifecycle.rbs +34 -0
  44. data/sig/generated/zizq/middleware.rbs +50 -0
  45. data/sig/generated/zizq/query.rbs +327 -0
  46. data/sig/generated/zizq/resources/error_enumerator.rbs +148 -0
  47. data/sig/generated/zizq/resources/error_page.rbs +13 -0
  48. data/sig/generated/zizq/resources/error_record.rbs +20 -0
  49. data/sig/generated/zizq/resources/job.rbs +89 -0
  50. data/sig/generated/zizq/resources/job_page.rbs +33 -0
  51. data/sig/generated/zizq/resources/page.rbs +47 -0
  52. data/sig/generated/zizq/resources/resource.rbs +26 -0
  53. data/sig/generated/zizq/version.rbs +5 -0
  54. data/sig/generated/zizq/worker.rbs +152 -0
  55. data/sig/generated/zizq.rbs +180 -0
  56. data/sig/zizq.rbs +111 -0
  57. metadata +134 -0
@@ -0,0 +1,122 @@
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
+ require_relative "job_config"
8
+
9
+ module Zizq
10
+ # Zizq configuration DSL for ActiveJob classes.
11
+ #
12
+ # Extend this module in an ActiveJob subclass to gain access to Zizq
13
+ # features like unique jobs, backoff, and retention:
14
+ #
15
+ # class SendEmailJob < ApplicationJob
16
+ # extend Zizq::ActiveJobConfig
17
+ #
18
+ # zizq_unique true, scope: :active
19
+ # zizq_backoff exponent: 4.0, base: 15, jitter: 30
20
+ #
21
+ # def perform(user_id, template:)
22
+ # # ...
23
+ # end
24
+ # end
25
+ #
26
+ # Serialization uses ActiveJob's own format so that GlobalID, Time, and
27
+ # other ActiveJob-supported types are handled correctly. The Zizq worker
28
+ # must use the ActiveJob dispatcher:
29
+ #
30
+ # Zizq.configure do |c|
31
+ # c.dispatcher = ActiveJob::QueueAdapters::ZizqAdapter::Dispatcher
32
+ # end
33
+ module ActiveJobConfig
34
+ include JobConfig
35
+
36
+ # @rbs!
37
+ # # ActiveJob::Base.new — invisible to steep without this.
38
+ # def new: (*untyped, **untyped) -> untyped
39
+
40
+ # Serialize arguments using ActiveJob's serialization format.
41
+ #
42
+ # Creates a temporary ActiveJob instance to produce the canonical
43
+ # serialized form, including `_aj_ruby2_keywords` markers for kwargs.
44
+ # This ensures unique key generation uses the same format as the
45
+ # enqueued payload.
46
+ #
47
+ # This is needed so that unique job keys can be correctly generated.
48
+ def zizq_serialize(*args, **kwargs) #: (*untyped, **untyped) -> Array[untyped]
49
+ new(*args, **kwargs).serialize["arguments"]
50
+ end
51
+
52
+ # Deserialization is handled by ActiveJob::Base.execute on the worker
53
+ # side. This method is not used in the ActiveJob dispatch path.
54
+ def zizq_deserialize(_payload) #: (untyped) -> [Array[untyped], Hash[Symbol, untyped]]
55
+ raise NotImplementedError,
56
+ "ActiveJob handles deserialization via ActiveJob::Base.execute"
57
+ end
58
+
59
+ # Generate a jq expression that exactly matches payloads with the given
60
+ # arguments.
61
+ #
62
+ # This is used for filtering in Zizq::Query.
63
+ #
64
+ # Generates an expression of the form:
65
+ #
66
+ # .arguments == ["a","b",{"example":true,"_aj_ruby2_keywords":["example"]}]
67
+ def zizq_payload_filter(*args, **kwargs) #: (*untyped, **untyped) -> String
68
+ payload = zizq_serialize(*args, **kwargs)
69
+ ".arguments == #{JSON.generate(payload)}"
70
+ end
71
+
72
+ # Generate a jq expression that matches jobs whose positional args
73
+ # start with the given values and whose kwargs contain the given
74
+ # key/value pairs.
75
+ #
76
+ # This is used for filtering in Zizq::Query.
77
+ #
78
+ # Generates expressions of the form:
79
+ #
80
+ # (.arguments[0:2] == ["a","b"])
81
+ #
82
+ # or
83
+ #
84
+ # (.arguments[0:2] == ["a","b"]) and
85
+ # (.arguments[-1] | has("_aj_ruby2_keywords")) and
86
+ # (.arguments[-1] | contains({"example":true}))
87
+ def zizq_payload_subset_filter(*args, **kwargs) #: (*untyped, **untyped) -> String
88
+ payload = zizq_serialize(*args, **kwargs)
89
+
90
+ # ActiveJob flattens arguments into a single array, but marks kwargs with
91
+ # "_aj_ruby2_keywords" => ["key1", "key2", ...] in the last element of
92
+ # the array where kwargs are present. We need to detect this to generate
93
+ # a suitable expression.
94
+ serialized_args, serialized_kwargs =
95
+ if payload.size > 0
96
+ # See what the last argument looks like. It might be kwargs.
97
+ maybe_kwargs = payload.pop
98
+
99
+ # If it's got "_aj_ruby2_keywords" then it is kwargs.
100
+ if maybe_kwargs.is_a?(Hash) && maybe_kwargs["_aj_ruby2_keywords"]
101
+ # We only want the actual kwargs, not the marker.
102
+ [payload, maybe_kwargs.except("_aj_ruby2_keywords")]
103
+ else
104
+ # It wasn't kwargs, so put it back.
105
+ [payload.push(maybe_kwargs), nil]
106
+ end
107
+ else
108
+ [payload, nil]
109
+ end
110
+
111
+ parts = [] #: Array[String]
112
+ parts << %Q<(.arguments[0:#{serialized_args.size}] == #{JSON.generate(serialized_args)})>
113
+
114
+ if serialized_kwargs
115
+ parts << %Q<(.arguments[-1] | has("_aj_ruby2_keywords"))>
116
+ parts << %Q<(.arguments[-1] | contains(#{JSON.generate(serialized_kwargs)}))>
117
+ end
118
+
119
+ parts.join(" and ")
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,50 @@
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
+ # Encapsulates exponential backoff state for retry loops.
9
+ #
10
+ # Each call to `wait` sleeps for the current duration and then advances
11
+ # to the next interval. Call `reset` to return to the initial wait time
12
+ # after a successful operation.
13
+ class Backoff
14
+ attr_reader :min_wait #: Float
15
+ attr_reader :max_wait #: Float
16
+ attr_reader :multiplier #: Float
17
+
18
+ # @rbs min_wait: (Float | Integer)
19
+ # @rbs max_wait: (Float | Integer)
20
+ # @rbs multiplier: (Float | Integer)
21
+ # @rbs return: void
22
+ def initialize(min_wait:, max_wait:, multiplier:)
23
+ @min_wait = min_wait.to_f
24
+ @max_wait = max_wait.to_f
25
+ @multiplier = multiplier.to_f
26
+ @current = @min_wait #: Float
27
+ end
28
+
29
+ # Returns the current backoff duration without advancing.
30
+ def duration #: () -> Float
31
+ @current
32
+ end
33
+
34
+ # Sleeps for the current backoff duration, then advances to the next.
35
+ def wait #: () -> void
36
+ sleep @current
37
+ @current = [@current * @multiplier, @max_wait].min
38
+ end
39
+
40
+ # Resets the backoff to the initial min_wait.
41
+ def reset #: () -> void
42
+ @current = @min_wait
43
+ end
44
+
45
+ # Returns a new Backoff with the same configuration but reset state.
46
+ def fresh #: () -> Backoff
47
+ self.class.new(min_wait: @min_wait, max_wait: @max_wait, multiplier: @multiplier)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,87 @@
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
+ # Builder for collecting multiple job params to be sent as a single bulk
9
+ # request via `Zizq.enqueue_bulk`.
10
+ #
11
+ # Zizq.enqueue_bulk do |b|
12
+ # b.enqueue(MyApp::FooJob, 42)
13
+ # b.enqueue(MyApp::OtherJob, 42, x: 7)
14
+ # end
15
+ class BulkEnqueue
16
+ attr_reader :requests #: Array[EnqueueRequest]
17
+
18
+ def initialize #: () -> void
19
+ @requests = [] #: Array[EnqueueRequest]
20
+ end
21
+
22
+ # Collect a job class enqueue. Accepts the same arguments as
23
+ # `Zizq.enqueue`.
24
+ #
25
+ # @rbs job_class: Class & Zizq::job_class
26
+ # @rbs args: Array[untyped]
27
+ # @rbs kwargs: Hash[Symbol, untyped]
28
+ # @rbs &block: ?(EnqueueRequest) -> void
29
+ # @rbs return: void
30
+ def enqueue(job_class, *args, **kwargs, &block)
31
+ @requests << Zizq.build_enqueue_request(job_class, *args, **kwargs, &block)
32
+ end
33
+
34
+ # Collect a raw enqueue. Accepts the same arguments as
35
+ # `Zizq.enqueue_raw`.
36
+ #
37
+ # @rbs queue: String
38
+ # @rbs type: String
39
+ # @rbs payload: untyped
40
+ # @rbs priority: Integer?
41
+ # @rbs ready_at: Zizq::to_f?
42
+ # @rbs retry_limit: Integer?
43
+ # @rbs backoff: Zizq::backoff?
44
+ # @rbs retention: Zizq::retention?
45
+ # @rbs unique_key: String?
46
+ # @rbs unique_while: Zizq::unique_scope?
47
+ # @rbs return: void
48
+ def enqueue_raw(queue:, type:, payload:, **opts)
49
+ @requests << EnqueueRequest.new(queue:, type:, payload:, **opts)
50
+ end
51
+
52
+ # Build a scoped enqueue helper that applies the given overrides to a
53
+ # single enqueue inside this bulk block. Sugar for the block form:
54
+ #
55
+ # b.enqueue_with(ready_at: Time.now + 3600).enqueue(OtherJob, 42)
56
+ #
57
+ # is equivalent to:
58
+ #
59
+ # b.enqueue(OtherJob, 42) { |req| req.ready_at = Time.now + 3600 }
60
+ #
61
+ # @rbs overrides: Hash[Symbol, untyped]
62
+ # @rbs return: EnqueueWith
63
+ def enqueue_with(**overrides)
64
+ EnqueueWith.new(self, overrides)
65
+ end
66
+
67
+ # Nested bulk is a no-op — we're already inside a bulk block, so we
68
+ # just yield this same builder. This exists to satisfy the
69
+ # `_EnqueueTarget` interface, which lets `EnqueueWith#enqueue_bulk`
70
+ # work uniformly against both the top-level `Zizq` module and a
71
+ # `BulkEnqueue` instance without branching on target type.
72
+ #
73
+ # Zizq.enqueue_bulk do |b|
74
+ # b.enqueue_with(priority: 0).enqueue_bulk do |b2|
75
+ # b2.enqueue(MyJob, 1)
76
+ # b2.enqueue(MyJob, 2)
77
+ # end
78
+ # end
79
+ #
80
+ # @rbs &block: (BulkEnqueue) -> void
81
+ # @rbs return: self
82
+ def enqueue_bulk(&block)
83
+ yield self
84
+ self
85
+ end
86
+ end
87
+ end