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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 55753a0af85b649115d306ab790978668958d8a4ebe43b44c4b955a00b525b3b
4
- data.tar.gz: 1f12837a2f561ff28f1fa00aedcd5cf92092766b75043a1937658964a0583d90
3
+ metadata.gz: 0f7f08701682539b873609e4f45432661e9d9e070e5b27593695a90b6266595e
4
+ data.tar.gz: c32b4f91f257b1e69e0417edc44fb3779c3f0567f4dd152d893fa781ca7d3142
5
5
  SHA512:
6
- metadata.gz: 98ad50661323d62f22593bf323d61a18716c059a0571fcf8057e0f8b18bdd39dd58214cc399e38b4c96297539cfeb1f841675879733a0ef5d7913261a0c0d42d
7
- data.tar.gz: e54665de99a9cb7c63f522bbefeb406958382831bf0a16e41daaa25ecd977fbdf215c12fd0b91411bed8a9981f454238007ea615767914d7046401c78ff9dc81
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 = Integer(params[:count] || 1)
71
- rows = Repository.claim_staged_jobs!(
72
- policy_name: @partition.policy_name,
73
- partition_key: @partition.partition_key,
74
- limit: count,
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
- rows = Repository.claim_staged_jobs!(
101
- policy_name: partition.policy_name,
102
- partition_key: partition.partition_key,
103
- limit: batch_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 rows.empty?
102
+ break if forwarded.zero?
108
103
 
109
- Forwarder.dispatch(rows)
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DispatchPolicy
4
- VERSION = "0.4.1"
4
+ VERSION = "0.4.2"
5
5
  end
@@ -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.1
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