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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +94 -0
- data/bin/profile-worker +145 -0
- data/bin/zizq-worker +174 -0
- data/lib/active_job/queue_adapters/zizq_adapter.rb +109 -0
- data/lib/zizq/ack_processor.rb +132 -0
- data/lib/zizq/active_job_config.rb +122 -0
- data/lib/zizq/backoff.rb +50 -0
- data/lib/zizq/bulk_enqueue.rb +87 -0
- data/lib/zizq/client.rb +982 -0
- data/lib/zizq/configuration.rb +164 -0
- data/lib/zizq/enqueue_request.rb +178 -0
- data/lib/zizq/enqueue_with.rb +109 -0
- data/lib/zizq/error.rb +43 -0
- data/lib/zizq/job.rb +188 -0
- data/lib/zizq/job_config.rb +244 -0
- data/lib/zizq/lifecycle.rb +58 -0
- data/lib/zizq/middleware.rb +79 -0
- data/lib/zizq/query.rb +566 -0
- data/lib/zizq/resources/error_enumerator.rb +241 -0
- data/lib/zizq/resources/error_page.rb +19 -0
- data/lib/zizq/resources/error_record.rb +19 -0
- data/lib/zizq/resources/job.rb +124 -0
- data/lib/zizq/resources/job_page.rb +57 -0
- data/lib/zizq/resources/page.rb +77 -0
- data/lib/zizq/resources/resource.rb +45 -0
- data/lib/zizq/resources.rb +16 -0
- data/lib/zizq/version.rb +9 -0
- data/lib/zizq/worker.rb +467 -0
- data/lib/zizq.rb +269 -0
- data/sig/generated/zizq/ack_processor.rbs +73 -0
- data/sig/generated/zizq/active_job_config.rbs +74 -0
- data/sig/generated/zizq/backoff.rbs +34 -0
- data/sig/generated/zizq/bulk_enqueue.rbs +72 -0
- data/sig/generated/zizq/client.rbs +419 -0
- data/sig/generated/zizq/configuration.rbs +95 -0
- data/sig/generated/zizq/enqueue_request.rbs +94 -0
- data/sig/generated/zizq/enqueue_with.rbs +88 -0
- data/sig/generated/zizq/error.rbs +41 -0
- data/sig/generated/zizq/job.rbs +136 -0
- data/sig/generated/zizq/job_config.rbs +150 -0
- data/sig/generated/zizq/lifecycle.rbs +34 -0
- data/sig/generated/zizq/middleware.rbs +50 -0
- data/sig/generated/zizq/query.rbs +327 -0
- data/sig/generated/zizq/resources/error_enumerator.rbs +148 -0
- data/sig/generated/zizq/resources/error_page.rbs +13 -0
- data/sig/generated/zizq/resources/error_record.rbs +20 -0
- data/sig/generated/zizq/resources/job.rbs +89 -0
- data/sig/generated/zizq/resources/job_page.rbs +33 -0
- data/sig/generated/zizq/resources/page.rbs +47 -0
- data/sig/generated/zizq/resources/resource.rbs +26 -0
- data/sig/generated/zizq/version.rbs +5 -0
- data/sig/generated/zizq/worker.rbs +152 -0
- data/sig/generated/zizq.rbs +180 -0
- data/sig/zizq.rbs +111 -0
- 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
|
data/lib/zizq/backoff.rb
ADDED
|
@@ -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
|