dispatch_policy 0.4.1 → 0.4.2
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 +4 -4
- data/CHANGELOG.md +61 -0
- data/app/controllers/dispatch_policy/partitions_controller.rb +11 -17
- data/lib/dispatch_policy/manual_admission.rb +49 -0
- data/lib/dispatch_policy/tick.rb +14 -0
- data/lib/dispatch_policy/version.rb +1 -1
- data/lib/dispatch_policy.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0f7f08701682539b873609e4f45432661e9d9e070e5b27593695a90b6266595e
|
|
4
|
+
data.tar.gz: c32b4f91f257b1e69e0417edc44fb3779c3f0567f4dd152d893fa781ca7d3142
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 982defdd7fda9aae96d31b83666bd291bf4c36dde33e673f43322ee35cbb830c547a9ada0bbcbe142b47a00f10112d6aae24f1631a06f94e97e2e9ef71365239
|
|
7
|
+
data.tar.gz: e928eb7605905fb7de6867d4933332789ebd36e932be2f6784ed672fdcd485bf3228b659a5947b5965e7e47ee3e2c9089dd6cdd5b6069764008f682eacbacaa2
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.2
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- The engine UI's **"admit"** and **"drain"** buttons now claim and
|
|
7
|
+
forward jobs inside a single transaction, matching the Tick's
|
|
8
|
+
at-least-once guarantee. They previously ran
|
|
9
|
+
`Repository.claim_staged_jobs!` (a `DELETE … RETURNING` that
|
|
10
|
+
autocommits on its own) and then a bare `Forwarder.dispatch` in a
|
|
11
|
+
separate statement — so if the forward raised (deserialize, adapter,
|
|
12
|
+
network), the staged rows were already deleted and the jobs were
|
|
13
|
+
lost. The atomic primitive now lives in
|
|
14
|
+
`DispatchPolicy::ManualAdmission.force!` and both controller actions
|
|
15
|
+
delegate to it.
|
|
16
|
+
- The same UI paths now regenerate `active_job_id` per row before the
|
|
17
|
+
adapter handoff, as the Tick admission path already did in 0.4.1.
|
|
18
|
+
Without it the manual buttons could raise
|
|
19
|
+
`ActiveRecord::RecordNotUnique` against a residual adapter row from a
|
|
20
|
+
previous admission — which, combined with the missing transaction,
|
|
21
|
+
both 500'd the request and lost the staged rows.
|
|
22
|
+
- Fixes a latent `NoMethodError` in the `admit` action: the old
|
|
23
|
+
`rows.size - Forwarder.dispatch(rows).size` raised when the claim
|
|
24
|
+
came back empty (`dispatch` returns `nil`) and otherwise reported a
|
|
25
|
+
misleading forwarded count.
|
|
26
|
+
- Tick pass-2 budget redistribution no longer double-spends the
|
|
27
|
+
`throttle` token bucket. When `tick_admission_budget` is set, pass-2
|
|
28
|
+
re-evaluates a partition's gates against the in-memory partition
|
|
29
|
+
hash; the throttle bucket lives in `partitions.gate_state`, which was
|
|
30
|
+
persisted to the DB in pass-1 but never mirrored back in memory. So
|
|
31
|
+
pass-2 re-read the pre-pass-1 token count, admitted again from a full
|
|
32
|
+
bucket (above the configured rate), and persisted a patch computed
|
|
33
|
+
off the stale base — silently dropping pass-1's consumption, so the
|
|
34
|
+
effective rate drifted upward tick after tick. The committed
|
|
35
|
+
`gate_state` patch is now shallow-merged back onto the in-memory
|
|
36
|
+
partition after each admit. (`concurrency` / `adaptive_concurrency`
|
|
37
|
+
were unaffected — they re-read their counts from the DB on every
|
|
38
|
+
evaluate.)
|
|
39
|
+
|
|
3
40
|
## 0.4.1
|
|
4
41
|
|
|
5
42
|
### Fixed
|
|
@@ -26,6 +63,30 @@
|
|
|
26
63
|
never drain again. -700 still yields a finite ~9.86e-305, which is
|
|
27
64
|
effectively zero for the EWMA.
|
|
28
65
|
|
|
66
|
+
## 0.4.0
|
|
67
|
+
|
|
68
|
+
### Added
|
|
69
|
+
- Admin UI dark mode with an auto / light / dark selector in the
|
|
70
|
+
header that persists across pages.
|
|
71
|
+
- Conceptual logo (chevrons + gate + admitted dot across 3
|
|
72
|
+
partitions), surfaced in the admin header and README. Theme-aware
|
|
73
|
+
lockup keeps the wordmark readable on both light and dark
|
|
74
|
+
backgrounds.
|
|
75
|
+
- Vendored Turbo, served by the engine, so the admin UI no longer
|
|
76
|
+
depends on the host app shipping Turbo itself.
|
|
77
|
+
- `screenshots` Rake task that regenerates all README screenshots
|
|
78
|
+
from the dummy app instead of importing static assets.
|
|
79
|
+
|
|
80
|
+
### Changed
|
|
81
|
+
- Slimmer brand bundle in the gem: only the two masters actually
|
|
82
|
+
used are shipped; the full art set stays in `arts/` for source.
|
|
83
|
+
- README install section drops the obsolete v2-branch banner — the
|
|
84
|
+
gem now installs cleanly from RubyGems.
|
|
85
|
+
|
|
86
|
+
### Fixed
|
|
87
|
+
- Dummy app opts into the gem's `db/migrate` path so its setup
|
|
88
|
+
picks up new tables without manual copying.
|
|
89
|
+
|
|
29
90
|
## 0.3.0
|
|
30
91
|
|
|
31
92
|
### Added
|
|
@@ -67,15 +67,12 @@ module DispatchPolicy
|
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
def admit
|
|
70
|
-
count
|
|
71
|
-
|
|
72
|
-
policy_name:
|
|
73
|
-
partition_key:
|
|
74
|
-
limit:
|
|
75
|
-
gate_state_patch: {},
|
|
76
|
-
retry_after: nil
|
|
70
|
+
count = Integer(params[:count] || 1)
|
|
71
|
+
forwarded = ManualAdmission.force!(
|
|
72
|
+
policy_name: @partition.policy_name,
|
|
73
|
+
partition_key: @partition.partition_key,
|
|
74
|
+
limit: count
|
|
77
75
|
)
|
|
78
|
-
forwarded = rows.size - Forwarder.dispatch(rows).size
|
|
79
76
|
redirect_to partition_path(@partition), notice: "Forwarded #{forwarded} job(s)."
|
|
80
77
|
end
|
|
81
78
|
|
|
@@ -97,17 +94,14 @@ module DispatchPolicy
|
|
|
97
94
|
drained = 0
|
|
98
95
|
while drained < DRAIN_MAX_PER_REQUEST
|
|
99
96
|
batch_limit = [DRAIN_BATCH_SIZE, DRAIN_MAX_PER_REQUEST - drained].min
|
|
100
|
-
|
|
101
|
-
policy_name:
|
|
102
|
-
partition_key:
|
|
103
|
-
limit:
|
|
104
|
-
gate_state_patch: {},
|
|
105
|
-
retry_after: nil
|
|
97
|
+
forwarded = ManualAdmission.force!(
|
|
98
|
+
policy_name: partition.policy_name,
|
|
99
|
+
partition_key: partition.partition_key,
|
|
100
|
+
limit: batch_limit
|
|
106
101
|
)
|
|
107
|
-
break if
|
|
102
|
+
break if forwarded.zero?
|
|
108
103
|
|
|
109
|
-
|
|
110
|
-
drained += rows.size
|
|
104
|
+
drained += forwarded
|
|
111
105
|
end
|
|
112
106
|
remaining = partition.class.where(id: partition.id).pick(:pending_count) || 0
|
|
113
107
|
[drained, remaining]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module DispatchPolicy
|
|
6
|
+
# Force-admit staged jobs for a partition, bypassing every gate. Backs
|
|
7
|
+
# the engine UI's "admit" and "drain" buttons.
|
|
8
|
+
#
|
|
9
|
+
# Mirrors the atomicity guarantee of Tick#admit_partition: the DELETE
|
|
10
|
+
# from staged_jobs (via Repository.claim_staged_jobs!) and the adapter
|
|
11
|
+
# enqueue (Forwarder.dispatch) run in a SINGLE transaction, so any
|
|
12
|
+
# failure — deserialize, adapter, network — rolls the claim back and the
|
|
13
|
+
# staged rows survive. Without this the UI buttons would DELETE staged
|
|
14
|
+
# rows and then lose them whenever the forward raised, breaking the
|
|
15
|
+
# gem's at-least-once contract on a path the Tick already protects.
|
|
16
|
+
#
|
|
17
|
+
# active_job_id is regenerated per row for the same reason Tick does it
|
|
18
|
+
# (see Tick#admit_partition): adapters that key their jobs table on
|
|
19
|
+
# active_job_id (good_job, solid_queue) raise RecordNotUnique against a
|
|
20
|
+
# residual row from a previous admission, which would abort the TX.
|
|
21
|
+
module ManualAdmission
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
# Force-admit up to `limit` staged jobs for the partition, bypassing
|
|
25
|
+
# all gates, atomically. Returns the number of jobs forwarded.
|
|
26
|
+
def force!(policy_name:, partition_key:, limit:)
|
|
27
|
+
return 0 unless limit.positive?
|
|
28
|
+
|
|
29
|
+
forwarded = 0
|
|
30
|
+
Repository.with_connection do
|
|
31
|
+
ActiveRecord::Base.transaction(requires_new: true) do
|
|
32
|
+
rows = Repository.claim_staged_jobs!(
|
|
33
|
+
policy_name: policy_name,
|
|
34
|
+
partition_key: partition_key,
|
|
35
|
+
limit: limit,
|
|
36
|
+
gate_state_patch: {},
|
|
37
|
+
retry_after: nil
|
|
38
|
+
)
|
|
39
|
+
next if rows.empty?
|
|
40
|
+
|
|
41
|
+
rows.each { |row| row["job_data"]["job_id"] = SecureRandom.uuid }
|
|
42
|
+
Forwarder.dispatch(rows)
|
|
43
|
+
forwarded = rows.size
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
forwarded
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/dispatch_policy/tick.rb
CHANGED
|
@@ -286,6 +286,20 @@ module DispatchPolicy
|
|
|
286
286
|
end
|
|
287
287
|
end
|
|
288
288
|
|
|
289
|
+
# Reflect the just-committed gate_state patch on the in-memory
|
|
290
|
+
# partition. `record_partition_admit!` persisted `gate_state || patch`
|
|
291
|
+
# inside the TX above; without mirroring it here, a pass-2 re-admit
|
|
292
|
+
# (Tick budget redistribution) would re-evaluate the gates against
|
|
293
|
+
# the STALE pre-pass-1 snapshot. For the throttle that means reading
|
|
294
|
+
# the token bucket at its original level and double-spending —
|
|
295
|
+
# admitting above the configured rate and overwriting pass-1's
|
|
296
|
+
# consumption. The shallow merge matches Postgres jsonb `||`.
|
|
297
|
+
# Only runs on a committed admit: if the TX raised we fall through to
|
|
298
|
+
# the rescue below and never touch the in-memory state.
|
|
299
|
+
if result.gate_state_patch&.any?
|
|
300
|
+
partition["gate_state"] = (partition["gate_state"] || {}).merge(result.gate_state_patch)
|
|
301
|
+
end
|
|
302
|
+
|
|
289
303
|
if admitted.zero?
|
|
290
304
|
{ admitted: 0, failures: 0, reasons: ["no_rows_claimed"] }
|
|
291
305
|
else
|
data/lib/dispatch_policy.rb
CHANGED
|
@@ -21,6 +21,7 @@ require_relative "dispatch_policy/cursor_pagination"
|
|
|
21
21
|
require_relative "dispatch_policy/pipeline"
|
|
22
22
|
require_relative "dispatch_policy/repository"
|
|
23
23
|
require_relative "dispatch_policy/forwarder"
|
|
24
|
+
require_relative "dispatch_policy/manual_admission"
|
|
24
25
|
require_relative "dispatch_policy/inflight_tracker"
|
|
25
26
|
require_relative "dispatch_policy/tick"
|
|
26
27
|
require_relative "dispatch_policy/tick_loop"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dispatch_policy
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- José Galisteo
|
|
@@ -233,6 +233,7 @@ files:
|
|
|
233
233
|
- lib/dispatch_policy/gates/throttle.rb
|
|
234
234
|
- lib/dispatch_policy/inflight_tracker.rb
|
|
235
235
|
- lib/dispatch_policy/job_extension.rb
|
|
236
|
+
- lib/dispatch_policy/manual_admission.rb
|
|
236
237
|
- lib/dispatch_policy/operator_hints.rb
|
|
237
238
|
- lib/dispatch_policy/pipeline.rb
|
|
238
239
|
- lib/dispatch_policy/policy.rb
|