good_job 2.3.1 → 2.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +90 -1
- data/README.md +50 -19
- data/engine/app/controllers/good_job/base_controller.rb +8 -0
- data/engine/app/controllers/good_job/cron_schedules_controller.rb +1 -1
- data/engine/app/controllers/good_job/jobs_controller.rb +36 -0
- data/engine/app/filters/good_job/base_filter.rb +6 -2
- data/engine/app/filters/good_job/jobs_filter.rb +3 -1
- data/engine/app/helpers/good_job/application_helper.rb +4 -0
- data/engine/app/models/good_job/active_job_job.rb +130 -12
- data/engine/app/views/good_job/cron_schedules/index.html.erb +51 -7
- data/engine/app/views/good_job/jobs/index.html.erb +14 -1
- data/engine/app/views/good_job/shared/_executions_table.erb +1 -1
- data/engine/app/views/good_job/shared/_jobs_table.erb +18 -6
- data/engine/app/views/good_job/shared/icons/_arrow_clockwise.html.erb +5 -0
- data/engine/app/views/good_job/shared/icons/_skip_forward.html.erb +4 -0
- data/engine/app/views/good_job/shared/icons/_stop.html.erb +4 -0
- data/engine/app/views/layouts/good_job/base.html.erb +2 -1
- data/engine/config/routes.rb +7 -1
- data/lib/generators/good_job/install_generator.rb +6 -0
- data/lib/generators/good_job/templates/install/migrations/create_good_jobs.rb.erb +3 -1
- data/lib/generators/good_job/templates/update/migrations/{01_create_good_jobs.rb → 01_create_good_jobs.rb.erb} +1 -1
- data/lib/generators/good_job/templates/update/migrations/02_add_cron_at_to_good_jobs.rb.erb +14 -0
- data/lib/generators/good_job/templates/update/migrations/03_add_cron_key_cron_at_index_to_good_jobs.rb.erb +20 -0
- data/lib/generators/good_job/update_generator.rb +6 -0
- data/lib/good_job/active_job_extensions/concurrency.rb +3 -4
- data/lib/good_job/adapter.rb +4 -2
- data/lib/good_job/cli.rb +3 -1
- data/lib/good_job/configuration.rb +4 -0
- data/lib/good_job/cron_entry.rb +67 -0
- data/lib/good_job/cron_manager.rb +20 -30
- data/lib/good_job/current_thread.rb +15 -0
- data/lib/good_job/execution.rb +37 -14
- data/lib/good_job/lockable.rb +1 -1
- data/lib/good_job/log_subscriber.rb +3 -3
- data/lib/good_job/scheduler.rb +1 -0
- data/lib/good_job/version.rb +1 -1
- metadata +9 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5a173683ec5e5879728536005c7dfd11827ffe87c268c59315f540911277f2df
|
4
|
+
data.tar.gz: a5990d902838da25344ff96fc859bad39803f70f888dd95de8e3e6c201da4b0f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 92378343ecf6f3750a98ac1e146c748e7440b267fad30ac04eaf984649e5640fad8cfdd58b9c627bb7f7792202a849222ee7ed7583b7ac847f2d15839c25519f
|
7
|
+
data.tar.gz: f093bda085b00d82210e9bcf02ed6e39fec556aeeaa17c6f73a9465119867833f3145905482a20ec57840ae6bb6fa350f79e64209425cef8db9f45c0b59c4ea0
|
data/CHANGELOG.md
CHANGED
@@ -1,13 +1,102 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [v2.5.0](https://github.com/bensheldon/good_job/tree/v2.5.0) (2021-10-25)
|
4
|
+
|
5
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v2.4.2...v2.5.0)
|
6
|
+
|
7
|
+
**Implemented enhancements:**
|
8
|
+
|
9
|
+
- Add Reschedule, Discard, Retry Job buttons to Dashboard [\#425](https://github.com/bensheldon/good_job/pull/425) ([bensheldon](https://github.com/bensheldon))
|
10
|
+
- Use unique index on \[cron\_key, cron\_at\] columns to prevent duplicate cron jobs from being enqueued [\#423](https://github.com/bensheldon/good_job/pull/423) ([bensheldon](https://github.com/bensheldon))
|
11
|
+
|
12
|
+
**Fixed bugs:**
|
13
|
+
|
14
|
+
- Dashboard fix preservation of `limit` and `queue_name` filter params; add pager to jobs [\#434](https://github.com/bensheldon/good_job/pull/434) ([bensheldon](https://github.com/bensheldon))
|
15
|
+
|
16
|
+
**Closed issues:**
|
17
|
+
|
18
|
+
- PgLock state inspection is not isolated to current database [\#431](https://github.com/bensheldon/good_job/issues/431)
|
19
|
+
- Race condition with concurency control [\#378](https://github.com/bensheldon/good_job/issues/378)
|
20
|
+
|
21
|
+
**Merged pull requests:**
|
22
|
+
|
23
|
+
- Add Readme note about race conditions in Concurrency's `enqueue\_limit` and `perform\_limit [\#433](https://github.com/bensheldon/good_job/pull/433) ([bensheldon](https://github.com/bensheldon))
|
24
|
+
- Test harness should only force-unlock db connections for the current database [\#430](https://github.com/bensheldon/good_job/pull/430) ([bensheldon](https://github.com/bensheldon))
|
25
|
+
|
26
|
+
## [v2.4.2](https://github.com/bensheldon/good_job/tree/v2.4.2) (2021-10-19)
|
27
|
+
|
28
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v2.4.1...v2.4.2)
|
29
|
+
|
30
|
+
**Implemented enhancements:**
|
31
|
+
|
32
|
+
- Add migration version to install/update generator templates [\#426](https://github.com/bensheldon/good_job/pull/426) ([bensheldon](https://github.com/bensheldon))
|
33
|
+
|
34
|
+
**Fixed bugs:**
|
35
|
+
|
36
|
+
- Explicitly unscope queries within block yielded to Lockable.within\_advisory\_lock [\#429](https://github.com/bensheldon/good_job/pull/429) ([bensheldon](https://github.com/bensheldon))
|
37
|
+
- Fix Demo CleanupJob args [\#427](https://github.com/bensheldon/good_job/pull/427) ([bensheldon](https://github.com/bensheldon))
|
38
|
+
|
39
|
+
**Merged pull requests:**
|
40
|
+
|
41
|
+
- Remove v1.99/v2 transitional extra advisory lock [\#428](https://github.com/bensheldon/good_job/pull/428) ([bensheldon](https://github.com/bensheldon))
|
42
|
+
|
43
|
+
## [v2.4.1](https://github.com/bensheldon/good_job/tree/v2.4.1) (2021-10-11)
|
44
|
+
|
45
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v2.4.0...v2.4.1)
|
46
|
+
|
47
|
+
**Implemented enhancements:**
|
48
|
+
|
49
|
+
- Support Datadog APM / `dd-trace-rb` [\#323](https://github.com/bensheldon/good_job/issues/323)
|
50
|
+
- Display info about used timezone. [\#398](https://github.com/bensheldon/good_job/pull/398) ([morgoth](https://github.com/morgoth))
|
51
|
+
- Display cron schedules args in dashboard [\#396](https://github.com/bensheldon/good_job/pull/396) ([aried3r](https://github.com/aried3r))
|
52
|
+
|
53
|
+
**Fixed bugs:**
|
54
|
+
|
55
|
+
- Inline adapter should raise unhandled exceptions during execution [\#416](https://github.com/bensheldon/good_job/pull/416) ([bensheldon](https://github.com/bensheldon))
|
56
|
+
- Enforce english locale in UI [\#407](https://github.com/bensheldon/good_job/pull/407) ([morgoth](https://github.com/morgoth))
|
57
|
+
|
58
|
+
**Closed issues:**
|
59
|
+
|
60
|
+
- Finished jobs don't show up as finished [\#415](https://github.com/bensheldon/good_job/issues/415)
|
61
|
+
- Inline adapter should raise unhandled exceptions during execution [\#410](https://github.com/bensheldon/good_job/issues/410)
|
62
|
+
- Rewrite Scheduler "worker" thread name to be `thread` [\#406](https://github.com/bensheldon/good_job/issues/406)
|
63
|
+
- "WARNING: you don't own a lock of type ExclusiveLock" in Development [\#388](https://github.com/bensheldon/good_job/issues/388)
|
64
|
+
- Improve Readme's "Optimize queues, threads, processes" section [\#132](https://github.com/bensheldon/good_job/issues/132)
|
65
|
+
|
66
|
+
**Merged pull requests:**
|
67
|
+
|
68
|
+
- Ignore Rails HEAD Appraisal until `rails new` fixed [\#419](https://github.com/bensheldon/good_job/pull/419) ([bensheldon](https://github.com/bensheldon))
|
69
|
+
- Warn in Readme that configuration should not go into `config/initializers/*.rb` [\#418](https://github.com/bensheldon/good_job/pull/418) ([bensheldon](https://github.com/bensheldon))
|
70
|
+
- Replace worker wording [\#409](https://github.com/bensheldon/good_job/pull/409) ([Hugo-Hache](https://github.com/Hugo-Hache))
|
71
|
+
- Improve Readme's "Optimize queues, threads, processes" section [\#405](https://github.com/bensheldon/good_job/pull/405) ([Hugo-Hache](https://github.com/Hugo-Hache))
|
72
|
+
- Update GH Test Matrix with more PG versions [\#401](https://github.com/bensheldon/good_job/pull/401) ([tedhexaflow](https://github.com/tedhexaflow))
|
73
|
+
- Extract cron configuration hash into CronEntry ActiveModel objects [\#400](https://github.com/bensheldon/good_job/pull/400) ([bensheldon](https://github.com/bensheldon))
|
74
|
+
- Remove errant copy-paste from app.json [\#397](https://github.com/bensheldon/good_job/pull/397) ([morgoth](https://github.com/morgoth))
|
75
|
+
|
76
|
+
## [v2.4.0](https://github.com/bensheldon/good_job/tree/v2.4.0) (2021-10-02)
|
77
|
+
|
78
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v2.3.1...v2.4.0)
|
79
|
+
|
80
|
+
**Implemented enhancements:**
|
81
|
+
|
82
|
+
- Display schedule time relative to now. [\#394](https://github.com/bensheldon/good_job/pull/394) ([morgoth](https://github.com/morgoth))
|
83
|
+
- Display cron schedules properties in dashboard [\#391](https://github.com/bensheldon/good_job/pull/391) ([aried3r](https://github.com/aried3r))
|
84
|
+
|
85
|
+
**Fixed bugs:**
|
86
|
+
|
87
|
+
- Correct icon for alert flash [\#395](https://github.com/bensheldon/good_job/pull/395) ([morgoth](https://github.com/morgoth))
|
88
|
+
|
3
89
|
## [v2.3.1](https://github.com/bensheldon/good_job/tree/v2.3.1) (2021-09-30)
|
4
90
|
|
5
91
|
[Full Changelog](https://github.com/bensheldon/good_job/compare/v2.3.0...v2.3.1)
|
6
92
|
|
93
|
+
**Fixed bugs:**
|
94
|
+
|
95
|
+
- Wrap Scheduler task execution with Rails `reloader` instead of `executor` to avoid database connection changing during code reload [\#389](https://github.com/bensheldon/good_job/pull/389) ([bensheldon](https://github.com/bensheldon))
|
96
|
+
|
7
97
|
**Merged pull requests:**
|
8
98
|
|
9
99
|
- Log Cleanup thread tests, introduce "Slow" ExampleJob type, refactor ExampleJob types, run cron and log Postgres warnings in GoodJob Development harness [\#390](https://github.com/bensheldon/good_job/pull/390) ([bensheldon](https://github.com/bensheldon))
|
10
|
-
- Wrap Scheduler task execution with Rails `reloader` instead of `executor` to avoid database connection changing during code reload [\#389](https://github.com/bensheldon/good_job/pull/389) ([bensheldon](https://github.com/bensheldon))
|
11
100
|
|
12
101
|
## [v2.3.0](https://github.com/bensheldon/good_job/tree/v2.3.0) (2021-09-25)
|
13
102
|
|
data/README.md
CHANGED
@@ -212,7 +212,11 @@ to delete old records and preserve space in your database.
|
|
212
212
|
|
213
213
|
To use GoodJob, you can set `config.active_job.queue_adapter` to a `:good_job`.
|
214
214
|
|
215
|
-
Additional configuration can be provided via `config.good_job.OPTION =
|
215
|
+
Additional configuration can be provided via `config.good_job.OPTION = ...`.
|
216
|
+
|
217
|
+
_Configuration **must** be placed into `config/application.rb` or `config/environments/{RAILS_ENV}.rb`; configuration may not work correctly if placed into `config/initializers/*.rb` because application initializers run _after_ gem initialization (see [Rails#36650](https://github.com/rails/rails/issues/36650) and [GoodJob#380](https://github.com/bensheldon/good_job/issues/380))._
|
218
|
+
|
219
|
+
Configuration examples:
|
216
220
|
|
217
221
|
```ruby
|
218
222
|
# config/application.rb
|
@@ -359,11 +363,19 @@ class MyJob < ApplicationJob
|
|
359
363
|
total_limit: 1,
|
360
364
|
|
361
365
|
# Or, if more control is needed:
|
362
|
-
# Maximum number of jobs with the concurrency key to be
|
366
|
+
# Maximum number of jobs with the concurrency key to be
|
367
|
+
# concurrently enqueued (excludes performing jobs)
|
363
368
|
enqueue_limit: 2,
|
364
|
-
|
369
|
+
|
370
|
+
# Maximum number of jobs with the concurrency key to be
|
371
|
+
# concurrently performed (excludes enqueued jobs)
|
365
372
|
perform_limit: 1,
|
366
373
|
|
374
|
+
# Note: Under heavy load, the total number of jobs may exceed the
|
375
|
+
# sum of `enqueue_limit` and `perform_limit` because of race conditions
|
376
|
+
# caused by imperfectly disjunctive states. If you need to constrain
|
377
|
+
# the total number of jobs, use `total_limit` instead. See #378.
|
378
|
+
|
367
379
|
# A unique key to be globally locked against.
|
368
380
|
# Can be String or Lambda/Proc that is invoked in the context of the job.
|
369
381
|
# Note: Arguments passed to #perform_later must be accessed through `arguments` method.
|
@@ -387,7 +399,7 @@ job.good_job_concurrency_key #=> "Unique-Alice"
|
|
387
399
|
|
388
400
|
GoodJob can enqueue jobs on a recurring basis that can be used as a replacement for cron.
|
389
401
|
|
390
|
-
Cron-style jobs are run on every GoodJob process (e.g. CLI or `async` execution mode) when `config.good_job.enable_cron = true
|
402
|
+
Cron-style jobs are run on every GoodJob process (e.g. CLI or `async` execution mode) when `config.good_job.enable_cron = true`, but GoodJob's cron uses unique indexes to ensure that only a single job is enqeued at the given time interval.
|
391
403
|
|
392
404
|
Cron-format is parsed by the [`fugit`](https://github.com/floraison/fugit) gem, which has support for seconds-level resolution (e.g. `* * * * * *`).
|
393
405
|
|
@@ -576,43 +588,62 @@ end
|
|
576
588
|
|
577
589
|
By default, GoodJob creates a single thread execution pool that will execute jobs from any queue. Depending on your application's workload, job types, and service level objectives, you may wish to optimize execution resources. For example, providing dedicated execution resources for transactional emails so they are not delayed by long-running batch jobs. Some options:
|
578
590
|
|
579
|
-
- Multiple execution pools within a single process:
|
591
|
+
- Multiple isolated execution pools within a single process:
|
592
|
+
|
593
|
+
For moderate workloads, multiple isolated thread execution pools offers a good balance between congestion management and economy.
|
594
|
+
|
595
|
+
A pool is configured with the following syntax `<participating_queues>:<thread_count>`:
|
596
|
+
|
597
|
+
- `<participating_queues>`: either `queue1,queue2` (only those queues), `*` (all) or `-queue1,queue2` (all except those queues).
|
598
|
+
- `<thread_count>`: a count overriding for this specific pool the global `max-threads`.
|
599
|
+
|
600
|
+
Pool configurations are separated with a semicolon (;) in the `queues` configuration
|
580
601
|
|
581
602
|
```bash
|
582
|
-
$ bundle exec good_job
|
603
|
+
$ bundle exec good_job \
|
604
|
+
--queues="transactional_messages:2;batch_processing:1;-transactional_messages,batch_processing:2;*" \
|
605
|
+
--max-threads=5
|
583
606
|
```
|
584
607
|
|
585
|
-
This configuration will result in a single process with 4 isolated thread execution pools.
|
608
|
+
This configuration will result in a single process with 4 isolated thread execution pools.
|
586
609
|
|
587
|
-
- `transactional_messages:2`: execute jobs enqueued on `transactional_messages
|
588
|
-
- `batch_processing:1` execute jobs enqueued on `batch_processing
|
589
|
-
- `-transactional_messages,batch_processing`: execute jobs enqueued on _any_ queue _excluding_ `transactional_messages` or `batch_processing
|
590
|
-
- `*`: execute jobs on any queue
|
591
|
-
|
592
|
-
For moderate workloads, multiple isolated thread execution pools offers a good balance between congestion management and economy.
|
610
|
+
- `transactional_messages:2`: execute jobs enqueued on `transactional_messages`, with up to 2 threads.
|
611
|
+
- `batch_processing:1` execute jobs enqueued on `batch_processing`, with a single thread.
|
612
|
+
- `-transactional_messages,batch_processing`: execute jobs enqueued on _any_ queue _excluding_ `transactional_messages` or `batch_processing`, with up to 2 threads.
|
613
|
+
- `*`: execute jobs on any queue, with up to 5 threads (as configured by `--max-threads=5`).
|
593
614
|
|
594
615
|
Configuration can be injected by environment variables too:
|
595
616
|
|
596
617
|
```bash
|
597
|
-
$ GOOD_JOB_QUEUES="transactional_messages:2;batch_processing:1;-transactional_messages,batch_processing:2;*"
|
618
|
+
$ GOOD_JOB_QUEUES="transactional_messages:2;batch_processing:1;-transactional_messages,batch_processing:2;*" \
|
619
|
+
GOOD_JOB_MAX_THREADS=5 \
|
620
|
+
bundle exec good_job
|
598
621
|
```
|
599
622
|
|
600
|
-
- Multiple processes
|
623
|
+
- Multiple processes:
|
624
|
+
|
625
|
+
While multiple isolated thread execution pools offer a way to provide dedicated execution resources, those resources are bound to a single machine. To scale them independently, define several processes.
|
626
|
+
|
627
|
+
For example, this configuration on Heroku allows to customize the dyno count (instances), or type (CPU/RAM), per process type:
|
601
628
|
|
602
629
|
```procfile
|
603
630
|
# Procfile
|
604
631
|
|
605
|
-
# Separate
|
632
|
+
# Separate process types
|
606
633
|
worker: bundle exec good_job --max-threads=5
|
607
634
|
transactional_worker: bundle exec good_job --queues="transactional_messages" --max-threads=2
|
608
635
|
batch_worker: bundle exec good_job --queues="batch_processing" --max-threads=1
|
636
|
+
```
|
637
|
+
|
638
|
+
To optimize for CPU performance at the expense of greater memory and system resource usage, while keeping a single process type (and thus a single dyno), combine several processes and wait for them:
|
609
639
|
|
610
|
-
|
640
|
+
```procfile
|
641
|
+
# Procfile
|
642
|
+
|
643
|
+
# Combined multi-process
|
611
644
|
combined_worker: bundle exec good_job --max-threads=5 & bundle exec good_job --queues="transactional_messages" --max-threads=2 & bundle exec good_job --queues="batch_processing" --max-threads=1 & wait -n
|
612
645
|
```
|
613
646
|
|
614
|
-
Running multiple processes can optimize for CPU performance at the expense of greater memory and system resource usage.
|
615
|
-
|
616
647
|
Keep in mind, queue operations and management is an advanced discipline. This stuff is complex, especially for heavy workloads and unique processing requirements. Good job 👍
|
617
648
|
|
618
649
|
### Database connections
|
@@ -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
|
@@ -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,6 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module GoodJob
|
3
3
|
class BaseFilter
|
4
|
+
DEFAULT_LIMIT = 25
|
5
|
+
|
4
6
|
attr_accessor :params
|
5
7
|
|
6
8
|
def initialize(params)
|
@@ -13,7 +15,7 @@ module GoodJob
|
|
13
15
|
filtered_query.display_all(
|
14
16
|
after_scheduled_at: after_scheduled_at,
|
15
17
|
after_id: params[:after_id]
|
16
|
-
).limit(params.fetch(:limit,
|
18
|
+
).limit(params.fetch(:limit, DEFAULT_LIMIT))
|
17
19
|
end
|
18
20
|
|
19
21
|
def last
|
@@ -38,8 +40,10 @@ module GoodJob
|
|
38
40
|
|
39
41
|
def to_params(override)
|
40
42
|
{
|
41
|
-
state: params[:state],
|
42
43
|
job_class: params[:job_class],
|
44
|
+
limit: params[:limit],
|
45
|
+
queue_name: params[:queue_name],
|
46
|
+
state: params[:state],
|
43
47
|
}.merge(override).delete_if { |_, v| v.nil? }
|
44
48
|
end
|
45
49
|
|
@@ -19,7 +19,9 @@ module GoodJob
|
|
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
|
|
@@ -1,5 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module GoodJob
|
3
3
|
module ApplicationHelper
|
4
|
+
def relative_time(timestamp)
|
5
|
+
text = timestamp.future? ? "in #{time_ago_in_words(timestamp)}" : "#{time_ago_in_words(timestamp)} ago"
|
6
|
+
tag.time(text, datetime: timestamp, title: timestamp)
|
7
|
+
end
|
4
8
|
end
|
5
9
|
end
|
@@ -1,13 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module GoodJob
|
3
3
|
# ActiveRecord model that represents an +ActiveJob+ job.
|
4
|
-
#
|
4
|
+
# There is not a table in the database whose discrete rows represents "Jobs".
|
5
|
+
# The +good_jobs+ table is a table of individual {GoodJob::Execution}s that share the same +active_job_id+.
|
6
|
+
# A single row from the +good_jobs+ table of executions is fetched to represent an ActiveJobJob
|
5
7
|
# Parent class can be configured with +GoodJob.active_record_parent_class+.
|
6
8
|
# @!parse
|
7
9
|
# class ActiveJob < ActiveRecord::Base; end
|
8
10
|
class ActiveJobJob < Object.const_get(GoodJob.active_record_parent_class)
|
9
11
|
include GoodJob::Lockable
|
10
12
|
|
13
|
+
# Raised when an inappropriate action is applied to a Job based on its state.
|
14
|
+
ActionForStateMismatchError = Class.new(StandardError)
|
15
|
+
# Raised when an action requires GoodJob to be the ActiveJob Queue Adapter but GoodJob is not.
|
16
|
+
AdapterNotGoodJobError = Class.new(StandardError)
|
17
|
+
# Attached to a Job's Execution when the Job is discarded.
|
18
|
+
DiscardJobError = Class.new(StandardError)
|
19
|
+
|
11
20
|
self.table_name = 'good_jobs'
|
12
21
|
self.primary_key = 'active_job_id'
|
13
22
|
self.advisory_lockable_column = 'active_job_id'
|
@@ -56,27 +65,41 @@ module GoodJob
|
|
56
65
|
query
|
57
66
|
end)
|
58
67
|
|
68
|
+
# The job's ActiveJob UUID
|
69
|
+
# @return [String]
|
59
70
|
def id
|
60
71
|
active_job_id
|
61
72
|
end
|
62
73
|
|
63
|
-
|
64
|
-
|
65
|
-
end
|
66
|
-
|
74
|
+
# The ActiveJob job class, as a string
|
75
|
+
# @return [String]
|
67
76
|
def job_class
|
68
77
|
serialized_params['job_class']
|
69
78
|
end
|
70
79
|
|
80
|
+
# The status of the Job, based on the state of its most recent execution.
|
81
|
+
# There are 3 buckets of non-overlapping statuses:
|
82
|
+
# 1. The job will be executed
|
83
|
+
# - queued: The job will execute immediately when an execution thread becomes available.
|
84
|
+
# - scheduled: The job is scheduled to execute in the future.
|
85
|
+
# - retried: The job previously errored on execution and will be re-executed in the future.
|
86
|
+
# 2. The job is being executed
|
87
|
+
# - running: the job is actively being executed by an execution thread
|
88
|
+
# 3. The job will not execute
|
89
|
+
# - finished: The job executed successfully
|
90
|
+
# - discarded: The job previously errored on execution and will not be re-executed in the future.
|
91
|
+
#
|
92
|
+
# @return [Symbol]
|
71
93
|
def status
|
72
|
-
|
73
|
-
|
94
|
+
execution = head_execution
|
95
|
+
if execution.finished_at.present?
|
96
|
+
if execution.error.present?
|
74
97
|
:discarded
|
75
98
|
else
|
76
99
|
:finished
|
77
100
|
end
|
78
|
-
elsif (scheduled_at || created_at) > DateTime.current
|
79
|
-
if serialized_params.fetch('executions', 0) > 1
|
101
|
+
elsif (execution.scheduled_at || execution.created_at) > DateTime.current
|
102
|
+
if execution.serialized_params.fetch('executions', 0) > 1
|
80
103
|
:retried
|
81
104
|
else
|
82
105
|
:scheduled
|
@@ -88,16 +111,25 @@ module GoodJob
|
|
88
111
|
end
|
89
112
|
end
|
90
113
|
|
91
|
-
|
114
|
+
# This job's most recent {Execution}
|
115
|
+
# @param reload [Booelan] whether to reload executions
|
116
|
+
# @return [Execution]
|
117
|
+
def head_execution(reload: false)
|
118
|
+
executions.reload if reload
|
119
|
+
executions.load # memoize the results
|
92
120
|
executions.last
|
93
121
|
end
|
94
122
|
|
123
|
+
# This job's initial/oldest {Execution}
|
124
|
+
# @return [Execution]
|
95
125
|
def tail_execution
|
96
126
|
executions.first
|
97
127
|
end
|
98
128
|
|
129
|
+
# The number of times this job has been executed, according to ActiveJob's serialized state.
|
130
|
+
# @return [Numeric]
|
99
131
|
def executions_count
|
100
|
-
aj_count = serialized_params.fetch('executions', 0)
|
132
|
+
aj_count = head_execution.serialized_params.fetch('executions', 0)
|
101
133
|
# The execution count within serialized_params is not updated
|
102
134
|
# once the underlying execution has been executed.
|
103
135
|
if status.in? [:discarded, :finished, :running]
|
@@ -107,14 +139,21 @@ module GoodJob
|
|
107
139
|
end
|
108
140
|
end
|
109
141
|
|
142
|
+
# The number of times this job has been executed, according to the number of GoodJob {Execution} records.
|
143
|
+
# @return [Numeric]
|
110
144
|
def preserved_executions_count
|
111
145
|
executions.size
|
112
146
|
end
|
113
147
|
|
148
|
+
# The most recent error message.
|
149
|
+
# If the job has been retried, the error will be fetched from the previous {Execution} record.
|
150
|
+
# @return [String]
|
114
151
|
def recent_error
|
115
|
-
error
|
152
|
+
head_execution.error || executions[-2]&.error
|
116
153
|
end
|
117
154
|
|
155
|
+
# Tests whether the job is being executed right now.
|
156
|
+
# @return [Boolean]
|
118
157
|
def running?
|
119
158
|
# Avoid N+1 Query: `.joins_advisory_locks.select('good_jobs.*', 'pg_locks.locktype AS locktype')`
|
120
159
|
if has_attribute?(:locktype)
|
@@ -123,5 +162,84 @@ module GoodJob
|
|
123
162
|
advisory_locked?
|
124
163
|
end
|
125
164
|
end
|
165
|
+
|
166
|
+
# Retry a job that has errored and been discarded.
|
167
|
+
# This action will create a new job {Execution} record.
|
168
|
+
# @return [ActiveJob::Base]
|
169
|
+
def retry_job
|
170
|
+
with_advisory_lock do
|
171
|
+
execution = head_execution(reload: true)
|
172
|
+
active_job = execution.active_job
|
173
|
+
|
174
|
+
raise AdapterNotGoodJobError unless active_job.class.queue_adapter.is_a? GoodJob::Adapter
|
175
|
+
raise ActionForStateMismatchError unless status == :discarded
|
176
|
+
|
177
|
+
# Update the executions count because the previous execution will not have been preserved
|
178
|
+
# Do not update `exception_executions` because that comes from rescue_from's arguments
|
179
|
+
active_job.executions = (active_job.executions || 0) + 1
|
180
|
+
|
181
|
+
new_active_job = nil
|
182
|
+
GoodJob::CurrentThread.within do |current_thread|
|
183
|
+
current_thread.execution = execution
|
184
|
+
|
185
|
+
execution.class.transaction(joinable: false, requires_new: true) do
|
186
|
+
new_active_job = active_job.retry_job(wait: 0, error: error)
|
187
|
+
execution.save
|
188
|
+
end
|
189
|
+
end
|
190
|
+
new_active_job
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# Discard a job so that it will not be executed further.
|
195
|
+
# This action will add a {DiscardJobError} to the job's {Execution} and mark it as finished.
|
196
|
+
# @return [void]
|
197
|
+
def discard_job(message)
|
198
|
+
with_advisory_lock do
|
199
|
+
raise ActionForStateMismatchError unless status.in? [:scheduled, :queued, :retried]
|
200
|
+
|
201
|
+
execution = head_execution(reload: true)
|
202
|
+
active_job = execution.active_job
|
203
|
+
|
204
|
+
job_error = GoodJob::ActiveJobJob::DiscardJobError.new(message)
|
205
|
+
|
206
|
+
update_execution = proc do
|
207
|
+
execution.update(
|
208
|
+
finished_at: Time.current,
|
209
|
+
error: [job_error.class, GoodJob::Execution::ERROR_MESSAGE_SEPARATOR, job_error.message].join
|
210
|
+
)
|
211
|
+
end
|
212
|
+
|
213
|
+
if active_job.respond_to?(:instrument)
|
214
|
+
active_job.send :instrument, :discard, error: job_error, &update_execution
|
215
|
+
else
|
216
|
+
update_execution.call
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
# Reschedule a scheduled job so that it executes immediately (or later) by the next available execution thread.
|
222
|
+
# @param scheduled_at [DateTime, Time] When to reschedule the job
|
223
|
+
# @return [void]
|
224
|
+
def reschedule_job(scheduled_at = Time.current)
|
225
|
+
with_advisory_lock do
|
226
|
+
raise ActionForStateMismatchError unless status.in? [:scheduled, :queued, :retried]
|
227
|
+
|
228
|
+
execution = head_execution(reload: true)
|
229
|
+
execution.update(scheduled_at: scheduled_at)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Utility method to determine which execution record is used to represent this job
|
234
|
+
# @return [String]
|
235
|
+
def _execution_id
|
236
|
+
attributes['id']
|
237
|
+
end
|
238
|
+
|
239
|
+
# Utility method to test whether this job's underlying attributes represents its most recent execution.
|
240
|
+
# @return [Boolean]
|
241
|
+
def _head?
|
242
|
+
_execution_id == head_execution(reload: true).id
|
243
|
+
end
|
126
244
|
end
|
127
245
|
end
|
@@ -1,22 +1,66 @@
|
|
1
|
-
<% if @
|
1
|
+
<% if @cron_entries.present? %>
|
2
2
|
<div class="card my-3">
|
3
3
|
<div class="table-responsive">
|
4
4
|
<table class="table card-table table-bordered table-hover table-sm mb-0">
|
5
5
|
<thead>
|
6
6
|
<th>Cron Job Name</th>
|
7
7
|
<th>Configuration</th>
|
8
|
+
<th>
|
9
|
+
Set
|
10
|
+
<%= tag.button "Toggle", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
|
11
|
+
data: { bs_toggle: "collapse", bs_target: ".job-properties" },
|
12
|
+
aria: { expanded: false, controls: @cron_entries.map { |cron_entry| dom_id(cron_entry, 'properties') }.join(" ") }
|
13
|
+
%>
|
14
|
+
</th>
|
15
|
+
<th>
|
16
|
+
Args
|
17
|
+
<%= tag.button "Toggle", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
|
18
|
+
data: { bs_toggle: "collapse", bs_target: ".job-args" },
|
19
|
+
aria: { expanded: false, controls: @cron_entries.map { |cron_entry| dom_id(cron_entry, 'args') }.join(" ") }
|
20
|
+
%>
|
21
|
+
</th>
|
8
22
|
<th>Class</th>
|
9
23
|
<th>Description</th>
|
10
24
|
<th>Next scheduled</th>
|
11
25
|
</thead>
|
12
26
|
<tbody>
|
13
|
-
<% @
|
27
|
+
<% @cron_entries.each do |cron_entry| %>
|
14
28
|
<tr>
|
15
|
-
<td class="font-monospace"><%=
|
16
|
-
<td class="font-monospace"><%=
|
17
|
-
<td
|
18
|
-
|
19
|
-
|
29
|
+
<td class="font-monospace"><%= cron_entry.key %></td>
|
30
|
+
<td class="font-monospace"><%= cron_entry.cron %></td>
|
31
|
+
<td>
|
32
|
+
<%=
|
33
|
+
case cron_entry.set
|
34
|
+
when NilClass
|
35
|
+
"None"
|
36
|
+
when Proc
|
37
|
+
"Lambda/Callable"
|
38
|
+
when Hash
|
39
|
+
tag.button("Preview", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
|
40
|
+
data: { bs_toggle: "collapse", bs_target: "##{dom_id(cron_entry, 'properties')}" },
|
41
|
+
aria: { expanded: false, controls: dom_id(cron_entry, 'properties') }) +
|
42
|
+
tag.pre(JSON.pretty_generate(cron_entry.set), id: dom_id(cron_entry, 'properties'), class: "collapse job-properties")
|
43
|
+
end
|
44
|
+
%>
|
45
|
+
</td>
|
46
|
+
<td>
|
47
|
+
<%=
|
48
|
+
case cron_entry.args
|
49
|
+
when NilClass
|
50
|
+
"None"
|
51
|
+
when Proc
|
52
|
+
"Lambda/Callable"
|
53
|
+
when Hash
|
54
|
+
tag.button("Preview", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
|
55
|
+
data: { bs_toggle: "collapse", bs_target: "##{dom_id(cron_entry, 'args')}" },
|
56
|
+
aria: { expanded: false, controls: dom_id(cron_entry, 'args') }) +
|
57
|
+
tag.pre(JSON.pretty_generate(cron_entry.args), id: dom_id(cron_entry, 'args'), class: "collapse job-args")
|
58
|
+
end
|
59
|
+
%>
|
60
|
+
</td>
|
61
|
+
<td class="font-monospace"><%= cron_entry.job_class %></td>
|
62
|
+
<td><%= cron_entry.description %></td>
|
63
|
+
<td><%= cron_entry.next_at %></td>
|
20
64
|
</tr>
|
21
65
|
<% end %>
|
22
66
|
</tbody>
|