solid_job_monitor 1.0.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 (147) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +106 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/config/mission_control_jobs_manifest.js +4 -0
  6. data/app/assets/stylesheets/mission_control/jobs/application.css +16 -0
  7. data/app/assets/stylesheets/mission_control/jobs/bulma.min.css +21013 -0
  8. data/app/assets/stylesheets/mission_control/jobs/forms.css +8 -0
  9. data/app/assets/stylesheets/mission_control/jobs/jobs.css +68 -0
  10. data/app/controllers/concerns/mission_control/jobs/adapter_features.rb +24 -0
  11. data/app/controllers/concerns/mission_control/jobs/application_scoped.rb +38 -0
  12. data/app/controllers/concerns/mission_control/jobs/basic_authentication.rb +33 -0
  13. data/app/controllers/concerns/mission_control/jobs/failed_jobs_bulk_operations.rb +17 -0
  14. data/app/controllers/concerns/mission_control/jobs/job_filters.rb +41 -0
  15. data/app/controllers/concerns/mission_control/jobs/job_scoped.rb +16 -0
  16. data/app/controllers/concerns/mission_control/jobs/not_found_redirections.rb +36 -0
  17. data/app/controllers/concerns/mission_control/jobs/queue_scoped.rb +12 -0
  18. data/app/controllers/mission_control/jobs/application_controller.rb +32 -0
  19. data/app/controllers/mission_control/jobs/bulk_discards_controller.rb +20 -0
  20. data/app/controllers/mission_control/jobs/bulk_retries_controller.rb +10 -0
  21. data/app/controllers/mission_control/jobs/discards_controller.rb +18 -0
  22. data/app/controllers/mission_control/jobs/dispatches_controller.rb +18 -0
  23. data/app/controllers/mission_control/jobs/jobs_controller.rb +54 -0
  24. data/app/controllers/mission_control/jobs/progresses_controller.rb +27 -0
  25. data/app/controllers/mission_control/jobs/queues/pauses_controller.rb +15 -0
  26. data/app/controllers/mission_control/jobs/queues_controller.rb +16 -0
  27. data/app/controllers/mission_control/jobs/recurring_tasks_controller.rb +38 -0
  28. data/app/controllers/mission_control/jobs/retries_controller.rb +13 -0
  29. data/app/controllers/mission_control/jobs/stops_controller.rb +22 -0
  30. data/app/controllers/mission_control/jobs/workers_controller.rb +23 -0
  31. data/app/helpers/mission_control/jobs/application_helper.rb +9 -0
  32. data/app/helpers/mission_control/jobs/dates_helper.rb +19 -0
  33. data/app/helpers/mission_control/jobs/interface_helper.rb +23 -0
  34. data/app/helpers/mission_control/jobs/jobs_helper.rb +94 -0
  35. data/app/helpers/mission_control/jobs/navigation_helper.rb +60 -0
  36. data/app/javascript/mission_control/jobs/application.js +4 -0
  37. data/app/javascript/mission_control/jobs/controllers/application.js +9 -0
  38. data/app/javascript/mission_control/jobs/controllers/form_controller.js +21 -0
  39. data/app/javascript/mission_control/jobs/controllers/index.js +11 -0
  40. data/app/javascript/mission_control/jobs/controllers/progress_controller.js +55 -0
  41. data/app/javascript/mission_control/jobs/helpers/debounce_helpers.js +9 -0
  42. data/app/javascript/mission_control/jobs/helpers/index.js +1 -0
  43. data/app/models/mission_control/jobs/application_record.rb +7 -0
  44. data/app/models/mission_control/jobs/current.rb +3 -0
  45. data/app/models/mission_control/jobs/job_progress.rb +7 -0
  46. data/app/models/mission_control/jobs/page.rb +48 -0
  47. data/app/models/mission_control/jobs/recurring_task.rb +25 -0
  48. data/app/models/mission_control/jobs/worker.rb +17 -0
  49. data/app/views/layouts/mission_control/jobs/_application_selection.html.erb +14 -0
  50. data/app/views/layouts/mission_control/jobs/_flash.html.erb +9 -0
  51. data/app/views/layouts/mission_control/jobs/_navigation.html.erb +9 -0
  52. data/app/views/layouts/mission_control/jobs/application.html.erb +27 -0
  53. data/app/views/layouts/mission_control/jobs/application_selection/_applications.html.erb +13 -0
  54. data/app/views/layouts/mission_control/jobs/application_selection/_servers.html.erb +15 -0
  55. data/app/views/mission_control/jobs/jobs/_error_information.html.erb +22 -0
  56. data/app/views/mission_control/jobs/jobs/_filters.html.erb +55 -0
  57. data/app/views/mission_control/jobs/jobs/_general_information.html.erb +85 -0
  58. data/app/views/mission_control/jobs/jobs/_job.html.erb +13 -0
  59. data/app/views/mission_control/jobs/jobs/_jobs_page.html.erb +13 -0
  60. data/app/views/mission_control/jobs/jobs/_progress_bar.html.erb +16 -0
  61. data/app/views/mission_control/jobs/jobs/_raw_data.html.erb +4 -0
  62. data/app/views/mission_control/jobs/jobs/_title.html.erb +22 -0
  63. data/app/views/mission_control/jobs/jobs/_toolbar.html.erb +18 -0
  64. data/app/views/mission_control/jobs/jobs/blocked/_actions.html.erb +3 -0
  65. data/app/views/mission_control/jobs/jobs/blocked/_job.html.erb +8 -0
  66. data/app/views/mission_control/jobs/jobs/failed/_actions.html.erb +5 -0
  67. data/app/views/mission_control/jobs/jobs/failed/_backtrace_toggle.html.erb +14 -0
  68. data/app/views/mission_control/jobs/jobs/failed/_job.html.erb +7 -0
  69. data/app/views/mission_control/jobs/jobs/finished/_job.html.erb +2 -0
  70. data/app/views/mission_control/jobs/jobs/in_progress/_actions.html.erb +6 -0
  71. data/app/views/mission_control/jobs/jobs/in_progress/_job.html.erb +13 -0
  72. data/app/views/mission_control/jobs/jobs/index.html.erb +19 -0
  73. data/app/views/mission_control/jobs/jobs/scheduled/_actions.html.erb +5 -0
  74. data/app/views/mission_control/jobs/jobs/scheduled/_job.html.erb +13 -0
  75. data/app/views/mission_control/jobs/jobs/show.html.erb +6 -0
  76. data/app/views/mission_control/jobs/queues/_actions.html.erb +7 -0
  77. data/app/views/mission_control/jobs/queues/_job.html.erb +15 -0
  78. data/app/views/mission_control/jobs/queues/_queue.html.erb +16 -0
  79. data/app/views/mission_control/jobs/queues/_queue_title.html.erb +17 -0
  80. data/app/views/mission_control/jobs/queues/index.html.erb +16 -0
  81. data/app/views/mission_control/jobs/queues/show.html.erb +25 -0
  82. data/app/views/mission_control/jobs/recurring_tasks/_actions.html.erb +5 -0
  83. data/app/views/mission_control/jobs/recurring_tasks/_general_information.html.erb +37 -0
  84. data/app/views/mission_control/jobs/recurring_tasks/_recurring_task.html.erb +22 -0
  85. data/app/views/mission_control/jobs/recurring_tasks/_title.html.erb +10 -0
  86. data/app/views/mission_control/jobs/recurring_tasks/index.html.erb +18 -0
  87. data/app/views/mission_control/jobs/recurring_tasks/show.html.erb +14 -0
  88. data/app/views/mission_control/jobs/shared/_job.html.erb +27 -0
  89. data/app/views/mission_control/jobs/shared/_jobs.html.erb +14 -0
  90. data/app/views/mission_control/jobs/shared/_pagination_toolbar.html.erb +7 -0
  91. data/app/views/mission_control/jobs/workers/_configuration.html.erb +6 -0
  92. data/app/views/mission_control/jobs/workers/_raw_data.html.erb +6 -0
  93. data/app/views/mission_control/jobs/workers/_title.html.erb +11 -0
  94. data/app/views/mission_control/jobs/workers/_worker.html.erb +21 -0
  95. data/app/views/mission_control/jobs/workers/_workers_page.html.erb +15 -0
  96. data/app/views/mission_control/jobs/workers/index.html.erb +6 -0
  97. data/app/views/mission_control/jobs/workers/show.html.erb +14 -0
  98. data/config/importmap.rb +6 -0
  99. data/config/routes.rb +35 -0
  100. data/db/migrate/20260828105144_create_mission_control_job_progresses.rb +16 -0
  101. data/lib/active_job/errors/invalid_operation.rb +5 -0
  102. data/lib/active_job/errors/job_not_found_error.rb +14 -0
  103. data/lib/active_job/errors/query_error.rb +5 -0
  104. data/lib/active_job/executing.rb +40 -0
  105. data/lib/active_job/execution_error.rb +8 -0
  106. data/lib/active_job/failed.rb +7 -0
  107. data/lib/active_job/job_proxy.rb +38 -0
  108. data/lib/active_job/jobs_relation.rb +318 -0
  109. data/lib/active_job/progress_reporting.rb +21 -0
  110. data/lib/active_job/querying.rb +44 -0
  111. data/lib/active_job/queue.rb +62 -0
  112. data/lib/active_job/queue_adapters/async_ext.rb +49 -0
  113. data/lib/active_job/queue_adapters/resque_ext.rb +317 -0
  114. data/lib/active_job/queue_adapters/solid_queue_ext/recurring_tasks.rb +52 -0
  115. data/lib/active_job/queue_adapters/solid_queue_ext/workers.rb +41 -0
  116. data/lib/active_job/queue_adapters/solid_queue_ext.rb +313 -0
  117. data/lib/active_job/queues.rb +33 -0
  118. data/lib/mission_control/jobs/adapter.rb +156 -0
  119. data/lib/mission_control/jobs/application.rb +20 -0
  120. data/lib/mission_control/jobs/applications.rb +8 -0
  121. data/lib/mission_control/jobs/arguments_filter.rb +24 -0
  122. data/lib/mission_control/jobs/authentication.rb +67 -0
  123. data/lib/mission_control/jobs/console/connect_to.rb +15 -0
  124. data/lib/mission_control/jobs/console/context.rb +11 -0
  125. data/lib/mission_control/jobs/console/jobs_help.rb +23 -0
  126. data/lib/mission_control/jobs/engine.rb +118 -0
  127. data/lib/mission_control/jobs/errors/incompatible_adapter.rb +2 -0
  128. data/lib/mission_control/jobs/errors/resource_not_found.rb +2 -0
  129. data/lib/mission_control/jobs/i18n_config.rb +17 -0
  130. data/lib/mission_control/jobs/identified_by_name.rb +18 -0
  131. data/lib/mission_control/jobs/identified_elements.rb +23 -0
  132. data/lib/mission_control/jobs/job_stopped_error.rb +5 -0
  133. data/lib/mission_control/jobs/progress/null_store.rb +25 -0
  134. data/lib/mission_control/jobs/progress/reporter.rb +63 -0
  135. data/lib/mission_control/jobs/progress/store.rb +72 -0
  136. data/lib/mission_control/jobs/progress.rb +17 -0
  137. data/lib/mission_control/jobs/server/recurring_tasks.rb +15 -0
  138. data/lib/mission_control/jobs/server/serializable.rb +24 -0
  139. data/lib/mission_control/jobs/server/workers.rb +13 -0
  140. data/lib/mission_control/jobs/server.rb +27 -0
  141. data/lib/mission_control/jobs/tasks.rb +8 -0
  142. data/lib/mission_control/jobs/version.rb +5 -0
  143. data/lib/mission_control/jobs/workers_relation.rb +78 -0
  144. data/lib/mission_control/jobs.rb +43 -0
  145. data/lib/resque/thread_safe_redis.rb +34 -0
  146. data/lib/solid_job_monitor.rb +6 -0
  147. metadata +527 -0
@@ -0,0 +1,118 @@
1
+ require "importmap-rails"
2
+ require "turbo-rails"
3
+ require "stimulus-rails"
4
+
5
+ module MissionControl
6
+ module Jobs
7
+ class Engine < ::Rails::Engine
8
+ isolate_namespace MissionControl::Jobs
9
+
10
+ rake_tasks do
11
+ load "mission_control/jobs/tasks.rb"
12
+ end
13
+
14
+ initializer "mission_control-jobs.middleware" do |app|
15
+ if app.config.api_only
16
+ config.middleware.use ActionDispatch::Flash
17
+ config.middleware.use ::Rack::MethodOverride
18
+ end
19
+ end
20
+
21
+ config.mission_control = ActiveSupport::OrderedOptions.new unless config.try(:mission_control)
22
+ config.mission_control.jobs = ActiveSupport::OrderedOptions.new
23
+
24
+ config.before_initialize do
25
+ config.mission_control.jobs.applications = MissionControl::Jobs::Applications.new
26
+ config.mission_control.jobs.backtrace_cleaner ||= Rails::BacktraceCleaner.new
27
+
28
+ config.mission_control.jobs.each do |key, value|
29
+ MissionControl::Jobs.public_send("#{key}=", value)
30
+ end
31
+
32
+ if MissionControl::Jobs.adapters.empty?
33
+ MissionControl::Jobs.adapters << (config.active_job.queue_adapter || :async)
34
+ end
35
+ end
36
+
37
+ initializer "mission_control-jobs.http_basic_auth" do |app|
38
+ MissionControl::Jobs.http_basic_auth_user ||= app.credentials.dig(:mission_control, :http_basic_auth_user)
39
+ MissionControl::Jobs.http_basic_auth_password ||= app.credentials.dig(:mission_control, :http_basic_auth_password)
40
+ end
41
+
42
+ initializer "mission_control-jobs.active_job.extensions", before: "active_job.set_configs" do
43
+ ActiveSupport.on_load :active_job do
44
+ include ActiveJob::Querying
45
+ include ActiveJob::Executing
46
+ include ActiveJob::Failed
47
+ include ActiveJob::ProgressReporting
48
+ ActiveJob.extend ActiveJob::Querying::Root
49
+ end
50
+ end
51
+
52
+ initializer "mission_control-jobs.append_migrations" do |app|
53
+ next if app.root.to_s.start_with?(root.to_s)
54
+
55
+ config.paths["db/migrate"].expanded.each do |expanded_path|
56
+ app.config.paths["db/migrate"] << expanded_path
57
+ end
58
+ end
59
+
60
+ config.before_initialize do
61
+ if MissionControl::Jobs.adapters.include?(:resque)
62
+ require "resque/thread_safe_redis"
63
+ ActiveJob::QueueAdapters::ResqueAdapter.prepend ActiveJob::QueueAdapters::ResqueExt
64
+ Resque.prepend Resque::ThreadSafeRedis
65
+ end
66
+
67
+ if MissionControl::Jobs.adapters.include?(:solid_queue)
68
+ ActiveJob::QueueAdapters::SolidQueueAdapter.prepend ActiveJob::QueueAdapters::SolidQueueExt
69
+ end
70
+
71
+ ActiveJob::QueueAdapters::AsyncAdapter.include ActiveJob::QueueAdapters::AsyncExt
72
+ end
73
+
74
+ config.after_initialize do |app|
75
+ if MissionControl::Jobs.applications.empty?
76
+ queue_adapters_by_name = MissionControl::Jobs.adapters.each_with_object({}) do |adapter, hsh|
77
+ hsh[adapter] = ActiveJob::QueueAdapters.lookup(adapter).new
78
+ end
79
+
80
+ MissionControl::Jobs.applications.add(app.class.module_parent.name, queue_adapters_by_name)
81
+ end
82
+ end
83
+
84
+ console do
85
+ require "irb"
86
+
87
+ IRB::Command.register :connect_to, Console::ConnectTo
88
+ IRB::Command.register :jobs_help, Console::JobsHelp
89
+
90
+ IRB::Context.prepend(MissionControl::Jobs::Console::Context)
91
+
92
+ MissionControl::Jobs.delay_between_bulk_operation_batches = 2
93
+ MissionControl::Jobs.logger = ActiveSupport::Logger.new(STDOUT)
94
+
95
+ if MissionControl::Jobs.show_console_help
96
+ puts "\n\nType 'jobs_help' to see how to connect to the available job servers to manage jobs\n\n"
97
+ end
98
+ end
99
+
100
+ initializer "mission_control-jobs.assets" do |app|
101
+ app.config.assets.paths << root.join("app/assets/stylesheets")
102
+ app.config.assets.paths << root.join("app/javascript")
103
+ app.config.assets.precompile += %w[ mission_control_jobs_manifest ]
104
+ end
105
+
106
+ initializer "mission_control-jobs.importmap", after: "importmap" do |app|
107
+ MissionControl::Jobs.importmap.draw(root.join("config/importmap.rb"))
108
+ if app.config.importmap.sweep_cache && app.config.reloading_enabled?
109
+ MissionControl::Jobs.importmap.cache_sweeper(watches: root.join("app/javascript"))
110
+
111
+ ActiveSupport.on_load(:action_controller_base) do
112
+ before_action { MissionControl::Jobs.importmap.cache_sweeper.execute_if_updated }
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,2 @@
1
+ class MissionControl::Jobs::Errors::IncompatibleAdapter < StandardError
2
+ end
@@ -0,0 +1,2 @@
1
+ class MissionControl::Jobs::Errors::ResourceNotFound < StandardError
2
+ end
@@ -0,0 +1,17 @@
1
+ class MissionControl::Jobs::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 MissionControl::Jobs::IdentifiedByName
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ attr_reader :name
6
+ alias 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 to_param id
18
+ end
@@ -0,0 +1,23 @@
1
+ # A collection of elements offering a Hash-like access based on
2
+ # their +id+.
3
+ class MissionControl::Jobs::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
+ attr_reader :elements
23
+ end
@@ -0,0 +1,5 @@
1
+ class MissionControl::Jobs::JobStoppedError < StandardError
2
+ def initialize(job_id = nil)
3
+ super(job_id ? "Job #{job_id} was stopped" : "Job was stopped")
4
+ end
5
+ end
@@ -0,0 +1,25 @@
1
+ class MissionControl::Jobs::Progress::NullStore
2
+ def upsert(**)
3
+ nil
4
+ end
5
+
6
+ def find(_job_id)
7
+ nil
8
+ end
9
+
10
+ def find_multi(_job_ids)
11
+ {}
12
+ end
13
+
14
+ def request_stop(_job_id)
15
+ nil
16
+ end
17
+
18
+ def stop_requested?(_job_id)
19
+ false
20
+ end
21
+
22
+ def complete(_job_id)
23
+ nil
24
+ end
25
+ end
@@ -0,0 +1,63 @@
1
+ class MissionControl::Jobs::Progress::Reporter
2
+ def initialize(job_id)
3
+ @job_id = job_id
4
+ @current = 0
5
+ @total = nil
6
+ end
7
+
8
+ def register(total:, message: nil)
9
+ check_stopped!
10
+ @total = total
11
+ @current = 0
12
+ persist(percent: computed_percent(0), message: message)
13
+ end
14
+
15
+ def update(current: nil, total: nil, percent: nil, message: nil)
16
+ check_stopped!
17
+ @current = current unless current.nil?
18
+ @total = total unless total.nil?
19
+ persist(percent: computed_percent(percent), message: message)
20
+ end
21
+
22
+ def increment(by: 1)
23
+ check_stopped!
24
+ @current = @current.to_i + by
25
+ persist(percent: computed_percent(nil))
26
+ end
27
+
28
+ def check_stopped!
29
+ raise MissionControl::Jobs::JobStoppedError.new(@job_id) if store.stop_requested?(@job_id)
30
+ end
31
+
32
+ def complete
33
+ store.complete(@job_id)
34
+ end
35
+
36
+ private
37
+ def persist(percent:, message: nil)
38
+ store.upsert(
39
+ job_id: @job_id,
40
+ current: @current.to_i,
41
+ total: @total,
42
+ percent: percent,
43
+ message: message
44
+ )
45
+ end
46
+
47
+ def computed_percent(explicit)
48
+ return clamp_percent(explicit) unless explicit.nil?
49
+ return nil if @total.nil? || @total.to_i <= 0
50
+
51
+ clamp_percent(((@current.to_f / @total) * 100).round)
52
+ end
53
+
54
+ def clamp_percent(value)
55
+ return nil if value.nil?
56
+
57
+ value.to_i.clamp(0, 100)
58
+ end
59
+
60
+ def store
61
+ MissionControl::Jobs::Progress::Store.current
62
+ end
63
+ end
@@ -0,0 +1,72 @@
1
+ class MissionControl::Jobs::Progress::Store
2
+ class << self
3
+ def current
4
+ @current ||= available? ? new : null_store
5
+ end
6
+
7
+ def available?
8
+ return @available unless @available.nil?
9
+
10
+ @available = MissionControl::Jobs::JobProgress.table_exists?
11
+ rescue ActiveRecord::ActiveRecordError
12
+ @available = false
13
+ end
14
+
15
+ def reset!
16
+ @current = nil
17
+ @available = nil
18
+ end
19
+
20
+ private
21
+ def null_store
22
+ warn_if_missing
23
+ MissionControl::Jobs::Progress::NullStore.new
24
+ end
25
+
26
+ def warn_if_missing
27
+ return if @warned
28
+
29
+ @warned = true
30
+ logger = (defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger) || MissionControl::Jobs.logger
31
+ logger&.warn(
32
+ "[solid_job_monitor] mission_control_job_progresses table is missing. " \
33
+ "Run rails db:migrate to enable job progress and cooperative stop."
34
+ )
35
+ end
36
+ end
37
+
38
+ def upsert(job_id:, current: 0, total: nil, percent: nil, message: nil)
39
+ record = MissionControl::Jobs::JobProgress.find_or_initialize_by(job_id: job_id)
40
+ record.current = current
41
+ record.total = total unless total.nil?
42
+ record.percent = percent unless percent.nil?
43
+ record.message = message unless message.nil?
44
+ record.save!
45
+ record
46
+ end
47
+
48
+ def find(job_id)
49
+ MissionControl::Jobs::JobProgress.find_by(job_id: job_id)
50
+ end
51
+
52
+ def find_multi(job_ids)
53
+ return {} if job_ids.blank?
54
+
55
+ MissionControl::Jobs::JobProgress.where(job_id: job_ids).index_by(&:job_id)
56
+ end
57
+
58
+ def request_stop(job_id)
59
+ record = MissionControl::Jobs::JobProgress.find_or_initialize_by(job_id: job_id)
60
+ record.stop_requested = true
61
+ record.save!
62
+ record
63
+ end
64
+
65
+ def stop_requested?(job_id)
66
+ MissionControl::Jobs::JobProgress.where(job_id: job_id, stop_requested: true).exists?
67
+ end
68
+
69
+ def complete(job_id)
70
+ MissionControl::Jobs::JobProgress.where(job_id: job_id).delete_all
71
+ end
72
+ end
@@ -0,0 +1,17 @@
1
+ module MissionControl::Jobs::Progress
2
+ def self.available?
3
+ Store.available?
4
+ end
5
+
6
+ def self.find(job_id)
7
+ Store.current.find(job_id)
8
+ end
9
+
10
+ def self.find_multi(job_ids)
11
+ Store.current.find_multi(job_ids)
12
+ end
13
+
14
+ def self.request_stop(job_id)
15
+ Store.current.request_stop(job_id)
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ module MissionControl::Jobs::Server::RecurringTasks
2
+ def recurring_tasks
3
+ queue_adapter.recurring_tasks.collect do |task|
4
+ MissionControl::Jobs::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
+ MissionControl::Jobs::RecurringTask.new(queue_adapter: queue_adapter, **task)
11
+ else
12
+ raise MissionControl::Jobs::Errors::ResourceNotFound, "Recurring task with id '#{task_id}' not found"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,24 @@
1
+ module MissionControl::Jobs::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:resque_chicago
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 = MissionControl::Jobs.applications[app_id] or raise MissionControl::Jobs::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 MissionControl::Jobs::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 MissionControl::Jobs::Server::Workers
2
+ def workers_relation
3
+ MissionControl::Jobs::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
+ MissionControl::Jobs::Worker.new(queue_adapter: queue_adapter, **worker)
9
+ else
10
+ raise MissionControl::Jobs::Errors::ResourceNotFound, "Worker with id '#{worker_id}' not found"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,27 @@
1
+ require "active_job/queue_adapter"
2
+
3
+ class MissionControl::Jobs::Server
4
+ include MissionControl::Jobs::IdentifiedByName
5
+ include Serializable, RecurringTasks, Workers
6
+
7
+ attr_reader :name, :queue_adapter, :application, :backtrace_cleaner
8
+
9
+ def initialize(name:, queue_adapter:, application:, backtrace_cleaner: nil)
10
+ super(name: name)
11
+ @queue_adapter = queue_adapter
12
+ @application = application
13
+ @backtrace_cleaner = backtrace_cleaner || MissionControl::Jobs.backtrace_cleaner
14
+ end
15
+
16
+ def activating(&block)
17
+ previous_adapter = ActiveJob::Base.current_queue_adapter
18
+ ActiveJob::Base.current_queue_adapter = queue_adapter
19
+ queue_adapter.activating(&block)
20
+ ensure
21
+ ActiveJob::Base.current_queue_adapter = previous_adapter
22
+ end
23
+
24
+ def queue_adapter_name
25
+ ActiveJob.adapter_name(queue_adapter).underscore.to_sym
26
+ end
27
+ end
@@ -0,0 +1,8 @@
1
+ namespace :mission_control do
2
+ namespace :jobs do
3
+ desc "Configure HTTP Basic Authentication"
4
+ task "authentication:configure" => :environment do
5
+ MissionControl::Jobs::Authentication.configure
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,5 @@
1
+ module MissionControl
2
+ module Jobs
3
+ VERSION = "1.0.0"
4
+ end
5
+ end
@@ -0,0 +1,78 @@
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 MissionControl::Jobs::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 length count
51
+ alias size count
52
+
53
+ private
54
+ def set_defaults
55
+ self.offset_value = 0
56
+ self.limit_value = ALL_WORKERS_LIMIT
57
+ end
58
+
59
+ def workers
60
+ @workers ||= @queue_adapter.fetch_workers(self)
61
+ end
62
+
63
+ def query_count
64
+ @count ||= @queue_adapter.count_workers(self)
65
+ end
66
+
67
+ def loaded?
68
+ !@workers.nil?
69
+ end
70
+
71
+ def clone_with(**properties)
72
+ dup.reload.tap do |relation|
73
+ properties.each do |key, value|
74
+ relation.send("#{key}=", value)
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,43 @@
1
+ require "mission_control/jobs/version"
2
+ require "mission_control/jobs/engine"
3
+
4
+ require "zeitwerk"
5
+
6
+ loader = Zeitwerk::Loader.new
7
+ loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
8
+ loader.push_dir(File.expand_path("..", __dir__))
9
+ loader.ignore("#{File.expand_path("..", __dir__)}/resque")
10
+ loader.ignore("#{File.expand_path("..", __dir__)}/solid_job_monitor.rb")
11
+ loader.ignore("#{File.expand_path("..", __dir__)}/mission_control/jobs/tasks.rb")
12
+ loader.ignore("#{File.expand_path("..", __dir__)}/generators")
13
+ loader.setup
14
+
15
+ module MissionControl
16
+ module Jobs
17
+ mattr_accessor :adapters, default: Set.new
18
+ mattr_accessor :applications, default: MissionControl::Jobs::Applications.new
19
+ mattr_accessor :base_controller_class, default: "::ApplicationController"
20
+
21
+ mattr_accessor :internal_query_count_limit, default: 500_000 # Hard limit to keep unlimited count queries fast enough
22
+ mattr_accessor :delay_between_bulk_operation_batches, default: 0
23
+ mattr_accessor :scheduled_job_delay_threshold, default: 1.minute
24
+
25
+ mattr_accessor :logger, default: ActiveSupport::Logger.new(nil)
26
+
27
+ mattr_accessor :show_console_help, default: true
28
+ mattr_accessor :backtrace_cleaner
29
+ mattr_accessor :back_to_main_app_path
30
+
31
+ mattr_accessor :importmap, default: Importmap::Map.new
32
+
33
+ mattr_accessor :http_basic_auth_user
34
+ mattr_accessor :http_basic_auth_password
35
+ mattr_accessor :http_basic_auth_enabled, default: true
36
+
37
+ mattr_accessor :filter_arguments, default: []
38
+
39
+ def self.job_arguments_filter
40
+ MissionControl::Jobs::ArgumentsFilter.new(filter_arguments)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,34 @@
1
+ # Set and access +Resque.redis+ in a thread-safe way.
2
+ module Resque::ThreadSafeRedis
3
+ thread_mattr_accessor :thread_resque_override
4
+
5
+ def self.resque_override
6
+ self.thread_resque_override ||= ResqueOverride.new
7
+ end
8
+
9
+ def redis
10
+ Resque::ThreadSafeRedis.resque_override.data_store_override || super
11
+ end
12
+ alias :data_store :redis
13
+
14
+ def with_per_thread_redis_override(redis_instance, &block)
15
+ Resque::ThreadSafeRedis.resque_override.enable_with(redis_instance, &block)
16
+ end
17
+
18
+ class ResqueOverride
19
+ include Resque
20
+
21
+ attr_accessor :data_store_override
22
+
23
+ def enable_with(server, &block)
24
+ previous_redis, previous_data_store_override = redis, data_store_override
25
+ self.redis = server
26
+ self.data_store_override = @data_store
27
+
28
+ block.call
29
+ ensure
30
+ self.redis = previous_redis
31
+ self.data_store_override = previous_data_store_override
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,6 @@
1
+ require "mission_control/jobs"
2
+
3
+ # The gem is named solid_job_monitor; the engine remains MissionControl::Jobs.
4
+ module SolidJobMonitor
5
+ VERSION = MissionControl::Jobs::VERSION
6
+ end