dispatch_policy 0.2.0 → 0.4.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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +98 -28
  3. data/MIT-LICENSE +16 -17
  4. data/README.md +452 -388
  5. data/app/assets/images/dispatch_policy/logo-large.svg +9 -0
  6. data/app/assets/images/dispatch_policy/logo-small.svg +7 -0
  7. data/app/assets/javascripts/dispatch_policy/turbo.es2017-umd.min.js +35 -0
  8. data/app/assets/stylesheets/dispatch_policy/application.css +294 -0
  9. data/app/controllers/dispatch_policy/application_controller.rb +45 -1
  10. data/app/controllers/dispatch_policy/assets_controller.rb +31 -0
  11. data/app/controllers/dispatch_policy/dashboard_controller.rb +91 -0
  12. data/app/controllers/dispatch_policy/partitions_controller.rb +122 -0
  13. data/app/controllers/dispatch_policy/policies_controller.rb +94 -267
  14. data/app/controllers/dispatch_policy/staged_jobs_controller.rb +9 -0
  15. data/app/models/dispatch_policy/adaptive_concurrency_stats.rb +11 -81
  16. data/app/models/dispatch_policy/inflight_job.rb +12 -0
  17. data/app/models/dispatch_policy/partition.rb +21 -0
  18. data/app/models/dispatch_policy/staged_job.rb +4 -97
  19. data/app/models/dispatch_policy/tick_sample.rb +11 -0
  20. data/app/views/dispatch_policy/dashboard/index.html.erb +109 -0
  21. data/app/views/dispatch_policy/partitions/index.html.erb +63 -0
  22. data/app/views/dispatch_policy/partitions/show.html.erb +106 -0
  23. data/app/views/dispatch_policy/policies/index.html.erb +15 -37
  24. data/app/views/dispatch_policy/policies/show.html.erb +139 -223
  25. data/app/views/dispatch_policy/shared/_capacity.html.erb +67 -0
  26. data/app/views/dispatch_policy/shared/_hints.html.erb +13 -0
  27. data/app/views/dispatch_policy/shared/_partition_row.html.erb +12 -0
  28. data/app/views/dispatch_policy/staged_jobs/show.html.erb +31 -0
  29. data/app/views/layouts/dispatch_policy/application.html.erb +164 -231
  30. data/config/routes.rb +21 -2
  31. data/db/migrate/20260501000001_create_dispatch_policy_tables.rb +103 -0
  32. data/lib/dispatch_policy/assets.rb +38 -0
  33. data/lib/dispatch_policy/bypass.rb +23 -0
  34. data/lib/dispatch_policy/config.rb +85 -0
  35. data/lib/dispatch_policy/context.rb +50 -0
  36. data/lib/dispatch_policy/cursor_pagination.rb +121 -0
  37. data/lib/dispatch_policy/decision.rb +22 -0
  38. data/lib/dispatch_policy/engine.rb +5 -27
  39. data/lib/dispatch_policy/forwarder.rb +63 -0
  40. data/lib/dispatch_policy/gate.rb +10 -38
  41. data/lib/dispatch_policy/gates/adaptive_concurrency.rb +99 -97
  42. data/lib/dispatch_policy/gates/concurrency.rb +45 -26
  43. data/lib/dispatch_policy/gates/throttle.rb +65 -41
  44. data/lib/dispatch_policy/inflight_tracker.rb +174 -0
  45. data/lib/dispatch_policy/job_extension.rb +155 -0
  46. data/lib/dispatch_policy/operator_hints.rb +126 -0
  47. data/lib/dispatch_policy/pipeline.rb +48 -0
  48. data/lib/dispatch_policy/policy.rb +61 -59
  49. data/lib/dispatch_policy/policy_dsl.rb +120 -0
  50. data/lib/dispatch_policy/railtie.rb +35 -0
  51. data/lib/dispatch_policy/registry.rb +46 -0
  52. data/lib/dispatch_policy/repository.rb +723 -0
  53. data/lib/dispatch_policy/serializer.rb +36 -0
  54. data/lib/dispatch_policy/tick.rb +260 -256
  55. data/lib/dispatch_policy/tick_loop.rb +59 -26
  56. data/lib/dispatch_policy/version.rb +1 -1
  57. data/lib/dispatch_policy.rb +72 -52
  58. data/lib/generators/dispatch_policy/install/install_generator.rb +70 -0
  59. data/lib/generators/dispatch_policy/install/templates/create_dispatch_policy_tables.rb.tt +95 -0
  60. data/lib/generators/dispatch_policy/install/templates/dispatch_tick_loop_job.rb.tt +53 -0
  61. data/lib/generators/dispatch_policy/install/templates/initializer.rb.tt +11 -0
  62. metadata +134 -42
  63. data/app/models/dispatch_policy/partition_inflight_count.rb +0 -42
  64. data/app/models/dispatch_policy/partition_observation.rb +0 -76
  65. data/app/models/dispatch_policy/throttle_bucket.rb +0 -41
  66. data/db/migrate/20260424000001_create_dispatch_policy_tables.rb +0 -80
  67. data/db/migrate/20260424000002_create_adaptive_concurrency_stats.rb +0 -22
  68. data/db/migrate/20260424000003_create_adaptive_concurrency_samples.rb +0 -25
  69. data/db/migrate/20260424000004_rename_samples_to_partition_observations.rb +0 -32
  70. data/db/migrate/20260425000001_add_duration_to_partition_observations.rb +0 -8
  71. data/lib/dispatch_policy/active_job_perform_all_later_patch.rb +0 -32
  72. data/lib/dispatch_policy/dispatch_context.rb +0 -53
  73. data/lib/dispatch_policy/dispatchable.rb +0 -123
  74. data/lib/dispatch_policy/gates/fair_interleave.rb +0 -32
  75. data/lib/dispatch_policy/gates/global_cap.rb +0 -26
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6eb153991642d0669fffd7cd3c8c2133c837978faf934bf6b14bf44ae8628907
4
- data.tar.gz: 6ce8ff07f09fbd7763cb191b6305f316106b21a5f11b20a25ecdac3e3e2e1f2c
3
+ metadata.gz: 24ab8c2fe85abc57507f84edc955c8263f59a96505522ecd9ceb6ce60e14bcba
4
+ data.tar.gz: 152dc560f5b1169d5ef6f4a27065ae629da426ceefb0725fa6bc7a8d13c62a3f
5
5
  SHA512:
6
- metadata.gz: ea97e0959378bedd5a024888a8beab4880b30025216102b420b03deb5dff0c0b2ffd07beec2eb8e37d713ea3ae763378c8e50b143850563c01bf17bfb1ff47d9
7
- data.tar.gz: 5ca3f5205c79ab6c8cbe76cd60820602b5ed69bb57c80109dd0ac240988a7a021a3473c3d0c67ae14181913516bae3cdf46e00ec47cf7310a22c8bf49da087ae
6
+ metadata.gz: 88f5adb73f3e7bb1893eab32e098a5dda1d7b147c0cd000ee35eb6771b2d2c21f0fe1541c16725b537a1e3a1121166b2681331e7fcea79de26e3d3926137bea0
7
+ data.tar.gz: c2b17c50ccd765c95bb6d316a68f3e7dece5134ae9ca26fcb918356e0baf611ebbfba19f695e41b1b7402f0021f0b9d3d08111c7938448efb9fb5accda0a4131
data/CHANGELOG.md CHANGED
@@ -1,43 +1,113 @@
1
1
  # Changelog
2
2
 
3
- ## 0.2.0
3
+ ## 0.3.0
4
4
 
5
5
  ### Added
6
- - `round_robin_by` supports `weight: :time` to balance per-tick quanta
7
- by recent perform compute time instead of by request count (#3).
8
- - GitHub Actions CI matrix covering Ruby 3.4 and Rails 7.2 / 8.1 (#5).
9
- - Integration tests for gate combinations and throttle bucket
10
- boundaries (#12).
11
- - Resilience tests covering failure paths and dedupe state transitions
12
- (#13).
13
- - `bin/release` wrapper around `rake release` (#2).
6
+ - TX-atomic admission: the DELETE on `staged_jobs`, the pre-INSERT
7
+ in `inflight_jobs` and the adapter handoff (`good_job` /
8
+ `solid_queue`) all run inside the same transaction, so any failure
9
+ rolls everything back with no loss window between admission COMMIT
10
+ and adapter enqueue.
11
+ - `:adaptive_concurrency` gate that auto-tunes per-partition
12
+ `current_max` via AIMD against an EWMA of `queue_lag` (time from
13
+ admission to perform start), with a safety valve that floors
14
+ `remaining` at `initial_max` when `in_flight == 0` so idle
15
+ partitions can recover after a shrink.
16
+ - In-tick fairness layer: claimed partitions are reordered by
17
+ `decayed_admits` (EWMA, default `half_life = 60s`) and capped by
18
+ `fair_share = ceil(tick_cap / N)`. Composes with
19
+ `:adaptive_concurrency` — fairness writes
20
+ `partitions.decayed_admits`, adaptive writes
21
+ `dispatch_policy_adaptive_concurrency_stats.current_max`, no
22
+ shared locks.
23
+ - `shard_by` to split a policy's partitions across parallel tick
24
+ loops; the shard is pinned on first write so partitions don't jump
25
+ between tick workers.
26
+ - Policy-level `partition_by`: a single canonical scope shared by
27
+ the staged job's `partition_key` and the concurrency gate's
28
+ `inflight_partition_key`, so no gate suffers scope dilution.
29
+ - Gates are no longer required — a policy with `partition_by` and
30
+ no gates is valid and still benefits from in-tick fairness.
31
+ - `dispatch_policy_inflight_jobs` is populated for every admitted
32
+ job (not only concurrency-gated ones), with a heartbeat thread
33
+ refreshing `heartbeat_at` during perform.
34
+ - Bulk handoff via `ActiveJob.perform_all_later` and bulk-flush of
35
+ deny-path partition state at the end of a tick (single
36
+ `UPDATE…FROM(VALUES…)` instead of N per-partition statements).
37
+ - Per-tick metrics layer (`dispatch_policy_tick_samples`) feeding
38
+ the admin UI: throughput, P50/P95 round-trip ages, capacity
39
+ headroom, pending trend, fail %, and operator hints.
40
+ - Admin UI improvements: cursor-based pagination of `/partitions`,
41
+ sort + only-pending filter, auto-refresh control (off / 2s / 5s /
42
+ 10s) via Turbo Drive, per-partition and per-policy Drain action,
43
+ redesigned dummy demo page with cards + storm controls.
44
+ - `config.enabled` master switch for cutovers.
45
+ - `TickLoop` `busy_pause` to throttle busy iterations.
46
+ - `bin/release` wrapper around `rake release`.
47
+ - Manual benchmark suite, plus a real-adapter end-to-end bench
48
+ covering `good_job` and `solid_queue`.
14
49
 
15
50
  ### Changed
16
- - Admin partition breakdown caps its aggregations to keep the page
17
- responsive on policies with many partitions (#9).
18
- - Admin pending list no longer loads the `arguments` jsonb column
19
- (#6).
51
+ - **Breaking:** `partition_by` is policy-level only. Per-gate
52
+ `partition_by:` was removed; if omitted, `Policy#validate!` raises
53
+ `InvalidPolicy: partition_by required`. For different per-gate
54
+ scopes, use separate policies.
55
+ - `partitions.context` is refreshed on every `perform_later` via
56
+ UPSERT, so changes in the host DB take effect on the next enqueue
57
+ without redeploys. Gates read this ctx, not the historical
58
+ `staged_jobs.context`.
59
+ - Tick-claim ordering kept at `last_checked_at NULLS FIRST, id`
60
+ (anti-stagnation): each partition with pending is processed every
61
+ ⌈N/B⌉ ticks. Fairness reorder happens after the claim, in memory.
62
+ - Non-PG adapters now warn at boot (`warn_unsupported_adapter`)
63
+ instead of hard-failing — a custom PG-backed adapter still works.
64
+ - `config.database_role` lets the admission TX target a specific
65
+ Rails multi-DB role (e.g. `solid_queue` on a separate DB).
20
66
 
21
67
  ### Fixed
22
- - Admission is reverted when the underlying adapter silently declines
23
- to enqueue, so the staged row doesn't stay marked as admitted (#14).
24
- - `consumed_ms_by_partition` window is padded to survive
25
- minute-boundary races in the time-weighted round-robin fetch (#11).
26
- - ThrottleBucket row locks are taken in a deterministic key order to
27
- remove a deadlock window when multiple ticks contend on the same
28
- set of partitions (#8).
68
+ - `BulkEnqueue.perform_all_later` checks `Bypass.active?` and
69
+ delegates to `super` when active, breaking an infinite re-staging
70
+ loop on the deserialize + `perform_all_later` path under Bypass.
71
+ - `JobExtension.ensure_arguments_materialized!` is called before
72
+ reading `job.arguments` in both single and bulk paths previously
73
+ the public `arguments` getter returned `[]` for deserialised jobs
74
+ until `perform_now` triggered private materialization, so the
75
+ context proc fell back to its defaults.
76
+ - `:adaptive_concurrency` updates `current_max` in a single SQL
77
+ statement that uses the post-update `ewma_latency_ms` value in
78
+ its CASE expression, removing read-modify-write races between
79
+ concurrent workers.
80
+ - Adaptive's feedback signal is measured in `InflightTracker.track`
81
+ before `block.call` so perform duration doesn't pollute the
82
+ `queue_lag` signal.
83
+ - Heartbeat thread refreshes `inflight_jobs.heartbeat_at` during
84
+ perform so long-running jobs aren't reaped as stale.
85
+ - Deny-only ticks persist `next_eligible_at`.
86
+ - Tick samples query no longer depends on `date_bin` (works on
87
+ Postgres 13).
88
+ - Admin UI preserves scroll position on auto-refresh, and skips
89
+ auto-refresh while a Turbo visit is in flight.
90
+ - P95/P50 round-trip ages were inverted in the metrics view.
91
+ - Railtie no longer auto-merges the gem's `db/migrate` into the
92
+ host's paths.
93
+ - "Pending is growing" hint silenced when the backlog has drained.
29
94
 
30
95
  ### Removed
31
- - Stale custom `InstallGenerator` the engine's migration generator
32
- is the supported install path (#7).
96
+ - Per-gate `partition_by:` declarations (see Changed).
97
+ - Denormalised `partitions.in_flight_count` counter `inflight_jobs`
98
+ is the source of truth.
99
+ - `unclaim!` / `preinserted_inflight_ids` — TX rollback covers the
100
+ failure case.
33
101
 
34
102
  ## 0.1.0
35
103
 
36
104
  Initial release.
37
105
 
38
- - Rails engine + ActiveJob integration (`DispatchPolicy::Dispatchable`).
39
- - Gates: `:throttle`, `:concurrency`, `:global_cap`, `:fair_interleave`, `:adaptive_concurrency`.
40
- - Staged jobs with dedupe, round-robin fairness, per-partition counters, and throttle buckets.
41
- - Admin UI (Chart.js + Turbo) with watched partitions, sparklines, and EWMA queue-lag charts.
42
- - PostgreSQL required (uses `FOR UPDATE SKIP LOCKED`, `ON CONFLICT`, and `jsonb`).
43
- - Experimental being trialed on pulso.run.
106
+ - Rails engine + ActiveJob integration intercepting `perform_later`
107
+ via `JobExtension`.
108
+ - Gates: `:throttle`, `:concurrency`.
109
+ - Staged jobs admitted by a periodic tick loop, per-partition
110
+ counters, and token-bucket throttle state.
111
+ - Admin UI showing partitions, pending counts, and recent ticks.
112
+ - PostgreSQL required (uses `FOR UPDATE SKIP LOCKED`, `ON CONFLICT`,
113
+ and `jsonb`).
data/MIT-LICENSE CHANGED
@@ -1,21 +1,20 @@
1
- MIT License
2
-
3
1
  Copyright (c) 2026 José Galisteo
4
2
 
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
11
10
 
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
14
13
 
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.