flight_control 2.0.0.beta.1
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 +7 -0
- data/CHANGELOG.md +12 -0
- data/MIT-LICENSE +20 -0
- data/README.md +265 -0
- data/Rakefile +6 -0
- data/lib/active_job/errors/invalid_operation.rb +5 -0
- data/lib/active_job/errors/job_not_found_error.rb +14 -0
- data/lib/active_job/errors/query_error.rb +5 -0
- data/lib/active_job/executing.rb +41 -0
- data/lib/active_job/execution_error.rb +8 -0
- data/lib/active_job/failed.rb +7 -0
- data/lib/active_job/job_proxy.rb +39 -0
- data/lib/active_job/jobs_relation.rb +321 -0
- data/lib/active_job/querying.rb +45 -0
- data/lib/active_job/queue.rb +63 -0
- data/lib/active_job/queue_adapters/solid_queue_ext/recurring_tasks.rb +53 -0
- data/lib/active_job/queue_adapters/solid_queue_ext/workers.rb +42 -0
- data/lib/active_job/queue_adapters/solid_queue_ext.rb +309 -0
- data/lib/active_job/queues.rb +34 -0
- data/lib/flight_control/adapter.rb +155 -0
- data/lib/flight_control/application.rb +20 -0
- data/lib/flight_control/applications.rb +8 -0
- data/lib/flight_control/arguments_filter.rb +25 -0
- data/lib/flight_control/authentication.rb +68 -0
- data/lib/flight_control/console/connect_to.rb +15 -0
- data/lib/flight_control/console/context.rb +11 -0
- data/lib/flight_control/console/jobs_help.rb +23 -0
- data/lib/flight_control/engine.rb +102 -0
- data/lib/flight_control/errors/incompatible_adapter.rb +2 -0
- data/lib/flight_control/errors/resource_not_found.rb +2 -0
- data/lib/flight_control/errors/unsupported_adapter.rb +2 -0
- data/lib/flight_control/i18n_config.rb +17 -0
- data/lib/flight_control/identified_by_name.rb +18 -0
- data/lib/flight_control/identified_elements.rb +24 -0
- data/lib/flight_control/server/recurring_tasks.rb +15 -0
- data/lib/flight_control/server/serializable.rb +24 -0
- data/lib/flight_control/server/workers.rb +13 -0
- data/lib/flight_control/server.rb +33 -0
- data/lib/flight_control/tasks.rb +6 -0
- data/lib/flight_control/version.rb +3 -0
- data/lib/flight_control/workers_relation.rb +79 -0
- data/lib/flight_control.rb +39 -0
- metadata +377 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
require "importmap-rails"
|
|
2
|
+
require "turbo-rails"
|
|
3
|
+
require "stimulus-rails"
|
|
4
|
+
|
|
5
|
+
module FlightControl
|
|
6
|
+
class Engine < ::Rails::Engine
|
|
7
|
+
isolate_namespace FlightControl
|
|
8
|
+
|
|
9
|
+
rake_tasks do
|
|
10
|
+
load "flight_control/tasks.rb"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
initializer "flight_control.middleware" do |app|
|
|
14
|
+
if app.config.api_only
|
|
15
|
+
config.middleware.use ActionDispatch::Flash
|
|
16
|
+
config.middleware.use ::Rack::MethodOverride
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
config.flight_control = ActiveSupport::OrderedOptions.new unless config.try(:flight_control)
|
|
21
|
+
|
|
22
|
+
config.before_initialize do
|
|
23
|
+
config.flight_control.applications = FlightControl::Applications.new
|
|
24
|
+
config.flight_control.backtrace_cleaner ||= Rails::BacktraceCleaner.new
|
|
25
|
+
|
|
26
|
+
config.flight_control.each do |key, value|
|
|
27
|
+
FlightControl.public_send("#{key}=", value)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
if FlightControl.adapters.empty?
|
|
31
|
+
FlightControl.adapters << (config.active_job.queue_adapter || :solid_queue)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
unless FlightControl.adapters.all? { |adapter| adapter.to_sym == :solid_queue }
|
|
35
|
+
unsupported = FlightControl.adapters.reject { |adapter| adapter.to_sym == :solid_queue }
|
|
36
|
+
raise FlightControl::Errors::UnsupportedAdapter,
|
|
37
|
+
"Flight Control only supports Solid Queue, but the following adapters are configured: #{unsupported.join(", ")}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
initializer "flight_control.http_basic_auth" do |app|
|
|
42
|
+
FlightControl.http_basic_auth_user ||= app.credentials.dig(:flight_control, :http_basic_auth_user)
|
|
43
|
+
FlightControl.http_basic_auth_password ||= app.credentials.dig(:flight_control, :http_basic_auth_password)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
initializer "flight_control.active_job.extensions" do
|
|
47
|
+
ActiveSupport.on_load :active_job do
|
|
48
|
+
include ActiveJob::Querying
|
|
49
|
+
include ActiveJob::Executing
|
|
50
|
+
include ActiveJob::Failed
|
|
51
|
+
ActiveJob.extend ActiveJob::Querying::Root
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
config.before_initialize do
|
|
56
|
+
ActiveJob::QueueAdapters::SolidQueueAdapter.prepend ActiveJob::QueueAdapters::SolidQueueExt
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
config.after_initialize do |app|
|
|
60
|
+
if FlightControl.applications.empty?
|
|
61
|
+
queue_adapters_by_name = FlightControl.adapters.each_with_object({}) do |adapter, hsh|
|
|
62
|
+
hsh[adapter] = ActiveJob::QueueAdapters.lookup(adapter).new
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
FlightControl.applications.add(app.class.module_parent.name, queue_adapters_by_name)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
console do
|
|
70
|
+
require "irb"
|
|
71
|
+
|
|
72
|
+
IRB::Command.register :connect_to, Console::ConnectTo
|
|
73
|
+
IRB::Command.register :jobs_help, Console::JobsHelp
|
|
74
|
+
|
|
75
|
+
IRB::Context.prepend(FlightControl::Console::Context)
|
|
76
|
+
|
|
77
|
+
FlightControl.delay_between_bulk_operation_batches = 2
|
|
78
|
+
FlightControl.logger = ActiveSupport::Logger.new($stdout)
|
|
79
|
+
|
|
80
|
+
if FlightControl.show_console_help
|
|
81
|
+
puts "\n\nType 'jobs_help' to see how to connect to the available job servers to manage jobs\n\n"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
initializer "flight_control.assets" do |app|
|
|
86
|
+
app.config.assets.paths << root.join("app/assets/stylesheets")
|
|
87
|
+
app.config.assets.paths << root.join("app/javascript")
|
|
88
|
+
app.config.assets.precompile += %w[flight_control_manifest]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
initializer "flight_control.importmap", after: "importmap" do |app|
|
|
92
|
+
FlightControl.importmap.draw(root.join("config/importmap.rb"))
|
|
93
|
+
if app.config.importmap.sweep_cache && app.config.reloading_enabled?
|
|
94
|
+
FlightControl.importmap.cache_sweeper(watches: root.join("app/javascript"))
|
|
95
|
+
|
|
96
|
+
ActiveSupport.on_load(:action_controller_base) do
|
|
97
|
+
before_action { FlightControl.importmap.cache_sweeper.execute_if_updated }
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class FlightControl::I18nConfig < ::I18n::Config
|
|
2
|
+
AVAILABLE_LOCALES = [:en]
|
|
3
|
+
AVAILABLE_LOCALES_SET = [:en, "en"]
|
|
4
|
+
DEFAULT_LOCALE = :en
|
|
5
|
+
|
|
6
|
+
def available_locales
|
|
7
|
+
AVAILABLE_LOCALES
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def available_locales_set
|
|
11
|
+
AVAILABLE_LOCALES_SET
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def default_locale
|
|
15
|
+
DEFAULT_LOCALE
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module FlightControl::IdentifiedByName
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
included do
|
|
5
|
+
attr_reader :name
|
|
6
|
+
alias_method :to_s, :name
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def initialize(name:)
|
|
10
|
+
@name = name.to_s
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def id
|
|
14
|
+
name.parameterize
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
alias_method :to_param, :id
|
|
18
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# A collection of elements offering a Hash-like access based on
|
|
2
|
+
# their +id+.
|
|
3
|
+
class FlightControl::IdentifiedElements
|
|
4
|
+
include Enumerable
|
|
5
|
+
|
|
6
|
+
delegate :[], :empty?, to: :elements
|
|
7
|
+
delegate :each, :last, :length, to: :to_a
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@elements = HashWithIndifferentAccess.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def <<(item)
|
|
14
|
+
@elements[item.id] = item
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_a
|
|
18
|
+
@elements.values
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
attr_reader :elements
|
|
24
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module FlightControl::Server::RecurringTasks
|
|
2
|
+
def recurring_tasks
|
|
3
|
+
queue_adapter.recurring_tasks.collect do |task|
|
|
4
|
+
FlightControl::RecurringTask.new(queue_adapter: queue_adapter, **task)
|
|
5
|
+
end.sort_by(&:id)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def find_recurring_task(task_id)
|
|
9
|
+
if (task = queue_adapter.find_recurring_task(task_id))
|
|
10
|
+
FlightControl::RecurringTask.new(queue_adapter: queue_adapter, **task)
|
|
11
|
+
else
|
|
12
|
+
raise FlightControl::Errors::ResourceNotFound, "Recurring task with id '#{task_id}' not found"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module FlightControl::Server::Serializable
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
class_methods do
|
|
5
|
+
# Loads a server from a locator string with the format +<application>:<server>+. For example:
|
|
6
|
+
#
|
|
7
|
+
# bc4:us_east
|
|
8
|
+
#
|
|
9
|
+
# When the +<server>+ fragment is omitted it will return the first server for the application.
|
|
10
|
+
def from_global_id(global_id)
|
|
11
|
+
app_id, server_id = global_id.split(":")
|
|
12
|
+
|
|
13
|
+
application = FlightControl.applications[app_id] or raise FlightControl::Errors::ResourceNotFound, "No application with id #{app_id} found"
|
|
14
|
+
server = server_id ? application.servers[server_id] : application.servers.first
|
|
15
|
+
|
|
16
|
+
server or raise FlightControl::Errors::ResourceNotFound, "No server for #{global_id} found"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_global_id
|
|
21
|
+
suffix = ":#{id}" if application.servers.many?
|
|
22
|
+
"#{application&.id}#{suffix}"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module FlightControl::Server::Workers
|
|
2
|
+
def workers_relation
|
|
3
|
+
FlightControl::WorkersRelation.new(queue_adapter: queue_adapter)
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
def find_worker(worker_id)
|
|
7
|
+
if (worker = queue_adapter.find_worker(worker_id))
|
|
8
|
+
FlightControl::Worker.new(queue_adapter: queue_adapter, **worker)
|
|
9
|
+
else
|
|
10
|
+
raise FlightControl::Errors::ResourceNotFound, "Worker with id '#{worker_id}' not found"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require "active_job/queue_adapter"
|
|
2
|
+
|
|
3
|
+
class FlightControl::Server
|
|
4
|
+
include FlightControl::IdentifiedByName
|
|
5
|
+
include Workers
|
|
6
|
+
include RecurringTasks
|
|
7
|
+
include Serializable
|
|
8
|
+
|
|
9
|
+
attr_reader :name, :queue_adapter, :application, :backtrace_cleaner
|
|
10
|
+
|
|
11
|
+
def initialize(name:, queue_adapter:, application:, backtrace_cleaner: nil)
|
|
12
|
+
super(name: name)
|
|
13
|
+
unless queue_adapter.is_a?(ActiveJob::QueueAdapters::SolidQueueAdapter)
|
|
14
|
+
raise FlightControl::Errors::UnsupportedAdapter,
|
|
15
|
+
"Flight Control only supports Solid Queue, can't register server #{name} with #{queue_adapter.class}"
|
|
16
|
+
end
|
|
17
|
+
@queue_adapter = queue_adapter
|
|
18
|
+
@application = application
|
|
19
|
+
@backtrace_cleaner = backtrace_cleaner || FlightControl.backtrace_cleaner
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def activating(&block)
|
|
23
|
+
previous_adapter = ActiveJob::Base.current_queue_adapter
|
|
24
|
+
ActiveJob::Base.current_queue_adapter = queue_adapter
|
|
25
|
+
queue_adapter.activating(&block)
|
|
26
|
+
ensure
|
|
27
|
+
ActiveJob::Base.current_queue_adapter = previous_adapter
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def queue_adapter_name
|
|
31
|
+
ActiveJob.adapter_name(queue_adapter).underscore.to_sym
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# A relation of workers.
|
|
2
|
+
#
|
|
3
|
+
# Relations are enumerable, so you can use +Enumerable+ methods on them.
|
|
4
|
+
# Notice however that using these methods will imply loading all the relation
|
|
5
|
+
# in memory, which could introduce performance concerns.
|
|
6
|
+
class FlightControl::WorkersRelation
|
|
7
|
+
include Enumerable
|
|
8
|
+
|
|
9
|
+
attr_accessor :offset_value, :limit_value
|
|
10
|
+
|
|
11
|
+
delegate :last, :[], :to_s, :reverse, to: :to_a
|
|
12
|
+
|
|
13
|
+
ALL_WORKERS_LIMIT = 100_000_000 # When no limit value it defaults to "all workers"
|
|
14
|
+
|
|
15
|
+
def initialize(queue_adapter:)
|
|
16
|
+
@queue_adapter = queue_adapter
|
|
17
|
+
|
|
18
|
+
set_defaults
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def offset(offset)
|
|
22
|
+
clone_with offset_value: offset
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def limit(limit)
|
|
26
|
+
clone_with limit_value: limit
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def each(&block)
|
|
30
|
+
workers.each(&block)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def reload
|
|
34
|
+
@count = @workers = nil
|
|
35
|
+
self
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def count
|
|
39
|
+
if loaded?
|
|
40
|
+
to_a.length
|
|
41
|
+
else
|
|
42
|
+
query_count
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def empty?
|
|
47
|
+
count == 0
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
alias_method :length, :count
|
|
51
|
+
alias_method :size, :count
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def set_defaults
|
|
56
|
+
self.offset_value = 0
|
|
57
|
+
self.limit_value = ALL_WORKERS_LIMIT
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def workers
|
|
61
|
+
@workers ||= @queue_adapter.fetch_workers(self)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def query_count
|
|
65
|
+
@count ||= @queue_adapter.count_workers(self)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def loaded?
|
|
69
|
+
!@workers.nil?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def clone_with(**properties)
|
|
73
|
+
dup.reload.tap do |relation|
|
|
74
|
+
properties.each do |key, value|
|
|
75
|
+
relation.send("#{key}=", value)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require "solid_queue"
|
|
2
|
+
|
|
3
|
+
require "flight_control/version"
|
|
4
|
+
require "flight_control/engine"
|
|
5
|
+
|
|
6
|
+
require "zeitwerk"
|
|
7
|
+
|
|
8
|
+
loader = Zeitwerk::Loader.new
|
|
9
|
+
loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
|
|
10
|
+
loader.push_dir(__dir__)
|
|
11
|
+
loader.ignore("#{__dir__}/flight_control/tasks.rb")
|
|
12
|
+
loader.setup
|
|
13
|
+
|
|
14
|
+
module FlightControl
|
|
15
|
+
mattr_accessor :adapters, default: Set.new
|
|
16
|
+
mattr_accessor :applications, default: FlightControl::Applications.new
|
|
17
|
+
mattr_accessor :base_controller_class, default: "::ApplicationController"
|
|
18
|
+
|
|
19
|
+
mattr_accessor :internal_query_count_limit, default: 500_000 # Hard limit to keep unlimited count queries fast enough
|
|
20
|
+
mattr_accessor :delay_between_bulk_operation_batches, default: 0
|
|
21
|
+
mattr_accessor :scheduled_job_delay_threshold, default: 1.minute
|
|
22
|
+
|
|
23
|
+
mattr_accessor :logger, default: ActiveSupport::Logger.new(nil)
|
|
24
|
+
|
|
25
|
+
mattr_accessor :show_console_help, default: true
|
|
26
|
+
mattr_accessor :backtrace_cleaner
|
|
27
|
+
|
|
28
|
+
mattr_accessor :importmap, default: Importmap::Map.new
|
|
29
|
+
|
|
30
|
+
mattr_accessor :http_basic_auth_user
|
|
31
|
+
mattr_accessor :http_basic_auth_password
|
|
32
|
+
mattr_accessor :http_basic_auth_enabled, default: true
|
|
33
|
+
|
|
34
|
+
mattr_accessor :filter_arguments, default: []
|
|
35
|
+
|
|
36
|
+
def self.job_arguments_filter
|
|
37
|
+
FlightControl::ArgumentsFilter.new(filter_arguments)
|
|
38
|
+
end
|
|
39
|
+
end
|