good_job 2.13.0 → 2.14.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 (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);