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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +73 -0
  3. data/README.md +39 -16
  4. data/engine/app/controllers/good_job/base_controller.rb +8 -0
  5. data/engine/app/controllers/good_job/cron_schedules_controller.rb +1 -2
  6. data/engine/app/controllers/good_job/executions_controller.rb +5 -1
  7. data/engine/app/controllers/good_job/{active_jobs_controller.rb → jobs_controller.rb} +6 -2
  8. data/engine/app/filters/good_job/base_filter.rb +101 -0
  9. data/engine/app/filters/good_job/executions_filter.rb +40 -0
  10. data/engine/app/filters/good_job/jobs_filter.rb +46 -0
  11. data/engine/app/helpers/good_job/application_helper.rb +4 -0
  12. data/engine/app/models/good_job/active_job_job.rb +127 -0
  13. data/engine/app/views/good_job/cron_schedules/index.html.erb +51 -7
  14. data/engine/app/views/good_job/executions/index.html.erb +21 -0
  15. data/engine/app/views/good_job/jobs/index.html.erb +7 -0
  16. data/engine/app/views/good_job/{active_jobs → jobs}/show.html.erb +0 -0
  17. data/engine/app/views/good_job/shared/_executions_table.erb +3 -3
  18. data/engine/app/views/good_job/shared/_filter.erb +52 -0
  19. data/engine/app/views/good_job/shared/_jobs_table.erb +56 -0
  20. data/engine/app/views/layouts/good_job/base.html.erb +5 -1
  21. data/engine/config/routes.rb +2 -2
  22. data/lib/good_job/adapter.rb +6 -4
  23. data/lib/good_job/cli.rb +3 -1
  24. data/lib/good_job/configuration.rb +4 -0
  25. data/lib/good_job/cron_entry.rb +65 -0
  26. data/lib/good_job/cron_manager.rb +18 -30
  27. data/lib/good_job/log_subscriber.rb +3 -3
  28. data/lib/good_job/scheduler.rb +2 -1
  29. data/lib/good_job/version.rb +1 -1
  30. metadata +13 -6
  31. data/engine/app/controllers/good_job/dashboards_controller.rb +0 -107
  32. 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: dd267297ac5f8e889d5af7d3efb0d12f3564ddedb8c645fde37bc044e18d48d0
4
- data.tar.gz: 5fc5715400914b895fb38fc087bb7bb08ea63ffb4db8938cea9e2933b32ade36
3
+ metadata.gz: a0546a16ecd5760e3ca0dd5097ec950767b5993ac613489d5591d5d44eadc021
4
+ data.tar.gz: 9e21d8930806b1843d4c32abef233f89f48292229386da593ba82b3a94617eae
5
5
  SHA512:
6
- metadata.gz: d3da94d9ad1b43e5102d0795d0c3909912ba90178678187d7340170a48c2df733ba7aba4bd33da8fdb50084f82fb4ff960c71dd4db0c23bc1a68278a1d1fbd21
7
- data.tar.gz: 9810497b9bdef82a95e79bf98414e60652dc912ccb6cfe9ce0893a898708fe6e547b89d67530c62b721fff01cc2c7bfa679f450c511ca5f68d3a5ef0d0a387ca
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 = ...` for example:
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 --queues="transactional_messages:2;batch_processing:1;-transactional_messages,batch_processing:2;*" --max-threads=5
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. Isolated execution pools are separated with a semicolon (`;`) and queue names and thread counts with a colon (`:`)
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` with up to 2 threads.
588
- - `batch_processing:1` execute jobs enqueued on `batch_processing` with a single thread.
589
- - `-transactional_messages,batch_processing`: execute jobs enqueued on _any_ queue _excluding_ `transactional_messages` or `batch_processing` with up to 2 threads.
590
- - `*`: execute jobs on any queue on up to 5 threads, as configured by `--max-threads=5`
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;*" GOOD_JOB_MAX_THREADS=5 bundle exec good_job
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; for example, on Heroku:
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 dyno types
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
- # Combined multi-process dyno
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
- @cron_schedules = configuration.cron
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
- redirect_to root_path, **message
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 ActiveJobsController < GoodJob::BaseController
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
- raise ActiveRecord::RecordNotFound if @executions.empty?
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 @cron_schedules.present? %>
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&nbsp;
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&nbsp;
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
- <% @cron_schedules.each do |job_key, job| %>
27
+ <% @cron_entries.each do |cron_entry| %>
14
28
  <tr>
15
- <td class="font-monospace"><%= job_key %></td>
16
- <td class="font-monospace"><%= job[:cron] %></td>
17
- <td class="font-monospace"><%= job[:class] %></td>
18
- <td><%= job[:description] %></td>
19
- <td><%= Fugit.parse_cron(job[:cron]).next_time.to_local_time %></td>
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>