dispatch_policy 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/CHANGELOG.md +12 -0
- data/MIT-LICENSE +21 -0
- data/README.md +435 -0
- data/app/controllers/dispatch_policy/application_controller.rb +9 -0
- data/app/controllers/dispatch_policy/policies_controller.rb +269 -0
- data/app/models/dispatch_policy/adaptive_concurrency_stats.rb +89 -0
- data/app/models/dispatch_policy/application_record.rb +7 -0
- data/app/models/dispatch_policy/partition_inflight_count.rb +42 -0
- data/app/models/dispatch_policy/partition_observation.rb +49 -0
- data/app/models/dispatch_policy/staged_job.rb +105 -0
- data/app/models/dispatch_policy/throttle_bucket.rb +41 -0
- data/app/views/dispatch_policy/policies/index.html.erb +52 -0
- data/app/views/dispatch_policy/policies/show.html.erb +241 -0
- data/app/views/layouts/dispatch_policy/application.html.erb +266 -0
- data/config/routes.rb +6 -0
- data/db/migrate/20260424000001_create_dispatch_policy_tables.rb +80 -0
- data/db/migrate/20260424000002_create_adaptive_concurrency_stats.rb +22 -0
- data/db/migrate/20260424000003_create_adaptive_concurrency_samples.rb +25 -0
- data/db/migrate/20260424000004_rename_samples_to_partition_observations.rb +32 -0
- data/lib/dispatch_policy/active_job_perform_all_later_patch.rb +32 -0
- data/lib/dispatch_policy/dispatch_context.rb +53 -0
- data/lib/dispatch_policy/dispatchable.rb +120 -0
- data/lib/dispatch_policy/engine.rb +36 -0
- data/lib/dispatch_policy/gate.rb +49 -0
- data/lib/dispatch_policy/gates/adaptive_concurrency.rb +123 -0
- data/lib/dispatch_policy/gates/concurrency.rb +43 -0
- data/lib/dispatch_policy/gates/fair_interleave.rb +32 -0
- data/lib/dispatch_policy/gates/global_cap.rb +26 -0
- data/lib/dispatch_policy/gates/throttle.rb +52 -0
- data/lib/dispatch_policy/install_generator.rb +23 -0
- data/lib/dispatch_policy/policy.rb +73 -0
- data/lib/dispatch_policy/tick.rb +214 -0
- data/lib/dispatch_policy/tick_loop.rb +45 -0
- data/lib/dispatch_policy/version.rb +5 -0
- data/lib/dispatch_policy.rb +64 -0
- metadata +182 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DispatchPolicy
|
|
4
|
+
class Tick
|
|
5
|
+
THROTTLE_ZERO_THRESHOLD = 0.001
|
|
6
|
+
|
|
7
|
+
# Single admission pass: fetch pending staged jobs per policy, evaluate
|
|
8
|
+
# gates, mark survivors as admitted, then enqueue them on the real
|
|
9
|
+
# backend outside the locking transaction.
|
|
10
|
+
def self.run(policy_name: nil)
|
|
11
|
+
return 0 unless DispatchPolicy.enabled?
|
|
12
|
+
|
|
13
|
+
pending_enqueue = []
|
|
14
|
+
|
|
15
|
+
StagedJob.transaction do
|
|
16
|
+
active_policies(policy_name).each do |pname|
|
|
17
|
+
policy = lookup_policy(pname)
|
|
18
|
+
next unless policy
|
|
19
|
+
|
|
20
|
+
batch = fetch_batch(policy)
|
|
21
|
+
next if batch.empty?
|
|
22
|
+
|
|
23
|
+
pending_enqueue.concat(run_policy(policy, batch))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
admitted_count = 0
|
|
28
|
+
pending_enqueue.each do |staged, job|
|
|
29
|
+
begin
|
|
30
|
+
job.enqueue(_bypass_staging: true)
|
|
31
|
+
admitted_count += 1
|
|
32
|
+
rescue StandardError => e
|
|
33
|
+
Rails.logger&.error("[DispatchPolicy] enqueue failed staged=#{staged.id}: #{e.class}: #{e.message}")
|
|
34
|
+
revert_admission(staged)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
admitted_count
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.prune_idle_partitions
|
|
42
|
+
ttl = DispatchPolicy.config.partition_idle_ttl
|
|
43
|
+
return if ttl.nil? || ttl <= 0
|
|
44
|
+
|
|
45
|
+
cutoff = Time.current - ttl
|
|
46
|
+
PartitionInflightCount.where(in_flight: 0).where("updated_at < ?", cutoff).delete_all
|
|
47
|
+
ThrottleBucket.where("tokens <= ? AND refilled_at < ?", THROTTLE_ZERO_THRESHOLD, cutoff).delete_all
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.prune_orphan_gate_rows
|
|
51
|
+
[ PartitionInflightCount, ThrottleBucket ].each do |model|
|
|
52
|
+
model.distinct.pluck(:policy_name, :gate_name).each do |policy_name, gate_name|
|
|
53
|
+
policy = lookup_policy(policy_name)
|
|
54
|
+
next if policy && policy.gates.any? { |g| g.name == gate_name.to_sym }
|
|
55
|
+
|
|
56
|
+
model.where(policy_name: policy_name, gate_name: gate_name).delete_all
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.reap
|
|
62
|
+
StagedJob.expired_leases.find_each do |staged|
|
|
63
|
+
(staged.partitions || {}).each do |gate_name, partition_key|
|
|
64
|
+
policy = lookup_policy(staged.policy_name)
|
|
65
|
+
gate = policy&.gates&.find { |g| g.name == gate_name.to_sym }
|
|
66
|
+
next unless gate&.tracks_inflight?
|
|
67
|
+
|
|
68
|
+
PartitionInflightCount.decrement(
|
|
69
|
+
policy_name: staged.policy_name,
|
|
70
|
+
gate_name: gate_name.to_s,
|
|
71
|
+
partition_key: partition_key.to_s
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
staged.update!(lease_expires_at: nil, completed_at: Time.current)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.release(policy_name:, partitions:)
|
|
79
|
+
partitions.each do |gate_name, partition_key|
|
|
80
|
+
policy = lookup_policy(policy_name)
|
|
81
|
+
gate = policy&.gates&.find { |g| g.name == gate_name.to_sym }
|
|
82
|
+
next unless gate&.tracks_inflight?
|
|
83
|
+
|
|
84
|
+
PartitionInflightCount.decrement(
|
|
85
|
+
policy_name: policy_name,
|
|
86
|
+
gate_name: gate_name.to_s,
|
|
87
|
+
partition_key: partition_key.to_s
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def self.active_policies(policy_name)
|
|
93
|
+
return [ policy_name ] if policy_name
|
|
94
|
+
|
|
95
|
+
StagedJob.pending
|
|
96
|
+
.where("not_before_at IS NULL OR not_before_at <= ?", Time.current)
|
|
97
|
+
.distinct
|
|
98
|
+
.pluck(:policy_name)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def self.fetch_batch(policy)
|
|
102
|
+
if policy.round_robin?
|
|
103
|
+
fetch_round_robin_batch(policy)
|
|
104
|
+
else
|
|
105
|
+
fetch_plain_batch(policy)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def self.fetch_plain_batch(policy)
|
|
110
|
+
StagedJob.pending
|
|
111
|
+
.where(policy_name: policy.name)
|
|
112
|
+
.where("not_before_at IS NULL OR not_before_at <= ?", Time.current)
|
|
113
|
+
.order(:priority, :staged_at)
|
|
114
|
+
.limit(DispatchPolicy.config.batch_size)
|
|
115
|
+
.lock("FOR UPDATE SKIP LOCKED")
|
|
116
|
+
.to_a
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def self.fetch_round_robin_batch(policy)
|
|
120
|
+
quantum = DispatchPolicy.config.round_robin_quantum
|
|
121
|
+
batch_size = DispatchPolicy.config.batch_size
|
|
122
|
+
now = Time.current
|
|
123
|
+
|
|
124
|
+
sql = <<~SQL.squish
|
|
125
|
+
SELECT rows.*
|
|
126
|
+
FROM (
|
|
127
|
+
SELECT DISTINCT round_robin_key
|
|
128
|
+
FROM dispatch_policy_staged_jobs
|
|
129
|
+
WHERE policy_name = ?
|
|
130
|
+
AND admitted_at IS NULL
|
|
131
|
+
AND round_robin_key IS NOT NULL
|
|
132
|
+
AND (not_before_at IS NULL OR not_before_at <= ?)
|
|
133
|
+
) AS keys
|
|
134
|
+
CROSS JOIN LATERAL (
|
|
135
|
+
SELECT *
|
|
136
|
+
FROM dispatch_policy_staged_jobs
|
|
137
|
+
WHERE policy_name = ?
|
|
138
|
+
AND admitted_at IS NULL
|
|
139
|
+
AND round_robin_key = keys.round_robin_key
|
|
140
|
+
AND (not_before_at IS NULL OR not_before_at <= ?)
|
|
141
|
+
ORDER BY priority, staged_at
|
|
142
|
+
LIMIT ?
|
|
143
|
+
FOR UPDATE SKIP LOCKED
|
|
144
|
+
) AS rows
|
|
145
|
+
LIMIT ?
|
|
146
|
+
SQL
|
|
147
|
+
|
|
148
|
+
batch = StagedJob.find_by_sql([ sql, policy.name, now, policy.name, now, quantum, batch_size ])
|
|
149
|
+
|
|
150
|
+
remaining = batch_size - batch.size
|
|
151
|
+
return batch if remaining <= 0
|
|
152
|
+
|
|
153
|
+
top_up = StagedJob.pending
|
|
154
|
+
.where(policy_name: policy.name)
|
|
155
|
+
.where("not_before_at IS NULL OR not_before_at <= ?", now)
|
|
156
|
+
.where.not(id: batch.map(&:id))
|
|
157
|
+
.order(:priority, :staged_at)
|
|
158
|
+
.limit(remaining)
|
|
159
|
+
.lock("FOR UPDATE SKIP LOCKED")
|
|
160
|
+
.to_a
|
|
161
|
+
|
|
162
|
+
batch + top_up
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def self.lookup_policy(policy_name)
|
|
166
|
+
job_class = DispatchPolicy.registry[policy_name] || autoload_job_for(policy_name)
|
|
167
|
+
return nil unless job_class
|
|
168
|
+
job_class.resolved_dispatch_policy
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def self.autoload_job_for(policy_name)
|
|
172
|
+
const_name = policy_name.tr("-", "/").camelize
|
|
173
|
+
const_name.safe_constantize
|
|
174
|
+
DispatchPolicy.registry[policy_name]
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def self.run_policy(policy, batch)
|
|
178
|
+
context = DispatchContext.new(policy: policy, batch: batch)
|
|
179
|
+
survivors = batch
|
|
180
|
+
policy.gates.each do |gate|
|
|
181
|
+
survivors = gate.filter(survivors, context)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
survivors.map do |staged|
|
|
185
|
+
partitions = context.partitions_for(staged)
|
|
186
|
+
|
|
187
|
+
partitions.each do |gate_name, partition_key|
|
|
188
|
+
gate = policy.gates.find { |g| g.name == gate_name.to_sym }
|
|
189
|
+
next unless gate&.tracks_inflight?
|
|
190
|
+
|
|
191
|
+
PartitionInflightCount.increment(
|
|
192
|
+
policy_name: policy.name,
|
|
193
|
+
gate_name: gate_name.to_s,
|
|
194
|
+
partition_key: partition_key.to_s
|
|
195
|
+
)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
job = staged.mark_admitted!(partitions: partitions)
|
|
199
|
+
[ staged, job ]
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def self.revert_admission(staged)
|
|
204
|
+
partitions = staged.partitions || {}
|
|
205
|
+
release(policy_name: staged.policy_name, partitions: partitions)
|
|
206
|
+
staged.update_columns(
|
|
207
|
+
admitted_at: nil,
|
|
208
|
+
lease_expires_at: nil,
|
|
209
|
+
active_job_id: nil,
|
|
210
|
+
partitions: {}
|
|
211
|
+
)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DispatchPolicy
|
|
4
|
+
# Shared driver for DispatchTickLoopJob and any foreground tick (e.g. a
|
|
5
|
+
# rake task). Loops Tick.reap + Tick.run with an interruptible sleep and
|
|
6
|
+
# bails when stop_when returns true.
|
|
7
|
+
class TickLoop
|
|
8
|
+
def self.run(policy_name: nil, sleep_for: nil, sleep_for_busy: nil, stop_when: -> { false })
|
|
9
|
+
idle_sleep = (sleep_for || DispatchPolicy.config.tick_sleep).to_f
|
|
10
|
+
busy_sleep = (sleep_for_busy || DispatchPolicy.config.tick_sleep_busy).to_f
|
|
11
|
+
|
|
12
|
+
loop do
|
|
13
|
+
break if stop_when.call
|
|
14
|
+
|
|
15
|
+
admitted = 0
|
|
16
|
+
begin
|
|
17
|
+
ActiveRecord::Base.uncached do
|
|
18
|
+
Tick.reap
|
|
19
|
+
admitted = Tick.run(policy_name: policy_name).to_i
|
|
20
|
+
end
|
|
21
|
+
rescue StandardError => e
|
|
22
|
+
Rails.logger&.error("[DispatchPolicy] tick error: #{e.class}: #{e.message}")
|
|
23
|
+
Rails.error.report(e, handled: true) if defined?(Rails) && Rails.respond_to?(:error)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
break if stop_when.call
|
|
27
|
+
|
|
28
|
+
interruptible_sleep(admitted.positive? ? busy_sleep : idle_sleep, stop_when)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.interruptible_sleep(total, stop_when)
|
|
33
|
+
return unless total.positive?
|
|
34
|
+
|
|
35
|
+
remaining = total
|
|
36
|
+
step = 0.1
|
|
37
|
+
while remaining.positive?
|
|
38
|
+
break if stop_when.call
|
|
39
|
+
chunk = [ remaining, step ].min
|
|
40
|
+
sleep(chunk)
|
|
41
|
+
remaining -= chunk
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_job"
|
|
4
|
+
require "active_record"
|
|
5
|
+
|
|
6
|
+
require "dispatch_policy/version"
|
|
7
|
+
require "dispatch_policy/engine" if defined?(Rails)
|
|
8
|
+
|
|
9
|
+
module DispatchPolicy
|
|
10
|
+
Config = Struct.new(
|
|
11
|
+
:enabled,
|
|
12
|
+
:lease_duration,
|
|
13
|
+
:batch_size,
|
|
14
|
+
:round_robin_quantum,
|
|
15
|
+
:tick_max_duration,
|
|
16
|
+
:tick_sleep,
|
|
17
|
+
:tick_sleep_busy,
|
|
18
|
+
:partition_idle_ttl,
|
|
19
|
+
keyword_init: true
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def self.config
|
|
23
|
+
@config ||= Config.new(
|
|
24
|
+
enabled: true,
|
|
25
|
+
lease_duration: 15 * 60, # 15.minutes
|
|
26
|
+
batch_size: 500,
|
|
27
|
+
round_robin_quantum: 50,
|
|
28
|
+
tick_max_duration: 60, # 1.minute
|
|
29
|
+
tick_sleep: 1, # idle sleep
|
|
30
|
+
tick_sleep_busy: 0.05, # busy sleep
|
|
31
|
+
partition_idle_ttl: 30 * 60 # 30.minutes
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.configure
|
|
36
|
+
yield config
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.enabled?
|
|
40
|
+
config.enabled != false
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Registry: policy_name => job_class. Populated by Policy#initialize.
|
|
44
|
+
def self.registry
|
|
45
|
+
@registry ||= {}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.reset_registry!
|
|
49
|
+
@registry = {}
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
require "dispatch_policy/policy"
|
|
54
|
+
require "dispatch_policy/gate"
|
|
55
|
+
require "dispatch_policy/gates/concurrency"
|
|
56
|
+
require "dispatch_policy/gates/throttle"
|
|
57
|
+
require "dispatch_policy/gates/global_cap"
|
|
58
|
+
require "dispatch_policy/gates/fair_interleave"
|
|
59
|
+
require "dispatch_policy/gates/adaptive_concurrency"
|
|
60
|
+
require "dispatch_policy/dispatch_context"
|
|
61
|
+
require "dispatch_policy/dispatchable"
|
|
62
|
+
require "dispatch_policy/tick"
|
|
63
|
+
require "dispatch_policy/tick_loop"
|
|
64
|
+
require "dispatch_policy/active_job_perform_all_later_patch"
|
metadata
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: dispatch_policy
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- José Galisteo
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activejob
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '7.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: activerecord
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '7.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '7.1'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: railties
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '7.1'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '7.1'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: pg
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: minitest
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: simplecov
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: sprockets-rails
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '0'
|
|
103
|
+
type: :development
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '0'
|
|
110
|
+
description: DispatchPolicy stages ActiveJob enqueues into a policy table and admits
|
|
111
|
+
them through declared gates. Supports per-partition throttle/concurrency, dedupe,
|
|
112
|
+
round-robin fairness, and ships a minimal Rails engine to inspect pending/admitted
|
|
113
|
+
state.
|
|
114
|
+
email:
|
|
115
|
+
- ceritium@gmail.com
|
|
116
|
+
executables: []
|
|
117
|
+
extensions: []
|
|
118
|
+
extra_rdoc_files: []
|
|
119
|
+
files:
|
|
120
|
+
- CHANGELOG.md
|
|
121
|
+
- MIT-LICENSE
|
|
122
|
+
- README.md
|
|
123
|
+
- app/controllers/dispatch_policy/application_controller.rb
|
|
124
|
+
- app/controllers/dispatch_policy/policies_controller.rb
|
|
125
|
+
- app/models/dispatch_policy/adaptive_concurrency_stats.rb
|
|
126
|
+
- app/models/dispatch_policy/application_record.rb
|
|
127
|
+
- app/models/dispatch_policy/partition_inflight_count.rb
|
|
128
|
+
- app/models/dispatch_policy/partition_observation.rb
|
|
129
|
+
- app/models/dispatch_policy/staged_job.rb
|
|
130
|
+
- app/models/dispatch_policy/throttle_bucket.rb
|
|
131
|
+
- app/views/dispatch_policy/policies/index.html.erb
|
|
132
|
+
- app/views/dispatch_policy/policies/show.html.erb
|
|
133
|
+
- app/views/layouts/dispatch_policy/application.html.erb
|
|
134
|
+
- config/routes.rb
|
|
135
|
+
- db/migrate/20260424000001_create_dispatch_policy_tables.rb
|
|
136
|
+
- db/migrate/20260424000002_create_adaptive_concurrency_stats.rb
|
|
137
|
+
- db/migrate/20260424000003_create_adaptive_concurrency_samples.rb
|
|
138
|
+
- db/migrate/20260424000004_rename_samples_to_partition_observations.rb
|
|
139
|
+
- lib/dispatch_policy.rb
|
|
140
|
+
- lib/dispatch_policy/active_job_perform_all_later_patch.rb
|
|
141
|
+
- lib/dispatch_policy/dispatch_context.rb
|
|
142
|
+
- lib/dispatch_policy/dispatchable.rb
|
|
143
|
+
- lib/dispatch_policy/engine.rb
|
|
144
|
+
- lib/dispatch_policy/gate.rb
|
|
145
|
+
- lib/dispatch_policy/gates/adaptive_concurrency.rb
|
|
146
|
+
- lib/dispatch_policy/gates/concurrency.rb
|
|
147
|
+
- lib/dispatch_policy/gates/fair_interleave.rb
|
|
148
|
+
- lib/dispatch_policy/gates/global_cap.rb
|
|
149
|
+
- lib/dispatch_policy/gates/throttle.rb
|
|
150
|
+
- lib/dispatch_policy/install_generator.rb
|
|
151
|
+
- lib/dispatch_policy/policy.rb
|
|
152
|
+
- lib/dispatch_policy/tick.rb
|
|
153
|
+
- lib/dispatch_policy/tick_loop.rb
|
|
154
|
+
- lib/dispatch_policy/version.rb
|
|
155
|
+
homepage: https://github.com/ceritium/dispatch_policy
|
|
156
|
+
licenses:
|
|
157
|
+
- MIT
|
|
158
|
+
metadata:
|
|
159
|
+
homepage_uri: https://github.com/ceritium/dispatch_policy
|
|
160
|
+
source_code_uri: https://github.com/ceritium/dispatch_policy
|
|
161
|
+
bug_tracker_uri: https://github.com/ceritium/dispatch_policy/issues
|
|
162
|
+
changelog_uri: https://github.com/ceritium/dispatch_policy/blob/master/CHANGELOG.md
|
|
163
|
+
rubygems_mfa_required: 'true'
|
|
164
|
+
rdoc_options: []
|
|
165
|
+
require_paths:
|
|
166
|
+
- lib
|
|
167
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
168
|
+
requirements:
|
|
169
|
+
- - ">="
|
|
170
|
+
- !ruby/object:Gem::Version
|
|
171
|
+
version: '3.1'
|
|
172
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
173
|
+
requirements:
|
|
174
|
+
- - ">="
|
|
175
|
+
- !ruby/object:Gem::Version
|
|
176
|
+
version: '0'
|
|
177
|
+
requirements: []
|
|
178
|
+
rubygems_version: 4.0.3
|
|
179
|
+
specification_version: 4
|
|
180
|
+
summary: Per-partition admission control (throttle, concurrency, dedupe, fairness)
|
|
181
|
+
for ActiveJob.
|
|
182
|
+
test_files: []
|