good_job 2.6.1 → 2.7.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3a413bcbddac29378e8fb0de0cc9305b57bd2e31041973f5db2a9dac6f04b7b1
4
- data.tar.gz: d99f0e37ad2c99ac0d69337fefccff11bc522ad370f02ae1ac12d59adab619d8
3
+ metadata.gz: e4843101f6fce50527e8d0157ff7ced91fabb0ae41c3c92006abd49e9005ea78
4
+ data.tar.gz: f8f243fb7f9e3ea2ec17fc795ad5bbd79b017f68665d98b911dfe27177e13389
5
5
  SHA512:
6
- metadata.gz: 3d6cddf9581a852f72f4c8b337149afc7fc4d2189d6304e510f19ca101cb661c1452b3eca3a123e28750c9729d9f6ca9224855a020296f68693246104864eb4f
7
- data.tar.gz: a042a1f1c29525e4c101b512f74a06cb4f2405a790d5e199e1dac87029e220b404d1b310683a151ef67ed7c50c8d94ae8e28c578b270175085d4c7e60f9040d3
6
+ metadata.gz: f2dbdd1c1811bfc09153a8eed32b95f337919d22434b004622a0ab78eb6803e68f5a66761e828bf2054421d5d5f9b798be54e7f60cc486602a997c804ab9fb2d
7
+ data.tar.gz: 953672a066c27a231fb76c3c9839b4cb9685deb0ed4f25a3e153f4ede0833afa0a263f4d37532715a62ed7a96825384b8f7d6fef7352416d0c64068864798fee
data/CHANGELOG.md CHANGED
@@ -1,5 +1,61 @@
1
1
  # Changelog
2
2
 
3
+ ## [v2.7.2](https://github.com/bensheldon/good_job/tree/v2.7.2) (2021-11-29)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.7.1...v2.7.2)
6
+
7
+ **Implemented enhancements:**
8
+
9
+ - Allow GoodJob global configuration accessors to also be set via Rails config hash [\#460](https://github.com/bensheldon/good_job/pull/460) ([bensheldon](https://github.com/bensheldon))
10
+
11
+ **Merged pull requests:**
12
+
13
+ - Use `ActiveRecord::Relation::QueryAttribute` when setting up bindings for `exec_query` [\#461](https://github.com/bensheldon/good_job/pull/461) ([bensheldon](https://github.com/bensheldon))
14
+ - Configure RSpec `config.example_status_persistence_file_path` [\#459](https://github.com/bensheldon/good_job/pull/459) ([bensheldon](https://github.com/bensheldon))
15
+ - Defer async initialization until Rails fully initialized [\#454](https://github.com/bensheldon/good_job/pull/454) ([bensheldon](https://github.com/bensheldon))
16
+
17
+ ## [v2.7.1](https://github.com/bensheldon/good_job/tree/v2.7.1) (2021-11-26)
18
+
19
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.7.0...v2.7.1)
20
+
21
+ **Fixed bugs:**
22
+
23
+ - Unclear error when database can't be reached [\#457](https://github.com/bensheldon/good_job/issues/457)
24
+ - Remove Concurrent::Delay wrapping of database-loading methods [\#458](https://github.com/bensheldon/good_job/pull/458) ([bensheldon](https://github.com/bensheldon))
25
+ - Do not delete csp policies when checking csp policies [\#456](https://github.com/bensheldon/good_job/pull/456) ([JonathanFrias](https://github.com/JonathanFrias))
26
+
27
+ **Closed issues:**
28
+
29
+ - How to suppress job scheduler logs? [\#455](https://github.com/bensheldon/good_job/issues/455)
30
+ - Configuration in environments/\*.rb overrides application.rb [\#453](https://github.com/bensheldon/good_job/issues/453)
31
+ - Testing jobs synchronously [\#435](https://github.com/bensheldon/good_job/issues/435)
32
+ - HTTP health check endpoint [\#403](https://github.com/bensheldon/good_job/issues/403)
33
+
34
+ ## [v2.7.0](https://github.com/bensheldon/good_job/tree/v2.7.0) (2021-11-10)
35
+
36
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.6.2...v2.7.0)
37
+
38
+ **Implemented enhancements:**
39
+
40
+ - Add http probe for CLI healthcheck/readiness/liveliness [\#452](https://github.com/bensheldon/good_job/pull/452) ([bensheldon](https://github.com/bensheldon))
41
+ - Add explicit Content Security Policy \(CSP\) for Dashboard [\#449](https://github.com/bensheldon/good_job/pull/449) ([bensheldon](https://github.com/bensheldon))
42
+
43
+ **Closed issues:**
44
+
45
+ - Add a default Content-Security-Policy for the Dashboard [\#420](https://github.com/bensheldon/good_job/issues/420)
46
+
47
+ ## [v2.6.2](https://github.com/bensheldon/good_job/tree/v2.6.2) (2021-11-05)
48
+
49
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.6.1...v2.6.2)
50
+
51
+ **Fixed bugs:**
52
+
53
+ - Rename Filterable\#search to Filterable\#search\_text to avoid name collision [\#451](https://github.com/bensheldon/good_job/pull/451) ([bensheldon](https://github.com/bensheldon))
54
+
55
+ **Closed issues:**
56
+
57
+ - v2.6.1 is incompatible with gem thinking-sphinx [\#450](https://github.com/bensheldon/good_job/issues/450)
58
+
3
59
  ## [v2.6.1](https://github.com/bensheldon/good_job/tree/v2.6.1) (2021-11-05)
4
60
 
5
61
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.6.0...v2.6.1)
data/README.md CHANGED
@@ -55,6 +55,7 @@ For more of the story of GoodJob, read the [introductory blog post](https://isla
55
55
  - [Migrate to GoodJob from a different ActiveJob backend](#migrate-to-goodjob-from-a-different-activejob-backend)
56
56
  - [Monitor and preserve worked jobs](#monitor-and-preserve-worked-jobs)
57
57
  - [PgBouncer compatibility](#pgbouncer-compatibility)
58
+ - [CLI HTTP health check probes](#cli-http-health-check-probes)
58
59
  - [Contribute](#contribute)
59
60
  - [Gem development](#gem-development)
60
61
  - [Release](#release)
@@ -96,7 +97,7 @@ For more of the story of GoodJob, read the [introductory blog post](https://isla
96
97
  1. Configure the ActiveJob adapter:
97
98
 
98
99
  ```ruby
99
- # config/application.rb
100
+ # config/application.rb or config/environments/{RAILS_ENV}.rb
100
101
  config.active_job.queue_adapter = :good_job
101
102
  ```
102
103
 
@@ -170,6 +171,7 @@ Options:
170
171
  [--enable-cron] # Whether to run cron process (default: false)
171
172
  [--daemonize] # Run as a background daemon (default: false)
172
173
  [--pidfile=PIDFILE] # Path to write daemonized Process ID (env var: GOOD_JOB_PIDFILE, default: tmp/pids/good_job.pid)
174
+ [--probe-port=PORT] # Port for http health check (env var: GOOD_JOB_PROBE_PORT, default: nil)
173
175
 
174
176
  Executes queued jobs.
175
177
 
@@ -210,43 +212,48 @@ to delete old records and preserve space in your database.
210
212
 
211
213
  ### Configuration options
212
214
 
213
- To use GoodJob, you can set `config.active_job.queue_adapter` to a `:good_job`.
215
+ ActiveJob configuration depends on where the code is placed:
214
216
 
215
- Additional configuration can be provided via `config.good_job.OPTION = ...`.
217
+ - `config.active_job.queue_adapter = :good_job` within `config/application.rb` or `config/environments/*.rb`.
218
+ - `ActiveJob::Base.queue_adapter = :good_job` within an initializer (e.g. `config/initializers/active_job.rb`).
216
219
 
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))._
220
+ GoodJob configuration can be placed within Rails `config` directory for all environments (`config/application.rb`), within a particular environment (e.g. `config/environments/development.rb`), or within an initializer (e.g. `config/initializers/good_job.rb`).
218
221
 
219
222
  Configuration examples:
220
223
 
221
224
  ```ruby
222
- # config/application.rb
223
-
224
- config.active_job.queue_adapter = :good_job
225
-
226
- # Configure options individually...
227
- config.good_job.execution_mode = :async
228
- config.good_job.max_threads = 5
229
- config.good_job.poll_interval = 30 # seconds
230
- config.good_job.shutdown_timeout = 25 # seconds
231
- config.good_job.enable_cron = true
232
- config.good_job.cron = { example: { cron: '0 * * * *', class: 'ExampleJob' } }
233
- config.good_job.queues = '*'
234
-
235
- # ...or all at once.
236
- config.good_job = {
237
- execution_mode: :async,
238
- max_threads: 5,
239
- poll_interval: 30,
240
- shutdown_timeout: 25,
241
- enable_cron: true,
242
- cron: {
243
- example: {
244
- cron: '0 * * * *',
245
- class: 'ExampleJob'
225
+ Rails.application.configure do
226
+ # Configure options individually...
227
+ config.good_job.preserve_job_records = true
228
+ config.good_job.retry_on_unhandled_error = false
229
+ config.good_job.on_thread_error = -> (exception) { Raven.capture_exception(exception) }
230
+ config.good_job.execution_mode = :async
231
+ config.good_job.max_threads = 5
232
+ config.good_job.poll_interval = 30 # seconds
233
+ config.good_job.shutdown_timeout = 25 # seconds
234
+ config.good_job.enable_cron = true
235
+ config.good_job.cron = { example: { cron: '0 * * * *', class: 'ExampleJob' } }
236
+ config.good_job.queues = '*'
237
+
238
+ # ...or all at once.
239
+ config.good_job = {
240
+ preserve_job_records: true,
241
+ retry_on_unhandled_error: false,
242
+ on_thread_error: -> (exception) { Raven.capture_exception(exception) },
243
+ execution_mode: :async,
244
+ max_threads: 5,
245
+ poll_interval: 30,
246
+ shutdown_timeout: 25,
247
+ enable_cron: true,
248
+ cron: {
249
+ example: {
250
+ cron: '0 * * * *',
251
+ class: 'ExampleJob'
252
+ },
246
253
  },
247
- },
248
- queues: '*',
249
- }
254
+ queues: '*',
255
+ }
256
+ end
250
257
  ```
251
258
 
252
259
  Available configuration options are:
@@ -263,6 +270,14 @@ Available configuration options are:
263
270
  - `shutdown_timeout` (float) number of seconds to wait for jobs to finish when shutting down before stopping the thread. Defaults to forever: `-1`. You can also set this with the environment variable `GOOD_JOB_SHUTDOWN_TIMEOUT`.
264
271
  - `enable_cron` (boolean) whether to run cron process. Defaults to `false`. You can also set this with the environment variable `GOOD_JOB_ENABLE_CRON`.
265
272
  - `cron` (hash) cron configuration. Defaults to `{}`. You can also set this as a JSON string with the environment variable `GOOD_JOB_CRON`
273
+ - `logger` ([Rails Logger](https://api.rubyonrails.org/classes/ActiveSupport/Logger.html)) lets you set a custom logger for GoodJob. It should be an instance of a Rails `Logger` (Default: `Rails.logger`).
274
+ - `preserve_job_records` (boolean) keeps job records in your database even after jobs are completed. (Default: `false`)
275
+ - `retry_on_unhandled_error` (boolean) causes jobs to be re-queued and retried if they raise an instance of `StandardError`. Instances of `Exception`, like SIGINT, will *always* be retried, regardless of this attribute’s value. (Default: `true`)
276
+ - `on_thread_error` (proc, lambda, or callable) will be called when an Exception. It can be useful for logging errors to bug tracking services, like Sentry or Airbrake. Example:
277
+
278
+ ```ruby
279
+ config.good_job.on_thread_error = -> (exception) { Raven.capture_exception(exception) }
280
+ ```
266
281
 
267
282
  By default, GoodJob configures the following execution modes per environment:
268
283
 
@@ -283,7 +298,7 @@ config.good_job.execution_mode = :external
283
298
 
284
299
  ### Global options
285
300
 
286
- Good Job’s general behavior can also be configured via several attributes directly on the `GoodJob` module:
301
+ Good Job’s general behavior can also be configured via attributes directly on the `GoodJob` module:
287
302
 
288
303
  - **`GoodJob.active_record_parent_class`** (string) The ActiveRecord parent class inherited by GoodJob's ActiveRecord model `GoodJob::Job` (defaults to `"ActiveRecord::Base"`). Configure this when using [multiple databases with ActiveRecord](https://guides.rubyonrails.org/active_record_multiple_databases.html) or when other custom configuration is necessary for the ActiveRecord model to connect to the Postgres database. _The value must be a String to avoid premature initialization of ActiveRecord._
289
304
  - **`GoodJob.logger`** ([Rails Logger](https://api.rubyonrails.org/classes/ActiveSupport/Logger.html)) lets you set a custom logger for GoodJob. It should be an instance of a Rails `Logger`.
@@ -837,6 +852,61 @@ A workaround to this limitation is to make a direct database connection availabl
837
852
  GoodJob.active_record_parent_class = "ApplicationDirectRecord"
838
853
  ```
839
854
 
855
+ ### CLI HTTP health check probes
856
+
857
+ GoodJob's CLI offers an http health check probe to better manage process lifecycle in containerized environments like Kubernetes:
858
+
859
+ ```bash
860
+ # Run the CLI with a health check on port 7001
861
+ good_job start --probe-port=7001
862
+
863
+ # or via an environment variable
864
+ GOOD_JOB_PROBE_PORT=7001 good_job start
865
+
866
+ # Probe the status
867
+ curl localhost:7001/status
868
+ curl localhost:7001/status/started
869
+ curl localhost:7001/status/connected
870
+ ```
871
+
872
+ Multiple health checks are available at different paths:
873
+
874
+ - `/` or `/status`: the CLI process is running
875
+ - `/status/started`: the multithreaded job executor is running
876
+ - `/status/connected`: the database connection is established
877
+
878
+ This can be configured, for example with Kubernetes:
879
+
880
+ ```yaml
881
+ spec:
882
+ containers:
883
+ - name: good_job
884
+ image: my_app:latest
885
+ env:
886
+ - name: RAILS_ENV
887
+ value: production
888
+ - name: GOOD_JOB_PROBE_PORT
889
+ value: 7001
890
+ command:
891
+ - good_job
892
+ - start
893
+ ports:
894
+ - name: probe-port
895
+ containerPort: 7001
896
+ startupProbe:
897
+ httpGet:
898
+ path: "/status/started"
899
+ port: probe-port
900
+ failureThreshold: 30
901
+ periodSeconds: 10
902
+ livenessProbe:
903
+ httpGet:
904
+ path: "/status/connected"
905
+ port: probe-port
906
+ failureThreshold: 1
907
+ periodSeconds: 10
908
+ ```
909
+
840
910
  ## Contribute
841
911
 
842
912
  Contributions are welcomed and appreciated 🙏
@@ -30,7 +30,10 @@ module GoodJob
30
30
  ORDER BY timestamp ASC
31
31
  SQL
32
32
 
33
- binds = [[nil, start_time], [nil, end_time]]
33
+ binds = [
34
+ ActiveRecord::Relation::QueryAttribute.new('start_time', start_time, ActiveRecord::Type::DateTime.new),
35
+ ActiveRecord::Relation::QueryAttribute.new('end_time', end_time, ActiveRecord::Type::DateTime.new),
36
+ ]
34
37
  executions_data = GoodJob::Execution.connection.exec_query(GoodJob::Execution.pg_or_jdbc_query(count_query), "GoodJob Dashboard Chart", binds)
35
38
 
36
39
  queue_names = executions_data.reject { |d| d['count'].nil? }.map { |d| d['queue_name'] || BaseFilter::EMPTY }.uniq
@@ -5,6 +5,25 @@ module GoodJob
5
5
 
6
6
  around_action :switch_locale
7
7
 
8
+ content_security_policy do |policy|
9
+ policy.default_src(:none) if policy.default_src(*policy.default_src).blank?
10
+ policy.connect_src(:self) if policy.connect_src(*policy.connect_src).blank?
11
+ policy.base_uri(:none) if policy.base_uri(*policy.base_uri).blank?
12
+ policy.font_src(:self) if policy.font_src(*policy.font_src).blank?
13
+ policy.img_src(:self, :data) if policy.img_src(*policy.img_src).blank?
14
+ policy.object_src(:none) if policy.object_src(*policy.object_src).blank?
15
+ policy.script_src(:self) if policy.script_src(*policy.script_src).blank?
16
+ policy.style_src(:self) if policy.style_src(*policy.style_src).blank?
17
+ policy.form_action(:self) if policy.form_action(*policy.form_action).blank?
18
+ policy.frame_ancestors(:none) if policy.frame_ancestors(*policy.frame_ancestors).blank?
19
+ end
20
+
21
+ before_action do
22
+ next if request.content_security_policy_nonce_generator
23
+
24
+ request.content_security_policy_nonce_generator = ->(_request) { SecureRandom.base64(16) }
25
+ end
26
+
8
27
  private
9
28
 
10
29
  def switch_locale(&action)
@@ -14,7 +14,7 @@ module GoodJob
14
14
  query = base_query
15
15
  query = query.job_class(params[:job_class]) if params[:job_class].present?
16
16
  query = query.where(queue_name: params[:queue_name]) if params[:queue_name].present?
17
- query = query.search(params['query']) if params[:query].present?
17
+ query = query.search_text(params[:query]) if params[:query].present?
18
18
 
19
19
  if params[:state]
20
20
  case params[:state]
@@ -18,7 +18,7 @@ module GoodJob
18
18
 
19
19
  query = query.job_class(params[:job_class]) if params[:job_class].present?
20
20
  query = query.where(queue_name: params[:queue_name]) if params[:queue_name].present?
21
- query = query.search(params['query']) if params[:query].present?
21
+ query = query.search_text(params[:query]) if params[:query].present?
22
22
 
23
23
  if params[:state]
24
24
  case params[:state]
@@ -4,6 +4,12 @@ module GoodJob
4
4
  # ActiveJob Adapter.
5
5
  #
6
6
  class Adapter
7
+ # @!attribute [r] instances
8
+ # @!scope class
9
+ # List of all instantiated Adapters in the current process.
10
+ # @return [Array<GoodJob::Adapter>, nil]
11
+ cattr_reader :instances, default: [], instance_reader: false
12
+
7
13
  # @param execution_mode [Symbol, nil] specifies how and where jobs should be executed. You can also set this with the environment variable +GOOD_JOB_EXECUTION_MODE+.
8
14
  #
9
15
  # - +:inline+ executes jobs immediately in whatever process queued them (usually the web server process). This should only be used in test and development environments.
@@ -20,7 +26,8 @@ module GoodJob
20
26
  # @param max_threads [Integer, nil] 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
27
  # @param queues [String, nil] 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
28
  # @param poll_interval [Integer, nil] 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
- def initialize(execution_mode: nil, queues: nil, max_threads: nil, poll_interval: nil)
29
+ # @param start_async_on_initialize [Boolean] whether to start the async scheduler when the adapter is initialized.
30
+ def initialize(execution_mode: nil, queues: nil, max_threads: nil, poll_interval: nil, start_async_on_initialize: Rails.application.initialized?)
24
31
  @configuration = GoodJob::Configuration.new(
25
32
  {
26
33
  execution_mode: execution_mode,
@@ -30,16 +37,9 @@ module GoodJob
30
37
  }
31
38
  )
32
39
  @configuration.validate!
40
+ self.class.instances << self
33
41
 
34
- if execute_async? # rubocop:disable Style/GuardClause
35
- @notifier = GoodJob::Notifier.new
36
- @poller = GoodJob::Poller.new(poll_interval: @configuration.poll_interval)
37
- @scheduler = GoodJob::Scheduler.from_configuration(@configuration, warm_cache_on_initialize: Rails.application.initialized?)
38
- @notifier.recipients << [@scheduler, :create_thread]
39
- @poller.recipients << [@scheduler, :create_thread]
40
-
41
- @cron_manager = GoodJob::CronManager.new(@configuration.cron_entries, start_on_initialize: Rails.application.initialized?) if @configuration.enable_cron?
42
- end
42
+ start_async if start_async_on_initialize
43
43
  end
44
44
 
45
45
  # Enqueues the ActiveJob job to be performed.
@@ -74,7 +74,7 @@ module GoodJob
74
74
  job_state = { queue_name: execution.queue_name }
75
75
  job_state[:scheduled_at] = execution.scheduled_at if execution.scheduled_at
76
76
 
77
- executed_locally = execute_async? && @scheduler.create_thread(job_state)
77
+ executed_locally = execute_async? && @scheduler&.create_thread(job_state)
78
78
  Notifier.notify(job_state) unless executed_locally
79
79
  end
80
80
 
@@ -97,6 +97,7 @@ module GoodJob
97
97
 
98
98
  executables = [@notifier, @poller, @scheduler].compact
99
99
  GoodJob._shutdown_all(executables, timeout: timeout)
100
+ @_async_started = false
100
101
  end
101
102
 
102
103
  # Whether in +:async+ execution mode.
@@ -119,6 +120,28 @@ module GoodJob
119
120
  @configuration.execution_mode == :inline
120
121
  end
121
122
 
123
+ # Start async executors
124
+ # @return void
125
+ def start_async
126
+ return unless execute_async?
127
+
128
+ @notifier = GoodJob::Notifier.new
129
+ @poller = GoodJob::Poller.new(poll_interval: @configuration.poll_interval)
130
+ @scheduler = GoodJob::Scheduler.from_configuration(@configuration, warm_cache_on_initialize: true)
131
+ @notifier.recipients << [@scheduler, :create_thread]
132
+ @poller.recipients << [@scheduler, :create_thread]
133
+
134
+ @cron_manager = GoodJob::CronManager.new(@configuration.cron_entries, start_on_initialize: true) if @configuration.enable_cron?
135
+
136
+ @_async_started = true
137
+ end
138
+
139
+ # Whether the async executors are running
140
+ # @return [Boolean]
141
+ def async_started?
142
+ @_async_started
143
+ end
144
+
122
145
  private
123
146
 
124
147
  # Whether running in a web server process.
data/lib/good_job/cli.rb CHANGED
@@ -79,6 +79,9 @@ module GoodJob
79
79
  method_option :pidfile,
80
80
  type: :string,
81
81
  desc: "Path to write daemonized Process ID (env var: GOOD_JOB_PIDFILE, default: tmp/pids/good_job.pid)"
82
+ method_option :probe_port,
83
+ type: :numeric,
84
+ desc: "Port for http health check (env var: GOOD_JOB_PROBE_PORT, default: nil)"
82
85
 
83
86
  def start
84
87
  set_up_application!
@@ -94,6 +97,11 @@ module GoodJob
94
97
 
95
98
  cron_manager = GoodJob::CronManager.new(configuration.cron_entries, start_on_initialize: true) if configuration.enable_cron?
96
99
 
100
+ if configuration.probe_port
101
+ probe_server = GoodJob::ProbeServer.new(port: configuration.probe_port)
102
+ probe_server.start
103
+ end
104
+
97
105
  @stop_good_job_executable = false
98
106
  %w[INT TERM].each do |signal|
99
107
  trap(signal) { @stop_good_job_executable = true }
@@ -106,6 +114,7 @@ module GoodJob
106
114
 
107
115
  executors = [notifier, poller, cron_manager, scheduler].compact
108
116
  GoodJob._shutdown_all(executors, timeout: configuration.shutdown_timeout)
117
+ probe_server&.stop
109
118
  end
110
119
 
111
120
  default_task :start
@@ -50,24 +50,22 @@ module GoodJob
50
50
  # for more details on possible values.
51
51
  # @return [Symbol]
52
52
  def execution_mode
53
- @_execution_mode ||= begin
54
- mode = if GoodJob::CLI.within_exe?
55
- :external
56
- else
57
- options[:execution_mode] ||
58
- rails_config[:execution_mode] ||
59
- env['GOOD_JOB_EXECUTION_MODE']
60
- end
61
-
62
- if mode
63
- mode.to_sym
64
- elsif Rails.env.development?
65
- :async
66
- elsif Rails.env.test?
67
- :inline
68
- else
69
- :external
70
- end
53
+ mode = if GoodJob::CLI.within_exe?
54
+ :external
55
+ else
56
+ options[:execution_mode] ||
57
+ rails_config[:execution_mode] ||
58
+ env['GOOD_JOB_EXECUTION_MODE']
59
+ end
60
+
61
+ if mode
62
+ mode.to_sym
63
+ elsif Rails.env.development?
64
+ :async
65
+ elsif Rails.env.test?
66
+ :inline
67
+ else
68
+ :external
71
69
  end
72
70
  end
73
71
 
@@ -195,6 +193,13 @@ module GoodJob
195
193
  Rails.application.root.join('tmp', 'pids', 'good_job.pid')
196
194
  end
197
195
 
196
+ # Port of the probe server
197
+ # @return [nil,Integer]
198
+ def probe_port
199
+ options[:probe_port] ||
200
+ env['GOOD_JOB_PROBE_PORT']
201
+ end
202
+
198
203
  private
199
204
 
200
205
  def rails_config
@@ -21,7 +21,7 @@ module GoodJob # :nodoc:
21
21
  def self.task_observer(time, output, thread_error) # rubocop:disable Lint/UnusedMethodArgument
22
22
  return if thread_error.is_a? Concurrent::CancelledOperationError
23
23
 
24
- GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
24
+ GoodJob._on_thread_error(thread_error) if thread_error
25
25
  end
26
26
 
27
27
  # Execution configuration to be scheduled
@@ -24,12 +24,12 @@ module GoodJob
24
24
  end)
25
25
 
26
26
  # Search records by text query.
27
- # @!method search(query)
27
+ # @!method search_text(query)
28
28
  # @!scope class
29
29
  # @param query [String]
30
30
  # Search Query
31
31
  # @return [ActiveRecord::Relation]
32
- scope :search, (lambda do |query|
32
+ scope :search_text, (lambda do |query|
33
33
  query = query.to_s.strip
34
34
  next if query.blank?
35
35
 
@@ -13,9 +13,6 @@ module GoodJob
13
13
  # @param queue_string [String] Queues to execute jobs from
14
14
  def initialize(queue_string)
15
15
  @queue_string = queue_string
16
-
17
- @job_query = Concurrent::Delay.new { GoodJob::Execution.queue_string(queue_string) }
18
- @parsed_queues = Concurrent::Delay.new { GoodJob::Execution.queue_parser(queue_string) }
19
16
  end
20
17
 
21
18
  # A meaningful name to identify the performer in logs and for debugging.
@@ -65,11 +62,11 @@ module GoodJob
65
62
  attr_reader :queue_string
66
63
 
67
64
  def job_query
68
- @job_query.value
65
+ @_job_query ||= GoodJob::Execution.queue_string(queue_string)
69
66
  end
70
67
 
71
68
  def parsed_queues
72
- @parsed_queues.value
69
+ @_parsed_queues ||= GoodJob::Execution.queue_parser(queue_string)
73
70
  end
74
71
  end
75
72
  end
@@ -24,7 +24,7 @@ module GoodJob
24
24
 
25
25
  included do
26
26
  # Default column to be used when creating Advisory Locks
27
- class_attribute :advisory_lockable_column, instance_accessor: false, default: Concurrent::Delay.new { primary_key }
27
+ class_attribute :advisory_lockable_column, instance_accessor: false, default: nil
28
28
 
29
29
  # Default Postgres function to be used for Advisory Locks
30
30
  class_attribute :advisory_lockable_function, default: "pg_try_advisory_lock"
@@ -161,10 +161,8 @@ module GoodJob
161
161
  end
162
162
  end
163
163
 
164
- # Allow advisory_lockable_column to be a `Concurrent::Delay`
165
164
  def _advisory_lockable_column
166
- column = advisory_lockable_column
167
- column.respond_to?(:value) ? column.value : column
165
+ advisory_lockable_column || primary_key
168
166
  end
169
167
 
170
168
  def supports_cte_materialization_specifiers?
@@ -217,7 +215,9 @@ module GoodJob
217
215
  SQL
218
216
  end
219
217
 
220
- binds = [[nil, key]]
218
+ binds = [
219
+ ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
220
+ ]
221
221
  self.class.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Lock', binds).first['locked']
222
222
  end
223
223
 
@@ -231,7 +231,9 @@ module GoodJob
231
231
  query = <<~SQL.squish
232
232
  SELECT #{function}(('x'||substr(md5($1::text), 1, 16))::bit(64)::bigint) AS unlocked
233
233
  SQL
234
- binds = [[nil, key]]
234
+ binds = [
235
+ ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
236
+ ]
235
237
  self.class.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Unlock', binds).first['unlocked']
236
238
  end
237
239
 
@@ -281,7 +283,10 @@ module GoodJob
281
283
  AND pg_locks.classid = ('x' || substr(md5($1::text), 1, 16))::bit(32)::int
282
284
  AND pg_locks.objid = (('x' || substr(md5($2::text), 1, 16))::bit(64) << 32)::bit(32)::int
283
285
  SQL
284
- binds = [[nil, key], [nil, key]]
286
+ binds = [
287
+ ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
288
+ ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
289
+ ]
285
290
  self.class.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Locked?', binds).any?
286
291
  end
287
292
 
@@ -305,7 +310,10 @@ module GoodJob
305
310
  AND pg_locks.objid = (('x' || substr(md5($2::text), 1, 16))::bit(64) << 32)::bit(32)::int
306
311
  AND pg_locks.pid = pg_backend_pid()
307
312
  SQL
308
- binds = [[nil, key], [nil, key]]
313
+ binds = [
314
+ ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
315
+ ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
316
+ ]
309
317
  self.class.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Owns Advisory Lock?', binds).any?
310
318
  end
311
319
 
@@ -120,7 +120,7 @@ module GoodJob # :nodoc:
120
120
  return if thread_error.is_a? AdapterCannotListenError
121
121
 
122
122
  if thread_error
123
- GoodJob.on_thread_error.call(thread_error) if GoodJob.on_thread_error.respond_to?(:call)
123
+ GoodJob._on_thread_error(thread_error)
124
124
  ActiveSupport::Notifications.instrument("notifier_notify_error.good_job", { error: thread_error })
125
125
 
126
126
  connection_error = CONNECTION_ERRORS.any? do |error_string|
@@ -91,7 +91,7 @@ module GoodJob # :nodoc:
91
91
  # @param thread_error [Exception, nil]
92
92
  # @return [void]
93
93
  def timer_observer(time, executed_task, thread_error)
94
- GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
94
+ GoodJob._on_thread_error(thread_error) if thread_error
95
95
  ActiveSupport::Notifications.instrument("finished_timer_task", { result: executed_task, error: thread_error, time: time })
96
96
  end
97
97
 
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GoodJob
4
+ class ProbeServer
5
+ RACK_SERVER = 'webrick'
6
+
7
+ def self.task_observer(time, output, thread_error) # rubocop:disable Lint/UnusedMethodArgument
8
+ return if thread_error.is_a? Concurrent::CancelledOperationError
9
+
10
+ GoodJob._on_thread_error(thread_error) if thread_error
11
+ end
12
+
13
+ def initialize(port:)
14
+ @port = port
15
+ end
16
+
17
+ def start
18
+ @handler = Rack::Handler.get(RACK_SERVER)
19
+ @future = Concurrent::Future.new(args: [@handler, @port, GoodJob.logger]) do |thr_handler, thr_port, thr_logger|
20
+ thr_handler.run(self, Port: thr_port, Logger: thr_logger, AccessLog: [])
21
+ end
22
+ @future.add_observer(self.class, :task_observer)
23
+ @future.execute
24
+ end
25
+
26
+ def running?
27
+ @handler&.instance_variable_get(:@server)&.status == :Running
28
+ end
29
+
30
+ def stop
31
+ @handler&.shutdown
32
+ @future&.value # wait for Future to exit
33
+ end
34
+
35
+ def call(env)
36
+ case Rack::Request.new(env).path
37
+ when '/', '/status'
38
+ [200, {}, ["OK"]]
39
+ when '/status/started'
40
+ started = GoodJob::Scheduler.instances.any? && GoodJob::Scheduler.instances.all?(&:running?)
41
+ started ? [200, {}, ["Started"]] : [503, {}, ["Not started"]]
42
+ when '/status/connected'
43
+ connected = GoodJob::Scheduler.instances.any? && GoodJob::Scheduler.instances.all?(&:running?) &&
44
+ GoodJob::Notifier.instances.any? && GoodJob::Notifier.instances.all?(&:listening?)
45
+ connected ? [200, {}, ["Connected"]] : [503, {}, ["Not connected"]]
46
+ else
47
+ [404, {}, ["Not found"]]
48
+ end
49
+ end
50
+ end
51
+ end
@@ -7,7 +7,7 @@ module GoodJob
7
7
 
8
8
  initializer "good_job.logger" do |_app|
9
9
  ActiveSupport.on_load(:good_job) do
10
- self.logger = ::Rails.logger
10
+ self.logger = ::Rails.logger if GoodJob.logger == GoodJob::DEFAULT_LOGGER
11
11
  end
12
12
  GoodJob::LogSubscriber.attach_to :good_job
13
13
  end
@@ -22,9 +22,19 @@ module GoodJob
22
22
  end
23
23
  end
24
24
 
25
- config.after_initialize do
26
- GoodJob::Scheduler.instances.each(&:warm_cache)
27
- GoodJob::CronManager.instances.each(&:start)
25
+ initializer 'good_job.rails_config' do
26
+ config.after_initialize do
27
+ GoodJob.logger = Rails.application.config.good_job.logger unless Rails.application.config.good_job.logger.nil?
28
+ GoodJob.on_thread_error = Rails.application.config.good_job.on_thread_error unless Rails.application.config.good_job.on_thread_error.nil?
29
+ GoodJob.preserve_job_records = Rails.application.config.good_job.preserve_job_records unless Rails.application.config.good_job.preserve_job_records.nil?
30
+ GoodJob.retry_on_unhandled_error = Rails.application.config.good_job.retry_on_unhandled_error unless Rails.application.config.good_job.retry_on_unhandled_error.nil?
31
+ end
32
+ end
33
+
34
+ initializer "good_job.start_async" do
35
+ config.after_initialize do
36
+ GoodJob::Adapter.instances.each(&:start_async)
37
+ end
28
38
  end
29
39
  end
30
40
  end
@@ -169,7 +169,7 @@ module GoodJob # :nodoc:
169
169
  # @return [void]
170
170
  def task_observer(time, output, thread_error)
171
171
  error = thread_error || (output.is_a?(GoodJob::ExecutionResult) ? output.unhandled_error : nil)
172
- GoodJob.on_thread_error.call(error) if error && GoodJob.on_thread_error.respond_to?(:call)
172
+ GoodJob._on_thread_error(error) if error
173
173
 
174
174
  instrument("finished_job_task", { result: output, error: thread_error, time: time })
175
175
  create_task if output
@@ -206,7 +206,7 @@ module GoodJob # :nodoc:
206
206
  end
207
207
 
208
208
  observer = lambda do |_time, _output, thread_error|
209
- GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call)
209
+ GoodJob._on_thread_error(thread_error) if thread_error
210
210
  create_task # If cache-warming exhausts the threads, ensure there isn't an executable task remaining
211
211
  end
212
212
  future.add_observer(observer, :call)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  module GoodJob
3
3
  # GoodJob gem version.
4
- VERSION = '2.6.1'
4
+ VERSION = '2.7.2'
5
5
  end
data/lib/good_job.rb CHANGED
@@ -18,6 +18,8 @@ require "good_job/railtie"
18
18
  #
19
19
  # +GoodJob+ is the top-level namespace and exposes configuration attributes.
20
20
  module GoodJob
21
+ DEFAULT_LOGGER = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new($stdout))
22
+
21
23
  # @!attribute [rw] active_record_parent_class
22
24
  # @!scope class
23
25
  # The ActiveRecord parent class inherited by +GoodJob::Execution+ (default: +ActiveRecord::Base+).
@@ -34,7 +36,7 @@ module GoodJob
34
36
  # @return [Logger, nil]
35
37
  # @example Output GoodJob logs to a file:
36
38
  # GoodJob.logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new("log/my_logs.log"))
37
- mattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new($stdout))
39
+ mattr_accessor :logger, default: DEFAULT_LOGGER
38
40
 
39
41
  # @!attribute [rw] preserve_job_records
40
42
  # @!scope class
@@ -66,6 +68,13 @@ module GoodJob
66
68
  # @return [Proc, nil]
67
69
  mattr_accessor :on_thread_error, default: nil
68
70
 
71
+ # Called with exception when a GoodJob thread raises an exception
72
+ # @param exception [Exception] Exception that was raised
73
+ # @return [void]
74
+ def self._on_thread_error(exception)
75
+ on_thread_error.call(exception) if on_thread_error.respond_to?(:call)
76
+ end
77
+
69
78
  # Stop executing jobs.
70
79
  # GoodJob does its work in pools of background threads.
71
80
  # When forking processes you should shut down these background threads before forking, and restart them after forking.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: good_job
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.6.1
4
+ version: 2.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Sheldon
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-11-05 00:00:00.000000000 Z
11
+ date: 2021-11-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: 0.14.1
97
+ - !ruby/object:Gem::Dependency
98
+ name: webrick
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '1.3'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '1.3'
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: zeitwerk
99
113
  requirement: !ruby/object:Gem::Requirement
@@ -410,6 +424,7 @@ files:
410
424
  - lib/good_job/multi_scheduler.rb
411
425
  - lib/good_job/notifier.rb
412
426
  - lib/good_job/poller.rb
427
+ - lib/good_job/probe_server.rb
413
428
  - lib/good_job/railtie.rb
414
429
  - lib/good_job/scheduler.rb
415
430
  - lib/good_job/version.rb