mission_control-jobs 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (111) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +244 -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/forms.css +8 -0
  8. data/app/assets/stylesheets/mission_control/jobs/jobs.css +7 -0
  9. data/app/controllers/concerns/mission_control/jobs/adapter_features.rb +20 -0
  10. data/app/controllers/concerns/mission_control/jobs/application_scoped.rb +38 -0
  11. data/app/controllers/concerns/mission_control/jobs/failed_jobs_bulk_operations.rb +17 -0
  12. data/app/controllers/concerns/mission_control/jobs/job_filters.rb +18 -0
  13. data/app/controllers/concerns/mission_control/jobs/job_scoped.rb +16 -0
  14. data/app/controllers/concerns/mission_control/jobs/not_found_redirections.rb +25 -0
  15. data/app/controllers/concerns/mission_control/jobs/queue_scoped.rb +12 -0
  16. data/app/controllers/mission_control/jobs/application_controller.rb +11 -0
  17. data/app/controllers/mission_control/jobs/bulk_discards_controller.rb +20 -0
  18. data/app/controllers/mission_control/jobs/bulk_retries_controller.rb +10 -0
  19. data/app/controllers/mission_control/jobs/discards_controller.rb +13 -0
  20. data/app/controllers/mission_control/jobs/jobs_controller.rb +37 -0
  21. data/app/controllers/mission_control/jobs/queues/pauses_controller.rb +15 -0
  22. data/app/controllers/mission_control/jobs/queues_controller.rb +24 -0
  23. data/app/controllers/mission_control/jobs/retries_controller.rb +13 -0
  24. data/app/controllers/mission_control/jobs/workers_controller.rb +18 -0
  25. data/app/helpers/mission_control/jobs/application_helper.rb +8 -0
  26. data/app/helpers/mission_control/jobs/dates_helper.rb +19 -0
  27. data/app/helpers/mission_control/jobs/jobs_helper.rb +63 -0
  28. data/app/helpers/mission_control/jobs/navigation_helper.rb +51 -0
  29. data/app/helpers/mission_control/jobs/ui_helper.rb +23 -0
  30. data/app/javascript/mission_control/jobs/application.js +4 -0
  31. data/app/javascript/mission_control/jobs/controllers/application.js +9 -0
  32. data/app/javascript/mission_control/jobs/controllers/form_controller.js +21 -0
  33. data/app/javascript/mission_control/jobs/controllers/index.js +11 -0
  34. data/app/javascript/mission_control/jobs/helpers/debounce_helpers.js +9 -0
  35. data/app/javascript/mission_control/jobs/helpers/index.js +1 -0
  36. data/app/jobs/mission_control/jobs/application_job.rb +6 -0
  37. data/app/mailers/mission_control/jobs/application_mailer.rb +8 -0
  38. data/app/models/mission_control/jobs/application_record.rb +7 -0
  39. data/app/models/mission_control/jobs/current.rb +3 -0
  40. data/app/models/mission_control/jobs/page.rb +48 -0
  41. data/app/models/mission_control/jobs/worker.rb +17 -0
  42. data/app/views/layouts/mission_control/jobs/_application_selection.html.erb +11 -0
  43. data/app/views/layouts/mission_control/jobs/_flash.html.erb +9 -0
  44. data/app/views/layouts/mission_control/jobs/_navigation.html.erb +9 -0
  45. data/app/views/layouts/mission_control/jobs/application.html.erb +25 -0
  46. data/app/views/layouts/mission_control/jobs/application_selection/_applications.html.erb +13 -0
  47. data/app/views/layouts/mission_control/jobs/application_selection/_servers.html.erb +15 -0
  48. data/app/views/mission_control/jobs/jobs/_error_information.html.erb +19 -0
  49. data/app/views/mission_control/jobs/jobs/_filters.html.erb +35 -0
  50. data/app/views/mission_control/jobs/jobs/_general_information.html.erb +54 -0
  51. data/app/views/mission_control/jobs/jobs/_job.html.erb +13 -0
  52. data/app/views/mission_control/jobs/jobs/_jobs_page.html.erb +15 -0
  53. data/app/views/mission_control/jobs/jobs/_raw_data.html.erb +4 -0
  54. data/app/views/mission_control/jobs/jobs/_title.html.erb +13 -0
  55. data/app/views/mission_control/jobs/jobs/_toolbar.html.erb +18 -0
  56. data/app/views/mission_control/jobs/jobs/blocked/_job.html.erb +3 -0
  57. data/app/views/mission_control/jobs/jobs/failed/_actions.html.erb +5 -0
  58. data/app/views/mission_control/jobs/jobs/failed/_job.html.erb +7 -0
  59. data/app/views/mission_control/jobs/jobs/finished/_job.html.erb +2 -0
  60. data/app/views/mission_control/jobs/jobs/in_progress/_job.html.erb +9 -0
  61. data/app/views/mission_control/jobs/jobs/index.html.erb +19 -0
  62. data/app/views/mission_control/jobs/jobs/scheduled/_job.html.erb +7 -0
  63. data/app/views/mission_control/jobs/jobs/show.html.erb +6 -0
  64. data/app/views/mission_control/jobs/queues/_actions.html.erb +7 -0
  65. data/app/views/mission_control/jobs/queues/_job.html.erb +15 -0
  66. data/app/views/mission_control/jobs/queues/_queue.html.erb +16 -0
  67. data/app/views/mission_control/jobs/queues/_queue_title.html.erb +17 -0
  68. data/app/views/mission_control/jobs/queues/index.html.erb +16 -0
  69. data/app/views/mission_control/jobs/queues/show.html.erb +25 -0
  70. data/app/views/mission_control/jobs/shared/_pagination_toolbar.html.erb +5 -0
  71. data/app/views/mission_control/jobs/workers/_configuration.html.erb +6 -0
  72. data/app/views/mission_control/jobs/workers/_job.html.erb +19 -0
  73. data/app/views/mission_control/jobs/workers/_jobs.html.erb +20 -0
  74. data/app/views/mission_control/jobs/workers/_raw_data.html.erb +6 -0
  75. data/app/views/mission_control/jobs/workers/_title.html.erb +11 -0
  76. data/app/views/mission_control/jobs/workers/_worker.html.erb +21 -0
  77. data/app/views/mission_control/jobs/workers/index.html.erb +17 -0
  78. data/app/views/mission_control/jobs/workers/show.html.erb +7 -0
  79. data/config/importmap.rb +6 -0
  80. data/config/routes.rb +33 -0
  81. data/lib/active_job/errors/invalid_operation.rb +5 -0
  82. data/lib/active_job/errors/job_not_found_error.rb +14 -0
  83. data/lib/active_job/errors/query_error.rb +5 -0
  84. data/lib/active_job/executing.rb +43 -0
  85. data/lib/active_job/execution_error.rb +8 -0
  86. data/lib/active_job/failed.rb +11 -0
  87. data/lib/active_job/job_proxy.rb +26 -0
  88. data/lib/active_job/jobs_relation.rb +300 -0
  89. data/lib/active_job/querying.rb +44 -0
  90. data/lib/active_job/queue.rb +62 -0
  91. data/lib/active_job/queue_adapters/resque_ext.rb +300 -0
  92. data/lib/active_job/queue_adapters/solid_queue_ext.rb +294 -0
  93. data/lib/active_job/queues.rb +29 -0
  94. data/lib/mission_control/jobs/adapter.rb +108 -0
  95. data/lib/mission_control/jobs/application.rb +17 -0
  96. data/lib/mission_control/jobs/applications.rb +8 -0
  97. data/lib/mission_control/jobs/console/context.rb +11 -0
  98. data/lib/mission_control/jobs/console/helpers.rb +26 -0
  99. data/lib/mission_control/jobs/engine.rb +88 -0
  100. data/lib/mission_control/jobs/errors/incompatible_adapter.rb +2 -0
  101. data/lib/mission_control/jobs/errors/resource_not_found.rb +2 -0
  102. data/lib/mission_control/jobs/identified_by_name.rb +18 -0
  103. data/lib/mission_control/jobs/identified_elements.rb +23 -0
  104. data/lib/mission_control/jobs/server/serializable.rb +24 -0
  105. data/lib/mission_control/jobs/server/workers.rb +15 -0
  106. data/lib/mission_control/jobs/server.rb +26 -0
  107. data/lib/mission_control/jobs/version.rb +5 -0
  108. data/lib/mission_control/jobs.rb +19 -0
  109. data/lib/resque/thread_safe_redis.rb +34 -0
  110. data/lib/tasks/mission_control/jobs_tasks.rake +4 -0
  111. metadata +364 -0
@@ -0,0 +1,7 @@
1
+ <td><%= link_to job.queue_name, application_queue_path(@application, job.queue) %></td>
2
+ <td>
3
+ <%= bidirectional_time_distance_in_words_with_title(job.scheduled_at) %>
4
+ <% if job.scheduled_at.before? 1.minute.ago %>
5
+ <div class="is-danger tag ml-4">delayed</div>
6
+ <% end %>
7
+ </td>
@@ -0,0 +1,6 @@
1
+ <% navigation(title: "Job #{@job.job_id}", section: navigation_section_for_status(@job.status)) %>
2
+
3
+ <%= render "mission_control/jobs/jobs/title", job: @job %>
4
+ <%= render "mission_control/jobs/jobs/general_information", job: @job %>
5
+ <%= render "mission_control/jobs/jobs/error_information", job: @job %>
6
+ <%= render "mission_control/jobs/jobs/raw_data", job: @job %>
@@ -0,0 +1,7 @@
1
+ <div class="buttons is-right">
2
+ <% if queue.active? %>
3
+ <%= button_to "Pause", application_queue_pause_path(@application, queue.name), method: :post, class: "button is-success is-light mr-0" %>
4
+ <% else %>
5
+ <%= button_to "Resume", application_queue_pause_path(@application, queue.name), method: :delete, class: "button is-warning is-light mr-0" %>
6
+ <% end %>
7
+ </div>
@@ -0,0 +1,15 @@
1
+ <tr class="job">
2
+ <td>
3
+ <%= link_to application_job_path(@application, job.job_id, filter: { queue_name: job.queue }) do %>
4
+ <%= job_title(job) %>
5
+ <% end %>
6
+ <div class="has-text-grey">Enqueued <%= time_ago_in_words_with_title(job.enqueued_at.to_datetime) %> ago</div>
7
+ </td>
8
+ <td>
9
+ <% if job.serialized_arguments.present? %>
10
+ <span class="is-family-monospace">
11
+ <%= job_arguments(job) %>
12
+ </span>
13
+ <% end %>
14
+ </td>
15
+ </tr>
@@ -0,0 +1,16 @@
1
+ <tr class="queue">
2
+ <td>
3
+ <%= link_to queue.name, application_queue_path(@application, queue.name) %>
4
+ <% if queue.paused? %>
5
+ <span class="ml-4 tag is-warning">Paused</span>
6
+ <% end %>
7
+ </td>
8
+ <td>
9
+ <%= queue.size %>
10
+ </td>
11
+ <td class="pr-0">
12
+ <% if queue_pausing_supported? %>
13
+ <%= render "mission_control/jobs/queues/actions", queue: queue %>
14
+ <% end %>
15
+ </td>
16
+ </tr>
@@ -0,0 +1,17 @@
1
+ <h1 class="title">
2
+ <div class="level">
3
+ <div class="level-left">
4
+ <%= queue.name %>
5
+ <% if queue.paused? %>
6
+ <span class="ml-4 tag is-warning">Paused</span>
7
+ <% end %>
8
+ </div>
9
+ <% if queue_pausing_supported? %>
10
+ <div class="level-right">
11
+ <%= render "mission_control/jobs/queues/actions", queue: queue %>
12
+ </div>
13
+ <% end %>
14
+ </div>
15
+ </h1>
16
+
17
+ <h2 class="subtitle"><%= queue.size %> pending jobs</h2>
@@ -0,0 +1,16 @@
1
+ <% navigation(title: "Queues", section: :queues) %>
2
+
3
+ <table class="queues table is-hoverable is-fullwidth">
4
+ <tbody>
5
+ <thead>
6
+ <tr>
7
+ <th style="width: 50%;">Queue</th>
8
+ <th style="width: 30%;">Pending jobs</th>
9
+ <th></th>
10
+ </tr>
11
+ </thead>
12
+
13
+ <%= render partial: "mission_control/jobs/queues/queue", collection: @queues %>
14
+
15
+ </tbody>
16
+ </table>
@@ -0,0 +1,25 @@
1
+ <% navigation(title: "Queue #{@queue.name}", section: :queues) %>
2
+
3
+ <%= render "mission_control/jobs/queues/queue_title", queue: @queue %>
4
+
5
+ <% if @jobs_page.empty? %>
6
+ <%= blank_status_notice "The queue is empty" %>
7
+ <% else %>
8
+ <table class="jobs table queues is-hoverable is-fullwidth">
9
+ <tbody>
10
+ <thead>
11
+ <tr>
12
+ <th style="width: 30%;">Job</th>
13
+ <th></th>
14
+ </tr>
15
+ </thead>
16
+
17
+ <%= render partial: "mission_control/jobs/queues/job", collection: @jobs_page.jobs %>
18
+
19
+ </tbody>
20
+ </table>
21
+
22
+ <%= render "mission_control/jobs/shared/pagination_toolbar", jobs_page: @jobs_page %>
23
+ <% end %>
24
+
25
+
@@ -0,0 +1,5 @@
1
+ <nav class="buttons is-right" role="navigation" aria-label="pagination">
2
+ <span class="mr-3"><%= jobs_page.index %> / <%= jobs_page.pages_count || "..." %></span>
3
+ <%= link_to "Previous page", url_for(page: jobs_page.previous_index, **jobs_filter_param), class: "pagination-previous", disabled: jobs_page.first? %>
4
+ <%= link_to "Next page", url_for(page: jobs_page.next_index, **jobs_filter_param), class: "pagination-next", disabled: jobs_page.last? %>
5
+ </nav>
@@ -0,0 +1,6 @@
1
+ <h2 class="subtitle mt-2">Configuration</h2>
2
+ <pre>
3
+ <%= JSON.pretty_generate(worker.configuration) %>
4
+ </pre>
5
+
6
+ <hr />
@@ -0,0 +1,19 @@
1
+ <tr class="job">
2
+ <td>
3
+ <%= link_to application_job_path(@application, job.job_id, filter: { queue_name: job.queue }) do %>
4
+ <%= job_title(job) %>
5
+ <% end %>
6
+ <div class="has-text-grey">Enqueued <%= time_ago_in_words_with_title(job.enqueued_at.to_datetime) %> ago</div>
7
+ </td>
8
+ <td>
9
+ <% if job.serialized_arguments.present? %>
10
+ <span class="is-family-monospace">
11
+ <%= job_arguments(job) %>
12
+ </span>
13
+ <% end %>
14
+ </td>
15
+
16
+ <td>
17
+ <div class="has-text-grey"><%= job.started_at ? time_distance_in_words_with_title(job.started_at) : "(Finished)" %></div>
18
+ </td>
19
+ </tr>
@@ -0,0 +1,20 @@
1
+ <% if @worker.jobs.empty? %>
2
+ <%= blank_status_notice "This worker is idle" %>
3
+ <% else %>
4
+ <h2 class="subtitle">Running <%= worker.jobs.size %> jobs</h2>
5
+
6
+ <table class="jobs table workers is-hoverable is-fullwidth">
7
+ <tbody>
8
+ <thead>
9
+ <tr>
10
+ <th style="width: 30%;">Job</th>
11
+ <th></th>
12
+ <th style="width: 15%;">Running for</th>
13
+ </tr>
14
+ </thead>
15
+
16
+ <%= render partial: "mission_control/jobs/workers/job", collection: @worker.jobs %>
17
+
18
+ </tbody>
19
+ </table>
20
+ <% end %>
@@ -0,0 +1,6 @@
1
+ <hr />
2
+
3
+ <h2 class="subtitle">Raw data</h2>
4
+ <pre>
5
+ <%= JSON.pretty_generate(worker.raw_data) %>
6
+ </pre>
@@ -0,0 +1,11 @@
1
+ <h1 class="title">
2
+ <div class="level">
3
+ <div class="level-left">
4
+ <%= "Worker #{worker.id} — #{worker.name}" %>
5
+ </div>
6
+
7
+ <div class="level-right is-family-monospace">
8
+ <%= worker.hostname %>
9
+ </div>
10
+ </div>
11
+ </h1>
@@ -0,0 +1,21 @@
1
+ <tr class="worker">
2
+ <td>
3
+ <%= link_to "worker #{worker.id}", application_worker_path(@application, worker.id) %>
4
+ <br />
5
+ <%= worker.name %>
6
+ </td>
7
+ <td><%= worker.hostname %></td>
8
+ <td>
9
+ <% worker.jobs.each do |job| %>
10
+ <div>
11
+ <%= link_to job_title(job), application_job_path(@application, job.job_id) %>
12
+
13
+ <% if job.serialized_arguments.present? %>
14
+ <div class="is-family-monospace is-size-7"><%= job_arguments(job) %></div>
15
+ <% end %>
16
+ </div>
17
+ <% end %>
18
+ </td>
19
+
20
+ <td><div class="has-text-grey"><%= time_ago_in_words_with_title(worker.last_heartbeat_at) %> ago</div></td>
21
+ </tr>
@@ -0,0 +1,17 @@
1
+ <% navigation(title: "Workers", section: :workers) %>
2
+
3
+ <table class="workers table jobs is-hoverable is-fullwidth">
4
+ <tbody>
5
+ <thead>
6
+ <tr>
7
+ <th>Worker</th>
8
+ <th>Hostname</th>
9
+ <th style="width: 35%;">Jobs</th>
10
+ <th>Last heartbeat</th>
11
+ </tr>
12
+ </thead>
13
+
14
+ <%= render partial: "mission_control/jobs/workers/worker", collection: @workers %>
15
+
16
+ </tbody>
17
+ </table>
@@ -0,0 +1,7 @@
1
+ <% navigation(title: "Worker #{@worker.id}", section: :workers) %>
2
+
3
+ <%= render "mission_control/jobs/workers/title", worker: @worker %>
4
+ <%= render "mission_control/jobs/workers/configuration", worker: @worker %>
5
+ <%= render "mission_control/jobs/workers/jobs", worker: @worker %>
6
+ <%= render "mission_control/jobs/workers/raw_data", worker: @worker %>
7
+
@@ -0,0 +1,6 @@
1
+ pin "application-mcj", to: "mission_control/jobs/application.js", preload: true
2
+ pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
3
+ pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
4
+ pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
5
+ pin_all_from MissionControl::Jobs::Engine.root.join("app/javascript/mission_control/jobs/controllers"), under: "controllers", to: "mission_control/jobs/controllers"
6
+ pin_all_from MissionControl::Jobs::Engine.root.join("app/javascript/mission_control/jobs/helpers"), under: "helpers", to: "mission_control/jobs/helpers"
data/config/routes.rb ADDED
@@ -0,0 +1,33 @@
1
+ MissionControl::Jobs::Engine.routes.draw do
2
+ resources :applications, only: [] do
3
+ resources :queues, only: [ :index, :show ] do
4
+ scope module: :queues do
5
+ resource :pause, only: [ :create, :destroy ]
6
+ end
7
+ end
8
+
9
+ resources :jobs, only: :show do
10
+ resource :retry, only: :create
11
+ resource :discard, only: :create
12
+
13
+ collection do
14
+ resource :bulk_retries, only: :create
15
+ resource :bulk_discards, only: :create
16
+ end
17
+ end
18
+
19
+ resources :jobs, only: :index, path: ":status/jobs"
20
+
21
+ resources :workers, only: [ :index, :show ]
22
+ end
23
+
24
+ # Allow referencing urls without providing an application_id. It will default to the first one.
25
+ resources :queues, only: [ :index, :show ]
26
+
27
+ resources :jobs, only: :show
28
+ resources :jobs, only: :index, path: ":status/jobs"
29
+
30
+ resources :workers, only: [ :index, :show ]
31
+
32
+ root to: "queues#index"
33
+ end
@@ -0,0 +1,5 @@
1
+ module ActiveJob
2
+ module Errors
3
+ class InvalidOperation < StandardError; end
4
+ end
5
+ end
@@ -0,0 +1,14 @@
1
+ module ActiveJob
2
+ module Errors
3
+ class JobNotFoundError < StandardError
4
+ attr_reader :job_relation
5
+
6
+ def initialize(job_or_job_id, job_relation)
7
+ @job_relation = job_relation
8
+
9
+ job_id = job_or_job_id.is_a?(ActiveJob::Base) ? job_or_job_id.job_id : job_or_job_id
10
+ super "Job with id '#{job_id}' not found"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ module ActiveJob
2
+ module Errors
3
+ class QueryError < StandardError; end
4
+ end
5
+ end
@@ -0,0 +1,43 @@
1
+ # TODO: These (or a version of them) should be moved to +ActiveJob::Core+
2
+ # and related concerns when upstreamed.
3
+ module ActiveJob::Executing
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ attr_accessor :raw_data, :position, :finished_at, :blocked_by, :blocked_until, :worker_id, :started_at
8
+ attr_reader :serialized_arguments
9
+ attr_writer :status
10
+
11
+ thread_cattr_accessor :current_queue_adapter
12
+ end
13
+
14
+ class_methods do
15
+ def queue_adapter
16
+ ActiveJob::Base.current_queue_adapter || super
17
+ end
18
+ end
19
+
20
+ def retry
21
+ ActiveJob.jobs.failed.retry_job(self)
22
+ end
23
+
24
+ def discard
25
+ jobs_relation_for_discarding.discard_job(self)
26
+ end
27
+
28
+ def status
29
+ return @status if @status.present?
30
+
31
+ failed? ? :failed : :pending
32
+ end
33
+
34
+ private
35
+ def jobs_relation_for_discarding
36
+ case status
37
+ when :failed then ActiveJob.jobs.failed
38
+ when :pending then ActiveJob.jobs.pending.where(queue_name: queue_name)
39
+ else
40
+ ActiveJob.jobs
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,8 @@
1
+ # Information about a given error when executing a job.
2
+ #
3
+ # It's attached to failed jobs at +ActiveJob::Base#last_execution_error+.
4
+ ActiveJob::ExecutionError = Struct.new(:error_class, :message, :backtrace, keyword_init: true) do
5
+ def to_s
6
+ "ERROR #{error_class}: #{message}\n#{backtrace&.collect { |line| "\t#{line}" }&.join("\n")}"
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ module ActiveJob::Failed
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ attr_accessor :last_execution_error, :failed_at
6
+ end
7
+
8
+ def failed?
9
+ last_execution_error.present?
10
+ end
11
+ end
@@ -0,0 +1,26 @@
1
+ # A proxy for managing jobs without having to load the corresponding
2
+ # job class.
3
+ #
4
+ # This is useful for managing jobs without having the job classes
5
+ # present in the code base.
6
+ class ActiveJob::JobProxy < ActiveJob::Base
7
+ class UnsupportedError < StandardError; end
8
+
9
+ attr_reader :job_class_name
10
+
11
+ def initialize(job_data)
12
+ super
13
+ @job_class_name = job_data["job_class"]
14
+ deserialize(job_data)
15
+ end
16
+
17
+ def serialize
18
+ super.tap do |json|
19
+ json["job_class"] = @job_class_name
20
+ end
21
+ end
22
+
23
+ def perform_now
24
+ raise UnsupportedError, "A JobProxy doesn't support immediate execution, only enqueuing."
25
+ end
26
+ end