good_job 2.9.3 → 2.10.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 815933ed8a435e9826bbb655e92c298d10d5bd74eb4f0474cb25d49a194d5cae
4
- data.tar.gz: 29f7753f2401599feb16fc4b25e9c7073855f1fa135d88e640051533c2c24593
3
+ metadata.gz: 6b22cbc6d37af9dd8db290a1dc8073d2cae14c63e5cf7180b06dce1a33fb9992
4
+ data.tar.gz: 8cd87b2d69afea84db87125bad625558783adea98fee138be852e6a51c43dc50
5
5
  SHA512:
6
- metadata.gz: 7dbf7489f53095634bb91e2a7e7fd8bb1a009ba3d81fb98312c7b8ae8f0516bc786e13b1100fdef1dd113e80c59b3d8433663ea4773d04ee517927f1bce1b560
7
- data.tar.gz: b01d4ca76bb300dd4f755db2eb43758dd8146fe4e4ab1061d47cc973fe8e559b936df004054ed9ed92c9aefc6abee8f7db0ab33bf1ca812b57a9d7c192771077
6
+ metadata.gz: 230b335c5b6a03397c3212cd2bc59d87b2c735073233d068cf2c3045933b2aace533b0699a6fa1ecd41d77890af5aa18e1685b2c85f4c97c868773a3211736b7
7
+ data.tar.gz: b3fb4cb11f129fa8576e36c4e7401cdab927cf309bda963c3b82480dbe92663847b46a8e641b123e5ad7b28c53aa361d5fb6968131e23e85b0302768cc38e5d0
data/CHANGELOG.md CHANGED
@@ -1,17 +1,84 @@
1
1
  # Changelog
2
2
 
3
- ## [v2.9.3](https://github.com/bensheldon/good_job/tree/v2.9.3) (2022-01-23)
3
+ ## [v2.10.0](https://github.com/bensheldon/good_job/tree/v2.10.0) (2022-02-18)
4
4
 
5
- [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.9.2...v2.9.3)
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.9.6...v2.10.0)
6
6
 
7
7
  **Closed issues:**
8
8
 
9
- - Assets not loaded when Rails is configured with a different hostname for assets [\#491](https://github.com/bensheldon/good_job/issues/491)
9
+ - Cron jobs not getting run [\#519](https://github.com/bensheldon/good_job/issues/519)
10
+ - Slow queries with many finished entries and concurrency control [\#514](https://github.com/bensheldon/good_job/issues/514)
11
+ - Make default retry behaviour safer [\#505](https://github.com/bensheldon/good_job/issues/505)
10
12
 
11
13
  **Merged pull requests:**
12
14
 
15
+ - Fix Benchmark job throughput script [\#522](https://github.com/bensheldon/good_job/pull/522) ([douglara](https://github.com/douglara))
16
+ - Update development Gemfile.lock [\#521](https://github.com/bensheldon/good_job/pull/521) ([bensheldon](https://github.com/bensheldon))
17
+ - Ensure Rails 6.0 is tested against Ruby 3.0; use Ruby 3.0 in demo environment [\#520](https://github.com/bensheldon/good_job/pull/520) ([bensheldon](https://github.com/bensheldon))
18
+ - Dashboard: update search filters and some small UI updates [\#518](https://github.com/bensheldon/good_job/pull/518) ([multiplegeorges](https://github.com/multiplegeorges))
19
+ - Document safer setting for retry\_on\_unhandled\_error [\#517](https://github.com/bensheldon/good_job/pull/517) ([tamaloa](https://github.com/tamaloa))
20
+
21
+ ## [v2.9.6](https://github.com/bensheldon/good_job/tree/v2.9.6) (2022-02-07)
22
+
23
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.9.5...v2.9.6)
24
+
25
+ **Merged pull requests:**
26
+
27
+ - Limit query for allowed concurrent jobs to unfinished [\#515](https://github.com/bensheldon/good_job/pull/515) ([til](https://github.com/til))
28
+
29
+ ## [v2.9.5](https://github.com/bensheldon/good_job/tree/v2.9.5) (2022-02-07)
30
+
31
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.9.4...v2.9.5)
32
+
33
+ **Fixed bugs:**
34
+
35
+ - Transactions in "aborting" threads do not commit; causes GoodJob::Process record not destroyed on exit [\#489](https://github.com/bensheldon/good_job/issues/489)
36
+ - Deserialize ActiveJob arguments when manually retrying a job [\#513](https://github.com/bensheldon/good_job/pull/513) ([bensheldon](https://github.com/bensheldon))
37
+
38
+ **Closed issues:**
39
+
40
+ - Concurrency key proc is missing `arguments` when retrying a discarded job. [\#512](https://github.com/bensheldon/good_job/issues/512)
41
+ - Cron Schedule not visible in dashboard [\#496](https://github.com/bensheldon/good_job/issues/496)
42
+
43
+ **Merged pull requests:**
44
+
45
+ - Rename methods to `advisory_lock_key` and allow it to take a block instead of `with_advisory_lock` [\#511](https://github.com/bensheldon/good_job/pull/511) ([bensheldon](https://github.com/bensheldon))
46
+ - README: Limiting concurrency - fetch symbol instead of string [\#510](https://github.com/bensheldon/good_job/pull/510) ([BenSto](https://github.com/BenSto))
47
+ - Add arbitrary lock on class level too [\#499](https://github.com/bensheldon/good_job/pull/499) ([pandwoter](https://github.com/pandwoter))
48
+
49
+ ## [v2.9.4](https://github.com/bensheldon/good_job/tree/v2.9.4) (2022-01-31)
50
+
51
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.9.3...v2.9.4)
52
+
53
+ **Fixed bugs:**
54
+
55
+ - Fix navbar toggler [\#506](https://github.com/bensheldon/good_job/pull/506) ([JuanVqz](https://github.com/JuanVqz))
56
+ - Guard LogSubscriber against tagged logger without a formatter [\#504](https://github.com/bensheldon/good_job/pull/504) ([bensheldon](https://github.com/bensheldon))
57
+ - Markdown lint fixes + Added missing responsive meta tag [\#492](https://github.com/bensheldon/good_job/pull/492) ([zeevy](https://github.com/zeevy))
58
+
59
+ **Closed issues:**
60
+
61
+ - The navbar icon doesn't show the navbar menu when clicking it [\#503](https://github.com/bensheldon/good_job/issues/503)
62
+ - Not all loggers have a formatter [\#502](https://github.com/bensheldon/good_job/issues/502)
63
+ - Error logs from failed jobs used all storage space [\#495](https://github.com/bensheldon/good_job/issues/495)
64
+
65
+ **Merged pull requests:**
66
+
67
+ - Update Code of Conduct to Contributor Covenant 2.1 [\#501](https://github.com/bensheldon/good_job/pull/501) ([bensheldon](https://github.com/bensheldon))
68
+ - Test with Ruby 3.1 [\#498](https://github.com/bensheldon/good_job/pull/498) ([aried3r](https://github.com/aried3r))
69
+
70
+ ## [v2.9.3](https://github.com/bensheldon/good_job/tree/v2.9.3) (2022-01-23)
71
+
72
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.9.2...v2.9.3)
73
+
74
+ **Fixed bugs:**
75
+
13
76
  - Use `*_url` route helpers for Dashboard assets to avoid being overridden by `config.asset_host` [\#493](https://github.com/bensheldon/good_job/pull/493) ([bensheldon](https://github.com/bensheldon))
14
77
 
78
+ **Closed issues:**
79
+
80
+ - Assets not loaded when Rails is configured with a different hostname for assets [\#491](https://github.com/bensheldon/good_job/issues/491)
81
+
15
82
  ## [v2.9.2](https://github.com/bensheldon/good_job/tree/v2.9.2) (2022-01-19)
16
83
 
17
84
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.9.1...v2.9.2)
data/README.md CHANGED
@@ -119,7 +119,7 @@ For more of the story of GoodJob, read the [introductory blog post](https://isla
119
119
  - By default, GoodJob separates job enqueuing from job execution so that jobs can be scaled independently of the web server. Use the GoodJob command-line tool to execute jobs:
120
120
 
121
121
  ```bash
122
- $ bundle exec good_job start
122
+ bundle exec good_job start
123
123
  ```
124
124
 
125
125
  Ideally the command-line tool should be run on a separate machine or container from the web process. For example, on Heroku:
@@ -133,8 +133,8 @@ For more of the story of GoodJob, read the [introductory blog post](https://isla
133
133
 
134
134
  - GoodJob can also be configured to execute jobs within the web server process to save on resources. This is useful for low-workloads when economy is paramount.
135
135
 
136
- ```
137
- $ GOOD_JOB_EXECUTION_MODE=async rails server
136
+ ```bash
137
+ GOOD_JOB_EXECUTION_MODE=async rails server
138
138
  ```
139
139
 
140
140
  Additional configuration is likely necessary, see the reference below for configuration.
@@ -276,7 +276,7 @@ Available configuration options are:
276
276
  - `cleanup_interval_seconds` (integer) Number of seconds a Scheduler will wait before cleaning up preserved jobs. Defaults to `nil`. Can also be set with the environment variable `GOOD_JOB_CLEANUP_INTERVAL_SECONDS`.
277
277
  - `logger` ([Rails Logger](https://api.rubyonrails.org/classes/ActiveSupport/Logger.html)) lets you set a custom logger for GoodJob. It should be an instance of a Rails `Logger` (Default: `Rails.logger`).
278
278
  - `preserve_job_records` (boolean) keeps job records in your database even after jobs are completed. (Default: `false`)
279
- - `retry_on_unhandled_error` (boolean) causes jobs to be re-queued and retried if they raise an instance of `StandardError`. Instances of `Exception`, like SIGINT, will *always* be retried, regardless of this attribute’s value. (Default: `true`)
279
+ - `retry_on_unhandled_error` (boolean) causes jobs to be re-queued and retried if they raise an instance of `StandardError`. Be advised this may lead to jobs being repeated infinitely ([see below for more on retries](#retries)). Instances of `Exception`, like SIGINT, will *always* be retried, regardless of this attribute’s value. (Default: `true`)
280
280
  - `on_thread_error` (proc, lambda, or callable) will be called when an Exception. It can be useful for logging errors to bug tracking services, like Sentry or Airbrake. Example:
281
281
 
282
282
  ```ruby
@@ -307,7 +307,7 @@ Good Job’s general behavior can also be configured via attributes directly on
307
307
  - **`GoodJob.active_record_parent_class`** (string) The ActiveRecord parent class inherited by GoodJob's ActiveRecord model `GoodJob::Job` (defaults to `"ActiveRecord::Base"`). Configure this when using [multiple databases with ActiveRecord](https://guides.rubyonrails.org/active_record_multiple_databases.html) or when other custom configuration is necessary for the ActiveRecord model to connect to the Postgres database. _The value must be a String to avoid premature initialization of ActiveRecord._
308
308
  - **`GoodJob.logger`** ([Rails Logger](https://api.rubyonrails.org/classes/ActiveSupport/Logger.html)) lets you set a custom logger for GoodJob. It should be an instance of a Rails `Logger`.
309
309
  - **`GoodJob.preserve_job_records`** (boolean) keeps job records in your database even after jobs are completed. (Default: `false`)
310
- - **`GoodJob.retry_on_unhandled_error`** (boolean) causes jobs to be re-queued and retried if they raise an instance of `StandardError`. Instances of `Exception`, like SIGINT, will *always* be retried, regardless of this attribute’s value. (Default: `true`)
310
+ - **`GoodJob.retry_on_unhandled_error`** (boolean) causes jobs to be re-queued and retried if they raise an instance of `StandardError`. Be advised this may lead to jobs being repeated infinitely ([see below for more on retries](#retries)). Instances of `Exception`, like SIGINT, will *always* be retried, regardless of this attribute’s value. (Default: `true`)
311
311
  - **`GoodJob.on_thread_error`** (proc, lambda, or callable) will be called when an Exception. It can be useful for logging errors to bug tracking services, like Sentry or Airbrake.
312
312
 
313
313
  You’ll generally want to configure these in `config/initializers/good_job.rb`, like so:
@@ -399,11 +399,9 @@ class MyJob < ApplicationJob
399
399
 
400
400
  # A unique key to be globally locked against.
401
401
  # Can be String or Lambda/Proc that is invoked in the context of the job.
402
- # Note: Arguments passed to #perform_later must be accessed through `arguments` method.
403
- key: -> { "Unique-#{arguments.first}" } # MyJob.perform_later("Alice") => "Unique-Alice"
404
-
405
- # If the method uses named parameters, they can be accessed like so:
406
- # key: -> { "Unique-#{arguments.first['name']}" } # MyJob.perform_later(name: "Alice")
402
+ # Note: Arguments passed to #perform_later can be accessed through ActiveJob's `arguments` method
403
+ # which is an array containing positional arguments and, optionally, a kwarg hash.
404
+ key: -> { "Unique-#{arguments.first}-#{arguments.last[:version]}" } # MyJob.perform_later("Alice", version: 'v2') => "Unique-Alice-v2"
407
405
  )
408
406
 
409
407
  def perform(first_name)
@@ -470,7 +468,7 @@ To perform upgrades to the GoodJob database tables:
470
468
  Optional: If using Rails' multiple databases with the `migrations_paths` configuration option, use the `--database` option:
471
469
 
472
470
  ```bash
473
- $ bin/rails g good_job:update --database animals
471
+ bin/rails g good_job:update --database animals
474
472
  ```
475
473
 
476
474
  1. Run the database migration locally
@@ -531,7 +529,7 @@ class ApplicationJob < ActiveJob::Base
531
529
  end
532
530
  ```
533
531
 
534
- When using `retry_on` with _a limited number of retries_, the final exception will not be rescued and will raise to GoodJob. GoodJob can be configured to discard un-handled exceptions instead of retrying them:
532
+ When using `retry_on` with _a limited number of retries_, the final exception will not be rescued and will raise to GoodJob. GoodJob can be configured to discard un-handled exceptions instead of retrying them. Be aware that if NOT setting `retry_on_unhandled_error` to `false` good_job will by default retry the failing job and may do this infinitely without pause thereby at least causing high load. In most cases `retry_on_unhandled_error` should be set as following:
535
533
 
536
534
  ```ruby
537
535
  # config/initializers/good_job.rb
@@ -703,7 +701,7 @@ GoodJob can execute jobs "async" in the same process as the web server (e.g. `bi
703
701
  - Or, with environment variables:
704
702
 
705
703
  ```bash
706
- $ GOOD_JOB_EXECUTION_MODE=async GOOD_JOB_MAX_THREADS=4 GOOD_JOB_POLL_INTERVAL=30 bin/rails server
704
+ GOOD_JOB_EXECUTION_MODE=async GOOD_JOB_MAX_THREADS=4 GOOD_JOB_POLL_INTERVAL=30 bin/rails server
707
705
  ```
708
706
 
709
707
  Depending on your application configuration, you may need to take additional steps:
@@ -821,7 +819,7 @@ It is also necessary to delete these preserved jobs from the database after a ce
821
819
  - For example, using the `good_job` command-line utility:
822
820
 
823
821
  ```bash
824
- $ bundle exec good_job cleanup_preserved_jobs --before-seconds-ago=86400
822
+ bundle exec good_job cleanup_preserved_jobs --before-seconds-ago=86400
825
823
  ```
826
824
 
827
825
  ### PgBouncer compatibility
@@ -2,7 +2,7 @@
2
2
  module GoodJob
3
3
  class ExecutionsFilter < BaseFilter
4
4
  def states
5
- {
5
+ @_states ||= {
6
6
  'finished' => base_query.finished.count,
7
7
  'unfinished' => base_query.unfinished.count,
8
8
  'running' => base_query.running.count,
@@ -5,5 +5,20 @@ module GoodJob
5
5
  text = timestamp.future? ? "in #{time_ago_in_words(timestamp)}" : "#{time_ago_in_words(timestamp)} ago"
6
6
  tag.time(text, datetime: timestamp, title: timestamp)
7
7
  end
8
+
9
+ def status_badge(status)
10
+ classes = case status
11
+ when :finished
12
+ "badge rounded-pill bg-success"
13
+ when :queued, :scheduled, :retried
14
+ "badge rounded-pill bg-secondary"
15
+ when :running
16
+ "badge rounded-pill bg-primary"
17
+ when :discarded
18
+ "badge rounded-pill bg-danger"
19
+ end
20
+
21
+ content_tag :span, status.to_s, class: classes
22
+ end
8
23
  end
9
24
  end
@@ -1,3 +1,7 @@
1
+ <div class="my-3 flex">
2
+ <h2>Cron Schedules</h2>
3
+ </div>
4
+
1
5
  <% if @cron_entries.present? %>
2
6
  <div class="card my-3">
3
7
  <div class="table-responsive">
@@ -47,5 +51,11 @@
47
51
  </div>
48
52
  </div>
49
53
  <% else %>
50
- <em>No cron schedules present.</em>
54
+ <div class="card my-3">
55
+ <div class="card-body">
56
+ <p class="card-text">
57
+ <em>No cron schedules found.</em>
58
+ </p>
59
+ </div>
60
+ </div>
51
61
  <% end %>
@@ -1,6 +1,6 @@
1
1
  <div class="card my-3">
2
2
  <div class="table-responsive">
3
- <table class="table card-table table-bordered table-hover table-sm mb-0">
3
+ <table class="table card-table table-bordered table-hover table-sm mb-0" id="executions_index_table">
4
4
  <thead>
5
5
  <tr>
6
6
  <th>ActiveJob ID</th>
@@ -20,34 +20,40 @@
20
20
  </tr>
21
21
  </thead>
22
22
  <tbody>
23
- <% executions.each do |execution| %>
24
- <tr id="<%= dom_id(execution) %>">
25
- <td>
26
- <%= link_to job_path(execution.serialized_params['job_id']) do %>
27
- <code><%= execution.active_job_id %></code>
28
- <% end %>
29
- </td>
30
- <td>
31
- <%= link_to job_path(execution.active_job_id, anchor: dom_id(execution)) do %>
32
- <code><%= execution.id %></code>
33
- <% end %>
34
- </td>
35
- <td><%= execution.serialized_params['job_class'] %></td>
36
- <td><%= execution.queue_name %></td>
37
- <td><%= relative_time(execution.scheduled_at || execution.created_at) %></td>
38
- <td class="text-break"><%= truncate(execution.error, length: 1_000) %></td>
39
- <td>
40
- <%= tag.button "Preview", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
41
- data: { bs_toggle: "collapse", bs_target: "##{dom_id(execution, 'params')}" },
42
- aria: { expanded: false, controls: dom_id(execution, "params") }
43
- %>
44
- <%= tag.pre JSON.pretty_generate(execution.serialized_params), id: dom_id(execution, "params"), class: "collapse job-params" %>
45
- </td>
46
- <td>
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
- <%= render "good_job/shared/icons/trash" %>
49
- <% end %>
50
- </td>
23
+ <% if executions.present? %>
24
+ <% executions.each do |execution| %>
25
+ <tr id="<%= dom_id(execution) %>">
26
+ <td>
27
+ <%= link_to job_path(execution.serialized_params['job_id']) do %>
28
+ <code><%= execution.active_job_id %></code>
29
+ <% end %>
30
+ </td>
31
+ <td>
32
+ <%= link_to job_path(execution.active_job_id, anchor: dom_id(execution)) do %>
33
+ <code><%= execution.id %></code>
34
+ <% end %>
35
+ </td>
36
+ <td><%= execution.serialized_params['job_class'] %></td>
37
+ <td><%= execution.queue_name %></td>
38
+ <td><%= relative_time(execution.scheduled_at || execution.created_at) %></td>
39
+ <td class="text-break"><%= truncate(execution.error, length: 1_000) %></td>
40
+ <td>
41
+ <%= tag.button "Preview", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
42
+ data: { bs_toggle: "collapse", bs_target: "##{dom_id(execution, 'params')}" },
43
+ aria: { expanded: false, controls: dom_id(execution, "params") }
44
+ %>
45
+ <%= tag.pre JSON.pretty_generate(execution.serialized_params), id: dom_id(execution, "params"), class: "collapse job-params" %>
46
+ </td>
47
+ <td>
48
+ <%= button_to execution_path(execution.id), method: :delete, class: "btn btn-sm btn-outline-danger", title: "Delete execution", data: { confirm: "Confirm delete" } do %>
49
+ <%= render "good_job/shared/icons/trash" %>
50
+ <% end %>
51
+ </td>
52
+ </tr>
53
+ <% end %>
54
+ <% else %>
55
+ <tr>
56
+ <td colspan="8" class="py-2 text-center text-muted">No executions found.</td>
51
57
  </tr>
52
58
  <% end %>
53
59
  </tbody>
@@ -4,9 +4,9 @@
4
4
 
5
5
  <%= render 'good_job/shared/filter', filter: @filter %>
6
6
 
7
- <% if @filter.records.present? %>
8
- <%= render 'good_job/executions/table', executions: @filter.records %>
7
+ <%= render 'good_job/executions/table', executions: @filter.records %>
9
8
 
9
+ <% if @filter.records.present? %>
10
10
  <nav aria-label="Job pagination" class="mt-3">
11
11
  <ul class="pagination">
12
12
  <li class="page-item">
@@ -16,6 +16,4 @@
16
16
  </li>
17
17
  </ul>
18
18
  </nav>
19
- <% else %>
20
- <em>No executions present.</em>
21
19
  <% end %>
@@ -21,45 +21,49 @@
21
21
  </tr>
22
22
  </thead>
23
23
  <tbody>
24
- <% jobs.each do |job| %>
25
- <tr class="<%= dom_class(job) %>" id="<%= dom_id(job) %>">
26
- <td>
27
- <%= link_to job_path(job.id) do %>
28
- <code><%= job.id %></code>
29
- <% end %>
30
- </td>
31
- <td>
32
- <span class="badge bg-secondary"><%= job.status %></span>
33
- </td>
34
- <td><%= job.job_class %></td>
35
- <td><%= job.queue_name %></td>
36
- <td><%= relative_time(job.scheduled_at || job.created_at) %></td>
37
- <td><%= job.executions_count %></td>
38
- <td class="text-break"><%= truncate(job.recent_error, length: 1_000) %></td>
39
- <td>
40
- <%= tag.button "Preview", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
41
- data: { bs_toggle: "collapse", bs_target: "##{dom_id(job, 'params')}" },
42
- aria: { expanded: false, controls: dom_id(job, "params") }
43
- %>
44
- <%= tag.pre JSON.pretty_generate(job.serialized_params), id: dom_id(job, "params"), class: "collapse job-params" %>
45
- </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" %>
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>
51
30
  <% 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 %>
52
51
 
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 %>
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 %>
57
56
 
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>
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" %>
59
+ <% end %>
60
+ </div>
61
+ </td>
62
+ </tr>
63
+ <% end %>
64
+ <% else %>
65
+ <tr>
66
+ <td colspan="8" class="py-2 text-center text-muted">No jobs found.</td>
63
67
  </tr>
64
68
  <% end %>
65
69
  </tbody>
@@ -1,11 +1,16 @@
1
+ <div class="my-3 flex">
2
+ <h2>All Jobs</h2>
3
+ </div>
4
+
1
5
  <div class="card my-3 p-6">
2
6
  <%= render 'good_job/shared/chart', chart_data: GoodJob::ScheduledByQueueChart.new(@filter).data %>
3
7
  </div>
4
8
 
5
9
  <%= render 'good_job/shared/filter', filter: @filter %>
6
10
 
11
+ <%= render 'good_job/jobs/table', jobs: @filter.records %>
12
+
7
13
  <% if @filter.records.present? %>
8
- <%= render 'good_job/jobs/table', jobs: @filter.records %>
9
14
  <nav aria-label="Job pagination" class="mt-3">
10
15
  <ul class="pagination">
11
16
  <li class="page-item">
@@ -15,6 +20,4 @@
15
20
  </li>
16
21
  </ul>
17
22
  </nav>
18
- <% else %>
19
- <em>No jobs present.</em>
20
23
  <% end %>
@@ -1,3 +1,7 @@
1
+ <div class="my-3 flex">
2
+ <h2>Processes</h2>
3
+ </div>
4
+
1
5
  <% if !GoodJob::Process.migrated? %>
2
6
  <div class="card my-3">
3
7
  <div class="card-body">
@@ -1,66 +1,58 @@
1
- <div class='card mb-2'>
2
- <div class='card-body d-flex flex-wrap'>
3
- <div class='mb-2 me-4'>
4
- <small>Filter by job class</small>
5
- <br>
6
- <% filter.job_classes.each do |name, count| %>
7
- <% if params[:job_class] == name %>
8
- <%= link_to(filter.to_params(job_class: nil), class: 'btn btn-sm btn-outline-secondary active', role: "button", "aria-pressed": true) do %>
9
- <%= name %> (<%= count %>)
10
- <% end %>
11
- <% else %>
12
- <%= link_to(filter.to_params(job_class: name), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
13
- <%= name %> (<%= count %>)
14
- <% end %>
1
+ <%= form_with(url: "", method: :get, local: true, id: "filter_form") do |form| %>
2
+ <div class="d-flex flex-row w-100">
3
+ <div class="me-2">
4
+ <label for="job_class_filter">Job class</label>
5
+ <select name="job_class" id="job_class_filter" class="form-select">
6
+ <option value="" <%= "selected='selected'" if params[:job_class].blank? %>>All jobs</option>
7
+
8
+ <% filter.job_classes.each do |name, count| %>
9
+ <option value="<%= name.to_param %>" <%= "selected='selected'" if params[:job_class] == name %>><%= name %> (<%= count %>)</option>
15
10
  <% end %>
16
- <% end %>
11
+ </select>
17
12
  </div>
18
13
 
19
- <div class='mb-2 me-4'>
20
- <small>Filter by state</small>
21
- <br>
22
- <% filter.states.each do |name, count| %>
23
- <% if params[:state] == name %>
24
- <%= link_to(filter.to_params(state: nil), class: 'btn btn-sm btn-outline-secondary active', role: "button", "aria-pressed": true) do %>
25
- <%= name %> (<%= count %>)
26
- <% end %>
27
- <% else %>
28
- <%= link_to(filter.to_params(state: name), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
29
- <%= name %> (<%= count %>)
30
- <% end %>
14
+ <div class="me-2">
15
+ <label for="job_state_filter">State</label>
16
+ <select name="state" id="job_state_filter" class="form-select">
17
+ <option value="" <%= "selected='selected'" if params[:state].blank? %>>All states</option>
18
+
19
+ <% filter.states.each do |name, count| %>
20
+ <option value="<%= name.to_param %>" <%= "selected='selected'" if params[:state] == name %>><%= name %> (<%= count %>)</option>
31
21
  <% end %>
32
- <% end %>
22
+ </select>
33
23
  </div>
34
24
 
35
- <div class='mb-2 me-4'>
36
- <small>Filter by queue</small>
37
- <br>
38
- <% filter.queues.each do |name, count| %>
39
- <% if params[:queue_name] == name %>
40
- <%= link_to(filter.to_params(queue_name: nil), class: 'btn btn-sm btn-outline-secondary active', role: "button", "aria-pressed": true) do %>
41
- <%= name %> (<%= count %>)
42
- <% end %>
43
- <% else %>
44
- <%= link_to(filter.to_params(queue_name: name), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
45
- <%= name %> (<%= count %>)
46
- <% end %>
25
+ <div class="me-2">
26
+ <label for="job_queue_filter">Queue</label>
27
+ <select name="queue_name" id="job_queue_filter" class="form-select">
28
+ <option value="" <%= "selected='selected'" if params[:queue_name].blank? %>>All queues</option>
29
+
30
+ <% filter.queues.each do |name, count| %>
31
+ <option value="<%= name.to_param %>" <%= "selected='selected'" if params[:queue_name] == name %>><%= name %> (<%= count %>)</option>
47
32
  <% end %>
48
- <% end %>
33
+ </select>
49
34
  </div>
50
35
 
51
- <div class="mb-2">
52
- <%= form_with(url: "", method: :get, local: true) do |form| %>
53
- <% filter.to_params(query: nil).each do |key, value| %>
54
- <%= form.hidden_field(key.to_sym, value: value) %>
55
- <% end %>
36
+ <div class="me-2 flex-fill d-flex flex-col align-items-end">
37
+ <label class="visually-hidden" for="query" aria-label="Search by class, job id, job params, and error text.">Search by class, job id, job params, and error text.</label>
38
+ <%= search_field_tag "query", params[:query], class: "form-control", placeholder: "Search by class, job id, job params, and error text." %>
39
+ </div>
56
40
 
57
- <small><%= form.label :query, "Search" %></small>
58
- <div class="input-group input-group-sm">
59
- <%= form.search_field :query, value: params[:query], class: "form-control" %>
60
- <%= form.button "Search", type: "submit", name: nil, class: "btn btn-sm btn-outline-secondary" %>
61
- <%= link_to "Clear", filter.to_params(query: nil), class: "btn btn-sm btn-outline-secondary" %>
62
- </div>
63
- <% end %>
41
+ <div class="d-flex flex-col align-items-end">
42
+ <div>
43
+ <%= form.submit "Search", name: nil, class: "btn btn-primary" %>
44
+ <%= link_to "Clear all", filter.to_params(job_class: nil, state: nil, queue_name: nil, query: nil), class: "btn btn-secondary" %>
45
+ </div>
64
46
  </div>
65
47
  </div>
66
- </div>
48
+ <% end %>
49
+
50
+ <%= javascript_tag nonce: true do %>
51
+ document.addEventListener("DOMContentLoaded", () => {
52
+ document.querySelectorAll("#job_class_filter, #job_state_filter, #job_queue_filter").forEach((filter) => {
53
+ filter.addEventListener("change", () => {
54
+ document.querySelector("#filter_form").submit();
55
+ });
56
+ })
57
+ })
58
+ <% end %>
@@ -2,6 +2,8 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <title>Good Job Dashboard</title>
5
+ <meta charset="utf-8">
6
+ <meta content="width=device-width, initial-scale=1, shrink-to-fit=no" name="viewport">
5
7
  <%= csrf_meta_tags %>
6
8
  <%= csp_meta_tag %>
7
9
 
@@ -19,7 +21,7 @@
19
21
  <nav class="navbar navbar-expand-lg navbar-light bg-light">
20
22
  <div class="container-fluid">
21
23
  <%= link_to "GoodJob 👍", root_path, class: 'navbar-brand mb-0 h1' %>
22
- <button class="navbar-toggler" type="button" data-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
24
+ <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
23
25
  <span class="navbar-toggler-icon"></span>
24
26
  </button>
25
27
 
@@ -30,7 +30,7 @@ module GoodJob
30
30
  key = job.good_job_concurrency_key
31
31
  next(block.call) if key.blank?
32
32
 
33
- GoodJob::Execution.new.with_advisory_lock(key: key, function: "pg_advisory_lock") do
33
+ GoodJob::Execution.advisory_lock_key(key, function: "pg_advisory_lock") do
34
34
  enqueue_concurrency = if enqueue_limit
35
35
  GoodJob::Execution.where(concurrency_key: key).unfinished.advisory_unlocked.count
36
36
  else
@@ -61,8 +61,8 @@ module GoodJob
61
61
  key = job.good_job_concurrency_key
62
62
  next if key.blank?
63
63
 
64
- GoodJob::Execution.new.with_advisory_lock(key: key, function: "pg_advisory_lock") do
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)
64
+ GoodJob::Execution.advisory_lock_key(key, function: "pg_advisory_lock") do
65
+ allowed_active_job_ids = GoodJob::Execution.unfinished.where(concurrency_key: key).advisory_locked.order(Arel.sql("COALESCE(performed_at, scheduled_at, created_at) ASC")).limit(perform_limit).pluck(:active_job_id)
66
66
  # The current job has already been locked and will appear in the previous query
67
67
  raise GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError unless allowed_active_job_ids.include? job.job_id
68
68
  end
@@ -267,7 +267,9 @@ module GoodJob
267
267
  end
268
268
 
269
269
  def active_job
270
- ActiveJob::Base.deserialize(active_job_data)
270
+ ActiveJob::Base.deserialize(active_job_data).tap do |aj|
271
+ aj.send(:deserialize_arguments_if_needed)
272
+ end
271
273
  end
272
274
 
273
275
  private
@@ -148,6 +148,7 @@ module GoodJob
148
148
  raise ArgumentError, "Must provide a block" unless block_given?
149
149
 
150
150
  records = advisory_lock(column: column, function: function).to_a
151
+
151
152
  begin
152
153
  unscoped { yield(records) }
153
154
  ensure
@@ -161,6 +162,53 @@ module GoodJob
161
162
  end
162
163
  end
163
164
 
165
+ # Acquires an advisory lock on this record if it is not already locked by
166
+ # another database session. Be careful to ensure you release the lock when
167
+ # you are done with {#advisory_unlock_key} to release all remaining locks.
168
+ # @param key [String, Symbol] Key to Advisory Lock against
169
+ # @param function [String, Symbol] Postgres Advisory Lock function name to use
170
+ # @return [Boolean] whether the lock was acquired.
171
+ def advisory_lock_key(key, function: advisory_lockable_function)
172
+ query = if function.include? "_try_"
173
+ <<~SQL.squish
174
+ SELECT #{function}(('x'||substr(md5($1::text), 1, 16))::bit(64)::bigint) AS locked
175
+ SQL
176
+ else
177
+ <<~SQL.squish
178
+ SELECT #{function}(('x'||substr(md5($1::text), 1, 16))::bit(64)::bigint)::text AS locked
179
+ SQL
180
+ end
181
+
182
+ binds = [
183
+ ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
184
+ ]
185
+ locked = connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Lock', binds).first['locked']
186
+ return locked unless block_given?
187
+ return nil unless locked
188
+
189
+ begin
190
+ yield
191
+ ensure
192
+ advisory_unlock_key(key, function: advisory_unlockable_function(function))
193
+ end
194
+ end
195
+
196
+ # Releases an advisory lock on this record if it is locked by this database
197
+ # session. Note that advisory locks stack, so you must call
198
+ # {#advisory_unlock} and {#advisory_lock} the same number of times.
199
+ # @param key [String, Symbol] Key to lock against
200
+ # @param function [String, Symbol] Postgres Advisory Lock function name to use
201
+ # @return [Boolean] whether the lock was released.
202
+ def advisory_unlock_key(key, function: advisory_unlockable_function)
203
+ query = <<~SQL.squish
204
+ SELECT #{function}(('x'||substr(md5($1::text), 1, 16))::bit(64)::bigint) AS unlocked
205
+ SQL
206
+ binds = [
207
+ ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
208
+ ]
209
+ connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Unlock', binds).first['unlocked']
210
+ end
211
+
164
212
  def _advisory_lockable_column
165
213
  advisory_lockable_column || primary_key
166
214
  end
@@ -205,20 +253,7 @@ module GoodJob
205
253
  # @param function [String, Symbol] Postgres Advisory Lock function name to use
206
254
  # @return [Boolean] whether the lock was acquired.
207
255
  def advisory_lock(key: lockable_key, function: advisory_lockable_function)
208
- query = if function.include? "_try_"
209
- <<~SQL.squish
210
- SELECT #{function}(('x'||substr(md5($1::text), 1, 16))::bit(64)::bigint) AS locked
211
- SQL
212
- else
213
- <<~SQL.squish
214
- SELECT #{function}(('x'||substr(md5($1::text), 1, 16))::bit(64)::bigint)::text AS locked
215
- SQL
216
- end
217
-
218
- binds = [
219
- ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
220
- ]
221
- self.class.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Lock', binds).first['locked']
256
+ self.class.advisory_lock_key(key, function: function)
222
257
  end
223
258
 
224
259
  # Releases an advisory lock on this record if it is locked by this database
@@ -228,13 +263,7 @@ module GoodJob
228
263
  # @param function [String, Symbol] Postgres Advisory Lock function name to use
229
264
  # @return [Boolean] whether the lock was released.
230
265
  def advisory_unlock(key: lockable_key, function: self.class.advisory_unlockable_function(advisory_lockable_function))
231
- query = <<~SQL.squish
232
- SELECT #{function}(('x'||substr(md5($1::text), 1, 16))::bit(64)::bigint) AS unlocked
233
- SQL
234
- binds = [
235
- ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
236
- ]
237
- self.class.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Unlock', binds).first['unlocked']
266
+ self.class.advisory_unlock_key(key, function: function)
238
267
  end
239
268
 
240
269
  # Acquires an advisory lock on this record or raises
@@ -245,8 +274,7 @@ module GoodJob
245
274
  # @raise [RecordAlreadyAdvisoryLockedError]
246
275
  # @return [Boolean] +true+
247
276
  def advisory_lock!(key: lockable_key, function: advisory_lockable_function)
248
- result = advisory_lock(key: key, function: function)
249
- result || raise(RecordAlreadyAdvisoryLockedError)
277
+ self.class.advisory_lock_key(key, function: function) || raise(RecordAlreadyAdvisoryLockedError)
250
278
  end
251
279
 
252
280
  # Acquires an advisory lock on this record and safely releases it after the
@@ -266,9 +294,11 @@ module GoodJob
266
294
  raise ArgumentError, "Must provide a block" unless block_given?
267
295
 
268
296
  advisory_lock!(key: key, function: function)
269
- yield
270
- ensure
271
- advisory_unlock(key: key, function: self.class.advisory_unlockable_function(function)) unless $ERROR_INFO.is_a? RecordAlreadyAdvisoryLockedError
297
+ begin
298
+ yield
299
+ ensure
300
+ advisory_unlock(key: key, function: self.class.advisory_unlockable_function(function))
301
+ end
272
302
  end
273
303
 
274
304
  # Tests whether this record has an advisory lock on it.
@@ -206,7 +206,7 @@ module GoodJob
206
206
  good_job_tag = ["ActiveJob"].freeze
207
207
 
208
208
  self.class.loggers.inject(block) do |inner, each_logger|
209
- if each_logger.respond_to?(:tagged)
209
+ if each_logger.respond_to?(:tagged) && each_logger.formatter
210
210
  tags_for_logger = if each_logger.formatter.current_tags.include?("ActiveJob")
211
211
  good_job_tag + tags
212
212
  else
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  module GoodJob
3
3
  # GoodJob gem version.
4
- VERSION = '2.9.3'
4
+ VERSION = '2.10.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.9.3
4
+ version: 2.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Sheldon
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-01-23 00:00:00.000000000 Z
11
+ date: 2022-02-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -448,7 +448,7 @@ metadata:
448
448
  homepage_uri: https://github.com/bensheldon/good_job
449
449
  source_code_uri: https://github.com/bensheldon/good_job
450
450
  rubygems_mfa_required: 'true'
451
- post_install_message:
451
+ post_install_message:
452
452
  rdoc_options:
453
453
  - "--title"
454
454
  - GoodJob - a multithreaded, Postgres-based ActiveJob backend for Ruby on Rails
@@ -471,8 +471,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
471
471
  - !ruby/object:Gem::Version
472
472
  version: '0'
473
473
  requirements: []
474
- rubygems_version: 3.1.6
475
- signing_key:
474
+ rubygems_version: 3.3.7
475
+ signing_key:
476
476
  specification_version: 4
477
477
  summary: A multithreaded, Postgres-based ActiveJob backend for Ruby on Rails
478
478
  test_files: []