good_job 2.13.0 → 2.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +53 -1
  3. data/engine/app/assets/good_job/modules/application.js +14 -0
  4. data/engine/app/assets/good_job/modules/charts.js +29 -0
  5. data/engine/app/assets/good_job/modules/checkbox_toggle.js +51 -0
  6. data/engine/app/assets/good_job/modules/document_ready.js +7 -0
  7. data/engine/app/assets/good_job/modules/poller.js +93 -0
  8. data/engine/app/assets/good_job/modules/toasts.js +8 -0
  9. data/engine/app/assets/good_job/scripts.js +3 -0
  10. data/engine/app/assets/{style.css → good_job/style.css} +4 -0
  11. data/engine/app/assets/{vendor → good_job/vendor}/bootstrap/bootstrap.bundle.min.js +0 -0
  12. data/engine/app/assets/{vendor → good_job/vendor}/bootstrap/bootstrap.min.css +0 -0
  13. data/engine/app/assets/{vendor → good_job/vendor}/chartjs/chart.min.js +0 -0
  14. data/engine/app/assets/good_job/vendor/es_module_shims.js +1 -0
  15. data/engine/app/assets/{vendor → good_job/vendor}/rails_ujs.js +0 -0
  16. data/engine/app/controllers/good_job/assets_controller.rb +23 -6
  17. data/engine/app/controllers/good_job/executions_controller.rb +1 -5
  18. data/engine/app/controllers/good_job/jobs_controller.rb +45 -2
  19. data/engine/app/filters/good_job/base_filter.rb +3 -0
  20. data/engine/app/filters/good_job/jobs_filter.rb +5 -2
  21. data/engine/app/helpers/good_job/application_helper.rb +6 -0
  22. data/engine/app/views/good_job/executions/_table.erb +1 -1
  23. data/engine/app/views/good_job/jobs/_table.erb +101 -62
  24. data/engine/app/views/good_job/jobs/index.html.erb +1 -1
  25. data/engine/app/views/good_job/shared/_alert.erb +20 -13
  26. data/engine/app/views/good_job/shared/_navbar.erb +1 -4
  27. data/engine/app/views/layouts/good_job/application.html.erb +11 -8
  28. data/engine/config/locales/en.yml +0 -1
  29. data/engine/config/locales/es.yml +0 -1
  30. data/engine/config/locales/nl.yml +0 -1
  31. data/engine/config/locales/ru.yml +0 -1
  32. data/engine/config/routes.rb +10 -3
  33. data/lib/good_job/active_job_job.rb +9 -7
  34. data/lib/good_job/lockable.rb +10 -0
  35. data/lib/good_job/version.rb +1 -1
  36. metadata +15 -10
  37. data/engine/app/assets/scripts.js +0 -133
  38. data/engine/app/filters/good_job/executions_filter.rb +0 -41
  39. data/engine/app/views/good_job/executions/index.html.erb +0 -15
@@ -1,72 +1,111 @@
1
1
  <div class="my-3" data-gj-poll-replace id="jobs-table">
2
2
  <div class="table-responsive">
3
- <table class="table table-hover table-sm mb-0">
4
- <thead>
5
- <tr>
6
- <th>ActiveJob ID</th>
7
- <th>State</th>
8
- <th>Job Class</th>
9
- <th>Queue</th>
10
- <th>Scheduled At</th>
11
- <th>Executions</th>
12
- <th>Error</th>
13
- <th>
14
- ActiveJob Params&nbsp;
15
- <%= tag.button "Toggle", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
16
- data: { bs_toggle: "collapse", bs_target: ".job-params" },
17
- aria: { expanded: false, controls: jobs.map { |job| "##{dom_id(job, "params")}" }.join(" ") }
18
- %>
19
- </th>
20
- <th>Actions</th>
21
- </tr>
22
- </thead>
23
- <tbody>
24
- <% if jobs.present? %>
25
- <% jobs.each do |job| %>
26
- <tr class="<%= dom_class(job) %>" id="<%= dom_id(job) %>">
27
- <td>
28
- <%= link_to job_path(job.id) do %>
29
- <code><%= job.id %></code>
3
+ <%= form_with(url: mass_update_jobs_path, method: :put, local: true, data: { "checkbox-toggle": "job_ids" }) do |form| %>
4
+ <table class="table table-hover table-sm mb-0">
5
+ <thead>
6
+ <tr>
7
+ <th><%= check_box_tag('toggle_job_ids', "1", false, data: { "checkbox-toggle-all": "job_ids" }) %></th>
8
+ <th>ActiveJob ID</th>
9
+ <th>State</th>
10
+ <th>Job Class</th>
11
+ <th>Queue</th>
12
+ <th>Scheduled At</th>
13
+ <th>Executions</th>
14
+ <th>Error</th>
15
+ <th>
16
+ ActiveJob Params&nbsp;
17
+ <%= tag.button "Toggle", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
18
+ data: { bs_toggle: "collapse", bs_target: ".job-params" },
19
+ aria: { expanded: false, controls: jobs.map { |job| "##{dom_id(job, "params")}" }.join(" ") }
20
+ %>
21
+ </th>
22
+ <th>
23
+ Actions<br>
24
+
25
+ <div class="d-inline text-nowrap">
26
+ <%= form.button type: 'submit', name: 'mass_action', value: 'reschedule', class: 'btn btn-sm btn-outline-primary', title: "Reschedule all", data: { confirm: "Confirm reschedule all", disable: true } do %>
27
+ <%= render_icon "skip_forward" %> All
30
28
  <% end %>
31
- </td>
32
- <td><%= status_badge(job.status) %></td>
33
- <td><%= job.job_class %></td>
34
- <td><%= job.queue_name %></td>
35
- <td><%= relative_time(job.scheduled_at || job.created_at) %></td>
36
- <td><%= job.executions_count %></td>
37
- <td class="text-break"><%= truncate(job.recent_error, length: 1_000) %></td>
38
- <td>
39
- <%= tag.button "Preview", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
40
- data: { bs_toggle: "collapse", bs_target: "##{dom_id(job, 'params')}" },
41
- aria: { expanded: false, controls: dom_id(job, "params") }
42
- %>
43
- <%= tag.pre JSON.pretty_generate(job.serialized_params), id: dom_id(job, "params"), class: "collapse job-params" %>
44
- </td>
45
- <td>
46
- <div class="text-nowrap">
47
- <% job_reschedulable = job.status.in? [:scheduled, :retried, :queued] %>
48
- <%= button_to reschedule_job_path(job.id), method: :put, class: "btn btn-sm #{job_reschedulable ? 'btn-outline-primary' : 'btn-outline-secondary'}", form_class: "d-inline-block", disabled: !job_reschedulable, aria: { label: "Reschedule job" }, title: "Reschedule job", data: { confirm: "Confirm reschedule" } do %>
49
- <%= render "good_job/shared/icons/skip_forward" %>
50
- <% end %>
51
29
 
52
- <% job_discardable = job.status.in? [:scheduled, :retried, :queued] %>
53
- <%= button_to discard_job_path(job.id), method: :put, class: "btn btn-sm #{job_discardable ? 'btn-outline-primary' : 'btn-outline-secondary'}", form_class: "d-inline-block", disabled: !job_discardable, aria: { label: "Discard job" }, title: "Discard job", data: { confirm: "Confirm discard" } do %>
54
- <%= render "good_job/shared/icons/stop" %>
55
- <% end %>
30
+ <%= form.button type: 'submit', name: 'mass_action', value: 'discard', class: 'btn btn-sm btn-outline-primary', title: "Discard all", data: { confirm: "Confirm discard all", disable: true } do %>
31
+ <%= render_icon "stop" %> All
32
+ <% end %>
56
33
 
57
- <%= button_to retry_job_path(job.id), method: :put, class: "btn btn-sm #{job.status == :discarded ? 'btn-outline-primary' : 'btn-outline-secondary'}", form_class: "d-inline-block", disabled: job.status != :discarded, aria: { label: "Retry job" }, title: "Retry job", data: { confirm: "Confirm retry" } do %>
58
- <%= render "good_job/shared/icons/arrow_clockwise" %>
34
+ <%= form.button type: 'submit', name: 'mass_action', value: 'retry', class: 'btn btn-sm btn-outline-primary', title: "Retry all", data: { confirm: "Confirm retry all", disable: true } do %>
35
+ <%= render_icon "arrow_clockwise" %> All
36
+ <% end %>
37
+ </div>
38
+ </tr>
39
+ <tr class="d-none" data-checkbox-toggle-show="job_ids">
40
+ <td class="text-center table-warning" colspan="10">
41
+ <% all_jobs_count = local_assigns[:all_jobs_count] %>
42
+ <label>
43
+ <%= check_box_tag "all_job_ids", 1, false, disabled: true, data: { "checkbox-toggle-show": "job_ids"} %>
44
+ Apply to all <%= all_jobs_count.present? ? number_with_delimiter(all_jobs_count) : "" %> <%= "job".pluralize(all_jobs_count || 99) %>.
45
+ <em>This could be a lot.</em>
46
+ </label>
47
+ </td>
48
+ </tr>
49
+ </thead>
50
+ <tbody>
51
+ <% if jobs.present? %>
52
+ <% jobs.each do |job| %>
53
+ <tr class="<%= dom_class(job) %>" id="<%= dom_id(job) %>">
54
+ <td><%= check_box_tag 'job_ids[]', job.id, false, data: { "checkbox-toggle-each": "job_ids" } %></td>
55
+ <td>
56
+ <%= link_to job_path(job.id) do %>
57
+ <code><%= job.id %></code>
59
58
  <% end %>
60
- </div>
61
- </td>
59
+ </td>
60
+ <td><%= status_badge(job.status) %></td>
61
+ <td><%= job.job_class %></td>
62
+ <td><%= job.queue_name %></td>
63
+ <td><%= relative_time(job.scheduled_at || job.created_at) %></td>
64
+ <td><%= job.executions_count %></td>
65
+ <td class="text-break"><%= truncate(job.recent_error, length: 1_000) %></td>
66
+ <td>
67
+ <%= tag.button "Preview", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
68
+ data: { bs_toggle: "collapse", bs_target: "##{dom_id(job, 'params')}" },
69
+ aria: { expanded: false, controls: dom_id(job, "params") }
70
+ %>
71
+ <%= tag.pre JSON.pretty_generate(job.serialized_params), id: dom_id(job, "params"), class: "collapse job-params" %>
72
+ </td>
73
+ <td>
74
+ <div class="text-nowrap">
75
+ <% if job.status.in? [:scheduled, :retried, :queued] %>
76
+ <%= link_to reschedule_job_path(job.id), method: :put, class: "btn btn-sm btn-outline-primary", title: "Reschedule job", data: { confirm: "Confirm reschedule", disable: true } do %>
77
+ <%= render_icon "skip_forward" %>
78
+ <% end %>
79
+ <% else %>
80
+ <button class="btn btn-sm btn-outline-secondary" disabled><%= render_icon "skip_forward" %></button>
81
+ <% end %>
82
+
83
+ <% if job.status.in? [:scheduled, :retried, :queued] %>
84
+ <%= link_to discard_job_path(job.id), method: :put, class: "btn btn-sm btn-outline-primary", title: "Discard job", data: { confirm: "Confirm discard", disable: true } do %>
85
+ <%= render_icon "stop" %>
86
+ <% end %>
87
+ <% else %>
88
+ <button class="btn btn-sm btn-outline-secondary" disabled><%= render_icon "stop" %></button>
89
+ <% end %>
90
+
91
+ <% if job.status == :discarded %>
92
+ <%= link_to retry_job_path(job.id), method: :put, class: "btn btn-sm btn-outline-primary", title: "Retry job", data: { confirm: "Confirm retry", disable: true } do %>
93
+ <%= render_icon "arrow_clockwise" %>
94
+ <% end %>
95
+ <% else %>
96
+ <button class="btn btn-sm btn-outline-secondary" disabled><%= render_icon "arrow_clockwise" %></button>
97
+ <% end %>
98
+ </div>
99
+ </td>
100
+ </tr>
101
+ <% end %>
102
+ <% else %>
103
+ <tr>
104
+ <td colspan="10" class="py-2 text-center text-muted">No jobs found.</td>
62
105
  </tr>
63
106
  <% end %>
64
- <% else %>
65
- <tr>
66
- <td colspan="8" class="py-2 text-center text-muted">No jobs found.</td>
67
- </tr>
68
- <% end %>
69
- </tbody>
70
- </table>
107
+ </tbody>
108
+ </table>
109
+ <% end %>
71
110
  </div>
72
111
  </div>
@@ -1,6 +1,6 @@
1
1
  <%= render 'good_job/shared/filter', title: "Jobs", filter: @filter %>
2
2
  <%= render 'good_job/shared/chart', chart_data: GoodJob::ScheduledByQueueChart.new(@filter).data %>
3
- <%= render 'good_job/jobs/table', jobs: @filter.records %>
3
+ <%= render 'good_job/jobs/table', jobs: @filter.records, all_jobs_count: @filter.filtered_query_count %>
4
4
 
5
5
  <% if @filter.records.present? %>
6
6
  <nav aria-label="Job pagination" class="mt-3" data-gj-poll-replace id="jobs-pagination">
@@ -1,13 +1,20 @@
1
- <% if notice %>
2
- <div class="alert alert-success alert-dismissible fade show d-flex align-items-center offset-md-3 col-6" role="alert">
3
- <%= render "good_job/shared/icons/check", class: "flex-shrink-0 me-2" %>
4
- <div><%= notice %></div>
5
- <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
6
- </div>
7
- <% elsif alert %>
8
- <div class="alert alert-warning alert-dismissible fade show d-flex align-items-center offset-md-3 col-6" role="alert">
9
- <%= render "good_job/shared/icons/exclamation", class: "flex-shrink-0 me-2" %>
10
- <div><%= alert %></div>
11
- <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
12
- </div>
13
- <% end %>
1
+ <div class="toast-container position-fixed p-3 start-50 translate-middle-x">
2
+ <% if notice %>
3
+ <div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
4
+ <div class="toast-body d-flex align-items-center gap-2">
5
+ <%= render "good_job/shared/icons/check", class: "flex-shrink-0 text-success" %>
6
+ <div class="flex-fill"><%= notice %></div>
7
+ <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
8
+ </div>
9
+ </div>
10
+ <% end %>
11
+ <% if alert %>
12
+ <div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
13
+ <div class="toast-body d-flex align-items-center gap-2">
14
+ <%= render "good_job/shared/icons/exclamation", class: "flex-shrink-0 text-danger" %>
15
+ <div class="flex-fill"><%= alert %></div>
16
+ <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
17
+ </div>
18
+ </div>
19
+ <% end %>
20
+ </div>
@@ -7,9 +7,6 @@
7
7
 
8
8
  <div class="collapse navbar-collapse" id="navbarSupportedContent">
9
9
  <ul class="navbar-nav me-auto">
10
- <li class="nav-item">
11
- <%= link_to t(".executions"), root_path, class: ["nav-link", ("active" if controller_name == 'executions')] %>
12
- </li>
13
10
  <li class="nav-item">
14
11
  <%= link_to t(".jobs"), jobs_path, class: ["nav-link", ("active" if controller_name == 'jobs')] %>
15
12
  </li>
@@ -22,7 +19,7 @@
22
19
  </ul>
23
20
  <div class="nav-item pe-2">
24
21
  <div class="form-check">
25
- <input type="checkbox" id="toggle-poll" name="toggle-poll" data-gj-action='change#togglePoll' <%= 'checked' if params[:poll].present? %>>
22
+ <input type="checkbox" id="toggle-poll" name="toggle-poll" <%= 'checked' if params[:poll].present? %>>
26
23
  <label for="toggle-poll"><%= t(".live_poll") %></label>
27
24
  </div>
28
25
  </div>
@@ -7,21 +7,24 @@
7
7
  <%= csrf_meta_tags %>
8
8
  <%= csp_meta_tag %>
9
9
 
10
- <%# Assets must use *_url route helpers to avoid being overriden by config.asset_host %>
11
- <%= stylesheet_link_tag bootstrap_url(format: :css, v: GoodJob::VERSION), skip_pipeline: true %>
12
- <%= stylesheet_link_tag style_url(format: :css, v: GoodJob::VERSION) %>
10
+ <%# Do not use asset tag helpers to avoid paths being overriden by config.asset_host %>
11
+ <%= tag.link rel: "stylesheet", media: "screen", href: bootstrap_path(format: :css, v: GoodJob::VERSION, locale: nil), nonce: content_security_policy_nonce %>
12
+ <%= tag.link rel: "stylesheet", media: "screen", href: style_path(format: :css, v: GoodJob::VERSION, locale: nil), nonce: content_security_policy_nonce %>
13
13
 
14
- <%= javascript_include_tag bootstrap_url(format: :js, v: GoodJob::VERSION), nonce: true %>
15
- <%= javascript_include_tag chartjs_url(format: :js, v: GoodJob::VERSION), nonce: true %>
16
- <%= javascript_include_tag scripts_url(format: :js, v: GoodJob::VERSION), nonce: true %>
14
+ <%= tag.script "", src: bootstrap_path(format: :js, v: GoodJob::VERSION, locale: nil), nonce: content_security_policy_nonce %>
15
+ <%= tag.script "", src: chartjs_path(format: :js, v: GoodJob::VERSION, locale: nil), nonce: content_security_policy_nonce %>
16
+ <%= tag.script "", src: rails_ujs_path(format: :js, v: GoodJob::VERSION, locale: nil), nonce: content_security_policy_nonce %>
17
17
 
18
- <%= javascript_include_tag rails_ujs_url(format: :js, v: GoodJob::VERSION), nonce: true %>
18
+ <%= tag.script "", src: es_module_shims_path(format: :js, v: GoodJob::VERSION, locale: nil), async: true, nonce: content_security_policy_nonce %>
19
+ <% importmaps = { imports: GoodJob::AssetsController.js_modules.keys.each_with_object({}) { |module_name, imports| imports[module_name] = modules_path(module_name, format: :js, locale: nil, v: GoodJob::VERSION) } } %>
20
+ <%= tag.script importmaps.to_json.html_safe, type: "importmap", nonce: content_security_policy_nonce %>
21
+ <%= tag.script "", src: scripts_path(format: :js, v: GoodJob::VERSION, locale: nil), type: "module", nonce: content_security_policy_nonce %>
19
22
  </head>
20
23
  <body>
21
24
  <div class="d-flex flex-column min-vh-100">
22
25
  <%= render "good_job/shared/navbar" %>
23
26
 
24
- <div class="container-fluid flex-grow-1">
27
+ <div class="container-fluid flex-grow-1 relative">
25
28
  <%= render "good_job/shared/alert" %>
26
29
 
27
30
  <%= yield %>
@@ -46,7 +46,6 @@ en:
46
46
  wording: Remember, you're doing a Good Job too!
47
47
  navbar:
48
48
  cron_schedules: Cron
49
- executions: Executions
50
49
  jobs: Jobs
51
50
  live_poll: Live Poll
52
51
  name: "GoodJob 👍"
@@ -46,7 +46,6 @@ es:
46
46
  wording: "¡Recuerda, también tú estás haciendo un buen trabajo!"
47
47
  navbar:
48
48
  cron_schedules: Cron
49
- executions: Ejecuciones
50
49
  jobs: Tareas
51
50
  live_poll: En vivo
52
51
  name: "GoodJob 👍"
@@ -46,7 +46,6 @@ nl:
46
46
  wording: 'Onthoud: jij levert ook goed werk!'
47
47
  navbar:
48
48
  cron_schedules: Cron
49
- executions: Uitvoeringen
50
49
  jobs: Taken
51
50
  live_poll: Live Poll
52
51
  name: "GoodJob 👍"
@@ -70,7 +70,6 @@ ru:
70
70
  wording: Запомни, ты делаешь Good Job тоже!
71
71
  navbar:
72
72
  cron_schedules: Cron
73
- executions: Исполнения
74
73
  jobs: Задачи
75
74
  live_poll: Живой Опрос
76
75
  name: "GoodJob 👍"
@@ -1,10 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
  GoodJob::Engine.routes.draw do
3
- root to: 'executions#index'
3
+ root to: redirect(path: 'jobs')
4
4
 
5
5
  resources :executions, only: %i[destroy]
6
6
 
7
7
  resources :jobs, only: %i[index show] do
8
+ collection do
9
+ get :mass_update, to: redirect(path: 'jobs')
10
+ put :mass_update
11
+ end
12
+
8
13
  member do
9
14
  put :discard
10
15
  put :reschedule
@@ -20,7 +25,7 @@ GoodJob::Engine.routes.draw do
20
25
 
21
26
  resources :processes, only: %i[index]
22
27
 
23
- scope controller: :assets do
28
+ scope :assets, controller: :assets do
24
29
  constraints(format: :css) do
25
30
  get :bootstrap, action: :bootstrap_css
26
31
  get :style, action: :style_css
@@ -28,8 +33,10 @@ GoodJob::Engine.routes.draw do
28
33
 
29
34
  constraints(format: :js) do
30
35
  get :bootstrap, action: :bootstrap_js
31
- get :rails_ujs, action: :rails_ujs_js
32
36
  get :chartjs, action: :chartjs_js
37
+ get :rails_ujs, action: :rails_ujs_js
38
+ get :es_module_shims, action: :es_module_shims_js
39
+ get "modules/:module", action: :modules_js, as: :modules
33
40
  get :scripts, action: :scripts_js
34
41
  end
35
42
  end
@@ -148,7 +148,7 @@ module GoodJob
148
148
  # Tests whether the job is being executed right now.
149
149
  # @return [Boolean]
150
150
  def running?
151
- # Avoid N+1 Query: `.joins_advisory_locks.select('good_jobs.*', 'pg_locks.locktype AS locktype')`
151
+ # Avoid N+1 Query: `.includes_advisory_locks`
152
152
  if has_attribute?(:locktype)
153
153
  self['locktype'].present?
154
154
  else
@@ -157,7 +157,7 @@ module GoodJob
157
157
  end
158
158
 
159
159
  # Retry a job that has errored and been discarded.
160
- # This action will create a new job {Execution} record.
160
+ # This action will create a new {Execution} record for the job.
161
161
  # @return [ActiveJob::Base]
162
162
  def retry_job
163
163
  with_advisory_lock do
@@ -165,7 +165,7 @@ module GoodJob
165
165
  active_job = execution.active_job
166
166
 
167
167
  raise AdapterNotGoodJobError unless active_job.class.queue_adapter.is_a? GoodJob::Adapter
168
- raise ActionForStateMismatchError unless status == :discarded
168
+ raise ActionForStateMismatchError if execution.finished_at.blank? || execution.error.blank?
169
169
 
170
170
  # Update the executions count because the previous execution will not have been preserved
171
171
  # Do not update `exception_executions` because that comes from rescue_from's arguments
@@ -176,7 +176,7 @@ module GoodJob
176
176
  current_thread.execution = execution
177
177
 
178
178
  execution.class.transaction(joinable: false, requires_new: true) do
179
- new_active_job = active_job.retry_job(wait: 0, error: error)
179
+ new_active_job = active_job.retry_job(wait: 0, error: execution.error)
180
180
  execution.save
181
181
  end
182
182
  end
@@ -189,11 +189,11 @@ module GoodJob
189
189
  # @return [void]
190
190
  def discard_job(message)
191
191
  with_advisory_lock do
192
- raise ActionForStateMismatchError unless status.in? [:scheduled, :queued, :retried]
193
-
194
192
  execution = head_execution(reload: true)
195
193
  active_job = execution.active_job
196
194
 
195
+ raise ActionForStateMismatchError if execution.finished_at.present?
196
+
197
197
  job_error = GoodJob::ActiveJobJob::DiscardJobError.new(message)
198
198
 
199
199
  update_execution = proc do
@@ -216,7 +216,9 @@ module GoodJob
216
216
  # @return [void]
217
217
  def reschedule_job(scheduled_at = Time.current)
218
218
  with_advisory_lock do
219
- raise ActionForStateMismatchError unless status.in? [:scheduled, :queued, :retried]
219
+ execution = head_execution(reload: true)
220
+
221
+ raise ActionForStateMismatchError if execution.finished_at.present?
220
222
 
221
223
  execution = head_execution(reload: true)
222
224
  execution.update(scheduled_at: scheduled_at)
@@ -82,6 +82,16 @@ module GoodJob
82
82
  joins(sanitize_sql_for_conditions([join_sql, { table_name: table_name }]))
83
83
  end)
84
84
 
85
+ # Joins the current query with Postgres's +pg_locks+ table AND SELECTs the resulting columns
86
+ # @!method joins_advisory_locks(column: _advisory_lockable_column)
87
+ # @!scope class
88
+ # @param column [String, Symbol] column values to Advisory Lock against
89
+ # @return [ActiveRecord::Relation]
90
+ scope :includes_advisory_locks, (lambda do |column: _advisory_lockable_column|
91
+ owns_advisory_lock_sql = "#{connection.quote_table_name('pg_locks')}.#{connection.quote_column_name('pid')} = pg_backend_pid() AS owns_advisory_lock"
92
+ joins_advisory_locks(column: column).select("#{quoted_table_name}.*, #{connection.quote_table_name('pg_locks')}.locktype, #{owns_advisory_lock_sql}")
93
+ end)
94
+
85
95
  # Find records that do not have an advisory lock on them.
86
96
  # @!method advisory_unlocked(column: _advisory_lockable_column)
87
97
  # @!scope class
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  module GoodJob
3
3
  # GoodJob gem version.
4
- VERSION = '2.13.0'
4
+ VERSION = '2.14.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: good_job
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.13.0
4
+ version: 2.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Sheldon
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-04-19 00:00:00.000000000 Z
11
+ date: 2022-04-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -360,12 +360,19 @@ files:
360
360
  - CHANGELOG.md
361
361
  - LICENSE.txt
362
362
  - README.md
363
- - engine/app/assets/scripts.js
364
- - engine/app/assets/style.css
365
- - engine/app/assets/vendor/bootstrap/bootstrap.bundle.min.js
366
- - engine/app/assets/vendor/bootstrap/bootstrap.min.css
367
- - engine/app/assets/vendor/chartjs/chart.min.js
368
- - engine/app/assets/vendor/rails_ujs.js
363
+ - engine/app/assets/good_job/modules/application.js
364
+ - engine/app/assets/good_job/modules/charts.js
365
+ - engine/app/assets/good_job/modules/checkbox_toggle.js
366
+ - engine/app/assets/good_job/modules/document_ready.js
367
+ - engine/app/assets/good_job/modules/poller.js
368
+ - engine/app/assets/good_job/modules/toasts.js
369
+ - engine/app/assets/good_job/scripts.js
370
+ - engine/app/assets/good_job/style.css
371
+ - engine/app/assets/good_job/vendor/bootstrap/bootstrap.bundle.min.js
372
+ - engine/app/assets/good_job/vendor/bootstrap/bootstrap.min.css
373
+ - engine/app/assets/good_job/vendor/chartjs/chart.min.js
374
+ - engine/app/assets/good_job/vendor/es_module_shims.js
375
+ - engine/app/assets/good_job/vendor/rails_ujs.js
369
376
  - engine/app/charts/good_job/scheduled_by_queue_chart.rb
370
377
  - engine/app/controllers/good_job/application_controller.rb
371
378
  - engine/app/controllers/good_job/assets_controller.rb
@@ -374,13 +381,11 @@ files:
374
381
  - engine/app/controllers/good_job/jobs_controller.rb
375
382
  - engine/app/controllers/good_job/processes_controller.rb
376
383
  - engine/app/filters/good_job/base_filter.rb
377
- - engine/app/filters/good_job/executions_filter.rb
378
384
  - engine/app/filters/good_job/jobs_filter.rb
379
385
  - engine/app/helpers/good_job/application_helper.rb
380
386
  - engine/app/views/good_job/cron_entries/index.html.erb
381
387
  - engine/app/views/good_job/cron_entries/show.html.erb
382
388
  - engine/app/views/good_job/executions/_table.erb
383
- - engine/app/views/good_job/executions/index.html.erb
384
389
  - engine/app/views/good_job/jobs/_table.erb
385
390
  - engine/app/views/good_job/jobs/index.html.erb
386
391
  - engine/app/views/good_job/jobs/show.html.erb
@@ -1,133 +0,0 @@
1
- /*jshint esversion: 6, strict: false */
2
- const GOOD_JOB_DEFAULT_POLL_INTERVAL_SECONDS = 30;
3
- const GOOD_JOB_MINIMUM_POLL_INTERVAL = 1000;
4
-
5
- const GoodJob = {
6
- // Register functions to execute when the DOM is ready
7
- ready: (callback) => {
8
- if (document.readyState !== "loading") {
9
- callback();
10
- } else {
11
- document.addEventListener("DOMContentLoaded", callback);
12
- }
13
- },
14
-
15
- init: () => {
16
- GoodJob.updateSettings();
17
- GoodJob.addListeners();
18
- GoodJob.pollUpdates();
19
- GoodJob.renderCharts(true);
20
- },
21
-
22
- addListeners: () => {
23
- const gjActionEls = document.querySelectorAll('[data-gj-action]');
24
-
25
- for (let i = 0; i < gjActionEls.length; i++) {
26
- const el = gjActionEls[i];
27
- const [eventName, func] = el.dataset.gjAction.split('#');
28
-
29
- el.addEventListener(eventName, GoodJob[func]);
30
- }
31
- },
32
-
33
- updateSettings: () => {
34
- const queryString = window.location.search;
35
- const urlParams = new URLSearchParams(queryString);
36
-
37
- // live poll interval and enablement
38
- if (urlParams.has('poll')) {
39
- const parsedInterval = (parseInt(urlParams.get('poll')) || GOOD_JOB_DEFAULT_POLL_INTERVAL_SECONDS) * 1000;
40
- GoodJob.pollInterval = Math.max(parsedInterval, GOOD_JOB_MINIMUM_POLL_INTERVAL);
41
- GoodJob.setStorage('pollInterval', GoodJob.pollInterval);
42
-
43
- GoodJob.pollEnabled = true;
44
- } else {
45
- GoodJob.pollInterval = GoodJob.getStorage('pollInterval') || (GOOD_JOB_DEFAULT_POLL_INTERVAL_SECONDS * 1000);
46
- GoodJob.pollEnabled = GoodJob.getStorage('pollEnabled') || false;
47
- }
48
-
49
- document.getElementById('toggle-poll').checked = GoodJob.pollEnabled;
50
- },
51
-
52
- togglePoll: (ev) => {
53
- GoodJob.pollEnabled = ev.currentTarget.checked;
54
- GoodJob.setStorage('pollEnabled', GoodJob.pollEnabled);
55
- },
56
-
57
- pollUpdates: () => {
58
- setTimeout(() => {
59
- if (GoodJob.pollEnabled === true) {
60
- fetch(window.location.href)
61
- .then(resp => resp.text())
62
- .then(GoodJob.updateContent)
63
- .finally(GoodJob.pollUpdates);
64
- } else {
65
- GoodJob.pollUpdates();
66
- }
67
- }, GoodJob.pollInterval);
68
- },
69
-
70
- updateContent: (newContent) => {
71
- const domParser = new DOMParser();
72
- const parsedDOM = domParser.parseFromString(newContent, "text/html");
73
-
74
- const newElements = parsedDOM.querySelectorAll('[data-gj-poll-replace]');
75
-
76
- for (let i = 0; i < newElements.length; i++) {
77
- const newEl = newElements[i];
78
- const oldEl = document.getElementById(newEl.id);
79
-
80
- if (oldEl) {
81
- oldEl.replaceWith(newEl);
82
- }
83
- }
84
-
85
- GoodJob.renderCharts(false);
86
- },
87
-
88
- renderCharts: (animate) => {
89
- const charts = document.querySelectorAll('.chart');
90
-
91
- for (let i = 0; i < charts.length; i++) {
92
- const chartEl = charts[i];
93
- const chartData = JSON.parse(chartEl.dataset.json);
94
-
95
- const ctx = chartEl.getContext('2d');
96
- const chart = new Chart(ctx, {
97
- type: 'line',
98
- data: {
99
- labels: chartData.labels,
100
- datasets: chartData.datasets
101
- },
102
- options: {
103
- animation: animate,
104
- responsive: true,
105
- maintainAspectRatio: false,
106
- scales: {
107
- y: {
108
- beginAtZero: true
109
- }
110
- }
111
- }
112
- });
113
- }
114
- },
115
-
116
- getStorage: (key) => {
117
- const value = localStorage.getItem('good_job-' + key);
118
-
119
- if (value === 'true') {
120
- return true;
121
- } else if (value === 'false') {
122
- return false;
123
- } else {
124
- return value;
125
- }
126
- },
127
-
128
- setStorage: (key, value) => {
129
- localStorage.setItem('good_job-' + key, value);
130
- }
131
- };
132
-
133
- GoodJob.ready(GoodJob.init);