dispatch_policy 0.1.0 → 0.3.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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +16 -17
  3. data/README.md +449 -288
  4. data/app/assets/stylesheets/dispatch_policy/application.css +157 -0
  5. data/app/controllers/dispatch_policy/application_controller.rb +45 -1
  6. data/app/controllers/dispatch_policy/dashboard_controller.rb +91 -0
  7. data/app/controllers/dispatch_policy/partitions_controller.rb +122 -0
  8. data/app/controllers/dispatch_policy/policies_controller.rb +94 -241
  9. data/app/controllers/dispatch_policy/staged_jobs_controller.rb +9 -0
  10. data/app/models/dispatch_policy/adaptive_concurrency_stats.rb +11 -81
  11. data/app/models/dispatch_policy/inflight_job.rb +12 -0
  12. data/app/models/dispatch_policy/partition.rb +21 -0
  13. data/app/models/dispatch_policy/staged_job.rb +4 -97
  14. data/app/models/dispatch_policy/tick_sample.rb +11 -0
  15. data/app/views/dispatch_policy/dashboard/index.html.erb +109 -0
  16. data/app/views/dispatch_policy/partitions/index.html.erb +63 -0
  17. data/app/views/dispatch_policy/partitions/show.html.erb +106 -0
  18. data/app/views/dispatch_policy/policies/index.html.erb +15 -37
  19. data/app/views/dispatch_policy/policies/show.html.erb +140 -216
  20. data/app/views/dispatch_policy/shared/_capacity.html.erb +67 -0
  21. data/app/views/dispatch_policy/shared/_hints.html.erb +13 -0
  22. data/app/views/dispatch_policy/shared/_partition_row.html.erb +12 -0
  23. data/app/views/dispatch_policy/staged_jobs/show.html.erb +31 -0
  24. data/app/views/layouts/dispatch_policy/application.html.erb +95 -238
  25. data/config/routes.rb +18 -2
  26. data/db/migrate/20260501000001_create_dispatch_policy_tables.rb +103 -0
  27. data/lib/dispatch_policy/bypass.rb +23 -0
  28. data/lib/dispatch_policy/config.rb +85 -0
  29. data/lib/dispatch_policy/context.rb +50 -0
  30. data/lib/dispatch_policy/cursor_pagination.rb +121 -0
  31. data/lib/dispatch_policy/decision.rb +22 -0
  32. data/lib/dispatch_policy/engine.rb +4 -27
  33. data/lib/dispatch_policy/forwarder.rb +63 -0
  34. data/lib/dispatch_policy/gate.rb +10 -38
  35. data/lib/dispatch_policy/gates/adaptive_concurrency.rb +99 -97
  36. data/lib/dispatch_policy/gates/concurrency.rb +45 -26
  37. data/lib/dispatch_policy/gates/throttle.rb +65 -37
  38. data/lib/dispatch_policy/inflight_tracker.rb +174 -0
  39. data/lib/dispatch_policy/job_extension.rb +155 -0
  40. data/lib/dispatch_policy/operator_hints.rb +126 -0
  41. data/lib/dispatch_policy/pipeline.rb +48 -0
  42. data/lib/dispatch_policy/policy.rb +62 -47
  43. data/lib/dispatch_policy/policy_dsl.rb +120 -0
  44. data/lib/dispatch_policy/railtie.rb +35 -0
  45. data/lib/dispatch_policy/registry.rb +46 -0
  46. data/lib/dispatch_policy/repository.rb +723 -0
  47. data/lib/dispatch_policy/serializer.rb +36 -0
  48. data/lib/dispatch_policy/tick.rb +263 -172
  49. data/lib/dispatch_policy/tick_loop.rb +59 -26
  50. data/lib/dispatch_policy/version.rb +1 -1
  51. data/lib/dispatch_policy.rb +71 -46
  52. data/lib/generators/dispatch_policy/install/install_generator.rb +70 -0
  53. data/lib/generators/dispatch_policy/install/templates/create_dispatch_policy_tables.rb.tt +95 -0
  54. data/lib/generators/dispatch_policy/install/templates/dispatch_tick_loop_job.rb.tt +53 -0
  55. data/lib/generators/dispatch_policy/install/templates/initializer.rb.tt +11 -0
  56. metadata +101 -43
  57. data/CHANGELOG.md +0 -12
  58. data/app/models/dispatch_policy/partition_inflight_count.rb +0 -42
  59. data/app/models/dispatch_policy/partition_observation.rb +0 -49
  60. data/app/models/dispatch_policy/throttle_bucket.rb +0 -41
  61. data/db/migrate/20260424000001_create_dispatch_policy_tables.rb +0 -80
  62. data/db/migrate/20260424000002_create_adaptive_concurrency_stats.rb +0 -22
  63. data/db/migrate/20260424000003_create_adaptive_concurrency_samples.rb +0 -25
  64. data/db/migrate/20260424000004_rename_samples_to_partition_observations.rb +0 -32
  65. data/lib/dispatch_policy/active_job_perform_all_later_patch.rb +0 -32
  66. data/lib/dispatch_policy/dispatch_context.rb +0 -53
  67. data/lib/dispatch_policy/dispatchable.rb +0 -120
  68. data/lib/dispatch_policy/gates/fair_interleave.rb +0 -32
  69. data/lib/dispatch_policy/gates/global_cap.rb +0 -26
  70. data/lib/dispatch_policy/install_generator.rb +0 -23
@@ -1,64 +1,89 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support"
4
+ require "active_support/core_ext"
3
5
  require "active_job"
4
- require "active_record"
5
6
 
6
- require "dispatch_policy/version"
7
- require "dispatch_policy/engine" if defined?(Rails)
7
+ require_relative "dispatch_policy/version"
8
+ require_relative "dispatch_policy/config"
9
+ require_relative "dispatch_policy/context"
10
+ require_relative "dispatch_policy/policy"
11
+ require_relative "dispatch_policy/registry"
12
+ require_relative "dispatch_policy/serializer"
13
+ require_relative "dispatch_policy/bypass"
14
+ require_relative "dispatch_policy/decision"
15
+ require_relative "dispatch_policy/gate"
16
+ require_relative "dispatch_policy/gates/throttle"
17
+ require_relative "dispatch_policy/gates/concurrency"
18
+ require_relative "dispatch_policy/gates/adaptive_concurrency"
19
+ require_relative "dispatch_policy/policy_dsl"
20
+ require_relative "dispatch_policy/cursor_pagination"
21
+ require_relative "dispatch_policy/pipeline"
22
+ require_relative "dispatch_policy/repository"
23
+ require_relative "dispatch_policy/forwarder"
24
+ require_relative "dispatch_policy/inflight_tracker"
25
+ require_relative "dispatch_policy/tick"
26
+ require_relative "dispatch_policy/tick_loop"
27
+ require_relative "dispatch_policy/job_extension"
28
+ require_relative "dispatch_policy/operator_hints"
8
29
 
9
30
  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
- )
31
+ class Error < StandardError; end
32
+ class PolicyAlreadyRegistered < Error; end
33
+ class UnknownGate < Error; end
34
+ class InvalidPolicy < Error; end
35
+ class EnqueueFailed < Error; end
21
36
 
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
37
+ # Adapters whose enqueue runs against ActiveRecord::Base.connection (so
38
+ # the adapter INSERT can join the admission TX) or whose semantics make
39
+ # atomicity moot (test/inline). Substring match against the adapter
40
+ # class name keeps the check resilient to ActiveJob's wrapper renames.
41
+ PG_BACKED_ADAPTER_HINTS = %w[GoodJob SolidQueue].freeze
42
+ EXEMPT_ADAPTER_HINTS = %w[Test Inline Async].freeze
43
+
44
+ module_function
34
45
 
35
- def self.configure
46
+ def configure
36
47
  yield config
37
48
  end
38
49
 
39
- def self.enabled?
40
- config.enabled != false
50
+ def config
51
+ @config ||= Config.new
52
+ end
53
+
54
+ def reset_config!
55
+ @config = Config.new
41
56
  end
42
57
 
43
- # Registry: policy_name => job_class. Populated by Policy#initialize.
44
- def self.registry
45
- @registry ||= {}
58
+ def registry
59
+ @registry ||= Registry.new
46
60
  end
47
61
 
48
- def self.reset_registry!
49
- @registry = {}
62
+ def reset_registry!
63
+ @registry = Registry.new
64
+ end
65
+
66
+ # Logs a warning if the configured ActiveJob adapter is not one of the
67
+ # PG-backed ones the gem can guarantee atomic admission for. We do NOT
68
+ # raise: a host may use a custom PG-backed adapter we don't recognize,
69
+ # or may have accepted the trade-off knowingly. The warning is enough
70
+ # to surface the issue at boot.
71
+ def warn_unsupported_adapter
72
+ return unless defined?(::ActiveJob::Base)
73
+ adapter = ::ActiveJob::Base.queue_adapter
74
+ return unless adapter
75
+
76
+ klass_name = adapter.class.name.to_s
77
+ return if (PG_BACKED_ADAPTER_HINTS + EXEMPT_ADAPTER_HINTS).any? { |hint| klass_name.include?(hint) }
78
+
79
+ config.logger&.warn(
80
+ "[dispatch_policy] active_job adapter is #{klass_name}; atomic admission requires " \
81
+ "a PG-backed adapter that shares ActiveRecord::Base's connection (good_job, solid_queue). " \
82
+ "If the worker process crashes between admission COMMIT and adapter enqueue, the job is lost. " \
83
+ "Set DispatchPolicy.config.database_role if you use a separate DB role for queueing."
84
+ )
50
85
  end
51
86
  end
52
87
 
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"
88
+ require_relative "dispatch_policy/railtie" if defined?(Rails::Railtie)
89
+ require_relative "dispatch_policy/engine" if defined?(Rails::Engine)
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/migration"
5
+
6
+ module DispatchPolicy
7
+ module Generators
8
+ class InstallGenerator < ::Rails::Generators::Base
9
+ include ::Rails::Generators::Migration
10
+
11
+ source_root File.expand_path("templates", __dir__)
12
+ desc "Installs dispatch_policy: migration, initializer, and tick loop job."
13
+
14
+ def self.next_migration_number(_path)
15
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
16
+ end
17
+
18
+ def copy_migration
19
+ migration_template "create_dispatch_policy_tables.rb.tt",
20
+ "db/migrate/create_dispatch_policy_tables.rb"
21
+ end
22
+
23
+ def create_initializer
24
+ template "initializer.rb.tt", "config/initializers/dispatch_policy.rb"
25
+ end
26
+
27
+ def create_tick_loop_job
28
+ template "dispatch_tick_loop_job.rb.tt", "app/jobs/dispatch_tick_loop_job.rb"
29
+ end
30
+
31
+ def show_readme
32
+ readme_text = <<~MSG
33
+
34
+ dispatch_policy installed.
35
+
36
+ Next steps:
37
+ 1) bin/rails db:migrate
38
+ 2) Mount the engine in config/routes.rb:
39
+ mount DispatchPolicy::Engine, at: "/dispatch_policy"
40
+ 3) Schedule DispatchTickLoopJob (cron / good_job recurring / solid_queue recurring)
41
+ and start it once: DispatchTickLoopJob.perform_later
42
+ 4) Declare a policy in any ActiveJob:
43
+ dispatch_policy :name do
44
+ context ->(args) { { ... } }
45
+ partition_by ->(c) { c[:key] }
46
+ gate :throttle, rate: 10, per: 60
47
+ end
48
+
49
+ MSG
50
+ say readme_text, :green
51
+ end
52
+
53
+ private
54
+
55
+ def good_job?
56
+ adapter_name == "good_job"
57
+ end
58
+
59
+ def solid_queue?
60
+ adapter_name == "solid_queue"
61
+ end
62
+
63
+ def adapter_name
64
+ Rails.application.config.active_job.queue_adapter.to_s
65
+ rescue StandardError
66
+ nil
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,95 @@
1
+ class CreateDispatchPolicyTables < ActiveRecord::Migration[<%= Rails::VERSION::STRING.to_f %>]
2
+ def change
3
+ create_table :dispatch_policy_staged_jobs do |t|
4
+ t.string :policy_name, null: false
5
+ t.string :partition_key, null: false
6
+ t.string :queue_name
7
+ t.string :job_class, null: false
8
+ t.jsonb :job_data, null: false
9
+ t.datetime :scheduled_at
10
+ t.integer :priority, default: 0, null: false
11
+ t.datetime :enqueued_at, null: false, default: -> { "now()" }
12
+ t.jsonb :context, null: false, default: {}
13
+ end
14
+ add_index :dispatch_policy_staged_jobs,
15
+ [:policy_name, :partition_key, :scheduled_at, :id],
16
+ name: "idx_dp_staged_admission",
17
+ order: { scheduled_at: "ASC NULLS FIRST", id: :asc }
18
+ add_index :dispatch_policy_staged_jobs, :enqueued_at,
19
+ name: "idx_dp_staged_enqueued_at"
20
+
21
+ create_table :dispatch_policy_partitions do |t|
22
+ t.string :policy_name, null: false
23
+ t.string :partition_key, null: false
24
+ t.string :queue_name
25
+ t.string :shard, null: false, default: "default"
26
+ t.string :status, null: false, default: "active"
27
+ t.integer :pending_count, null: false, default: 0
28
+ t.bigint :total_admitted, null: false, default: 0
29
+ t.jsonb :context, null: false, default: {}
30
+ t.datetime :context_updated_at
31
+ t.datetime :last_enqueued_at
32
+ t.datetime :last_checked_at
33
+ t.datetime :last_admit_at
34
+ t.datetime :next_eligible_at
35
+ t.jsonb :gate_state, null: false, default: {}
36
+ t.float :decayed_admits, null: false, default: 0.0
37
+ t.datetime :decayed_admits_at
38
+ t.timestamps
39
+ end
40
+ add_index :dispatch_policy_partitions,
41
+ [:policy_name, :partition_key],
42
+ unique: true, name: "idx_dp_partitions_lookup"
43
+ add_index :dispatch_policy_partitions,
44
+ [:policy_name, :shard, :status, :next_eligible_at, :last_checked_at],
45
+ name: "idx_dp_partitions_tick_order",
46
+ order: { next_eligible_at: "ASC NULLS FIRST", last_checked_at: "ASC NULLS FIRST" }
47
+
48
+ create_table :dispatch_policy_inflight_jobs do |t|
49
+ t.string :policy_name, null: false
50
+ t.string :partition_key, null: false
51
+ t.string :active_job_id, null: false
52
+ t.datetime :admitted_at, null: false, default: -> { "now()" }
53
+ t.datetime :heartbeat_at, null: false, default: -> { "now()" }
54
+ end
55
+ add_index :dispatch_policy_inflight_jobs, :active_job_id, unique: true,
56
+ name: "idx_dp_inflight_active_job_id"
57
+ add_index :dispatch_policy_inflight_jobs, [:policy_name, :partition_key],
58
+ name: "idx_dp_inflight_partition"
59
+ add_index :dispatch_policy_inflight_jobs, :heartbeat_at,
60
+ name: "idx_dp_inflight_heartbeat"
61
+
62
+ create_table :dispatch_policy_tick_samples do |t|
63
+ t.string :policy_name, null: false
64
+ t.datetime :sampled_at, null: false, default: -> { "now()" }
65
+ t.integer :duration_ms, null: false, default: 0
66
+ t.integer :partitions_seen, null: false, default: 0
67
+ t.integer :partitions_admitted, null: false, default: 0
68
+ t.integer :partitions_denied, null: false, default: 0
69
+ t.integer :jobs_admitted, null: false, default: 0
70
+ t.integer :forward_failures, null: false, default: 0
71
+ t.integer :pending_total, null: false, default: 0
72
+ t.integer :inflight_total, null: false, default: 0
73
+ t.jsonb :denied_reasons, null: false, default: {}
74
+ end
75
+ add_index :dispatch_policy_tick_samples, [:policy_name, :sampled_at],
76
+ name: "idx_dp_tick_samples_lookup",
77
+ order: { sampled_at: :desc }
78
+ add_index :dispatch_policy_tick_samples, :sampled_at,
79
+ name: "idx_dp_tick_samples_sweep"
80
+
81
+ create_table :dispatch_policy_adaptive_concurrency_stats do |t|
82
+ t.string :policy_name, null: false
83
+ t.string :partition_key, null: false
84
+ t.integer :current_max, null: false
85
+ t.float :ewma_latency_ms, null: false, default: 0.0
86
+ t.integer :sample_count, null: false, default: 0
87
+ t.datetime :last_observed_at
88
+ t.timestamps
89
+ end
90
+ add_index :dispatch_policy_adaptive_concurrency_stats,
91
+ [:policy_name, :partition_key],
92
+ unique: true,
93
+ name: "idx_dp_adaptive_concurrency_lookup"
94
+ end
95
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Run one of these per (policy, shard) pair to parallelise admission.
4
+ # When called without arguments, processes every registered policy across
5
+ # every shard from a single worker — fine for small deployments.
6
+ #
7
+ # DispatchTickLoopJob.perform_later # all policies, all shards
8
+ # DispatchTickLoopJob.perform_later("events") # one policy, all shards
9
+ # DispatchTickLoopJob.perform_later("events", "shard-1") # one policy, one shard
10
+ #
11
+ # When a shard argument is provided, the job is also enqueued onto a queue
12
+ # named after the shard so the same worker pool can run both the tick loop
13
+ # and the admitted jobs (assuming the policy's shard_by returns the queue
14
+ # name your jobs use).
15
+ class DispatchTickLoopJob < ApplicationJob
16
+ queue_as { arguments[1].presence || :dispatch_loop }
17
+ <% if good_job? -%>
18
+
19
+ include GoodJob::ActiveJobExtensions::Concurrency
20
+ good_job_control_concurrency_with(
21
+ total_limit: 1,
22
+ key: -> { "dispatch_tick_loop:#{arguments[0] || 'all'}:#{arguments[1] || 'all'}" }
23
+ )
24
+ <% elsif solid_queue? -%>
25
+
26
+ limits_concurrency to: 1,
27
+ key: -> { "dispatch_tick_loop:#{arguments[0] || 'all'}:#{arguments[1] || 'all'}" }
28
+ <% end -%>
29
+
30
+ def perform(policy_name = nil, shard = nil)
31
+ deadline = Time.current + DispatchPolicy.config.tick_max_duration
32
+
33
+ DispatchPolicy::TickLoop.run(
34
+ policy_name: policy_name,
35
+ shard: shard,
36
+ stop_when: -> { adapter_shutting_down? || Time.current >= deadline }
37
+ )
38
+
39
+ self.class.set(wait: 1.second).perform_later(policy_name, shard)
40
+ end
41
+
42
+ private
43
+
44
+ def adapter_shutting_down?
45
+ <% if good_job? -%>
46
+ GoodJob.current_thread_shutting_down?
47
+ <% elsif solid_queue? -%>
48
+ defined?(SolidQueue::Process) && SolidQueue::Process.current_process&.shutdown?
49
+ <% else -%>
50
+ false
51
+ <% end -%>
52
+ end
53
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ DispatchPolicy.configure do |c|
4
+ c.tick_max_duration = 25 # seconds — total time the tick job stays admitting
5
+ c.partition_batch_size = 50 # partitions claimed per tick iteration
6
+ c.admission_batch_size = 100 # max jobs admitted per partition per iteration
7
+ c.idle_pause = 0.5 # seconds slept when no admissions happened
8
+ c.partition_inactive_after = 24 * 60 * 60 # GC partitions idle this long
9
+ c.inflight_stale_after = 5 * 60 # GC inflight rows whose worker stopped heartbeating
10
+ c.sweep_every_ticks = 50 # how often to run the sweepers
11
+ end
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.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - José Galisteo
@@ -10,7 +10,7 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: activejob
13
+ name: rails
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
16
  - - ">="
@@ -24,77 +24,119 @@ dependencies:
24
24
  - !ruby/object:Gem::Version
25
25
  version: '7.1'
26
26
  - !ruby/object:Gem::Dependency
27
- name: activerecord
27
+ name: pg
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: '7.1'
32
+ version: '1.4'
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: '7.1'
39
+ version: '1.4'
40
40
  - !ruby/object:Gem::Dependency
41
- name: railties
41
+ name: minitest
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '5.20'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '5.20'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rake
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '13.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '13.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: good_job
42
70
  requirement: !ruby/object:Gem::Requirement
43
71
  requirements:
44
72
  - - ">="
45
73
  - !ruby/object:Gem::Version
46
- version: '7.1'
47
- type: :runtime
74
+ version: '4.0'
75
+ type: :development
48
76
  prerelease: false
49
77
  version_requirements: !ruby/object:Gem::Requirement
50
78
  requirements:
51
79
  - - ">="
52
80
  - !ruby/object:Gem::Version
53
- version: '7.1'
81
+ version: '4.0'
54
82
  - !ruby/object:Gem::Dependency
55
- name: pg
83
+ name: solid_queue
56
84
  requirement: !ruby/object:Gem::Requirement
57
85
  requirements:
58
86
  - - ">="
59
87
  - !ruby/object:Gem::Version
60
- version: '0'
88
+ version: '1.0'
61
89
  type: :development
62
90
  prerelease: false
63
91
  version_requirements: !ruby/object:Gem::Requirement
64
92
  requirements:
65
93
  - - ">="
66
94
  - !ruby/object:Gem::Version
67
- version: '0'
95
+ version: '1.0'
68
96
  - !ruby/object:Gem::Dependency
69
- name: minitest
97
+ name: turbo-rails
70
98
  requirement: !ruby/object:Gem::Requirement
71
99
  requirements:
72
100
  - - ">="
73
101
  - !ruby/object:Gem::Version
74
- version: '0'
102
+ version: '1.5'
75
103
  type: :development
76
104
  prerelease: false
77
105
  version_requirements: !ruby/object:Gem::Requirement
78
106
  requirements:
79
107
  - - ">="
80
108
  - !ruby/object:Gem::Version
81
- version: '0'
109
+ version: '1.5'
82
110
  - !ruby/object:Gem::Dependency
83
- name: simplecov
111
+ name: puma
84
112
  requirement: !ruby/object:Gem::Requirement
85
113
  requirements:
86
114
  - - ">="
87
115
  - !ruby/object:Gem::Version
88
- version: '0'
116
+ version: '6.0'
89
117
  type: :development
90
118
  prerelease: false
91
119
  version_requirements: !ruby/object:Gem::Requirement
92
120
  requirements:
93
121
  - - ">="
94
122
  - !ruby/object:Gem::Version
95
- version: '0'
123
+ version: '6.0'
124
+ - !ruby/object:Gem::Dependency
125
+ name: foreman
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0.87'
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0.87'
96
138
  - !ruby/object:Gem::Dependency
97
- name: sprockets-rails
139
+ name: debug
98
140
  requirement: !ruby/object:Gem::Requirement
99
141
  requirements:
100
142
  - - ">="
@@ -107,60 +149,77 @@ dependencies:
107
149
  - - ">="
108
150
  - !ruby/object:Gem::Version
109
151
  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.
152
+ description: Stages perform_later into a dedicated table, runs a tick loop that admits
153
+ jobs through declared gates (throttle, concurrency), then forwards survivors to
154
+ the real ActiveJob adapter. Embedded as a periodic job. Compatible with good_job
155
+ and solid_queue.
114
156
  email:
115
157
  - ceritium@gmail.com
116
158
  executables: []
117
159
  extensions: []
118
160
  extra_rdoc_files: []
119
161
  files:
120
- - CHANGELOG.md
121
162
  - MIT-LICENSE
122
163
  - README.md
164
+ - app/assets/stylesheets/dispatch_policy/application.css
123
165
  - app/controllers/dispatch_policy/application_controller.rb
166
+ - app/controllers/dispatch_policy/dashboard_controller.rb
167
+ - app/controllers/dispatch_policy/partitions_controller.rb
124
168
  - app/controllers/dispatch_policy/policies_controller.rb
169
+ - app/controllers/dispatch_policy/staged_jobs_controller.rb
125
170
  - app/models/dispatch_policy/adaptive_concurrency_stats.rb
126
171
  - app/models/dispatch_policy/application_record.rb
127
- - app/models/dispatch_policy/partition_inflight_count.rb
128
- - app/models/dispatch_policy/partition_observation.rb
172
+ - app/models/dispatch_policy/inflight_job.rb
173
+ - app/models/dispatch_policy/partition.rb
129
174
  - app/models/dispatch_policy/staged_job.rb
130
- - app/models/dispatch_policy/throttle_bucket.rb
175
+ - app/models/dispatch_policy/tick_sample.rb
176
+ - app/views/dispatch_policy/dashboard/index.html.erb
177
+ - app/views/dispatch_policy/partitions/index.html.erb
178
+ - app/views/dispatch_policy/partitions/show.html.erb
131
179
  - app/views/dispatch_policy/policies/index.html.erb
132
180
  - app/views/dispatch_policy/policies/show.html.erb
181
+ - app/views/dispatch_policy/shared/_capacity.html.erb
182
+ - app/views/dispatch_policy/shared/_hints.html.erb
183
+ - app/views/dispatch_policy/shared/_partition_row.html.erb
184
+ - app/views/dispatch_policy/staged_jobs/show.html.erb
133
185
  - app/views/layouts/dispatch_policy/application.html.erb
134
186
  - 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
187
+ - db/migrate/20260501000001_create_dispatch_policy_tables.rb
139
188
  - 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
189
+ - lib/dispatch_policy/bypass.rb
190
+ - lib/dispatch_policy/config.rb
191
+ - lib/dispatch_policy/context.rb
192
+ - lib/dispatch_policy/cursor_pagination.rb
193
+ - lib/dispatch_policy/decision.rb
143
194
  - lib/dispatch_policy/engine.rb
195
+ - lib/dispatch_policy/forwarder.rb
144
196
  - lib/dispatch_policy/gate.rb
145
197
  - lib/dispatch_policy/gates/adaptive_concurrency.rb
146
198
  - lib/dispatch_policy/gates/concurrency.rb
147
- - lib/dispatch_policy/gates/fair_interleave.rb
148
- - lib/dispatch_policy/gates/global_cap.rb
149
199
  - lib/dispatch_policy/gates/throttle.rb
150
- - lib/dispatch_policy/install_generator.rb
200
+ - lib/dispatch_policy/inflight_tracker.rb
201
+ - lib/dispatch_policy/job_extension.rb
202
+ - lib/dispatch_policy/operator_hints.rb
203
+ - lib/dispatch_policy/pipeline.rb
151
204
  - lib/dispatch_policy/policy.rb
205
+ - lib/dispatch_policy/policy_dsl.rb
206
+ - lib/dispatch_policy/railtie.rb
207
+ - lib/dispatch_policy/registry.rb
208
+ - lib/dispatch_policy/repository.rb
209
+ - lib/dispatch_policy/serializer.rb
152
210
  - lib/dispatch_policy/tick.rb
153
211
  - lib/dispatch_policy/tick_loop.rb
154
212
  - lib/dispatch_policy/version.rb
213
+ - lib/generators/dispatch_policy/install/install_generator.rb
214
+ - lib/generators/dispatch_policy/install/templates/create_dispatch_policy_tables.rb.tt
215
+ - lib/generators/dispatch_policy/install/templates/dispatch_tick_loop_job.rb.tt
216
+ - lib/generators/dispatch_policy/install/templates/initializer.rb.tt
155
217
  homepage: https://github.com/ceritium/dispatch_policy
156
218
  licenses:
157
219
  - MIT
158
220
  metadata:
159
221
  homepage_uri: https://github.com/ceritium/dispatch_policy
160
222
  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
223
  rdoc_options: []
165
224
  require_paths:
166
225
  - lib
@@ -168,7 +227,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
168
227
  requirements:
169
228
  - - ">="
170
229
  - !ruby/object:Gem::Version
171
- version: '3.1'
230
+ version: 3.1.0
172
231
  required_rubygems_version: !ruby/object:Gem::Requirement
173
232
  requirements:
174
233
  - - ">="
@@ -177,6 +236,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
177
236
  requirements: []
178
237
  rubygems_version: 4.0.3
179
238
  specification_version: 4
180
- summary: Per-partition admission control (throttle, concurrency, dedupe, fairness)
181
- for ActiveJob.
239
+ summary: Per-partition admission control for ActiveJob (Postgres).
182
240
  test_files: []
data/CHANGELOG.md DELETED
@@ -1,12 +0,0 @@
1
- # Changelog
2
-
3
- ## 0.1.0
4
-
5
- Initial release.
6
-
7
- - Rails engine + ActiveJob integration (`DispatchPolicy::Dispatchable`).
8
- - Gates: `:throttle`, `:concurrency`, `:global_cap`, `:fair_interleave`, `:adaptive_concurrency`.
9
- - Staged jobs with dedupe, round-robin fairness, per-partition counters, and throttle buckets.
10
- - Admin UI (Chart.js + Turbo) with watched partitions, sparklines, and EWMA queue-lag charts.
11
- - PostgreSQL required (uses `FOR UPDATE SKIP LOCKED`, `ON CONFLICT`, and `jsonb`).
12
- - Experimental — being trialed on pulso.run.