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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +90 -1
  3. data/README.md +50 -19
  4. data/engine/app/controllers/good_job/base_controller.rb +8 -0
  5. data/engine/app/controllers/good_job/cron_schedules_controller.rb +1 -1
  6. data/engine/app/controllers/good_job/jobs_controller.rb +36 -0
  7. data/engine/app/filters/good_job/base_filter.rb +6 -2
  8. data/engine/app/filters/good_job/jobs_filter.rb +3 -1
  9. data/engine/app/helpers/good_job/application_helper.rb +4 -0
  10. data/engine/app/models/good_job/active_job_job.rb +130 -12
  11. data/engine/app/views/good_job/cron_schedules/index.html.erb +51 -7
  12. data/engine/app/views/good_job/jobs/index.html.erb +14 -1
  13. data/engine/app/views/good_job/shared/_executions_table.erb +1 -1
  14. data/engine/app/views/good_job/shared/_jobs_table.erb +18 -6
  15. data/engine/app/views/good_job/shared/icons/_arrow_clockwise.html.erb +5 -0
  16. data/engine/app/views/good_job/shared/icons/_skip_forward.html.erb +4 -0
  17. data/engine/app/views/good_job/shared/icons/_stop.html.erb +4 -0
  18. data/engine/app/views/layouts/good_job/base.html.erb +2 -1
  19. data/engine/config/routes.rb +7 -1
  20. data/lib/generators/good_job/install_generator.rb +6 -0
  21. data/lib/generators/good_job/templates/install/migrations/create_good_jobs.rb.erb +3 -1
  22. data/lib/generators/good_job/templates/update/migrations/{01_create_good_jobs.rb → 01_create_good_jobs.rb.erb} +1 -1
  23. data/lib/generators/good_job/templates/update/migrations/02_add_cron_at_to_good_jobs.rb.erb +14 -0
  24. data/lib/generators/good_job/templates/update/migrations/03_add_cron_key_cron_at_index_to_good_jobs.rb.erb +20 -0
  25. data/lib/generators/good_job/update_generator.rb +6 -0
  26. data/lib/good_job/active_job_extensions/concurrency.rb +3 -4
  27. data/lib/good_job/adapter.rb +4 -2
  28. data/lib/good_job/cli.rb +3 -1
  29. data/lib/good_job/configuration.rb +4 -0
  30. data/lib/good_job/cron_entry.rb +67 -0
  31. data/lib/good_job/cron_manager.rb +20 -30
  32. data/lib/good_job/current_thread.rb +15 -0
  33. data/lib/good_job/execution.rb +37 -14
  34. data/lib/good_job/lockable.rb +1 -1
  35. data/lib/good_job/log_subscriber.rb +3 -3
  36. data/lib/good_job/scheduler.rb +1 -0
  37. data/lib/good_job/version.rb +1 -1
  38. metadata +9 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3d37e613a1bbc15da2a5245b19443a220a49d69e49abd616ceb34f25bfea0ecc
4
- data.tar.gz: ab189fd086ad548dfaf0c71c39f30d9fd07a1c7fc72c3764794365ce69b027b5
3
+ metadata.gz: 5a173683ec5e5879728536005c7dfd11827ffe87c268c59315f540911277f2df
4
+ data.tar.gz: a5990d902838da25344ff96fc859bad39803f70f888dd95de8e3e6c201da4b0f
5
5
  SHA512:
6
- metadata.gz: add7f07d52a89171eecad11d31e3bf1f504ec72522a6f093204ee6bad8ec32a3cfa802f5d86394b5d98980ce74d3e333ef6d0a513871f4af38b782a0cffee625
7
- data.tar.gz: ddc8d64f20a5060512814676c07e647adf2711a3b1bcd0b1bfc8c8527841c9e2b1bea87a25b8e93e6b80176159d3c5fd65f54d1fa77b44e4baf1bf48fae6dc1f
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 = ...` 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
@@ -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 concurrently enqueued (excludes performing jobs)
366
+ # Maximum number of jobs with the concurrency key to be
367
+ # concurrently enqueued (excludes performing jobs)
363
368
  enqueue_limit: 2,
364
- # Maximum number of jobs with the concurrency key to be concurrently performed (excludes enqueued jobs)
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`; use GoodJob's [ActiveJob concurrency](#activejob-concurrency) extension to limit the number of jobs that are enqueued.
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 --queues="transactional_messages:2;batch_processing:1;-transactional_messages,batch_processing:2;*" --max-threads=5
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. Isolated execution pools are separated with a semicolon (`;`) and queue names and thread counts with a colon (`:`)
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` 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.
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;*" GOOD_JOB_MAX_THREADS=5 bundle exec good_job
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; for example, on Heroku:
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 dyno types
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
- # Combined multi-process dyno
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
@@ -3,7 +3,7 @@ module GoodJob
3
3
  class CronSchedulesController < GoodJob::BaseController
4
4
  def index
5
5
  configuration = GoodJob::Configuration.new({})
6
- @cron_schedules = configuration.cron
6
+ @cron_entries = configuration.cron_entries
7
7
  end
8
8
  end
9
9
  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, 25))
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
- # Is the same record data as a {GoodJob::Execution} but only the most recent execution.
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
- def _execution_id
64
- attributes['id']
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
- if finished_at.present?
73
- if error.present?
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
- def head_execution
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.presence || executions[-2]&.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 @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 %></td>
20
64
  </tr>
21
65
  <% end %>
22
66
  </tbody>