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,8 @@
1
+ /* Hide the HTML datalist wonky arrow in Safari and Chrome, since we add it via bulma. */
2
+ [list]::-webkit-calendar-picker-indicator {
3
+ opacity: 0;
4
+ }
5
+
6
+ a[disabled] {
7
+ pointer-events: none;
8
+ }
@@ -0,0 +1,68 @@
1
+ .filter input {
2
+ width: 15rem;
3
+ }
4
+
5
+ table.jobs {
6
+ td {
7
+ word-break: break-all;
8
+ }
9
+ th.job-header{
10
+ width: 30%;
11
+ }
12
+ th.duration-header {
13
+ width: 20%;
14
+ }
15
+ &.queues th.job-header {
16
+ width: 30%;
17
+ }
18
+ &.failed th.job-header {
19
+ width: 35%;
20
+ }
21
+ &.in_progress th.job-header {
22
+ width: 35%;
23
+ }
24
+ &.blocked th.job-header {
25
+ width: 45%;
26
+ }
27
+ &.scheduled th.job-header {
28
+ width: 40%;
29
+ }
30
+ &.finished th.job-header {
31
+ width: 65%;
32
+ }
33
+ &.workers th.job-header {
34
+ width: 40%;
35
+ }
36
+ }
37
+
38
+ .job-progress-wrap {
39
+ min-width: 7rem;
40
+ }
41
+
42
+ .job-progress {
43
+ position: relative;
44
+ flex: 1;
45
+ height: 1.25rem;
46
+ min-width: 6rem;
47
+ background: #ededed;
48
+ border-radius: 4px;
49
+ overflow: hidden;
50
+ }
51
+
52
+ .job-progress-bar {
53
+ width: 0;
54
+ height: 100%;
55
+ background: #00d1b2;
56
+ transition: width 0.3s ease;
57
+ }
58
+
59
+ .job-progress-label {
60
+ position: absolute;
61
+ inset: 0;
62
+ display: flex;
63
+ align-items: center;
64
+ justify-content: center;
65
+ font-size: 0.75rem;
66
+ font-weight: 600;
67
+ color: #363636;
68
+ }
@@ -0,0 +1,24 @@
1
+ module MissionControl::Jobs::AdapterFeatures
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ helper_method :supported_job_statuses, :queue_pausing_supported?, :workers_exposed?, :recurring_tasks_supported?
6
+ end
7
+
8
+ private
9
+ def supported_job_statuses
10
+ MissionControl::Jobs::Current.server.queue_adapter.supported_job_statuses & ActiveJob::JobsRelation::STATUSES
11
+ end
12
+
13
+ def queue_pausing_supported?
14
+ MissionControl::Jobs::Current.server.queue_adapter.supports_queue_pausing?
15
+ end
16
+
17
+ def workers_exposed?
18
+ MissionControl::Jobs::Current.server.queue_adapter.exposes_workers?
19
+ end
20
+
21
+ def recurring_tasks_supported?
22
+ MissionControl::Jobs::Current.server.queue_adapter.supports_recurring_tasks?
23
+ end
24
+ end
@@ -0,0 +1,38 @@
1
+ module MissionControl::Jobs::ApplicationScoped
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ before_action :set_application
6
+ around_action :activating_job_server
7
+
8
+ delegate :applications, to: MissionControl::Jobs
9
+ end
10
+
11
+ private
12
+ def set_application
13
+ @application = find_application or raise MissionControl::Jobs::Errors::ResourceNotFound, "Application not found"
14
+ MissionControl::Jobs::Current.application = @application
15
+ end
16
+
17
+ def find_application
18
+ if params[:application_id]
19
+ applications[params[:application_id]]
20
+ else
21
+ applications.first
22
+ end
23
+ end
24
+
25
+ def activating_job_server(&block)
26
+ @server = find_server or raise MissionControl::Jobs::Errors::ResourceNotFound, "Server not found"
27
+ MissionControl::Jobs::Current.server = @server
28
+ @server.activating(&block)
29
+ end
30
+
31
+ def find_server
32
+ if params[:server_id]
33
+ MissionControl::Jobs::Current.application.servers[params[:server_id]]
34
+ else
35
+ @application.servers.first
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,33 @@
1
+ module MissionControl::Jobs::BasicAuthentication
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ before_action :authenticate_by_http_basic
6
+ end
7
+
8
+ private
9
+ def authenticate_by_http_basic
10
+ if http_basic_authentication_enabled?
11
+ if http_basic_authentication_configured?
12
+ http_basic_authenticate_or_request_with(**http_basic_authentication_credentials)
13
+ else
14
+ head :unauthorized
15
+ end
16
+ end
17
+ end
18
+
19
+ def http_basic_authentication_enabled?
20
+ MissionControl::Jobs.http_basic_auth_enabled
21
+ end
22
+
23
+ def http_basic_authentication_configured?
24
+ http_basic_authentication_credentials.values.all?(&:present?)
25
+ end
26
+
27
+ def http_basic_authentication_credentials
28
+ {
29
+ name: MissionControl::Jobs.http_basic_auth_user,
30
+ password: MissionControl::Jobs.http_basic_auth_password
31
+ }.transform_values(&:presence)
32
+ end
33
+ end
@@ -0,0 +1,17 @@
1
+ module MissionControl::Jobs::FailedJobsBulkOperations
2
+ extend ActiveSupport::Concern
3
+
4
+ MAX_NUMBER_OF_JOBS_FOR_BULK_OPERATIONS = 3000
5
+
6
+ included do
7
+ include MissionControl::Jobs::JobFilters
8
+ end
9
+
10
+ private
11
+ # We set a hard limit to prevent problems with the data store (for example, overloading Redis
12
+ # or causing replication lag in MySQL). This should be enough for most scenarios. For
13
+ # cases where we need to retry a huge sets of jobs, we offer a runbook that uses the API.
14
+ def bulk_limited_filtered_failed_jobs
15
+ ActiveJob.jobs.failed.where(**@job_filters).limit(MAX_NUMBER_OF_JOBS_FOR_BULK_OPERATIONS)
16
+ end
17
+ end
@@ -0,0 +1,41 @@
1
+ module MissionControl::Jobs::JobFilters
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ before_action :set_filters
6
+
7
+ helper_method :active_filters?, :jobs_filter_param
8
+ end
9
+
10
+ private
11
+ def set_filters
12
+ @job_filters = {
13
+ job_class_name: params.dig(:filter, :job_class_name).to_s.strip.presence,
14
+ queue_name: params.dig(:filter, :queue_name).to_s.strip.presence,
15
+ finished_at: finished_at_range_params
16
+ }.compact
17
+ end
18
+
19
+ def active_filters?
20
+ @job_filters.any?
21
+ end
22
+
23
+ def jobs_filter_param
24
+ if @job_filters&.any?
25
+ { filter: @job_filters }
26
+ else
27
+ {}
28
+ end
29
+ end
30
+
31
+ def finished_at_range_params
32
+ range_start, range_end = params.dig(:filter, :finished_at_start), params.dig(:filter, :finished_at_end)
33
+ if range_start || range_end
34
+ (parse_with_time_zone(range_start)..parse_with_time_zone(range_end))
35
+ end
36
+ end
37
+
38
+ def parse_with_time_zone(date)
39
+ Time.zone.parse(date) if date.present?
40
+ end
41
+ end
@@ -0,0 +1,16 @@
1
+ module MissionControl::Jobs::JobScoped
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ before_action :set_job
6
+ end
7
+
8
+ private
9
+ def set_job
10
+ @job = jobs_relation.find_by_id!(params[:job_id] || params[:id])
11
+ end
12
+
13
+ def jobs_relation
14
+ raise NotImplementedError
15
+ end
16
+ end
@@ -0,0 +1,36 @@
1
+ module MissionControl::Jobs::NotFoundRedirections
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ rescue_from(ActiveJob::Errors::JobNotFoundError) do |error|
6
+ redirect_to best_location_for_job_relation(error.job_relation), alert: error.message
7
+ end
8
+
9
+ rescue_from(MissionControl::Jobs::Errors::ResourceNotFound) do |error|
10
+ redirect_to best_location_for_resource_not_found_error(error), alert: error.message
11
+ end
12
+ end
13
+
14
+ private
15
+ def best_location_for_job_relation(job_relation)
16
+ case
17
+ when job_relation.failed?
18
+ application_jobs_path(@application, :failed)
19
+ when job_relation.queue_name.present?
20
+ application_queue_path(@application, job_relation.queue_name)
21
+ else
22
+ root_path
23
+ end
24
+ end
25
+
26
+ def best_location_for_resource_not_found_error(error)
27
+ case error.message
28
+ when /recurring task/i
29
+ application_recurring_tasks_path(@application)
30
+ when /queue/i
31
+ application_queues_path(@application)
32
+ else
33
+ root_url
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,12 @@
1
+ module MissionControl::Jobs::QueueScoped
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ before_action :set_queue
6
+ end
7
+
8
+ private
9
+ def set_queue
10
+ @queue = ActiveJob.queues[params[:queue_id]] or raise MissionControl::Jobs::Errors::ResourceNotFound, "Queue '#{params[:queue_id]}' not found"
11
+ end
12
+ end
@@ -0,0 +1,32 @@
1
+ class MissionControl::Jobs::ApplicationController < MissionControl::Jobs.base_controller_class.constantize
2
+ ActionController::Base::MODULES.each do |mod|
3
+ include mod unless self < mod
4
+ end
5
+
6
+ layout "mission_control/jobs/application"
7
+
8
+ # Include helpers if not already included
9
+ helper MissionControl::Jobs::ApplicationHelper unless self < MissionControl::Jobs::ApplicationHelper
10
+ helper Importmap::ImportmapTagsHelper unless self < Importmap::ImportmapTagsHelper
11
+
12
+ include MissionControl::Jobs::BasicAuthentication
13
+ include MissionControl::Jobs::ApplicationScoped, MissionControl::Jobs::NotFoundRedirections
14
+ include MissionControl::Jobs::AdapterFeatures
15
+ include MissionControl::Jobs::JobFilters
16
+
17
+ around_action :set_current_locale
18
+
19
+ private
20
+ def default_url_options
21
+ { server_id: MissionControl::Jobs::Current.server }
22
+ end
23
+
24
+ def set_current_locale(&block)
25
+ @previous_config = I18n.config
26
+ I18n.config = MissionControl::Jobs::I18nConfig.new
27
+ I18n.with_locale(:en, &block)
28
+ ensure
29
+ I18n.config = @previous_config
30
+ @previous_config = nil
31
+ end
32
+ end
@@ -0,0 +1,20 @@
1
+ class MissionControl::Jobs::BulkDiscardsController < MissionControl::Jobs::ApplicationController
2
+ include MissionControl::Jobs::FailedJobsBulkOperations
3
+
4
+ def create
5
+ jobs_to_discard_count = jobs_to_discard.count
6
+ jobs_to_discard.discard_all
7
+
8
+ redirect_to application_jobs_url(@application, :failed), notice: "Discarded #{jobs_to_discard_count} jobs"
9
+ end
10
+
11
+ private
12
+ def jobs_to_discard
13
+ if active_filters?
14
+ bulk_limited_filtered_failed_jobs
15
+ else
16
+ # we don't want to apply any limit since "discarding all" without parameters can be optimized in the adapter as a much faster operation
17
+ ActiveJob.jobs.failed
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,10 @@
1
+ class MissionControl::Jobs::BulkRetriesController < MissionControl::Jobs::ApplicationController
2
+ include MissionControl::Jobs::FailedJobsBulkOperations
3
+
4
+ def create
5
+ jobs_to_retry_count = bulk_limited_filtered_failed_jobs.count
6
+ bulk_limited_filtered_failed_jobs.retry_all
7
+
8
+ redirect_to application_jobs_url(@application, :failed), notice: "Retried #{jobs_to_retry_count} jobs"
9
+ end
10
+ end
@@ -0,0 +1,18 @@
1
+ class MissionControl::Jobs::DiscardsController < MissionControl::Jobs::ApplicationController
2
+ include MissionControl::Jobs::JobScoped
3
+
4
+ def create
5
+ @job.discard
6
+ redirect_to redirect_location, notice: "Discarded job with id #{@job.job_id}"
7
+ end
8
+
9
+ private
10
+ def jobs_relation
11
+ ActiveJob.jobs
12
+ end
13
+
14
+ def redirect_location
15
+ status = @job.status.presence_in(supported_job_statuses) || :failed
16
+ application_jobs_url(@application, status, **jobs_filter_param)
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ class MissionControl::Jobs::DispatchesController < MissionControl::Jobs::ApplicationController
2
+ include MissionControl::Jobs::JobScoped
3
+
4
+ def create
5
+ @job.dispatch
6
+ redirect_to redirect_location, notice: "Dispatched job with id #{@job.job_id}"
7
+ end
8
+
9
+ private
10
+ def jobs_relation
11
+ ActiveJob.jobs
12
+ end
13
+
14
+ def redirect_location
15
+ status = @job.status.presence_in(supported_job_statuses) || :blocked
16
+ application_jobs_url(@application, status)
17
+ end
18
+ end
@@ -0,0 +1,54 @@
1
+ class MissionControl::Jobs::JobsController < MissionControl::Jobs::ApplicationController
2
+ include MissionControl::Jobs::JobScoped
3
+
4
+ skip_before_action :set_job, only: :index
5
+
6
+ def index
7
+ @job_class_names = jobs_with_status.job_class_names
8
+ @queue_names = ActiveJob.queues.map(&:name)
9
+
10
+ @jobs_page = MissionControl::Jobs::Page.new(filtered_jobs_with_status, page: params[:page].to_i)
11
+ @jobs_count = @jobs_page.total_count
12
+ load_progress_for_page
13
+ end
14
+
15
+ def show
16
+ load_progress_for_job
17
+ end
18
+
19
+ private
20
+ def jobs_relation
21
+ filtered_jobs
22
+ end
23
+
24
+ def filtered_jobs_with_status
25
+ filtered_jobs.with_status(jobs_status)
26
+ end
27
+
28
+ def jobs_with_status
29
+ ActiveJob.jobs.with_status(jobs_status)
30
+ end
31
+
32
+ def filtered_jobs
33
+ ActiveJob.jobs.where(**@job_filters)
34
+ end
35
+
36
+ helper_method :jobs_status
37
+
38
+ def jobs_status
39
+ params[:status].presence&.inquiry
40
+ end
41
+
42
+ def load_progress_for_page
43
+ return unless jobs_status&.in_progress? && MissionControl::Jobs::Progress.available?
44
+
45
+ job_ids = @jobs_page.records.map(&:job_id)
46
+ @progress_by_job_id = MissionControl::Jobs::Progress.find_multi(job_ids)
47
+ end
48
+
49
+ def load_progress_for_job
50
+ return unless MissionControl::Jobs::Progress.available?
51
+
52
+ @progress = MissionControl::Jobs::Progress.find(@job.job_id)
53
+ end
54
+ end
@@ -0,0 +1,27 @@
1
+ class MissionControl::Jobs::ProgressesController < MissionControl::Jobs::ApplicationController
2
+ def show
3
+ progress = MissionControl::Jobs::Progress.find(params[:job_id]) if MissionControl::Jobs::Progress.available?
4
+
5
+ respond_to do |format|
6
+ format.json { render json: progress_payload(progress) }
7
+ format.html { render partial: "mission_control/jobs/jobs/progress_bar", locals: { progress: progress, job_id: params[:job_id] } }
8
+ end
9
+ end
10
+
11
+ private
12
+ def progress_payload(progress)
13
+ if progress
14
+ {
15
+ job_id: progress.job_id,
16
+ current: progress.current,
17
+ total: progress.total,
18
+ percent: progress.percent,
19
+ message: progress.message,
20
+ stop_requested: progress.stop_requested,
21
+ missing: false
22
+ }
23
+ else
24
+ { job_id: params[:job_id], missing: true }
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,15 @@
1
+ class MissionControl::Jobs::Queues::PausesController < MissionControl::Jobs::ApplicationController
2
+ include MissionControl::Jobs::QueueScoped
3
+
4
+ def create
5
+ @queue.pause
6
+
7
+ redirect_back fallback_location: application_queues_url(@application)
8
+ end
9
+
10
+ def destroy
11
+ @queue.resume
12
+
13
+ redirect_back fallback_location: application_queues_url(@application)
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ class MissionControl::Jobs::QueuesController < MissionControl::Jobs::ApplicationController
2
+ before_action :set_queue, only: :show
3
+
4
+ def index
5
+ @queues = ActiveJob.queues.sort_by(&:name)
6
+ end
7
+
8
+ def show
9
+ @jobs_page = MissionControl::Jobs::Page.new(@queue.jobs, page: params[:page].to_i)
10
+ end
11
+
12
+ private
13
+ def set_queue
14
+ @queue = ActiveJob.queues[params[:id]] or raise MissionControl::Jobs::Errors::ResourceNotFound, "Queue '#{params[:id]}' not found"
15
+ end
16
+ end
@@ -0,0 +1,38 @@
1
+ class MissionControl::Jobs::RecurringTasksController < MissionControl::Jobs::ApplicationController
2
+ before_action :ensure_supported_recurring_tasks
3
+ before_action :set_recurring_task, only: [ :show, :update ]
4
+ before_action :ensure_recurring_task_can_be_enqueued, only: :update
5
+
6
+ def index
7
+ @recurring_tasks = MissionControl::Jobs::Current.server.recurring_tasks
8
+ end
9
+
10
+ def show
11
+ @jobs_page = MissionControl::Jobs::Page.new(@recurring_task.jobs, page: params[:page].to_i)
12
+ end
13
+
14
+ def update
15
+ if (job = @recurring_task.enqueue) && job.successfully_enqueued?
16
+ redirect_to application_job_path(@application, job.job_id), notice: "Enqueued recurring task #{@recurring_task.id}"
17
+ else
18
+ redirect_to application_recurring_task_path(@application, @recurring_task.id), alert: "Something went wrong enqueuing this recurring task"
19
+ end
20
+ end
21
+
22
+ private
23
+ def ensure_supported_recurring_tasks
24
+ unless recurring_tasks_supported?
25
+ redirect_to root_url, alert: "This server doesn't support recurring tasks"
26
+ end
27
+ end
28
+
29
+ def set_recurring_task
30
+ @recurring_task = MissionControl::Jobs::Current.server.find_recurring_task(params[:id])
31
+ end
32
+
33
+ def ensure_recurring_task_can_be_enqueued
34
+ unless @recurring_task.runnable?
35
+ redirect_to application_recurring_task_path(@application, @recurring_task.id), alert: "This task can't be enqueued"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,13 @@
1
+ class MissionControl::Jobs::RetriesController < MissionControl::Jobs::ApplicationController
2
+ include MissionControl::Jobs::JobScoped
3
+
4
+ def create
5
+ @job.retry
6
+ redirect_to application_jobs_url(@application, :failed, **jobs_filter_param), notice: "Retried job with id #{@job.job_id}"
7
+ end
8
+
9
+ private
10
+ def jobs_relation
11
+ ActiveJob.jobs.failed
12
+ end
13
+ end
@@ -0,0 +1,22 @@
1
+ class MissionControl::Jobs::StopsController < MissionControl::Jobs::ApplicationController
2
+ include MissionControl::Jobs::JobScoped
3
+
4
+ def create
5
+ unless MissionControl::Jobs::Progress.available?
6
+ redirect_to redirect_location, alert: "Progress tracking is not installed. Run rails db:migrate to enable cooperative stop."
7
+ return
8
+ end
9
+
10
+ MissionControl::Jobs::Progress.request_stop(@job.job_id)
11
+ redirect_to redirect_location, notice: "Stop requested for job #{@job.job_id}. It will stop the next time it reports progress or calls check_stopped!."
12
+ end
13
+
14
+ private
15
+ def jobs_relation
16
+ ActiveJob.jobs
17
+ end
18
+
19
+ def redirect_location
20
+ application_jobs_url(@application, :in_progress, **jobs_filter_param)
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ class MissionControl::Jobs::WorkersController < MissionControl::Jobs::ApplicationController
2
+ before_action :ensure_exposed_workers
3
+
4
+ def index
5
+ @workers_page = MissionControl::Jobs::Page.new(workers_relation, page: params[:page].to_i)
6
+ @workers_count = @workers_page.total_count
7
+ end
8
+
9
+ def show
10
+ @worker = MissionControl::Jobs::Current.server.find_worker(params[:id])
11
+ end
12
+
13
+ private
14
+ def ensure_exposed_workers
15
+ unless workers_exposed?
16
+ redirect_to root_url, alert: "This server doesn't expose workers"
17
+ end
18
+ end
19
+
20
+ def workers_relation
21
+ MissionControl::Jobs::Current.server.workers_relation
22
+ end
23
+ end
@@ -0,0 +1,9 @@
1
+ module MissionControl::Jobs
2
+ module ApplicationHelper
3
+ # Explicit helper inclusion because ApplicationController inherits from the host app.
4
+ #
5
+ # We can't rely on +config.action_controller.include_all_helpers = true+ in the host app.
6
+ include DatesHelper, JobsHelper, NavigationHelper, InterfaceHelper
7
+ include MissionControl::Jobs::Engine.routes.url_helpers
8
+ end
9
+ end
@@ -0,0 +1,19 @@
1
+ module MissionControl::Jobs::DatesHelper
2
+ def time_distance_in_words_with_title(time)
3
+ tag.span time_ago_in_words_with_default_options(time), title: "Since #{time.to_fs(:long)}"
4
+ end
5
+
6
+ def bidirectional_time_distance_in_words_with_title(time)
7
+ time_distance = if time.past?
8
+ "#{time_ago_in_words_with_default_options(time)} ago"
9
+ else
10
+ "in #{time_ago_in_words_with_default_options(time)}"
11
+ end
12
+
13
+ tag.span time_distance, title: time.to_fs(:long)
14
+ end
15
+
16
+ def time_ago_in_words_with_default_options(time)
17
+ time_ago_in_words(time, include_seconds: true, locale: :en)
18
+ end
19
+ end
@@ -0,0 +1,23 @@
1
+ module MissionControl::Jobs::InterfaceHelper
2
+ def blank_status_notice(message)
3
+ tag.div message, class: "mt-6 has-text-centered is-size-3 has-text-grey"
4
+ end
5
+
6
+ def blank_status_emoji(status)
7
+ case status.to_s
8
+ when "failed", "blocked" then "😌"
9
+ else ""
10
+ end
11
+ end
12
+
13
+ def modifier_for_status(status)
14
+ case status.to_s
15
+ when "failed" then "is-danger"
16
+ when "blocked" then "is-warning"
17
+ when "finished" then "is-success"
18
+ when "scheduled" then "is-info"
19
+ when "in_progress" then "is-primary"
20
+ else "is-primary is-light"
21
+ end
22
+ end
23
+ end