good_job 2.2.0 → 2.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +73 -0
- data/README.md +39 -16
- data/engine/app/controllers/good_job/base_controller.rb +8 -0
- data/engine/app/controllers/good_job/cron_schedules_controller.rb +1 -2
- data/engine/app/controllers/good_job/executions_controller.rb +5 -1
- data/engine/app/controllers/good_job/{active_jobs_controller.rb → jobs_controller.rb} +6 -2
- data/engine/app/filters/good_job/base_filter.rb +101 -0
- data/engine/app/filters/good_job/executions_filter.rb +40 -0
- data/engine/app/filters/good_job/jobs_filter.rb +46 -0
- data/engine/app/helpers/good_job/application_helper.rb +4 -0
- data/engine/app/models/good_job/active_job_job.rb +127 -0
- data/engine/app/views/good_job/cron_schedules/index.html.erb +51 -7
- data/engine/app/views/good_job/executions/index.html.erb +21 -0
- data/engine/app/views/good_job/jobs/index.html.erb +7 -0
- data/engine/app/views/good_job/{active_jobs → jobs}/show.html.erb +0 -0
- data/engine/app/views/good_job/shared/_executions_table.erb +3 -3
- data/engine/app/views/good_job/shared/_filter.erb +52 -0
- data/engine/app/views/good_job/shared/_jobs_table.erb +56 -0
- data/engine/app/views/layouts/good_job/base.html.erb +5 -1
- data/engine/config/routes.rb +2 -2
- data/lib/good_job/adapter.rb +6 -4
- data/lib/good_job/cli.rb +3 -1
- data/lib/good_job/configuration.rb +4 -0
- data/lib/good_job/cron_entry.rb +65 -0
- data/lib/good_job/cron_manager.rb +18 -30
- data/lib/good_job/log_subscriber.rb +3 -3
- data/lib/good_job/scheduler.rb +2 -1
- data/lib/good_job/version.rb +1 -1
- metadata +13 -6
- data/engine/app/controllers/good_job/dashboards_controller.rb +0 -107
- data/engine/app/views/good_job/dashboards/index.html.erb +0 -54
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a0546a16ecd5760e3ca0dd5097ec950767b5993ac613489d5591d5d44eadc021
|
4
|
+
data.tar.gz: 9e21d8930806b1843d4c32abef233f89f48292229386da593ba82b3a94617eae
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6e3aa1deaa9973f0d435bc6eb77e4557f509a5e9dbd38e267a4268987561576323eda9bfb8bfa3af5ad7aec72c51b17f88121f7d202b1455c1ea17c8272cb6de
|
7
|
+
data.tar.gz: b812a3371f589db1464f3dabe35badee94bf2a8f132e68e64c438e8ddde4415598d133f2e956d335e021a0ea82b59fbc61c3eb77cab230be760516dbca270402
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,78 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [v2.4.1](https://github.com/bensheldon/good_job/tree/v2.4.1) (2021-10-11)
|
4
|
+
|
5
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v2.4.0...v2.4.1)
|
6
|
+
|
7
|
+
**Implemented enhancements:**
|
8
|
+
|
9
|
+
- Support Datadog APM / `dd-trace-rb` [\#323](https://github.com/bensheldon/good_job/issues/323)
|
10
|
+
- Display info about used timezone. [\#398](https://github.com/bensheldon/good_job/pull/398) ([morgoth](https://github.com/morgoth))
|
11
|
+
- Display cron schedules args in dashboard [\#396](https://github.com/bensheldon/good_job/pull/396) ([aried3r](https://github.com/aried3r))
|
12
|
+
|
13
|
+
**Fixed bugs:**
|
14
|
+
|
15
|
+
- Inline adapter should raise unhandled exceptions during execution [\#416](https://github.com/bensheldon/good_job/pull/416) ([bensheldon](https://github.com/bensheldon))
|
16
|
+
- Enforce english locale in UI [\#407](https://github.com/bensheldon/good_job/pull/407) ([morgoth](https://github.com/morgoth))
|
17
|
+
|
18
|
+
**Closed issues:**
|
19
|
+
|
20
|
+
- Finished jobs don't show up as finished [\#415](https://github.com/bensheldon/good_job/issues/415)
|
21
|
+
- Inline adapter should raise unhandled exceptions during execution [\#410](https://github.com/bensheldon/good_job/issues/410)
|
22
|
+
- Rewrite Scheduler "worker" thread name to be `thread` [\#406](https://github.com/bensheldon/good_job/issues/406)
|
23
|
+
- "WARNING: you don't own a lock of type ExclusiveLock" in Development [\#388](https://github.com/bensheldon/good_job/issues/388)
|
24
|
+
- Improve Readme's "Optimize queues, threads, processes" section [\#132](https://github.com/bensheldon/good_job/issues/132)
|
25
|
+
|
26
|
+
**Merged pull requests:**
|
27
|
+
|
28
|
+
- Ignore Rails HEAD Appraisal until `rails new` fixed [\#419](https://github.com/bensheldon/good_job/pull/419) ([bensheldon](https://github.com/bensheldon))
|
29
|
+
- 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))
|
30
|
+
- Replace worker wording [\#409](https://github.com/bensheldon/good_job/pull/409) ([Hugo-Hache](https://github.com/Hugo-Hache))
|
31
|
+
- Improve Readme's "Optimize queues, threads, processes" section [\#405](https://github.com/bensheldon/good_job/pull/405) ([Hugo-Hache](https://github.com/Hugo-Hache))
|
32
|
+
- Update GH Test Matrix with more PG versions [\#401](https://github.com/bensheldon/good_job/pull/401) ([tedhexaflow](https://github.com/tedhexaflow))
|
33
|
+
- Extract cron configuration hash into CronEntry ActiveModel objects [\#400](https://github.com/bensheldon/good_job/pull/400) ([bensheldon](https://github.com/bensheldon))
|
34
|
+
- Remove errant copy-paste from app.json [\#397](https://github.com/bensheldon/good_job/pull/397) ([morgoth](https://github.com/morgoth))
|
35
|
+
|
36
|
+
## [v2.4.0](https://github.com/bensheldon/good_job/tree/v2.4.0) (2021-10-02)
|
37
|
+
|
38
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v2.3.1...v2.4.0)
|
39
|
+
|
40
|
+
**Implemented enhancements:**
|
41
|
+
|
42
|
+
- Display schedule time relative to now. [\#394](https://github.com/bensheldon/good_job/pull/394) ([morgoth](https://github.com/morgoth))
|
43
|
+
- Display cron schedules properties in dashboard [\#391](https://github.com/bensheldon/good_job/pull/391) ([aried3r](https://github.com/aried3r))
|
44
|
+
|
45
|
+
**Fixed bugs:**
|
46
|
+
|
47
|
+
- Correct icon for alert flash [\#395](https://github.com/bensheldon/good_job/pull/395) ([morgoth](https://github.com/morgoth))
|
48
|
+
|
49
|
+
## [v2.3.1](https://github.com/bensheldon/good_job/tree/v2.3.1) (2021-09-30)
|
50
|
+
|
51
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v2.3.0...v2.3.1)
|
52
|
+
|
53
|
+
**Fixed bugs:**
|
54
|
+
|
55
|
+
- 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))
|
56
|
+
|
57
|
+
**Merged pull requests:**
|
58
|
+
|
59
|
+
- 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))
|
60
|
+
|
61
|
+
## [v2.3.0](https://github.com/bensheldon/good_job/tree/v2.3.0) (2021-09-25)
|
62
|
+
|
63
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v2.2.0...v2.3.0)
|
64
|
+
|
65
|
+
**Implemented enhancements:**
|
66
|
+
|
67
|
+
- Create an ActiveJobJob model and Dashboard [\#383](https://github.com/bensheldon/good_job/pull/383) ([bensheldon](https://github.com/bensheldon))
|
68
|
+
- Preserve page filter when deleting execution [\#381](https://github.com/bensheldon/good_job/pull/381) ([morgoth](https://github.com/morgoth))
|
69
|
+
|
70
|
+
**Merged pull requests:**
|
71
|
+
|
72
|
+
- Update GH Test Matrix with latest JRuby 9.3.0.0 [\#387](https://github.com/bensheldon/good_job/pull/387) ([tedhexaflow](https://github.com/tedhexaflow))
|
73
|
+
- Improve test support's ShellOut command's process termination and add test logs [\#385](https://github.com/bensheldon/good_job/pull/385) ([bensheldon](https://github.com/bensheldon))
|
74
|
+
- @bensheldon Add Rails 7 alpha to Appraisal; update development dependencies [\#384](https://github.com/bensheldon/good_job/pull/384) ([bensheldon](https://github.com/bensheldon))
|
75
|
+
|
3
76
|
## [v2.2.0](https://github.com/bensheldon/good_job/tree/v2.2.0) (2021-09-15)
|
4
77
|
|
5
78
|
[Full Changelog](https://github.com/bensheldon/good_job/compare/v2.1.0...v2.2.0)
|
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
|
@@ -576,43 +580,62 @@ end
|
|
576
580
|
|
577
581
|
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
582
|
|
579
|
-
- Multiple execution pools within a single process:
|
583
|
+
- Multiple isolated execution pools within a single process:
|
584
|
+
|
585
|
+
For moderate workloads, multiple isolated thread execution pools offers a good balance between congestion management and economy.
|
586
|
+
|
587
|
+
A pool is configured with the following syntax `<participating_queues>:<thread_count>`:
|
588
|
+
|
589
|
+
- `<participating_queues>`: either `queue1,queue2` (only those queues), `*` (all) or `-queue1,queue2` (all except those queues).
|
590
|
+
- `<thread_count>`: a count overriding for this specific pool the global `max-threads`.
|
591
|
+
|
592
|
+
Pool configurations are separated with a semicolon (;) in the `queues` configuration
|
580
593
|
|
581
594
|
```bash
|
582
|
-
$ bundle exec good_job
|
595
|
+
$ bundle exec good_job \
|
596
|
+
--queues="transactional_messages:2;batch_processing:1;-transactional_messages,batch_processing:2;*" \
|
597
|
+
--max-threads=5
|
583
598
|
```
|
584
599
|
|
585
|
-
This configuration will result in a single process with 4 isolated thread execution pools.
|
600
|
+
This configuration will result in a single process with 4 isolated thread execution pools.
|
586
601
|
|
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.
|
602
|
+
- `transactional_messages:2`: execute jobs enqueued on `transactional_messages`, with up to 2 threads.
|
603
|
+
- `batch_processing:1` execute jobs enqueued on `batch_processing`, with a single thread.
|
604
|
+
- `-transactional_messages,batch_processing`: execute jobs enqueued on _any_ queue _excluding_ `transactional_messages` or `batch_processing`, with up to 2 threads.
|
605
|
+
- `*`: execute jobs on any queue, with up to 5 threads (as configured by `--max-threads=5`).
|
593
606
|
|
594
607
|
Configuration can be injected by environment variables too:
|
595
608
|
|
596
609
|
```bash
|
597
|
-
$ GOOD_JOB_QUEUES="transactional_messages:2;batch_processing:1;-transactional_messages,batch_processing:2;*"
|
610
|
+
$ GOOD_JOB_QUEUES="transactional_messages:2;batch_processing:1;-transactional_messages,batch_processing:2;*" \
|
611
|
+
GOOD_JOB_MAX_THREADS=5 \
|
612
|
+
bundle exec good_job
|
598
613
|
```
|
599
614
|
|
600
|
-
- Multiple processes
|
615
|
+
- Multiple processes:
|
616
|
+
|
617
|
+
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.
|
618
|
+
|
619
|
+
For example, this configuration on Heroku allows to customize the dyno count (instances), or type (CPU/RAM), per process type:
|
601
620
|
|
602
621
|
```procfile
|
603
622
|
# Procfile
|
604
623
|
|
605
|
-
# Separate
|
624
|
+
# Separate process types
|
606
625
|
worker: bundle exec good_job --max-threads=5
|
607
626
|
transactional_worker: bundle exec good_job --queues="transactional_messages" --max-threads=2
|
608
627
|
batch_worker: bundle exec good_job --queues="batch_processing" --max-threads=1
|
628
|
+
```
|
629
|
+
|
630
|
+
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
631
|
|
610
|
-
|
632
|
+
```procfile
|
633
|
+
# Procfile
|
634
|
+
|
635
|
+
# Combined multi-process
|
611
636
|
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
637
|
```
|
613
638
|
|
614
|
-
Running multiple processes can optimize for CPU performance at the expense of greater memory and system resource usage.
|
615
|
-
|
616
639
|
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
640
|
|
618
641
|
### 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,10 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
|
3
2
|
module GoodJob
|
4
3
|
class CronSchedulesController < GoodJob::BaseController
|
5
4
|
def index
|
6
5
|
configuration = GoodJob::Configuration.new({})
|
7
|
-
@
|
6
|
+
@cron_entries = configuration.cron_entries
|
8
7
|
end
|
9
8
|
end
|
10
9
|
end
|
@@ -1,10 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module GoodJob
|
3
3
|
class ExecutionsController < GoodJob::BaseController
|
4
|
+
def index
|
5
|
+
@filter = ExecutionsFilter.new(params)
|
6
|
+
end
|
7
|
+
|
4
8
|
def destroy
|
5
9
|
deleted_count = GoodJob::Execution.where(id: params[:id]).delete_all
|
6
10
|
message = deleted_count.positive? ? { notice: "Job execution deleted" } : { alert: "Job execution not deleted" }
|
7
|
-
|
11
|
+
redirect_back fallback_location: root_path, **message
|
8
12
|
end
|
9
13
|
end
|
10
14
|
end
|
@@ -1,10 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module GoodJob
|
3
|
-
class
|
3
|
+
class JobsController < GoodJob::BaseController
|
4
|
+
def index
|
5
|
+
@filter = JobsFilter.new(params)
|
6
|
+
end
|
7
|
+
|
4
8
|
def show
|
5
9
|
@executions = GoodJob::Execution.active_job_id(params[:id])
|
6
10
|
.order(Arel.sql("COALESCE(scheduled_at, created_at) DESC"))
|
7
|
-
|
11
|
+
redirect_to root_path, alert: "Executions for Active Job #{params[:id]} not found" if @executions.empty?
|
8
12
|
end
|
9
13
|
end
|
10
14
|
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GoodJob
|
3
|
+
class BaseFilter
|
4
|
+
attr_accessor :params
|
5
|
+
|
6
|
+
def initialize(params)
|
7
|
+
@params = params
|
8
|
+
end
|
9
|
+
|
10
|
+
def records
|
11
|
+
after_scheduled_at = params[:after_scheduled_at].present? ? Time.zone.parse(params[:after_scheduled_at]) : nil
|
12
|
+
|
13
|
+
filtered_query.display_all(
|
14
|
+
after_scheduled_at: after_scheduled_at,
|
15
|
+
after_id: params[:after_id]
|
16
|
+
).limit(params.fetch(:limit, 25))
|
17
|
+
end
|
18
|
+
|
19
|
+
def last
|
20
|
+
@_last ||= records.last
|
21
|
+
end
|
22
|
+
|
23
|
+
def job_classes
|
24
|
+
base_query.group("serialized_params->>'job_class'").count
|
25
|
+
.sort_by { |name, _count| name }
|
26
|
+
.to_h
|
27
|
+
end
|
28
|
+
|
29
|
+
def queues
|
30
|
+
base_query.group(:queue_name).count
|
31
|
+
.sort_by { |name, _count| name }
|
32
|
+
.to_h
|
33
|
+
end
|
34
|
+
|
35
|
+
def states
|
36
|
+
raise NotImplementedError
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_params(override)
|
40
|
+
{
|
41
|
+
state: params[:state],
|
42
|
+
job_class: params[:job_class],
|
43
|
+
}.merge(override).delete_if { |_, v| v.nil? }
|
44
|
+
end
|
45
|
+
|
46
|
+
def chart_data
|
47
|
+
count_query = Arel.sql(GoodJob::Execution.pg_or_jdbc_query(<<~SQL.squish))
|
48
|
+
SELECT *
|
49
|
+
FROM generate_series(
|
50
|
+
date_trunc('hour', $1::timestamp),
|
51
|
+
date_trunc('hour', $2::timestamp),
|
52
|
+
'1 hour'
|
53
|
+
) timestamp
|
54
|
+
LEFT JOIN (
|
55
|
+
SELECT
|
56
|
+
date_trunc('hour', scheduled_at) AS scheduled_at,
|
57
|
+
queue_name,
|
58
|
+
count(*) AS count
|
59
|
+
FROM (
|
60
|
+
#{filtered_query.except(:select).select('queue_name', 'COALESCE(good_jobs.scheduled_at, good_jobs.created_at)::timestamp AS scheduled_at').to_sql}
|
61
|
+
) sources
|
62
|
+
GROUP BY date_trunc('hour', scheduled_at), queue_name
|
63
|
+
) sources ON sources.scheduled_at = timestamp
|
64
|
+
ORDER BY timestamp ASC
|
65
|
+
SQL
|
66
|
+
|
67
|
+
current_time = Time.current
|
68
|
+
binds = [[nil, current_time - 1.day], [nil, current_time]]
|
69
|
+
executions_data = GoodJob::Execution.connection.exec_query(count_query, "GoodJob Dashboard Chart", binds)
|
70
|
+
|
71
|
+
queue_names = executions_data.map { |d| d['queue_name'] }.uniq
|
72
|
+
labels = []
|
73
|
+
queues_data = executions_data.to_a.group_by { |d| d['timestamp'] }.each_with_object({}) do |(timestamp, values), hash|
|
74
|
+
labels << timestamp.in_time_zone.strftime('%H:%M %z')
|
75
|
+
queue_names.each do |queue_name|
|
76
|
+
(hash[queue_name] ||= []) << values.find { |d| d['queue_name'] == queue_name }&.[]('count')
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
{
|
81
|
+
labels: labels,
|
82
|
+
series: queues_data.map do |queue, data|
|
83
|
+
{
|
84
|
+
name: queue,
|
85
|
+
data: data,
|
86
|
+
}
|
87
|
+
end,
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def base_query
|
94
|
+
raise NotImplementedError
|
95
|
+
end
|
96
|
+
|
97
|
+
def filtered_query
|
98
|
+
raise NotImplementedError
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GoodJob
|
3
|
+
class ExecutionsFilter < BaseFilter
|
4
|
+
def states
|
5
|
+
{
|
6
|
+
'finished' => base_query.finished.count,
|
7
|
+
'unfinished' => base_query.unfinished.count,
|
8
|
+
'running' => base_query.running.count,
|
9
|
+
'errors' => base_query.where.not(error: nil).count,
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def base_query
|
16
|
+
GoodJob::Execution.all
|
17
|
+
end
|
18
|
+
|
19
|
+
def filtered_query
|
20
|
+
query = base_query
|
21
|
+
query = query.job_class(params[:job_class]) if params[:job_class]
|
22
|
+
query = query.where(queue_name: params[:queue_name]) if params[:queue_name]
|
23
|
+
|
24
|
+
if params[:state]
|
25
|
+
case params[:state]
|
26
|
+
when 'finished'
|
27
|
+
query = query.finished
|
28
|
+
when 'unfinished'
|
29
|
+
query = query.unfinished
|
30
|
+
when 'running'
|
31
|
+
query = query.running
|
32
|
+
when 'errors'
|
33
|
+
query = query.where.not(error: nil)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
query
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GoodJob
|
3
|
+
class JobsFilter < BaseFilter
|
4
|
+
def states
|
5
|
+
{
|
6
|
+
'scheduled' => base_query.scheduled.count,
|
7
|
+
'retried' => base_query.retried.count,
|
8
|
+
'queued' => base_query.queued.count,
|
9
|
+
'running' => base_query.running.count,
|
10
|
+
'finished' => base_query.finished.count,
|
11
|
+
'discarded' => base_query.discarded.count,
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def base_query
|
18
|
+
GoodJob::ActiveJobJob.all
|
19
|
+
end
|
20
|
+
|
21
|
+
def filtered_query
|
22
|
+
query = base_query
|
23
|
+
query = query.job_class(params[:job_class]) if params[:job_class]
|
24
|
+
query = query.where(queue_name: params[:queue_name]) if params[:queue_name]
|
25
|
+
|
26
|
+
if params[:state]
|
27
|
+
case params[:state]
|
28
|
+
when 'discarded'
|
29
|
+
query = query.discarded
|
30
|
+
when 'finished'
|
31
|
+
query = query.finished
|
32
|
+
when 'retried'
|
33
|
+
query = query.retried
|
34
|
+
when 'scheduled'
|
35
|
+
query = query.scheduled
|
36
|
+
when 'running'
|
37
|
+
query = query.running.select('good_jobs.*', 'pg_locks.locktype')
|
38
|
+
when 'queued'
|
39
|
+
query = query.queued
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
query
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -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
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GoodJob
|
3
|
+
# ActiveRecord model that represents an +ActiveJob+ job.
|
4
|
+
# Is the same record data as a {GoodJob::Execution} but only the most recent execution.
|
5
|
+
# Parent class can be configured with +GoodJob.active_record_parent_class+.
|
6
|
+
# @!parse
|
7
|
+
# class ActiveJob < ActiveRecord::Base; end
|
8
|
+
class ActiveJobJob < Object.const_get(GoodJob.active_record_parent_class)
|
9
|
+
include GoodJob::Lockable
|
10
|
+
|
11
|
+
self.table_name = 'good_jobs'
|
12
|
+
self.primary_key = 'active_job_id'
|
13
|
+
self.advisory_lockable_column = 'active_job_id'
|
14
|
+
|
15
|
+
has_many :executions, -> { order(created_at: :asc) }, class_name: 'GoodJob::Execution', foreign_key: 'active_job_id'
|
16
|
+
|
17
|
+
# Only the most-recent unretried execution represents a "Job"
|
18
|
+
default_scope { where(retried_good_job_id: nil) }
|
19
|
+
|
20
|
+
# Get Jobs with given class name
|
21
|
+
# @!method job_class
|
22
|
+
# @!scope class
|
23
|
+
# @param string [String]
|
24
|
+
# Execution class name
|
25
|
+
# @return [ActiveRecord::Relation]
|
26
|
+
scope :job_class, ->(job_class) { where("serialized_params->>'job_class' = ?", job_class) }
|
27
|
+
|
28
|
+
# First execution will run in the future
|
29
|
+
scope :scheduled, -> { where(finished_at: nil).where('COALESCE(scheduled_at, created_at) > ?', DateTime.current).where("(serialized_params->>'executions')::integer < 2") }
|
30
|
+
# Execution errored, will run in the future
|
31
|
+
scope :retried, -> { where(finished_at: nil).where('COALESCE(scheduled_at, created_at) > ?', DateTime.current).where("(serialized_params->>'executions')::integer > 1") }
|
32
|
+
# Immediate/Scheduled time to run has passed, waiting for an available thread run
|
33
|
+
scope :queued, -> { where(finished_at: nil).where('COALESCE(scheduled_at, created_at) <= ?', DateTime.current).joins_advisory_locks.where(pg_locks: { locktype: nil }) }
|
34
|
+
# Advisory locked and executing
|
35
|
+
scope :running, -> { where(finished_at: nil).joins_advisory_locks.where.not(pg_locks: { locktype: nil }) }
|
36
|
+
# Completed executing successfully
|
37
|
+
scope :finished, -> { where.not(finished_at: nil).where(error: nil) }
|
38
|
+
# Errored but will not be retried
|
39
|
+
scope :discarded, -> { where.not(finished_at: nil).where.not(error: nil) }
|
40
|
+
|
41
|
+
# Get Jobs in display order with optional keyset pagination.
|
42
|
+
# @!method display_all(after_scheduled_at: nil, after_id: nil)
|
43
|
+
# @!scope class
|
44
|
+
# @param after_scheduled_at [DateTime, String, nil]
|
45
|
+
# Display records scheduled after this time for keyset pagination
|
46
|
+
# @param after_id [Numeric, String, nil]
|
47
|
+
# Display records after this ID for keyset pagination
|
48
|
+
# @return [ActiveRecord::Relation]
|
49
|
+
scope :display_all, (lambda do |after_scheduled_at: nil, after_id: nil|
|
50
|
+
query = order(Arel.sql('COALESCE(scheduled_at, created_at) DESC, id DESC'))
|
51
|
+
if after_scheduled_at.present? && after_id.present?
|
52
|
+
query = query.where(Arel.sql('(COALESCE(scheduled_at, created_at), id) < (:after_scheduled_at, :after_id)'), after_scheduled_at: after_scheduled_at, after_id: after_id)
|
53
|
+
elsif after_scheduled_at.present?
|
54
|
+
query = query.where(Arel.sql('(COALESCE(scheduled_at, created_at)) < (:after_scheduled_at)'), after_scheduled_at: after_scheduled_at)
|
55
|
+
end
|
56
|
+
query
|
57
|
+
end)
|
58
|
+
|
59
|
+
def id
|
60
|
+
active_job_id
|
61
|
+
end
|
62
|
+
|
63
|
+
def _execution_id
|
64
|
+
attributes['id']
|
65
|
+
end
|
66
|
+
|
67
|
+
def job_class
|
68
|
+
serialized_params['job_class']
|
69
|
+
end
|
70
|
+
|
71
|
+
def status
|
72
|
+
if finished_at.present?
|
73
|
+
if error.present?
|
74
|
+
:discarded
|
75
|
+
else
|
76
|
+
:finished
|
77
|
+
end
|
78
|
+
elsif (scheduled_at || created_at) > DateTime.current
|
79
|
+
if serialized_params.fetch('executions', 0) > 1
|
80
|
+
:retried
|
81
|
+
else
|
82
|
+
:scheduled
|
83
|
+
end
|
84
|
+
elsif running?
|
85
|
+
:running
|
86
|
+
else
|
87
|
+
:queued
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def head_execution
|
92
|
+
executions.last
|
93
|
+
end
|
94
|
+
|
95
|
+
def tail_execution
|
96
|
+
executions.first
|
97
|
+
end
|
98
|
+
|
99
|
+
def executions_count
|
100
|
+
aj_count = serialized_params.fetch('executions', 0)
|
101
|
+
# The execution count within serialized_params is not updated
|
102
|
+
# once the underlying execution has been executed.
|
103
|
+
if status.in? [:discarded, :finished, :running]
|
104
|
+
aj_count + 1
|
105
|
+
else
|
106
|
+
aj_count
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def preserved_executions_count
|
111
|
+
executions.size
|
112
|
+
end
|
113
|
+
|
114
|
+
def recent_error
|
115
|
+
error.presence || executions[-2]&.error
|
116
|
+
end
|
117
|
+
|
118
|
+
def running?
|
119
|
+
# Avoid N+1 Query: `.joins_advisory_locks.select('good_jobs.*', 'pg_locks.locktype AS locktype')`
|
120
|
+
if has_attribute?(:locktype)
|
121
|
+
self['locktype'].present?
|
122
|
+
else
|
123
|
+
advisory_locked?
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
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.to_local_time %></td>
|
20
64
|
</tr>
|
21
65
|
<% end %>
|
22
66
|
</tbody>
|