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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +12 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +265 -0
  5. data/Rakefile +6 -0
  6. data/lib/active_job/errors/invalid_operation.rb +5 -0
  7. data/lib/active_job/errors/job_not_found_error.rb +14 -0
  8. data/lib/active_job/errors/query_error.rb +5 -0
  9. data/lib/active_job/executing.rb +41 -0
  10. data/lib/active_job/execution_error.rb +8 -0
  11. data/lib/active_job/failed.rb +7 -0
  12. data/lib/active_job/job_proxy.rb +39 -0
  13. data/lib/active_job/jobs_relation.rb +321 -0
  14. data/lib/active_job/querying.rb +45 -0
  15. data/lib/active_job/queue.rb +63 -0
  16. data/lib/active_job/queue_adapters/solid_queue_ext/recurring_tasks.rb +53 -0
  17. data/lib/active_job/queue_adapters/solid_queue_ext/workers.rb +42 -0
  18. data/lib/active_job/queue_adapters/solid_queue_ext.rb +309 -0
  19. data/lib/active_job/queues.rb +34 -0
  20. data/lib/flight_control/adapter.rb +155 -0
  21. data/lib/flight_control/application.rb +20 -0
  22. data/lib/flight_control/applications.rb +8 -0
  23. data/lib/flight_control/arguments_filter.rb +25 -0
  24. data/lib/flight_control/authentication.rb +68 -0
  25. data/lib/flight_control/console/connect_to.rb +15 -0
  26. data/lib/flight_control/console/context.rb +11 -0
  27. data/lib/flight_control/console/jobs_help.rb +23 -0
  28. data/lib/flight_control/engine.rb +102 -0
  29. data/lib/flight_control/errors/incompatible_adapter.rb +2 -0
  30. data/lib/flight_control/errors/resource_not_found.rb +2 -0
  31. data/lib/flight_control/errors/unsupported_adapter.rb +2 -0
  32. data/lib/flight_control/i18n_config.rb +17 -0
  33. data/lib/flight_control/identified_by_name.rb +18 -0
  34. data/lib/flight_control/identified_elements.rb +24 -0
  35. data/lib/flight_control/server/recurring_tasks.rb +15 -0
  36. data/lib/flight_control/server/serializable.rb +24 -0
  37. data/lib/flight_control/server/workers.rb +13 -0
  38. data/lib/flight_control/server.rb +33 -0
  39. data/lib/flight_control/tasks.rb +6 -0
  40. data/lib/flight_control/version.rb +3 -0
  41. data/lib/flight_control/workers_relation.rb +79 -0
  42. data/lib/flight_control.rb +39 -0
  43. 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,2 @@
1
+ class FlightControl::Errors::IncompatibleAdapter < StandardError
2
+ end
@@ -0,0 +1,2 @@
1
+ class FlightControl::Errors::ResourceNotFound < StandardError
2
+ end
@@ -0,0 +1,2 @@
1
+ class FlightControl::Errors::UnsupportedAdapter < StandardError
2
+ 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,6 @@
1
+ namespace :flight_control do
2
+ desc "Configure HTTP Basic Authentication"
3
+ task "authentication:configure" => :environment do
4
+ FlightControl::Authentication.configure
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ module FlightControl
2
+ VERSION = "2.0.0.beta.1"
3
+ 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