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.
- checksums.yaml +4 -4
- data/MIT-LICENSE +16 -17
- data/README.md +449 -288
- data/app/assets/stylesheets/dispatch_policy/application.css +157 -0
- data/app/controllers/dispatch_policy/application_controller.rb +45 -1
- data/app/controllers/dispatch_policy/dashboard_controller.rb +91 -0
- data/app/controllers/dispatch_policy/partitions_controller.rb +122 -0
- data/app/controllers/dispatch_policy/policies_controller.rb +94 -241
- data/app/controllers/dispatch_policy/staged_jobs_controller.rb +9 -0
- data/app/models/dispatch_policy/adaptive_concurrency_stats.rb +11 -81
- data/app/models/dispatch_policy/inflight_job.rb +12 -0
- data/app/models/dispatch_policy/partition.rb +21 -0
- data/app/models/dispatch_policy/staged_job.rb +4 -97
- data/app/models/dispatch_policy/tick_sample.rb +11 -0
- data/app/views/dispatch_policy/dashboard/index.html.erb +109 -0
- data/app/views/dispatch_policy/partitions/index.html.erb +63 -0
- data/app/views/dispatch_policy/partitions/show.html.erb +106 -0
- data/app/views/dispatch_policy/policies/index.html.erb +15 -37
- data/app/views/dispatch_policy/policies/show.html.erb +140 -216
- data/app/views/dispatch_policy/shared/_capacity.html.erb +67 -0
- data/app/views/dispatch_policy/shared/_hints.html.erb +13 -0
- data/app/views/dispatch_policy/shared/_partition_row.html.erb +12 -0
- data/app/views/dispatch_policy/staged_jobs/show.html.erb +31 -0
- data/app/views/layouts/dispatch_policy/application.html.erb +95 -238
- data/config/routes.rb +18 -2
- data/db/migrate/20260501000001_create_dispatch_policy_tables.rb +103 -0
- data/lib/dispatch_policy/bypass.rb +23 -0
- data/lib/dispatch_policy/config.rb +85 -0
- data/lib/dispatch_policy/context.rb +50 -0
- data/lib/dispatch_policy/cursor_pagination.rb +121 -0
- data/lib/dispatch_policy/decision.rb +22 -0
- data/lib/dispatch_policy/engine.rb +4 -27
- data/lib/dispatch_policy/forwarder.rb +63 -0
- data/lib/dispatch_policy/gate.rb +10 -38
- data/lib/dispatch_policy/gates/adaptive_concurrency.rb +99 -97
- data/lib/dispatch_policy/gates/concurrency.rb +45 -26
- data/lib/dispatch_policy/gates/throttle.rb +65 -37
- data/lib/dispatch_policy/inflight_tracker.rb +174 -0
- data/lib/dispatch_policy/job_extension.rb +155 -0
- data/lib/dispatch_policy/operator_hints.rb +126 -0
- data/lib/dispatch_policy/pipeline.rb +48 -0
- data/lib/dispatch_policy/policy.rb +62 -47
- data/lib/dispatch_policy/policy_dsl.rb +120 -0
- data/lib/dispatch_policy/railtie.rb +35 -0
- data/lib/dispatch_policy/registry.rb +46 -0
- data/lib/dispatch_policy/repository.rb +723 -0
- data/lib/dispatch_policy/serializer.rb +36 -0
- data/lib/dispatch_policy/tick.rb +263 -172
- data/lib/dispatch_policy/tick_loop.rb +59 -26
- data/lib/dispatch_policy/version.rb +1 -1
- data/lib/dispatch_policy.rb +71 -46
- data/lib/generators/dispatch_policy/install/install_generator.rb +70 -0
- data/lib/generators/dispatch_policy/install/templates/create_dispatch_policy_tables.rb.tt +95 -0
- data/lib/generators/dispatch_policy/install/templates/dispatch_tick_loop_job.rb.tt +53 -0
- data/lib/generators/dispatch_policy/install/templates/initializer.rb.tt +11 -0
- metadata +101 -43
- data/CHANGELOG.md +0 -12
- data/app/models/dispatch_policy/partition_inflight_count.rb +0 -42
- data/app/models/dispatch_policy/partition_observation.rb +0 -49
- data/app/models/dispatch_policy/throttle_bucket.rb +0 -41
- data/db/migrate/20260424000001_create_dispatch_policy_tables.rb +0 -80
- data/db/migrate/20260424000002_create_adaptive_concurrency_stats.rb +0 -22
- data/db/migrate/20260424000003_create_adaptive_concurrency_samples.rb +0 -25
- data/db/migrate/20260424000004_rename_samples_to_partition_observations.rb +0 -32
- data/lib/dispatch_policy/active_job_perform_all_later_patch.rb +0 -32
- data/lib/dispatch_policy/dispatch_context.rb +0 -53
- data/lib/dispatch_policy/dispatchable.rb +0 -120
- data/lib/dispatch_policy/gates/fair_interleave.rb +0 -32
- data/lib/dispatch_policy/gates/global_cap.rb +0 -26
- data/lib/dispatch_policy/install_generator.rb +0 -23
data/lib/dispatch_policy.rb
CHANGED
|
@@ -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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
46
|
+
def configure
|
|
36
47
|
yield config
|
|
37
48
|
end
|
|
38
49
|
|
|
39
|
-
def
|
|
40
|
-
config
|
|
50
|
+
def config
|
|
51
|
+
@config ||= Config.new
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def reset_config!
|
|
55
|
+
@config = Config.new
|
|
41
56
|
end
|
|
42
57
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
@registry ||= {}
|
|
58
|
+
def registry
|
|
59
|
+
@registry ||= Registry.new
|
|
46
60
|
end
|
|
47
61
|
|
|
48
|
-
def
|
|
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
|
-
|
|
54
|
-
|
|
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.
|
|
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:
|
|
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:
|
|
27
|
+
name: pg
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
30
|
- - ">="
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '
|
|
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: '
|
|
39
|
+
version: '1.4'
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
|
-
name:
|
|
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: '
|
|
47
|
-
type: :
|
|
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: '
|
|
81
|
+
version: '4.0'
|
|
54
82
|
- !ruby/object:Gem::Dependency
|
|
55
|
-
name:
|
|
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:
|
|
97
|
+
name: turbo-rails
|
|
70
98
|
requirement: !ruby/object:Gem::Requirement
|
|
71
99
|
requirements:
|
|
72
100
|
- - ">="
|
|
73
101
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: '
|
|
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: '
|
|
109
|
+
version: '1.5'
|
|
82
110
|
- !ruby/object:Gem::Dependency
|
|
83
|
-
name:
|
|
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:
|
|
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:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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/
|
|
128
|
-
- app/models/dispatch_policy/
|
|
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/
|
|
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/
|
|
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/
|
|
141
|
-
- lib/dispatch_policy/
|
|
142
|
-
- lib/dispatch_policy/
|
|
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/
|
|
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:
|
|
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
|
|
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.
|