good_job 2.4.0 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +89 -0
  3. data/README.md +51 -20
  4. data/engine/app/assets/vendor/rails_ujs.js +747 -0
  5. data/engine/app/controllers/good_job/assets_controller.rb +4 -0
  6. data/engine/app/controllers/good_job/base_controller.rb +8 -0
  7. data/engine/app/controllers/good_job/cron_entries_controller.rb +19 -0
  8. data/engine/app/controllers/good_job/jobs_controller.rb +36 -0
  9. data/engine/app/filters/good_job/base_filter.rb +12 -7
  10. data/engine/app/filters/good_job/executions_filter.rb +1 -1
  11. data/engine/app/filters/good_job/jobs_filter.rb +4 -2
  12. data/engine/app/views/good_job/cron_entries/index.html.erb +51 -0
  13. data/engine/app/views/good_job/cron_entries/show.html.erb +4 -0
  14. data/engine/app/views/good_job/{shared/_executions_table.erb → executions/_table.erb} +1 -1
  15. data/engine/app/views/good_job/executions/index.html.erb +1 -1
  16. data/engine/app/views/good_job/{shared/_jobs_table.erb → jobs/_table.erb} +17 -5
  17. data/engine/app/views/good_job/jobs/index.html.erb +14 -1
  18. data/engine/app/views/good_job/jobs/show.html.erb +2 -2
  19. data/engine/app/views/good_job/shared/_filter.erb +9 -10
  20. data/engine/app/views/good_job/shared/icons/_arrow_clockwise.html.erb +5 -0
  21. data/engine/app/views/good_job/shared/icons/_play.html.erb +4 -0
  22. data/engine/app/views/good_job/shared/icons/_skip_forward.html.erb +4 -0
  23. data/engine/app/views/good_job/shared/icons/_stop.html.erb +4 -0
  24. data/engine/app/views/layouts/good_job/base.html.erb +3 -1
  25. data/engine/config/routes.rb +15 -2
  26. data/lib/generators/good_job/install_generator.rb +6 -0
  27. data/lib/generators/good_job/templates/install/migrations/create_good_jobs.rb.erb +3 -1
  28. data/lib/generators/good_job/templates/update/migrations/{01_create_good_jobs.rb → 01_create_good_jobs.rb.erb} +1 -1
  29. data/lib/generators/good_job/templates/update/migrations/02_add_cron_at_to_good_jobs.rb.erb +14 -0
  30. data/lib/generators/good_job/templates/update/migrations/03_add_cron_key_cron_at_index_to_good_jobs.rb.erb +20 -0
  31. data/lib/generators/good_job/update_generator.rb +6 -0
  32. data/lib/good_job/active_job_extensions/concurrency.rb +3 -4
  33. data/lib/good_job/active_job_job.rb +245 -0
  34. data/lib/good_job/adapter.rb +4 -2
  35. data/lib/good_job/cli.rb +3 -1
  36. data/lib/good_job/configuration.rb +5 -1
  37. data/lib/good_job/cron_entry.rb +138 -0
  38. data/lib/good_job/cron_manager.rb +17 -31
  39. data/lib/good_job/current_thread.rb +38 -5
  40. data/lib/good_job/execution.rb +50 -25
  41. data/lib/good_job/lockable.rb +1 -1
  42. data/lib/good_job/log_subscriber.rb +3 -3
  43. data/lib/good_job/scheduler.rb +1 -0
  44. data/lib/good_job/version.rb +1 -1
  45. metadata +21 -12
  46. data/engine/app/controllers/good_job/cron_schedules_controller.rb +0 -9
  47. data/engine/app/models/good_job/active_job_job.rb +0 -127
  48. data/engine/app/views/good_job/cron_schedules/index.html.erb +0 -50
@@ -23,6 +23,10 @@ module GoodJob
23
23
  render file: GoodJob::Engine.root.join("app", "assets", "vendor", "chartist", "chartist.js")
24
24
  end
25
25
 
26
+ def rails_ujs_js
27
+ render file: GoodJob::Engine.root.join("app", "assets", "vendor", "rails_ujs.js")
28
+ end
29
+
26
30
  def style_css
27
31
  render file: GoodJob::Engine.root.join("app", "assets", "style.css")
28
32
  end
@@ -2,5 +2,13 @@
2
2
  module GoodJob
3
3
  class BaseController < ActionController::Base # rubocop:disable Rails/ApplicationController
4
4
  protect_from_forgery with: :exception
5
+
6
+ around_action :switch_locale
7
+
8
+ private
9
+
10
+ def switch_locale(&action)
11
+ I18n.with_locale(:en, &action)
12
+ end
5
13
  end
6
14
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+ module GoodJob
3
+ class CronEntriesController < GoodJob::BaseController
4
+ def index
5
+ @cron_entries = CronEntry.all
6
+ end
7
+
8
+ def show
9
+ @cron_entry = CronEntry.find(params[:id])
10
+ @jobs_filter = JobsFilter.new(params, @cron_entry.jobs)
11
+ end
12
+
13
+ def enqueue
14
+ @cron_entry = CronEntry.find(params[:id])
15
+ @cron_entry.enqueue(Time.current)
16
+ redirect_back(fallback_location: cron_entries_path, notice: "Cron entry has been enqueued.")
17
+ end
18
+ end
19
+ end
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
  module GoodJob
3
3
  class JobsController < GoodJob::BaseController
4
+ rescue_from GoodJob::ActiveJobJob::AdapterNotGoodJobError,
5
+ GoodJob::ActiveJobJob::ActionForStateMismatchError,
6
+ with: :redirect_on_error
7
+
4
8
  def index
5
9
  @filter = JobsFilter.new(params)
6
10
  end
@@ -10,5 +14,37 @@ module GoodJob
10
14
  .order(Arel.sql("COALESCE(scheduled_at, created_at) DESC"))
11
15
  redirect_to root_path, alert: "Executions for Active Job #{params[:id]} not found" if @executions.empty?
12
16
  end
17
+
18
+ def discard
19
+ @job = ActiveJobJob.find(params[:id])
20
+ @job.discard_job("Discarded through dashboard")
21
+ redirect_back(fallback_location: jobs_path, notice: "Job has been discarded")
22
+ end
23
+
24
+ def reschedule
25
+ @job = ActiveJobJob.find(params[:id])
26
+ @job.reschedule_job
27
+ redirect_back(fallback_location: jobs_path, notice: "Job has been rescheduled")
28
+ end
29
+
30
+ def retry
31
+ @job = ActiveJobJob.find(params[:id])
32
+ @job.retry_job
33
+ redirect_back(fallback_location: jobs_path, notice: "Job has been retried")
34
+ end
35
+
36
+ private
37
+
38
+ def redirect_on_error(exception)
39
+ alert = case exception
40
+ when GoodJob::ActiveJobJob::AdapterNotGoodJobError
41
+ "ActiveJob Queue Adapter must be GoodJob."
42
+ when GoodJob::ActiveJobJob::ActionForStateMismatchError
43
+ "Job is not in an appropriate state for this action."
44
+ else
45
+ exception.to_s
46
+ end
47
+ redirect_back(fallback_location: jobs_path, alert: alert)
48
+ end
13
49
  end
14
50
  end
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
  module GoodJob
3
3
  class BaseFilter
4
- attr_accessor :params
4
+ DEFAULT_LIMIT = 25
5
5
 
6
- def initialize(params)
6
+ attr_accessor :params, :base_query
7
+
8
+ def initialize(params, base_query = nil)
7
9
  @params = params
10
+ @base_query = base_query || default_base_query
8
11
  end
9
12
 
10
13
  def records
@@ -13,7 +16,7 @@ module GoodJob
13
16
  filtered_query.display_all(
14
17
  after_scheduled_at: after_scheduled_at,
15
18
  after_id: params[:after_id]
16
- ).limit(params.fetch(:limit, 25))
19
+ ).limit(params.fetch(:limit, DEFAULT_LIMIT))
17
20
  end
18
21
 
19
22
  def last
@@ -22,13 +25,13 @@ module GoodJob
22
25
 
23
26
  def job_classes
24
27
  base_query.group("serialized_params->>'job_class'").count
25
- .sort_by { |name, _count| name }
28
+ .sort_by { |name, _count| name.to_s }
26
29
  .to_h
27
30
  end
28
31
 
29
32
  def queues
30
33
  base_query.group(:queue_name).count
31
- .sort_by { |name, _count| name }
34
+ .sort_by { |name, _count| name.to_s }
32
35
  .to_h
33
36
  end
34
37
 
@@ -38,8 +41,10 @@ module GoodJob
38
41
 
39
42
  def to_params(override)
40
43
  {
41
- state: params[:state],
42
44
  job_class: params[:job_class],
45
+ limit: params[:limit],
46
+ queue_name: params[:queue_name],
47
+ state: params[:state],
43
48
  }.merge(override).delete_if { |_, v| v.nil? }
44
49
  end
45
50
 
@@ -90,7 +95,7 @@ module GoodJob
90
95
 
91
96
  private
92
97
 
93
- def base_query
98
+ def default_base_query
94
99
  raise NotImplementedError
95
100
  end
96
101
 
@@ -12,7 +12,7 @@ module GoodJob
12
12
 
13
13
  private
14
14
 
15
- def base_query
15
+ def default_base_query
16
16
  GoodJob::Execution.all
17
17
  end
18
18
 
@@ -14,12 +14,14 @@ module GoodJob
14
14
 
15
15
  private
16
16
 
17
- def base_query
17
+ def default_base_query
18
18
  GoodJob::ActiveJobJob.all
19
19
  end
20
20
 
21
21
  def filtered_query
22
- query = base_query
22
+ query = base_query.includes(:executions)
23
+ .joins_advisory_locks.select('good_jobs.*', 'pg_locks.locktype AS locktype')
24
+
23
25
  query = query.job_class(params[:job_class]) if params[:job_class]
24
26
  query = query.where(queue_name: params[:queue_name]) if params[:queue_name]
25
27
 
@@ -0,0 +1,51 @@
1
+ <% if @cron_entries.present? %>
2
+ <div class="card my-3">
3
+ <div class="table-responsive">
4
+ <table class="table card-table table-bordered table-hover table-sm mb-0">
5
+ <thead>
6
+ <th>Key</th>
7
+ <th>Schedule</th>
8
+ <th>
9
+ Properties
10
+ <%= tag.button "Toggle", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
11
+ data: { bs_toggle: "collapse", bs_target: ".cron-entry-properties" },
12
+ aria: { expanded: false, controls: @cron_entries.map { |cron_entry| "##{dom_id(cron_entry, 'properties')}" }.join(" ") }
13
+ %>
14
+ </th>
15
+ <th>Description</th>
16
+ <th>Next scheduled</th>
17
+ <th>Last run</th>
18
+ <th>Actions</th>
19
+ </thead>
20
+ <tbody>
21
+ <% @cron_entries.each do |cron_entry| %>
22
+ <tr id="<%= dom_id(cron_entry) %>">
23
+ <td class="font-monospace"><%= cron_entry.key %></td>
24
+ <td class="font-monospace"><%= cron_entry.schedule %></td>
25
+ <td>
26
+ <%= tag.button("Preview", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
27
+ data: { bs_toggle: "collapse", bs_target: "##{dom_id(cron_entry, 'properties')}" },
28
+ aria: { expanded: false, controls: dom_id(cron_entry, 'properties') }) %>
29
+ <%= tag.pre(JSON.pretty_generate(cron_entry.display_properties), id: dom_id(cron_entry, 'properties'), class: "collapse cron-entry-properties") %>
30
+ </td>
31
+ <td><%= cron_entry.description %></td>
32
+ <td><%= cron_entry.next_at %></td>
33
+ <td>
34
+ <% if cron_entry.last_job.present? %>
35
+ <%= link_to cron_entry.last_at, cron_entry_path(cron_entry), title: "Job #{cron_entry.last_job.id}" %>
36
+ <% end %>
37
+ </td>
38
+ <td>
39
+ <%= button_to enqueue_cron_entry_path(cron_entry.id), method: :post, class: "btn btn-sm btn-outline-primary", form_class: "d-inline-block", aria: { label: "Run cron entry now" }, title: "Run cron entry now", data: { confirm: "Confirm run cron entry now" } do %>
40
+ <%= render "good_job/shared/icons/play" %>
41
+ <% end %>
42
+ </td>
43
+ </tr>
44
+ <% end %>
45
+ </tbody>
46
+ </table>
47
+ </div>
48
+ </div>
49
+ <% else %>
50
+ <em>No cron schedules present.</em>
51
+ <% end %>
@@ -0,0 +1,4 @@
1
+ <h1 class="mb-3">Cron Entry Key: <code><%= @cron_entry.id %></code></h1>
2
+
3
+ <%= render 'good_job/shared/filter', filter: @jobs_filter %>
4
+ <%= render 'good_job/jobs/table', jobs: @jobs_filter.records %>
@@ -44,7 +44,7 @@
44
44
  <%= tag.pre JSON.pretty_generate(execution.serialized_params), id: dom_id(execution, "params"), class: "collapse job-params" %>
45
45
  </td>
46
46
  <td>
47
- <%= button_to execution_path(execution.id), method: :delete, class: "btn btn-sm btn-outline-danger", title: "Delete execution" do %>
47
+ <%= button_to execution_path(execution.id), method: :delete, class: "btn btn-sm btn-outline-danger", title: "Delete execution", data: { confirm: "Confirm delete" } do %>
48
48
  <%= render "good_job/shared/icons/trash" %>
49
49
  <% end %>
50
50
  </td>
@@ -5,7 +5,7 @@
5
5
  <%= render 'good_job/shared/filter', filter: @filter %>
6
6
 
7
7
  <% if @filter.records.present? %>
8
- <%= render 'good_job/shared/executions_table', executions: @filter.records %>
8
+ <%= render 'good_job/executions/table', executions: @filter.records %>
9
9
 
10
10
  <nav aria-label="Job pagination" class="mt-3">
11
11
  <ul class="pagination">
@@ -43,11 +43,23 @@
43
43
  %>
44
44
  <%= tag.pre JSON.pretty_generate(job.serialized_params), id: dom_id(job, "params"), class: "collapse job-params" %>
45
45
  </td>
46
- <!-- <td>-->
47
- <%#= button_to execution_path(execution.id), method: :delete, class: "btn btn-sm btn-outline-danger", title: "Delete execution" do %>
48
- <%#= render "good_job/shared/icons/trash" %>
49
- <%# end %>
50
- <!-- </td>-->
46
+ <td>
47
+ <div class="text-nowrap">
48
+ <% job_reschedulable = job.status.in? [:scheduled, :retried, :queued] %>
49
+ <%= 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 %>
50
+ <%= render "good_job/shared/icons/skip_forward" %>
51
+ <% end %>
52
+
53
+ <% job_discardable = job.status.in? [:scheduled, :retried, :queued] %>
54
+ <%= 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 %>
55
+ <%= render "good_job/shared/icons/stop" %>
56
+ <% end %>
57
+
58
+ <%= 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 %>
59
+ <%= render "good_job/shared/icons/arrow_clockwise" %>
60
+ <% end %>
61
+ </div>
62
+ </td>
51
63
  </tr>
52
64
  <% end %>
53
65
  </tbody>
@@ -4,4 +4,17 @@
4
4
 
5
5
  <%= render 'good_job/shared/filter', filter: @filter %>
6
6
 
7
- <%= render 'good_job/shared/jobs_table', jobs: @filter.records %>
7
+ <% if @filter.records.present? %>
8
+ <%= render 'good_job/jobs/table', jobs: @filter.records %>
9
+ <nav aria-label="Job pagination" class="mt-3">
10
+ <ul class="pagination">
11
+ <li class="page-item">
12
+ <%= link_to(@filter.to_params(after_scheduled_at: (@filter.last.scheduled_at || @filter.last.created_at), after_id: @filter.last.id), class: "page-link") do %>
13
+ Older jobs <span aria-hidden="true">&raquo;</span>
14
+ <% end %>
15
+ </li>
16
+ </ul>
17
+ </nav>
18
+ <% else %>
19
+ <em>No jobs present.</em>
20
+ <% end %>
@@ -1,3 +1,3 @@
1
- <h1>ActiveJob ID: <code><%= @executions.first.id %></code></h1>
1
+ <h1 class="mb-3">ActiveJob ID: <code><%= @executions.first.id %></code></h1>
2
2
 
3
- <%= render 'good_job/shared/executions_table', executions: @executions %>
3
+ <%= render 'good_job/executions/table', executions: @executions %>
@@ -1,16 +1,15 @@
1
1
  <div class='card mb-2'>
2
2
  <div class='card-body d-flex flex-wrap'>
3
-
4
3
  <div class='me-4'>
5
4
  <small>Filter by job class</small>
6
5
  <br>
7
- <% @filter.job_classes.each do |name, count| %>
6
+ <% filter.job_classes.each do |name, count| %>
8
7
  <% if params[:job_class] == name %>
9
- <%= link_to(@filter.to_params(job_class: nil), class: 'btn btn-sm btn-outline-secondary active', role: "button", "aria-pressed": true) do %>
8
+ <%= link_to(filter.to_params(job_class: nil), class: 'btn btn-sm btn-outline-secondary active', role: "button", "aria-pressed": true) do %>
10
9
  <%= name %> (<%= count %>)
11
10
  <% end %>
12
11
  <% else %>
13
- <%= link_to(@filter.to_params(job_class: name), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
12
+ <%= link_to(filter.to_params(job_class: name), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
14
13
  <%= name %> (<%= count %>)
15
14
  <% end %>
16
15
  <% end %>
@@ -20,13 +19,13 @@
20
19
  <div class='me-4'>
21
20
  <small>Filter by state</small>
22
21
  <br>
23
- <% @filter.states.each do |name, count| %>
22
+ <% filter.states.each do |name, count| %>
24
23
  <% if params[:state] == name %>
25
- <%= link_to(@filter.to_params(state: nil), class: 'btn btn-sm btn-outline-secondary active', role: "button", "aria-pressed": true) do %>
24
+ <%= link_to(filter.to_params(state: nil), class: 'btn btn-sm btn-outline-secondary active', role: "button", "aria-pressed": true) do %>
26
25
  <%= name %> (<%= count %>)
27
26
  <% end %>
28
27
  <% else %>
29
- <%= link_to(@filter.to_params(state: name), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
28
+ <%= link_to(filter.to_params(state: name), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
30
29
  <%= name %> (<%= count %>)
31
30
  <% end %>
32
31
  <% end %>
@@ -36,13 +35,13 @@
36
35
  <div>
37
36
  <small>Filter by queue</small>
38
37
  <br>
39
- <% @filter.queues.each do |name, count| %>
38
+ <% filter.queues.each do |name, count| %>
40
39
  <% if params[:queue_name] == name %>
41
- <%= link_to(@filter.to_params(queue_name: nil), class: 'btn btn-sm btn-outline-secondary active', role: "button", "aria-pressed": true) do %>
40
+ <%= link_to(filter.to_params(queue_name: nil), class: 'btn btn-sm btn-outline-secondary active', role: "button", "aria-pressed": true) do %>
42
41
  <%= name %> (<%= count %>)
43
42
  <% end %>
44
43
  <% else %>
45
- <%= link_to(@filter.to_params(queue_name: name), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
44
+ <%= link_to(filter.to_params(queue_name: name), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
46
45
  <%= name %> (<%= count %>)
47
46
  <% end %>
48
47
  <% end %>
@@ -0,0 +1,5 @@
1
+ <!-- https://icons.getbootstrap.com/icons/arrow-clockwise/ -->
2
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
3
+ <path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z" />
4
+ <path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z" />
5
+ </svg>
@@ -0,0 +1,4 @@
1
+ <!-- https://icons.getbootstrap.com/icons/play/ -->
2
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-play" viewBox="0 0 16 16">
3
+ <path d="M10.804 8 5 4.633v6.734L10.804 8zm.792-.696a.802.802 0 0 1 0 1.392l-6.363 3.692C4.713 12.69 4 12.345 4 11.692V4.308c0-.653.713-.998 1.233-.696l6.363 3.692z" />
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <!-- https://icons.getbootstrap.com/icons/skip-forward/ -->
2
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-skip-forward" viewBox="0 0 16 16">
3
+ <path d="M15.5 3.5a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-1 0V8.752l-6.267 3.636c-.52.302-1.233-.043-1.233-.696v-2.94l-6.267 3.636C.713 12.69 0 12.345 0 11.692V4.308c0-.653.713-.998 1.233-.696L7.5 7.248v-2.94c0-.653.713-.998 1.233-.696L15 7.248V4a.5.5 0 0 1 .5-.5zM1 4.633v6.734L6.804 8 1 4.633zm7.5 0v6.734L14.304 8 8.5 4.633z" />
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <!-- https://icons.getbootstrap.com/icons/stop/ -->
2
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-stop" viewBox="0 0 16 16">
3
+ <path d="M3.5 5A1.5 1.5 0 0 1 5 3.5h6A1.5 1.5 0 0 1 12.5 5v6a1.5 1.5 0 0 1-1.5 1.5H5A1.5 1.5 0 0 1 3.5 11V5zM5 4.5a.5.5 0 0 0-.5.5v6a.5.5 0 0 0 .5.5h6a.5.5 0 0 0 .5-.5V5a.5.5 0 0 0-.5-.5H5z" />
4
+ </svg>
@@ -11,6 +11,7 @@
11
11
 
12
12
  <%= javascript_include_tag bootstrap_path(format: :js, v: GoodJob::VERSION) %>
13
13
  <%= javascript_include_tag chartist_path(format: :js, v: GoodJob::VERSION) %>
14
+ <%= javascript_include_tag rails_ujs_path(format: :js, v: GoodJob::VERSION) %>
14
15
  </head>
15
16
  <body>
16
17
  <nav class="navbar navbar-expand-lg navbar-light bg-light">
@@ -29,7 +30,7 @@
29
30
  <%= link_to "All Jobs", jobs_path, class: ["nav-link", ("active" if current_page?(jobs_path))] %>
30
31
  </li>
31
32
  <li class="nav-item">
32
- <%= link_to "Cron Schedules", cron_schedules_path, class: ["nav-link", ("active" if current_page?(cron_schedules_path))] %>
33
+ <%= link_to "Cron Schedules", cron_entries_path, class: ["nav-link", ("active" if current_page?(cron_entries_path))] %>
33
34
  </li>
34
35
  <li class="nav-item">
35
36
  <div class="nav-link">
@@ -49,6 +50,7 @@
49
50
  </li>
50
51
  -->
51
52
  </ul>
53
+ <div class="text-muted" title="Now is <%= Time.current %>">Times are displayed in <%= Time.current.zone %> timezone</div>
52
54
  </div>
53
55
  </div>
54
56
  </nav>
@@ -1,8 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
  GoodJob::Engine.routes.draw do
3
3
  root to: 'executions#index'
4
- resources :cron_schedules, only: %i[index]
5
- resources :jobs, only: %i[index show]
4
+
5
+ resources :cron_entries, only: %i[index show] do
6
+ member do
7
+ post :enqueue
8
+ end
9
+ end
10
+
11
+ resources :jobs, only: %i[index show] do
12
+ member do
13
+ put :discard
14
+ put :reschedule
15
+ put :retry
16
+ end
17
+ end
6
18
  resources :executions, only: %i[destroy]
7
19
 
8
20
  scope controller: :assets do
@@ -14,6 +26,7 @@ GoodJob::Engine.routes.draw do
14
26
 
15
27
  constraints(format: :js) do
16
28
  get :bootstrap, action: :bootstrap_js
29
+ get :rails_ujs, action: :rails_ujs_js
17
30
  get :chartist, action: :chartist_js
18
31
  end
19
32
  end
@@ -19,5 +19,11 @@ module GoodJob
19
19
  def create_migration_file
20
20
  migration_template 'migrations/create_good_jobs.rb.erb', File.join(db_migrate_path, "create_good_jobs.rb")
21
21
  end
22
+
23
+ private
24
+
25
+ def migration_version
26
+ "[#{ActiveRecord::VERSION::STRING.to_f}]"
27
+ end
22
28
  end
23
29
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- class CreateGoodJobs < ActiveRecord::Migration[5.2]
2
+ class CreateGoodJobs < ActiveRecord::Migration<%= migration_version %>
3
3
  def change
4
4
  enable_extension 'pgcrypto'
5
5
 
@@ -18,6 +18,7 @@ class CreateGoodJobs < ActiveRecord::Migration[5.2]
18
18
  t.text :concurrency_key
19
19
  t.text :cron_key
20
20
  t.uuid :retried_good_job_id
21
+ t.timestamp :cron_at
21
22
  end
22
23
 
23
24
  add_index :good_jobs, :scheduled_at, where: "(finished_at IS NULL)", name: "index_good_jobs_on_scheduled_at"
@@ -25,5 +26,6 @@ class CreateGoodJobs < ActiveRecord::Migration[5.2]
25
26
  add_index :good_jobs, [:active_job_id, :created_at], name: :index_good_jobs_on_active_job_id_and_created_at
26
27
  add_index :good_jobs, :concurrency_key, where: "(finished_at IS NULL)", name: :index_good_jobs_on_concurrency_key_when_unfinished
27
28
  add_index :good_jobs, [:cron_key, :created_at], name: :index_good_jobs_on_cron_key_and_created_at
29
+ add_index :good_jobs, [:cron_key, :cron_at], name: :index_good_jobs_on_cron_key_and_cron_at, unique: true
28
30
  end
29
31
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- class CreateGoodJobs < ActiveRecord::Migration[5.2]
2
+ class CreateGoodJobs < ActiveRecord::Migration<%= migration_version %>
3
3
  def change
4
4
  enable_extension 'pgcrypto'
5
5
 
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+ class AddCronAtToGoodJobs < ActiveRecord::Migration<%= migration_version %>
3
+ def change
4
+ reversible do |dir|
5
+ dir.up do
6
+ # Ensure this incremental update migration is idempotent
7
+ # with monolithic install migration.
8
+ return if connection.column_exists?(:good_jobs, :cron_at)
9
+ end
10
+ end
11
+
12
+ add_column :good_jobs, :cron_at, :timestamp
13
+ end
14
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ class AddCronKeyCronAtIndexToGoodJobs < ActiveRecord::Migration<%= migration_version %>
3
+ disable_ddl_transaction!
4
+
5
+ def change
6
+ reversible do |dir|
7
+ dir.up do
8
+ # Ensure this incremental update migration is idempotent
9
+ # with monolithic install migration.
10
+ return if connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_cron_at)
11
+ end
12
+ end
13
+
14
+ add_index :good_jobs,
15
+ [:cron_key, :cron_at],
16
+ algorithm: :concurrently,
17
+ name: :index_good_jobs_on_cron_key_and_cron_at,
18
+ unique: true
19
+ end
20
+ end
@@ -24,5 +24,11 @@ module GoodJob
24
24
  migration_template "migrations/#{template_file}", File.join(db_migrate_path, destination_file), skip: true
25
25
  end
26
26
  end
27
+
28
+ private
29
+
30
+ def migration_version
31
+ "[#{ActiveRecord::VERSION::STRING.to_f}]"
32
+ end
27
33
  end
28
34
  end
@@ -32,10 +32,9 @@ module GoodJob
32
32
 
33
33
  GoodJob::Execution.new.with_advisory_lock(key: key, function: "pg_advisory_lock") do
34
34
  enqueue_concurrency = if enqueue_limit
35
- # TODO: Why is `unscoped` necessary? Nested scope is bleeding into subsequent query?
36
- GoodJob::Execution.unscoped.where(concurrency_key: key).unfinished.advisory_unlocked.count
35
+ GoodJob::Execution.where(concurrency_key: key).unfinished.advisory_unlocked.count
37
36
  else
38
- GoodJob::Execution.unscoped.where(concurrency_key: key).unfinished.count
37
+ GoodJob::Execution.where(concurrency_key: key).unfinished.count
39
38
  end
40
39
 
41
40
  # The job has not yet been enqueued, so check if adding it will go over the limit
@@ -63,7 +62,7 @@ module GoodJob
63
62
  next if key.blank?
64
63
 
65
64
  GoodJob::Execution.new.with_advisory_lock(key: key, function: "pg_advisory_lock") do
66
- allowed_active_job_ids = GoodJob::Execution.unscoped.where(concurrency_key: key).advisory_locked.order(Arel.sql("COALESCE(performed_at, scheduled_at, created_at) ASC")).limit(perform_limit).pluck(:active_job_id)
65
+ allowed_active_job_ids = GoodJob::Execution.where(concurrency_key: key).advisory_locked.order(Arel.sql("COALESCE(performed_at, scheduled_at, created_at) ASC")).limit(perform_limit).pluck(:active_job_id)
67
66
  # The current job has already been locked and will appear in the previous query
68
67
  raise GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError unless allowed_active_job_ids.include? job.job_id
69
68
  end