good_job 1.2.4 → 1.2.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -7
  3. data/README.md +13 -10
  4. data/engine/app/controllers/good_job/active_jobs_controller.rb +8 -0
  5. data/engine/app/controllers/good_job/base_controller.rb +5 -0
  6. data/engine/app/controllers/good_job/dashboards_controller.rb +50 -0
  7. data/engine/app/helpers/good_job/application_helper.rb +4 -0
  8. data/engine/app/views/assets/_style.css.erb +16 -0
  9. data/engine/app/views/good_job/active_jobs/show.html.erb +1 -0
  10. data/engine/app/views/good_job/dashboards/index.html.erb +19 -0
  11. data/engine/app/views/layouts/good_job/base.html.erb +50 -0
  12. data/engine/app/views/shared/_chart.erb +51 -0
  13. data/engine/app/views/shared/_jobs_table.erb +26 -0
  14. data/engine/app/views/vendor/bootstrap/_bootstrap-native.js.erb +1662 -0
  15. data/engine/app/views/vendor/bootstrap/_bootstrap.css.erb +10258 -0
  16. data/engine/app/views/vendor/chartist/_chartist.css.erb +613 -0
  17. data/engine/app/views/vendor/chartist/_chartist.js.erb +4516 -0
  18. data/engine/config/routes.rb +4 -0
  19. data/engine/lib/good_job/engine.rb +5 -0
  20. data/lib/active_job/queue_adapters/good_job_adapter.rb +3 -2
  21. data/lib/generators/good_job/install_generator.rb +8 -0
  22. data/lib/good_job.rb +40 -24
  23. data/lib/good_job/adapter.rb +38 -0
  24. data/lib/good_job/cli.rb +30 -7
  25. data/lib/good_job/configuration.rb +44 -0
  26. data/lib/good_job/job.rb +116 -20
  27. data/lib/good_job/lockable.rb +119 -6
  28. data/lib/good_job/log_subscriber.rb +70 -4
  29. data/lib/good_job/multi_scheduler.rb +6 -0
  30. data/lib/good_job/notifier.rb +55 -29
  31. data/lib/good_job/performer.rb +38 -0
  32. data/lib/good_job/railtie.rb +1 -0
  33. data/lib/good_job/scheduler.rb +33 -20
  34. data/lib/good_job/version.rb +2 -1
  35. metadata +96 -9
@@ -0,0 +1,4 @@
1
+ GoodJob::Engine.routes.draw do
2
+ root to: 'dashboards#index'
3
+ resources :active_jobs, only: :show
4
+ end
@@ -0,0 +1,5 @@
1
+ module GoodJob
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace GoodJob
4
+ end
5
+ end
@@ -1,5 +1,6 @@
1
- module ActiveJob
2
- module QueueAdapters
1
+ module ActiveJob # :nodoc:
2
+ module QueueAdapters # :nodoc:
3
+ # See {GoodJob::Adapter} for details.
3
4
  class GoodJobAdapter < GoodJob::Adapter
4
5
  def initialize(execution_mode: nil, max_threads: nil, poll_interval: nil, scheduler: nil, inline: false)
5
6
  configuration = GoodJob::Configuration.new({ execution_mode: execution_mode }, env: ENV)
@@ -2,6 +2,13 @@ require 'rails/generators'
2
2
  require 'rails/generators/active_record'
3
3
 
4
4
  module GoodJob
5
+ #
6
+ # Implements the Rails generator used for setting up GoodJob in a Rails
7
+ # application. Run it with +bin/rails g good_job:install+ in your console.
8
+ #
9
+ # This generator is primarily dedicated to stubbing out a migration that adds
10
+ # a table to hold GoodJob's queued jobs in your database.
11
+ #
5
12
  class InstallGenerator < Rails::Generators::Base
6
13
  include Rails::Generators::Migration
7
14
 
@@ -11,6 +18,7 @@ module GoodJob
11
18
 
12
19
  source_paths << File.join(File.dirname(__FILE__), "templates")
13
20
 
21
+ # Generates the actual migration file and places it on disk.
14
22
  def create_migration_file
15
23
  migration_template 'migration.rb.erb', 'db/migrate/create_good_jobs.rb', migration_version: migration_version
16
24
  end
@@ -1,18 +1,18 @@
1
1
  require "rails"
2
- require 'good_job/railtie'
3
2
 
4
- require 'good_job/configuration'
5
- require 'good_job/log_subscriber'
6
- require 'good_job/lockable'
7
- require 'good_job/job'
8
- require 'good_job/scheduler'
9
- require 'good_job/multi_scheduler'
10
- require 'good_job/adapter'
11
- require 'good_job/performer'
12
- require 'good_job/current_execution'
13
- require 'good_job/notifier'
3
+ require "active_job"
4
+ require "active_job/queue_adapters"
14
5
 
15
- require 'active_job/queue_adapters/good_job_adapter'
6
+ require "zeitwerk"
7
+
8
+ loader = Zeitwerk::Loader.for_gem
9
+ loader.inflector.inflect(
10
+ 'cli' => "CLI"
11
+ )
12
+ loader.push_dir(File.join(__dir__, ["generators"]))
13
+ loader.setup
14
+
15
+ require "good_job/railtie"
16
16
 
17
17
  # GoodJob is a multithreaded, Postgres-based, ActiveJob backend for Ruby on Rails.
18
18
  #
@@ -20,48 +20,64 @@ require 'active_job/queue_adapters/good_job_adapter'
20
20
  module GoodJob
21
21
  # @!attribute [rw] logger
22
22
  # @!scope class
23
- # The logger used by GoodJob
23
+ # The logger used by GoodJob (default: +Rails.logger+).
24
+ # Use this to redirect logs to a special location or file.
24
25
  # @return [Logger]
25
- mattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
26
+ # @example Output GoodJob logs to a file:
27
+ # GoodJob.logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new("log/my_logs.log"))
28
+ mattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new($stdout))
26
29
 
27
30
  # @!attribute [rw] preserve_job_records
28
31
  # @!scope class
29
- # Whether to preserve job records in the database after they have finished for inspection
32
+ # Whether to preserve job records in the database after they have finished (default: +false+).
33
+ # By default, GoodJob deletes job records after the job is completed successfully.
34
+ # If you want to preserve jobs for latter inspection, set this to +true+.
35
+ # If +true+, you will need to clean out jobs using the +good_job cleanup_preserved_jobs+ CLI command.
30
36
  # @return [Boolean]
31
37
  mattr_accessor :preserve_job_records, default: false
32
38
 
33
39
  # @!attribute [rw] reperform_jobs_on_standard_error
34
40
  # @!scope class
35
- # Whether to re-perform a job when a type of +StandardError+ is raised and bubbles up to the GoodJob backend
41
+ # Whether to re-perform a job when a type of +StandardError+ is raised to GoodJob (default: +true+).
42
+ # If +true+, causes jobs to be re-queued and retried if they raise an instance of +StandardError+.
43
+ # If +false+, jobs will be discarded or marked as finished if they raise an instance of +StandardError+.
44
+ # Instances of +Exception+, like +SIGINT+, will *always* be retried, regardless of this attribute's value.
36
45
  # @return [Boolean]
37
46
  mattr_accessor :reperform_jobs_on_standard_error, default: true
38
47
 
39
48
  # @!attribute [rw] on_thread_error
40
49
  # @!scope class
41
- # Called when a thread raises an error
50
+ # This callable will be called when an exception reaches GoodJob (default: +nil+).
51
+ # It can be useful for logging errors to bug tracking services, like Sentry or Airbrake.
42
52
  # @example Send errors to Sentry
43
53
  # # config/initializers/good_job.rb
44
- #
45
- # # With Sentry (or Bugsnag, Airbrake, Honeybadger, etc.)
46
54
  # GoodJob.on_thread_error = -> (exception) { Raven.capture_exception(exception) }
47
55
  # @return [#call, nil]
48
56
  mattr_accessor :on_thread_error, default: nil
49
57
 
50
- # Shuts down all execution pools
58
+ # Stop executing jobs.
59
+ # GoodJob does its work in pools of background threads.
60
+ # When forking processes you should shut down these background threads before forking, and restart them after forking.
61
+ # For example, you should use +shutdown+ and +restart+ when using async execution mode with Puma.
62
+ # See the {file:README.md#executing-jobs-async--in-process} for more explanation and examples.
51
63
  # @param wait [Boolean] whether to wait for shutdown
52
64
  # @return [void]
53
65
  def self.shutdown(wait: true)
54
- Notifier.instances.each { |adapter| adapter.shutdown(wait: wait) }
66
+ Notifier.instances.each { |notifier| notifier.shutdown(wait: wait) }
55
67
  Scheduler.instances.each { |scheduler| scheduler.shutdown(wait: wait) }
56
68
  end
57
69
 
58
- # Tests if execution pools are shut down
59
- # @return [Boolean] whether execution pools are shut down
70
+ # Tests whether jobs have stopped executing.
71
+ # @return [Boolean] whether background threads are shut down
60
72
  def self.shutdown?
61
73
  Notifier.instances.all?(&:shutdown?) && Scheduler.instances.all?(&:shutdown?)
62
74
  end
63
75
 
64
- # Restarts all execution pools
76
+ # Stops and restarts executing jobs.
77
+ # GoodJob does its work in pools of background threads.
78
+ # When forking processes you should shut down these background threads before forking, and restart them after forking.
79
+ # For example, you should use +shutdown+ and +restart+ when using async execution mode with Puma.
80
+ # See the {file:README.md#executing-jobs-async--in-process} for more explanation and examples.
65
81
  # @return [void]
66
82
  def self.restart
67
83
  Notifier.instances.each(&:restart)
@@ -1,7 +1,28 @@
1
1
  module GoodJob
2
+ #
3
+ # ActiveJob Adapter.
4
+ #
2
5
  class Adapter
6
+ # Valid execution modes.
3
7
  EXECUTION_MODES = [:async, :external, :inline].freeze
4
8
 
9
+ # @param execution_mode [nil, Symbol] specifies how and where jobs should be executed. You can also set this with the environment variable +GOOD_JOB_EXECUTION_MODE+.
10
+ #
11
+ # - +:inline+ executes jobs immediately in whatever process queued them (usually the web server process). This should only be used in test and development environments.
12
+ # - +:external+ causes the adapter to enqueue jobs, but not execute them. When using this option (the default for production environments), you'll need to use the command-line tool to actually execute your jobs.
13
+ # - +:async+ causes the adapter to execute you jobs in separate threads in whatever process queued them (usually the web process). This is akin to running the command-line tool's code inside your web server. It can be more economical for small workloads (you don't need a separate machine or environment for running your jobs), but if your web server is under heavy load or your jobs require a lot of resources, you should choose `:external` instead.
14
+ #
15
+ # The default value depends on the Rails environment:
16
+ #
17
+ # - +development+ and +test+: +:inline+
18
+ # - +production+ and all other environments: +:external+
19
+ #
20
+ # @param max_threads [nil, Integer] sets the number of threads per scheduler to use when +execution_mode+ is set to +:async+. The +queues+ parameter can specify a number of threads for each group of queues which will override this value. You can also set this with the environment variable +GOOD_JOB_MAX_THREADS+. Defaults to +5+.
21
+ # @param queues [nil, String] determines which queues to execute jobs from when +execution_mode+ is set to +:async+. See {file:README.md#optimize-queues-threads-and-processes} for more details on the format of this string. You can also set this with the environment variable +GOOD_JOB_QUEUES+. Defaults to +"*"+.
22
+ # @param poll_interval [nil, Integer] sets the number of seconds between polls for jobs when +execution_mode+ is set to +:async+. You can also set this with the environment variable +GOOD_JOB_POLL_INTERVAL+. Defaults to +1+.
23
+ # @param scheduler [nil, Scheduler] (deprecated) a scheduler to be managed by the adapter
24
+ # @param notifier [nil, Notifier] (deprecated) a notifier to be managed by the adapter
25
+ # @param inline [nil, Boolean] (deprecated) whether to run in inline execution mode
5
26
  def initialize(execution_mode: nil, queues: nil, max_threads: nil, poll_interval: nil, scheduler: nil, notifier: nil, inline: false)
6
27
  if inline && execution_mode.nil?
7
28
  ActiveSupport::Deprecation.warn('GoodJob::Adapter#new(inline: true) is deprecated; use GoodJob::Adapter.new(execution_mode: :inline) instead')
@@ -27,10 +48,19 @@ module GoodJob
27
48
  end
28
49
  end
29
50
 
51
+ # Enqueues the ActiveJob job to be performed.
52
+ # For use by Rails; you should generally not call this directly.
53
+ # @param active_job [ActiveJob::Base] the job to be enqueued from +#perform_later+
54
+ # @return [GoodJob::Job]
30
55
  def enqueue(active_job)
31
56
  enqueue_at(active_job, nil)
32
57
  end
33
58
 
59
+ # Enqueues an ActiveJob job to be run at a specific time.
60
+ # For use by Rails; you should generally not call this directly.
61
+ # @param active_job [ActiveJob::Base] the job to be enqueued from +#perform_later+
62
+ # @param timestamp [Integer] the epoch time to perform the job
63
+ # @return [GoodJob::Job]
34
64
  def enqueue_at(active_job, timestamp)
35
65
  good_job = GoodJob::Job.enqueue(
36
66
  active_job,
@@ -52,23 +82,31 @@ module GoodJob
52
82
  good_job
53
83
  end
54
84
 
85
+ # Gracefully stop processing jobs.
86
+ # Waits for termination by default.
87
+ # @param wait [Boolean] Whether to wait for shut down.
88
+ # @return [void]
55
89
  def shutdown(wait: true)
56
90
  @notifier&.shutdown(wait: wait)
57
91
  @scheduler&.shutdown(wait: wait)
58
92
  end
59
93
 
94
+ # Whether in +:async+ execution mode.
60
95
  def execute_async?
61
96
  @execution_mode == :async
62
97
  end
63
98
 
99
+ # Whether in +:external+ execution mode.
64
100
  def execute_externally?
65
101
  @execution_mode == :external
66
102
  end
67
103
 
104
+ # Whether in +:inline+ execution mode.
68
105
  def execute_inline?
69
106
  @execution_mode == :inline
70
107
  end
71
108
 
109
+ # (deprecated) Whether in +:inline+ execution mode.
72
110
  def inline?
73
111
  ActiveSupport::Deprecation.warn('GoodJob::Adapter::inline? is deprecated; use GoodJob::Adapter::execute_inline? instead')
74
112
  execute_inline?
@@ -1,17 +1,33 @@
1
1
  require 'thor'
2
2
 
3
3
  module GoodJob
4
+ #
5
+ # Implements the +good_job+ command-line tool, which executes jobs and
6
+ # provides other utilities. The actual entry point is in +exe/good_job+, but
7
+ # it just sets up and calls this class.
8
+ #
9
+ # The +good_job+ command-line tool is based on Thor, a CLI framework for
10
+ # Ruby. For more on general usage, see http://whatisthor.com/ and
11
+ # https://github.com/erikhuda/thor/wiki.
12
+ #
4
13
  class CLI < Thor
14
+ # Path to the local Rails application's environment configuration.
15
+ # Requiring this loads the application's configuration and classes.
5
16
  RAILS_ENVIRONMENT_RB = File.expand_path("config/environment.rb")
6
17
 
7
- desc :start, <<~DESCRIPTION
18
+ # @!macro thor.desc
19
+ # @!method $1
20
+ # @return [void]
21
+ # The +good_job $1+ command. $2
22
+ desc :start, "Executes queued jobs."
23
+ long_desc <<~DESCRIPTION
8
24
  Executes queued jobs.
9
25
 
10
- All options can be configured with environment variables.
26
+ All options can be configured with environment variables.
11
27
  See option descriptions for the matching environment variable name.
12
28
 
13
29
  == Configuring queues
14
- Separate multiple queues with commas; exclude queues with a leading minus;
30
+ \x5Separate multiple queues with commas; exclude queues with a leading minus;
15
31
  separate isolated execution pools with semicolons and threads with colons.
16
32
 
17
33
  DESCRIPTION
@@ -51,8 +67,10 @@ module GoodJob
51
67
 
52
68
  default_task :start
53
69
 
54
- desc :cleanup_preserved_jobs, <<~DESCRIPTION
55
- Deletes preserved job records.
70
+ # @!macro thor.desc
71
+ desc :cleanup_preserved_jobs, "Deletes preserved job records."
72
+ long_desc <<~DESCRIPTION
73
+ Deletes preserved job records.
56
74
 
57
75
  By default, GoodJob deletes job records when the job is performed and this
58
76
  command is not necessary.
@@ -87,11 +105,16 @@ module GoodJob
87
105
  end
88
106
 
89
107
  no_commands do
108
+ # Load the current Rails application and configuration that the good_job
109
+ # command-line tool should be working within.
110
+ #
111
+ # GoodJob components that need access to constants, classes, etc. from
112
+ # Rails or from the application can be set up here.
90
113
  def set_up_application!
91
114
  require RAILS_ENVIRONMENT_RB
92
- return unless defined?(GOOD_JOB_LOG_TO_STDOUT) && GOOD_JOB_LOG_TO_STDOUT && !ActiveSupport::Logger.logger_outputs_to?(GoodJob.logger, STDOUT)
115
+ return unless defined?(GOOD_JOB_LOG_TO_STDOUT) && GOOD_JOB_LOG_TO_STDOUT && !ActiveSupport::Logger.logger_outputs_to?(GoodJob.logger, $stdout)
93
116
 
94
- GoodJob::LogSubscriber.loggers << ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
117
+ GoodJob::LogSubscriber.loggers << ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new($stdout))
95
118
  GoodJob::LogSubscriber.reset_logger
96
119
  end
97
120
  end
@@ -1,12 +1,40 @@
1
1
  module GoodJob
2
+ #
3
+ # +GoodJob::Configuration+ provides normalized configuration information to
4
+ # the rest of GoodJob. It combines environment information with explicitly
5
+ # set options to get the final values for each option.
6
+ #
2
7
  class Configuration
8
+ # @!attribute [r] options
9
+ # The options that were explicitly set when initializing +Configuration+.
10
+ # @return [Hash]
11
+ #
12
+ # @!attribute [r] env
13
+ # The environment from which to read GoodJob's environment variables. By
14
+ # default, this is the current process's environment, but it can be set
15
+ # to something else in {#initialize}.
16
+ # @return [Hash]
3
17
  attr_reader :options, :env
4
18
 
19
+ # @param options [Hash] Any explicitly specified configuration options to
20
+ # use. Keys are symbols that match the various methods on this class.
21
+ # @param env [Hash] A +Hash+ from which to read environment variables that
22
+ # might specify additional configuration values.
5
23
  def initialize(options, env: ENV)
6
24
  @options = options
7
25
  @env = env
8
26
  end
9
27
 
28
+ # Specifies how and where jobs should be executed. See {Adapter#initialize}
29
+ # for more details on possible values.
30
+ #
31
+ # When running inside a Rails app, you may want to use
32
+ # {#rails_execution_mode}, which takes the current Rails environment into
33
+ # account when determining the final value.
34
+ #
35
+ # @param default [Symbol]
36
+ # Value to use if none was specified in the configuration.
37
+ # @return [Symbol]
10
38
  def execution_mode(default: :external)
11
39
  if options[:execution_mode]
12
40
  options[:execution_mode]
@@ -17,6 +45,9 @@ module GoodJob
17
45
  end
18
46
  end
19
47
 
48
+ # Like {#execution_mode}, but takes the current Rails environment into
49
+ # account (e.g. in the +test+ environment, it falls back to +:inline+).
50
+ # @return [Symbol]
20
51
  def rails_execution_mode
21
52
  if execution_mode(default: nil)
22
53
  execution_mode
@@ -29,6 +60,10 @@ module GoodJob
29
60
  end
30
61
  end
31
62
 
63
+ # Indicates the number of threads to use per {Scheduler}. Note that
64
+ # {#queue_string} may provide more specific thread counts to use with
65
+ # individual schedulers.
66
+ # @return [Integer]
32
67
  def max_threads
33
68
  (
34
69
  options[:max_threads] ||
@@ -38,12 +73,21 @@ module GoodJob
38
73
  ).to_i
39
74
  end
40
75
 
76
+ # Describes which queues to execute jobs from and how those queues should
77
+ # be grouped into {Scheduler} instances. See
78
+ # {file:README.md#optimize-queues-threads-and-processes} for more details
79
+ # on the format of this string.
80
+ # @return [String]
41
81
  def queue_string
42
82
  options[:queues] ||
43
83
  env['GOOD_JOB_QUEUES'] ||
44
84
  '*'
45
85
  end
46
86
 
87
+ # The number of seconds between polls for jobs. GoodJob will execute jobs
88
+ # on queues continuously until a queue is empty, at which point it will
89
+ # poll (using this interval) for new queued jobs to execute.
90
+ # @return [Integer]
47
91
  def poll_interval
48
92
  (
49
93
  options[:poll_interval] ||
@@ -1,14 +1,32 @@
1
1
  module GoodJob
2
+ #
3
+ # Represents a request to perform an +ActiveJob+ job.
4
+ #
2
5
  class Job < ActiveRecord::Base
3
6
  include Lockable
4
7
 
8
+ # Raised if something attempts to execute a previously completed Job again.
5
9
  PreviouslyPerformedError = Class.new(StandardError)
6
10
 
11
+ # ActiveJob jobs without a +queue_name+ attribute are placed on this queue.
7
12
  DEFAULT_QUEUE_NAME = 'default'.freeze
13
+ # ActiveJob jobs without a +priority+ attribute are given this priority.
8
14
  DEFAULT_PRIORITY = 0
9
15
 
10
16
  self.table_name = 'good_jobs'.freeze
11
17
 
18
+ # Parse a string representing a group of queues into a more readable data
19
+ # structure.
20
+ # @return [Hash]
21
+ # How to match a given queue. It can have the following keys and values:
22
+ # - +{ all: true }+ indicates that all queues match.
23
+ # - +{ exclude: Array<String> }+ indicates the listed queue names should
24
+ # not match.
25
+ # - +{ include: Array<String> }+ indicates the listed queue names should
26
+ # match.
27
+ # @example
28
+ # GoodJob::Job.queue_parser('-queue1,queue2')
29
+ # => { exclude: [ 'queue1', 'queue2' ] }
12
30
  def self.queue_parser(string)
13
31
  string = string.presence || '*'
14
32
 
@@ -28,6 +46,10 @@ module GoodJob
28
46
  end
29
47
  end
30
48
 
49
+ # Get Jobs that have not yet been completed.
50
+ # @!method unfinished
51
+ # @!scope class
52
+ # @return [ActiveRecord::Relation]
31
53
  scope :unfinished, (lambda do
32
54
  if column_names.include?('finished_at')
33
55
  where(finished_at: nil)
@@ -36,9 +58,42 @@ module GoodJob
36
58
  nil
37
59
  end
38
60
  end)
61
+
62
+ # Get Jobs that are not scheduled for a later time than now (i.e. jobs that
63
+ # are not scheduled or scheduled for earlier than the current time).
64
+ # @!method only_scheduled
65
+ # @!scope class
66
+ # @return [ActiveRecord::Relation]
39
67
  scope :only_scheduled, -> { where(arel_table['scheduled_at'].lteq(Time.current)).or(where(scheduled_at: nil)) }
68
+
69
+ # Order jobs by priority (highest priority first).
70
+ # @!method priority_ordered
71
+ # @!scope class
72
+ # @return [ActiveRecord::Relation]
40
73
  scope :priority_ordered, -> { order('priority DESC NULLS LAST') }
74
+
75
+ # Get Jobs were completed before the given timestamp. If no timestamp is
76
+ # provided, get all jobs that have been completed. By default, GoodJob
77
+ # deletes jobs after they are completed and this will find no jobs.
78
+ # However, if you have changed {GoodJob.preserve_job_records}, this may
79
+ # find completed Jobs.
80
+ # @!method finished(timestamp = nil)
81
+ # @!scope class
82
+ # @param timestamp (Float)
83
+ # Get jobs that finished before this time (in epoch time).
84
+ # @return [ActiveRecord::Relation]
41
85
  scope :finished, ->(timestamp = nil) { timestamp ? where(arel_table['finished_at'].lteq(timestamp)) : where.not(finished_at: nil) }
86
+
87
+ # Get Jobs on queues that match the given queue string.
88
+ # @!method queue_string(string)
89
+ # @!scope class
90
+ # @param string [String]
91
+ # A string expression describing what queues to select. See
92
+ # {Job.queue_parser} or
93
+ # {file:README.md#optimize-queues-threads-and-processes} for more details
94
+ # on the format of the string. Note this only handles individual
95
+ # semicolon-separated segments of that string format.
96
+ # @return [ActiveRecord::Relation]
42
97
  scope :queue_string, (lambda do |string|
43
98
  parsed = queue_parser(string)
44
99
 
@@ -51,6 +106,31 @@ module GoodJob
51
106
  end
52
107
  end)
53
108
 
109
+ # Get Jobs in display order with optional keyset pagination.
110
+ # @!method display_all(after_scheduled_at: nil, after_id: nil)
111
+ # @!scope class
112
+ # @param after_scheduled_at [DateTime, String, nil]
113
+ # Display records scheduled after this time for keyset pagination
114
+ # @param after_id [Numeric, String, nil]
115
+ # Display records after this ID for keyset pagination
116
+ # @return [ActiveRecord::Relation]
117
+ scope :display_all, (lambda do |after_scheduled_at: nil, after_id: nil|
118
+ query = order(Arel.sql('COALESCE(scheduled_at, created_at) DESC, id DESC'))
119
+ if after_scheduled_at.present? && after_id.present?
120
+ 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)
121
+ elsif after_scheduled_at.present?
122
+ query = query.where(Arel.sql('(COALESCE(scheduled_at, created_at)) < (:after_scheduled_at)'), after_scheduled_at: after_scheduled_at)
123
+ end
124
+ query
125
+ end)
126
+
127
+ # Finds the next eligible Job, acquire an advisory lock related to it, and
128
+ # executes the job.
129
+ # @return [Array<(GoodJob::Job, Object, Exception)>, nil]
130
+ # If a job was executed, returns an array with the {Job} record, the
131
+ # return value for the job's +#perform+ method, and the exception the job
132
+ # raised, if any (if the job raised, then the second array entry will be
133
+ # +nil+). If there were no jobs to execute, returns +nil+.
54
134
  def self.perform_with_advisory_lock
55
135
  good_job = nil
56
136
  result = nil
@@ -67,6 +147,15 @@ module GoodJob
67
147
  [good_job, result, error] if good_job
68
148
  end
69
149
 
150
+ # Places an ActiveJob job on a queue by creating a new {Job} record.
151
+ # @param active_job [ActiveJob::Base]
152
+ # The job to enqueue.
153
+ # @param scheduled_at [Float]
154
+ # Epoch timestamp when the job should be executed.
155
+ # @param create_with_advisory_lock [Boolean]
156
+ # Whether to establish a lock on the {Job} record after it is created.
157
+ # @return [Job]
158
+ # The new {Job} instance representing the queued ActiveJob job.
70
159
  def self.enqueue(active_job, scheduled_at: nil, create_with_advisory_lock: false)
71
160
  good_job = nil
72
161
  ActiveSupport::Notifications.instrument("enqueue_job.good_job", { active_job: active_job, scheduled_at: scheduled_at, create_with_advisory_lock: create_with_advisory_lock }) do |instrument_payload|
@@ -87,32 +176,25 @@ module GoodJob
87
176
  good_job
88
177
  end
89
178
 
90
- def perform(destroy_after: !GoodJob.preserve_job_records, reperform_on_standard_error: GoodJob.reperform_jobs_on_standard_error)
179
+ # Execute the ActiveJob job this {Job} represents.
180
+ # @return [Array<(Object, Exception)>]
181
+ # An array of the return value of the job's +#perform+ method and the
182
+ # exception raised by the job, if any. If the job completed successfully,
183
+ # the second array entry (the exception) will be +nil+ and vice versa.
184
+ def perform
91
185
  raise PreviouslyPerformedError, 'Cannot perform a job that has already been performed' if finished_at
92
186
 
93
187
  GoodJob::CurrentExecution.reset
94
- result = nil
95
- rescued_error = nil
96
- error = nil
97
188
 
98
189
  self.performed_at = Time.current
99
- save! unless destroy_after
190
+ save! if GoodJob.preserve_job_records
100
191
 
101
- params = serialized_params.merge(
102
- "provider_job_id" => id
103
- )
104
-
105
- begin
106
- ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self, process_id: GoodJob::CurrentExecution.process_id, thread_name: GoodJob::CurrentExecution.thread_name }) do
107
- result = ActiveJob::Base.execute(params)
108
- end
109
- rescue StandardError => e
110
- rescued_error = e
111
- end
192
+ result, rescued_error = execute
112
193
 
113
194
  retry_or_discard_error = GoodJob::CurrentExecution.error_on_retry ||
114
195
  GoodJob::CurrentExecution.error_on_discard
115
196
 
197
+ error = nil
116
198
  if rescued_error
117
199
  error = rescued_error
118
200
  elsif result.is_a?(Exception)
@@ -125,19 +207,33 @@ module GoodJob
125
207
  error_message = "#{error.class}: #{error.message}" if error
126
208
  self.error = error_message
127
209
 
128
- if rescued_error && reperform_on_standard_error
210
+ if rescued_error && GoodJob.reperform_jobs_on_standard_error
129
211
  save!
130
212
  else
131
213
  self.finished_at = Time.current
132
214
 
133
- if destroy_after
134
- destroy!
135
- else
215
+ if GoodJob.preserve_job_records
136
216
  save!
217
+ else
218
+ destroy!
137
219
  end
138
220
  end
139
221
 
140
222
  [result, error]
141
223
  end
224
+
225
+ private
226
+
227
+ def execute
228
+ params = serialized_params.merge(
229
+ "provider_job_id" => id
230
+ )
231
+
232
+ ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self, process_id: GoodJob::CurrentExecution.process_id, thread_name: GoodJob::CurrentExecution.thread_name }) do
233
+ [ActiveJob::Base.execute(params), nil]
234
+ end
235
+ rescue StandardError => e
236
+ [nil, e]
237
+ end
142
238
  end
143
239
  end