good_job 2.10.0 → 2.11.2

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: 6b22cbc6d37af9dd8db290a1dc8073d2cae14c63e5cf7180b06dce1a33fb9992
4
- data.tar.gz: 8cd87b2d69afea84db87125bad625558783adea98fee138be852e6a51c43dc50
3
+ metadata.gz: 44ffd48ff0a352758f67aeaaf60eeb7242fdf95c71cf16fcb7089689d72709a6
4
+ data.tar.gz: da29ba7f5b8f212d989d24fec8c7e5672166d3950bae04f3f29a2ca6e8cb035a
5
5
  SHA512:
6
- metadata.gz: 230b335c5b6a03397c3212cd2bc59d87b2c735073233d068cf2c3045933b2aace533b0699a6fa1ecd41d77890af5aa18e1685b2c85f4c97c868773a3211736b7
7
- data.tar.gz: b3fb4cb11f129fa8576e36c4e7401cdab927cf309bda963c3b82480dbe92663847b46a8e641b123e5ad7b28c53aa361d5fb6968131e23e85b0302768cc38e5d0
6
+ metadata.gz: c72947a7302d52b2b4f81a6092fbaca28dc900095fa477e0f7abd6d3419c5825c9693063fcdf6081d00ed93b082be705cef2beb2d1088c90de54737098103df5
7
+ data.tar.gz: 4d9b6c34665cd02c840841460a2a162592d494fb27bd79e12987d4cda50a93d8959f130f29ccbad7e04b4be79ef5878dd13ae98ff87c6d8c87aad514b8937cd9
data/CHANGELOG.md CHANGED
@@ -1,9 +1,54 @@
1
1
  # Changelog
2
2
 
3
+ ## [v2.11.2](https://github.com/bensheldon/good_job/tree/v2.11.2) (2022-03-03)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.11.1...v2.11.2)
6
+
7
+ **Closed issues:**
8
+
9
+ - Best practices in deploying and monitoring a queue [\#523](https://github.com/bensheldon/good_job/issues/523)
10
+
11
+ **Merged pull requests:**
12
+
13
+ - Wrap Rspec before and example blocks with a mutex for JRuby [\#537](https://github.com/bensheldon/good_job/pull/537) ([bensheldon](https://github.com/bensheldon))
14
+ - Delegate `ActiveJobJob.table_name` to `Execution` and prevent it from being directly assignable [\#536](https://github.com/bensheldon/good_job/pull/536) ([bensheldon](https://github.com/bensheldon))
15
+ - Enable DB table names customization [\#535](https://github.com/bensheldon/good_job/pull/535) ([dimvic](https://github.com/dimvic))
16
+ - Added a chapter on how to prepare for production. [\#525](https://github.com/bensheldon/good_job/pull/525) ([stas](https://github.com/stas))
17
+
18
+ ## [v2.11.1](https://github.com/bensheldon/good_job/tree/v2.11.1) (2022-03-01)
19
+
20
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.11.0...v2.11.1)
21
+
22
+ **Fixed bugs:**
23
+
24
+ - Ensure sticky footer doesn't overlap paginater; fix polling interval to 30 seconds, not ms [\#534](https://github.com/bensheldon/good_job/pull/534) ([bensheldon](https://github.com/bensheldon))
25
+
26
+ **Closed issues:**
27
+
28
+ - Pagination buttons hidden behind footer [\#533](https://github.com/bensheldon/good_job/issues/533)
29
+
30
+ ## [v2.11.0](https://github.com/bensheldon/good_job/tree/v2.11.0) (2022-02-27)
31
+
32
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.10.0...v2.11.0)
33
+
34
+ **Implemented enhancements:**
35
+
36
+ - Add support for live polling the dashboard [\#528](https://github.com/bensheldon/good_job/pull/528) ([danielwestendorf](https://github.com/danielwestendorf))
37
+
38
+ **Closed issues:**
39
+
40
+ - How do I ensure that a the same job can't run twice? \(unique job / avoid duplicates\) [\#531](https://github.com/bensheldon/good_job/issues/531)
41
+ - Bulk reschedule and discard jobs via dashboard [\#527](https://github.com/bensheldon/good_job/issues/527)
42
+ - "Live Poll" dashboard [\#526](https://github.com/bensheldon/good_job/issues/526)
43
+
3
44
  ## [v2.10.0](https://github.com/bensheldon/good_job/tree/v2.10.0) (2022-02-18)
4
45
 
5
46
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.9.6...v2.10.0)
6
47
 
48
+ **Implemented enhancements:**
49
+
50
+ - Dashboard: update search filters and some small UI updates [\#518](https://github.com/bensheldon/good_job/pull/518) ([multiplegeorges](https://github.com/multiplegeorges))
51
+
7
52
  **Closed issues:**
8
53
 
9
54
  - Cron jobs not getting run [\#519](https://github.com/bensheldon/good_job/issues/519)
@@ -15,7 +60,6 @@
15
60
  - Fix Benchmark job throughput script [\#522](https://github.com/bensheldon/good_job/pull/522) ([douglara](https://github.com/douglara))
16
61
  - Update development Gemfile.lock [\#521](https://github.com/bensheldon/good_job/pull/521) ([bensheldon](https://github.com/bensheldon))
17
62
  - 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
63
  - Document safer setting for retry\_on\_unhandled\_error [\#517](https://github.com/bensheldon/good_job/pull/517) ([tamaloa](https://github.com/tamaloa))
20
64
 
21
65
  ## [v2.9.6](https://github.com/bensheldon/good_job/tree/v2.9.6) (2022-02-07)
data/README.md CHANGED
@@ -52,6 +52,7 @@ For more of the story of GoodJob, read the [introductory blog post](https://isla
52
52
  - [Timeouts](#timeouts)
53
53
  - [Optimize queues, threads, and processes](#optimize-queues-threads-and-processes)
54
54
  - [Database connections](#database-connections)
55
+ - [Production setup](#production-setup)
55
56
  - [Execute jobs async / in-process](#execute-jobs-async--in-process)
56
57
  - [Migrate to GoodJob from a different ActiveJob backend](#migrate-to-goodjob-from-a-different-activejob-backend)
57
58
  - [Monitor and preserve worked jobs](#monitor-and-preserve-worked-jobs)
@@ -369,6 +370,10 @@ GoodJob includes a Dashboard as a mountable `Rails::Engine`.
369
370
  end
370
371
  ```
371
372
 
373
+ #### Live Polling
374
+
375
+ The Dashboard can be set to automatically refresh by checking "Live Poll" in the Dashboard header, or by setting `?poll=10` with the interval in seconds (default 30 seconds).
376
+
372
377
  ### ActiveJob concurrency
373
378
 
374
379
  GoodJob can extend ActiveJob to provide limits on concurrently running jobs, either at time of _enqueue_ or at _perform_. Limiting concurrency can help prevent duplicate, double or unecessary jobs from being enqueued, or race conditions when performing, for example when interacting with 3rd-party APIs.
@@ -670,13 +675,38 @@ Keep in mind, queue operations and management is an advanced discipline. This st
670
675
 
671
676
  ### Database connections
672
677
 
673
- Each GoodJob execution thread requires its own database connection that is automatically checked out from Rails’ connection pool. _Allowing GoodJob to create more threads than available database connections can lead to timeouts and is not recommended._ For example:
678
+ Each GoodJob execution thread requires its own database connection that is automatically checked out from Rails’ connection pool. For example:
674
679
 
675
680
  ```yaml
676
681
  # config/database.yml
677
- pool: <%= [ENV.fetch("RAILS_MAX_THREADS", 5).to_i, ENV.fetch("GOOD_JOB_MAX_THREADS", 4).to_i].max %>
682
+ pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5).to_i + (ENV.fetch("GOOD_JOB_MAX_THREADS", 4).to_i %>
678
683
  ```
679
684
 
685
+ To calculate the total number of the database connections you'll need:
686
+
687
+ - 1 connection dedicated to the scheduler aka `LISTEN/NOTIFY`
688
+ - 1 connection per query pool thread e.g. `--queues=mice:2;elephants:1` is 3 threads. Pool thread size defaults to `--max-threads`
689
+ - (optional) 2 connections for Cron scheduler if you're running it
690
+ - (optional) 1 connection per subthread, if your application makes multithreaded database queries within a job
691
+ - When running `:async`, you must also add the number of threads by the webserver
692
+
693
+ The queue process will not crash if the connections pool is exhausted, instead it will report an exception (eg. `ActiveRecord::ConnectionTimeoutError`).
694
+
695
+ #### Production setup
696
+
697
+ When running GoodJob in a production environment, you should be mindful of:
698
+
699
+ - [Execution mode](execute-jobs-async--in-process)
700
+ - [Database connection pool size](#database-connections)
701
+ - [Health check probes](#cli-http-health-check-probes) and potentially the [instrumentation support](#monitor-and-preserve-worked-jobs)
702
+
703
+ The recommended way to monitor the queue in production is:
704
+
705
+ - have an exception notifier callback (see `on_thread_error`)
706
+ - if possible, run the queue as a dedicated instance and use available HTTP health check probes instead of pid-based monitoring
707
+ - keep an eye on the number of jobs in the queue (abnormal high number of unscheduled jobs means the queue could be underperforming)
708
+ - consider performance monitoring services which support the built-in Rails instrumentation (eg. Sentry, Skylight, etc.)
709
+
680
710
  ### Execute jobs async / in-process
681
711
 
682
712
  GoodJob can execute jobs "async" in the same process as the web server (e.g. `bin/rails s`). GoodJob's async execution mode offers benefits of economy by not requiring a separate job worker process, but with the tradeoff of increased complexity. Async mode can be configured in two ways:
@@ -1 +1,133 @@
1
- GoodJob = {};
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);
@@ -23,3 +23,8 @@
23
23
  position: relative;
24
24
  height: 200px;
25
25
  }
26
+
27
+ body {
28
+ /* Make room for the sticky footer */
29
+ margin-bottom: 100px;
30
+ }
@@ -9,6 +9,7 @@ module GoodJob
9
9
  def data
10
10
  end_time = Time.current
11
11
  start_time = end_time - 1.day
12
+ table_name = GoodJob::ActiveJobJob.table_name
12
13
 
13
14
  count_query = Arel.sql(GoodJob::Execution.pg_or_jdbc_query(<<~SQL.squish))
14
15
  SELECT *
@@ -23,7 +24,7 @@ module GoodJob
23
24
  queue_name,
24
25
  count(*) AS count
25
26
  FROM (
26
- #{@filter.filtered_query.except(:select, :order).select('queue_name', 'COALESCE(good_jobs.scheduled_at, good_jobs.created_at)::timestamp AS scheduled_at').to_sql}
27
+ #{@filter.filtered_query.except(:select, :order).select('queue_name', "COALESCE(#{table_name}.scheduled_at, #{table_name}.created_at)::timestamp AS scheduled_at").to_sql}
27
28
  ) sources
28
29
  GROUP BY date_trunc('hour', scheduled_at), queue_name
29
30
  ) sources ON sources.scheduled_at = timestamp
@@ -14,7 +14,7 @@ module GoodJob
14
14
 
15
15
  def filtered_query
16
16
  query = base_query.includes(:executions)
17
- .joins_advisory_locks.select('good_jobs.*', 'pg_locks.locktype AS locktype')
17
+ .joins_advisory_locks.select("#{GoodJob::ActiveJobJob.table_name}.*", 'pg_locks.locktype AS locktype')
18
18
 
19
19
  query = query.job_class(params[:job_class]) if params[:job_class].present?
20
20
  query = query.where(queue_name: params[:queue_name]) if params[:queue_name].present?
@@ -31,7 +31,7 @@ module GoodJob
31
31
  when 'scheduled'
32
32
  query = query.scheduled
33
33
  when 'running'
34
- query = query.running.select('good_jobs.*', 'pg_locks.locktype')
34
+ query = query.running.select("#{GoodJob::ActiveJobJob.table_name}.*", 'pg_locks.locktype')
35
35
  when 'queued'
36
36
  query = query.queued
37
37
  end
@@ -1,4 +1,4 @@
1
- <div class="card my-3">
1
+ <div class="card my-3" data-gj-poll-replace id="executions-table">
2
2
  <div class="table-responsive">
3
3
  <table class="table card-table table-bordered table-hover table-sm mb-0" id="executions_index_table">
4
4
  <thead>
@@ -1,4 +1,4 @@
1
- <div class="card my-3 p-6">
1
+ <div class="card my-3 p-6" data-gj-poll-replace id="executions-chart">
2
2
  <%= render 'good_job/shared/chart', chart_data: GoodJob::ScheduledByQueueChart.new(@filter).data %>
3
3
  </div>
4
4
 
@@ -7,7 +7,7 @@
7
7
  <%= render 'good_job/executions/table', executions: @filter.records %>
8
8
 
9
9
  <% if @filter.records.present? %>
10
- <nav aria-label="Job pagination" class="mt-3">
10
+ <nav aria-label="Job pagination" class="mt-3" data-gj-poll-replace id="executions-pagination">
11
11
  <ul class="pagination">
12
12
  <li class="page-item">
13
13
  <%= link_to({ after_scheduled_at: (@filter.last.scheduled_at || @filter.last.created_at), after_id: @filter.last.id }, class: "page-link") do %>
@@ -1,4 +1,4 @@
1
- <div class="card my-3">
1
+ <div class="card my-3" data-gj-poll-replace id="jobs-table">
2
2
  <div class="table-responsive">
3
3
  <table class="table card-table table-bordered table-hover table-sm mb-0">
4
4
  <thead>
@@ -2,7 +2,7 @@
2
2
  <h2>All Jobs</h2>
3
3
  </div>
4
4
 
5
- <div class="card my-3 p-6">
5
+ <div class="card my-3 p-6" data-gj-poll-replace id="jobs-chart">
6
6
  <%= render 'good_job/shared/chart', chart_data: GoodJob::ScheduledByQueueChart.new(@filter).data %>
7
7
  </div>
8
8
 
@@ -11,7 +11,7 @@
11
11
  <%= render 'good_job/jobs/table', jobs: @filter.records %>
12
12
 
13
13
  <% if @filter.records.present? %>
14
- <nav aria-label="Job pagination" class="mt-3">
14
+ <nav aria-label="Job pagination" class="mt-3" data-gj-poll-replace id="jobs-pagination">
15
15
  <ul class="pagination">
16
16
  <li class="page-item">
17
17
  <%= 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 %>
@@ -2,43 +2,45 @@
2
2
  <h2>Processes</h2>
3
3
  </div>
4
4
 
5
- <% if !GoodJob::Process.migrated? %>
6
- <div class="card my-3">
7
- <div class="card-body">
8
- <p class="card-text">
9
- <em>Feature unavailable because of pending database migration.</em>
10
- </p>
5
+ <div data-gj-poll-replace id="processes">
6
+ <% if !GoodJob::Process.migrated? %>
7
+ <div class="card my-3">
8
+ <div class="card-body">
9
+ <p class="card-text">
10
+ <em>Feature unavailable because of pending database migration.</em>
11
+ </p>
12
+ </div>
11
13
  </div>
12
- </div>
13
- <% elsif @processes.present? %>
14
- <div class="card my-3">
15
- <div class="table-responsive">
16
- <table class="table card-table table-bordered table-hover table-sm mb-0">
17
- <thead>
18
- <tr>
19
- <th>Process UUID</th>
20
- <th>Created At</th></th>
21
- <th>State</th>
22
- </tr>
23
- </thead>
24
- <tbody>
25
- <% @processes.each do |process| %>
26
- <tr class="<%= dom_class(process) %>" id="<%= dom_id(process) %>">
27
- <td><%= process.id %></td>
28
- <td><%= relative_time(process.created_at) %></td>
29
- <td><%= tag.pre JSON.pretty_generate(process.state) %></td>
14
+ <% elsif @processes.present? %>
15
+ <div class="card my-3">
16
+ <div class="table-responsive">
17
+ <table class="table card-table table-bordered table-hover table-sm mb-0">
18
+ <thead>
19
+ <tr>
20
+ <th>Process UUID</th>
21
+ <th>Created At</th></th>
22
+ <th>State</th>
30
23
  </tr>
31
- <% end %>
32
- </tbody>
33
- </table>
24
+ </thead>
25
+ <tbody>
26
+ <% @processes.each do |process| %>
27
+ <tr class="<%= dom_class(process) %>" id="<%= dom_id(process) %>">
28
+ <td><%= process.id %></td>
29
+ <td><%= relative_time(process.created_at) %></td>
30
+ <td><%= tag.pre JSON.pretty_generate(process.state) %></td>
31
+ </tr>
32
+ <% end %>
33
+ </tbody>
34
+ </table>
35
+ </div>
34
36
  </div>
35
- </div>
36
- <% else %>
37
- <div class="card my-3">
38
- <div class="card-body">
39
- <p class="card-text">
40
- <em>No GoodJob processes found.</em>
41
- </p>
37
+ <% else %>
38
+ <div class="card my-3">
39
+ <div class="card-body">
40
+ <p class="card-text">
41
+ <em>No GoodJob processes found.</em>
42
+ </p>
43
+ </div>
42
44
  </div>
43
- </div>
44
- <% end %>
45
+ <% end %>
46
+ </div>
@@ -1,25 +1,3 @@
1
1
  <div class="chart-wrapper">
2
- <canvas id="chart"></canvas>
2
+ <canvas class="chart" data-json="<%= chart_data.to_json %>"></canvas>
3
3
  </div>
4
-
5
- <%= javascript_tag nonce: true do %>
6
- const chartData = <%== chart_data.to_json %>;
7
-
8
- const ctx = document.getElementById('chart').getContext('2d');
9
- const chart = new Chart(ctx, {
10
- type: 'line',
11
- data: {
12
- labels: chartData.labels,
13
- datasets: chartData.datasets
14
- },
15
- options: {
16
- responsive: true,
17
- maintainAspectRatio: false,
18
- scales: {
19
- y: {
20
- beginAtZero: true
21
- }
22
- }
23
- }
24
- });
25
- <% end %>
@@ -1,4 +1,5 @@
1
1
  <%= form_with(url: "", method: :get, local: true, id: "filter_form") do |form| %>
2
+ <%= hidden_field_tag :poll, value: params[:poll] %>
2
3
  <div class="d-flex flex-row w-100">
3
4
  <div class="me-2">
4
5
  <label for="job_class_filter">Job class</label>
@@ -55,4 +56,4 @@
55
56
  });
56
57
  })
57
58
  })
58
- <% end %>
59
+ <% end %>
@@ -45,7 +45,10 @@
45
45
  </div>
46
46
  </li>
47
47
  </ul>
48
- <div class="text-muted" title="Now is <%= Time.current %>">Times are displayed in <%= Time.current.zone %> timezone</div>
48
+ <div>
49
+ <input type="checkbox" id="toggle-poll" name="toggle-poll" data-gj-action='change#togglePoll' <%= 'checked' if params[:poll].present? %>>
50
+ <label for="toggle-poll">Live Poll</label>
51
+ </div>
49
52
  </div>
50
53
  </div>
51
54
  </nav>
@@ -70,7 +73,24 @@
70
73
  <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
71
74
  </div>
72
75
  <% end %>
76
+
73
77
  <%= yield %>
74
78
  </div>
79
+
80
+ <footer class="footer mt-auto py-3 bg-light fixed-bottom" id="footer" data-gj-poll-replace>
81
+ <div class="container-fluid">
82
+ <div class="row">
83
+ <div class="col-6">
84
+ <span class="text-muted">
85
+ Last updated: <time id="page-updated-at" datetime="<%= Time.current.utc.iso8601 %>"><%= Time.current %></time>
86
+ </span>
87
+ </div>
88
+
89
+ <div class="col-6 text-end">
90
+ Remember, you're doing a Good Job too!
91
+ </div>
92
+ </div>
93
+ </div>
94
+ </footer>
75
95
  </body>
76
96
  </html>
@@ -15,7 +15,14 @@ module GoodJob
15
15
  # Attached to a Job's Execution when the Job is discarded.
16
16
  DiscardJobError = Class.new(StandardError)
17
17
 
18
- self.table_name = 'good_jobs'
18
+ class << self
19
+ delegate :table_name, to: Execution
20
+
21
+ def table_name=(_value)
22
+ raise NotImplementedError, 'Assign GoodJob::Execution.table_name directly'
23
+ end
24
+ end
25
+
19
26
  self.primary_key = 'active_job_id'
20
27
  self.advisory_lockable_column = 'active_job_id'
21
28
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  module GoodJob
3
3
  # GoodJob gem version.
4
- VERSION = '2.10.0'
4
+ VERSION = '2.11.2'
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.10.0
4
+ version: 2.11.2
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-02-18 00:00:00.000000000 Z
11
+ date: 2022-03-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob