good_job 0.7.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4f7dda29494df3fc05a199d6bf0efbc825a47653c3af507a0d1ceb92456d4307
4
- data.tar.gz: 881ad046bab6b17c5d532d035fddef54420639288a0a4bd8f4f07b980c1fa239
3
+ metadata.gz: ff9a5cf449c835415f08a2b5704ad5345519b4cf73ab987de72a011101700a96
4
+ data.tar.gz: 0d7a104ef5440c406d5bec5f65b80099d9a2678cc72bd6b399dde1912e49f0db
5
5
  SHA512:
6
- metadata.gz: c0cfc3c4d61666844a6bd7c532670e0b1be40fd6f805eff63919d3600d06933b8029f0febbc81efbf52b1f3d459317caeb522affe20dde9d3f4d7672622fd708
7
- data.tar.gz: c4c9a9fddb2108769e3672baeaeaca2f7c966ee708f95867ed57970e688debcf1ce41a3eacd65d88c0192700f1b293cd4a9ad63fee9e4af93d44934b0876120f
6
+ metadata.gz: 6c027b651e5d9bc9980f4b8e912aa64f5a393b78b0b291195446901b1f2dabddd2022b1ed50e4fab8783983496c4b00025339630d49dc7a894cfb63b8f8292b6
7
+ data.tar.gz: eea0bc9425b6aad9a6947336ffdad87da6180febb1128eec3d5f99a2d6b3613be5d1293ec6666b900f9ca38ad6a657fd3f6a2ee75fa1271cb6767513aa688374
@@ -1,15 +1,21 @@
1
1
  # Changelog
2
2
 
3
- ## [v0.7.0](https://github.com/bensheldon/good_job/tree/v0.7.0) (2020-07-15)
3
+ ## [v1.0.0](https://github.com/bensheldon/good_job/tree/v1.0.0) (2020-07-20)
4
4
 
5
- [Full Changelog](https://github.com/bensheldon/good_job/compare/v0.6.0...v0.7.0)
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v0.6.0...v1.0.0)
6
6
 
7
7
  **Closed issues:**
8
8
 
9
9
  - Always store a default priority \(0\) and scheduled\_at\(Time.current\) [\#30](https://github.com/bensheldon/good_job/issues/30)
10
+ - Add a job timeout configuration to time out jobs that have run too long [\#19](https://github.com/bensheldon/good_job/issues/19)
10
11
 
11
12
  **Merged pull requests:**
12
13
 
14
+ - Allow preservation of finished job records [\#46](https://github.com/bensheldon/good_job/pull/46) ([bensheldon](https://github.com/bensheldon))
15
+ - Run Github Action tests on PRs from forks [\#44](https://github.com/bensheldon/good_job/pull/44) ([bensheldon](https://github.com/bensheldon))
16
+ - Fix Rubygems homepage URL [\#43](https://github.com/bensheldon/good_job/pull/43) ([joshmn](https://github.com/joshmn))
17
+ - Move where\(scheduled\_at: Time.current\) into dynamic part of GoodJob::Job::Performer [\#42](https://github.com/bensheldon/good_job/pull/42) ([bensheldon](https://github.com/bensheldon))
18
+ - Replace Adapter inline boolean kwarg with execution\_mode instead [\#41](https://github.com/bensheldon/good_job/pull/41) ([bensheldon](https://github.com/bensheldon))
13
19
  - Add more examples to Readme [\#39](https://github.com/bensheldon/good_job/pull/39) ([bensheldon](https://github.com/bensheldon))
14
20
  - Add additional Rubocops and lint [\#38](https://github.com/bensheldon/good_job/pull/38) ([bensheldon](https://github.com/bensheldon))
15
21
  - Always store a default queue\_name, priority and scheduled\_at; index by queue\_name and scheduled\_at [\#37](https://github.com/bensheldon/good_job/pull/37) ([bensheldon](https://github.com/bensheldon))
data/README.md CHANGED
@@ -1,21 +1,13 @@
1
1
  # GoodJob
2
2
 
3
- GoodJob is a multithreaded, Postgres-based ActiveJob backend for Ruby on Rails.
3
+ GoodJob is a multithreaded, Postgres-based, ActiveJob backend for Ruby on Rails.
4
4
 
5
- Inspired by [Delayed::Job](https://github.com/collectiveidea/delayed_job) and [Que](https://github.com/que-rb/que), GoodJob’s design principles are:
5
+ **Inspired by [Delayed::Job](https://github.com/collectiveidea/delayed_job) and [Que](https://github.com/que-rb/que), GoodJob is designed for maximum compatibility with Ruby on Rails, ActiveJob, and Postgres to be simple and performant for most workloads.**
6
6
 
7
- - Stand on the shoulders of ActiveJob. For example, [exception](https://edgeguides.rubyonrails.org/active_job_basics.html#exceptions) and [retry](https://edgeguides.rubyonrails.org/active_job_basics.html#retrying-or-discarding-failed-jobs) behavior.
8
- - Stand on the shoulders of Ruby on Rails. For example, ActiveRecord ORM, connection pools, and [multithreaded support](https://guides.rubyonrails.org/threading_and_code_execution.html) with [Concurrent-Ruby](https://github.com/ruby-concurrency/concurrent-ruby).
9
- - Stand on the shoulders of Postgres. For example, Advisory Locks.
10
- - Convention over simplicity over performance.
11
-
12
- GoodJob supports all ActiveJob functionality:
13
- - Async. GoodJob has the ability to run the job in a non-blocking manner.
14
- - Queues. Jobs may set which queue they are run in with queue_as or by using the set method.
15
- - Delayed. GoodJob will run the job in the future through perform_later.
16
- - Priorities. The order in which jobs are processed can be configured differently.
17
- - Timeouts. GoodJob defers to ActiveJob where it can be implemented as an `around` hook. See [Taking advantage of ActiveJob](#taking-advantage-of-activejob).
18
- - Retries. GoodJob will automatically retry uncompleted jobs immediately. See [Taking advantage of ActiveJob](#taking-advantage-of-activejob).
7
+ - **Designed for ActiveJob.** Complete support for [async, queues, delays, priorities, timeouts, and retries](https://edgeguides.rubyonrails.org/active_job_basics.html) with near-zero configuration.
8
+ - **Built for Rails.** Fully adopts Ruby on Rails [threading and code execution guidelines](https://guides.rubyonrails.org/threading_and_code_execution.html) with [Concurrent::Ruby](https://github.com/ruby-concurrency/concurrent-ruby).
9
+ - **Backed by Postgres.** Relies upon Postgres integrity and session-level Advisory Locks to provide run-once safety and stay within the limits of `schema.rb`.
10
+ - **For most workloads.** Targets full-stack teams, economy-minded solo developers, and applications that enqueue less than 1-million jobs/day.
19
11
 
20
12
  ## Installation
21
13
 
@@ -51,9 +43,12 @@ $ bundle install
51
43
  t.integer :priority
52
44
  t.jsonb :serialized_params
53
45
  t.timestamp :scheduled_at
54
-
55
- t.index :scheduled_at
56
- t.index [:queue_name, :scheduled_at]
46
+ t.timestamp :performed_at
47
+ t.timestamp :finished_at
48
+ t.text :error
49
+
50
+ add_index :good_jobs, :scheduled_at, where: "(finished_at IS NULL)"
51
+ add_index :good_jobs, [:queue_name, :scheduled_at], where: "(finished_at IS NULL)"
57
52
  end
58
53
  end
59
54
  end
@@ -75,13 +70,13 @@ $ bundle install
75
70
 
76
71
  ```ruby
77
72
  # config/environments/development.rb
78
- config.active_job.queue_adapter = GoodJob::Adapter.new(inline: true)
73
+ config.active_job.queue_adapter = GoodJob::Adapter.new(execution_mode: :inline)
79
74
 
80
75
  # config/environments/test.rb
81
- config.active_job.queue_adapter = GoodJob::Adapter.new(inline: true)
76
+ config.active_job.queue_adapter = GoodJob::Adapter.new(execution_mode: :inline)
82
77
 
83
78
  # config/environments/production.rb
84
- config.active_job.queue_adapter = GoodJob::Adapter.new
79
+ config.active_job.queue_adapter = GoodJob::Adapter.new(execution_mode: :external)
85
80
  ```
86
81
 
87
82
  1. Queue your job 🎉:
@@ -137,6 +132,55 @@ GoodJob executes enqueued jobs using threads. There is a lot than can be said ab
137
132
  3. `$ RAILS_MAX_THREADS=4 bundle exec good_job`
138
133
  4. Implicitly via Rails's database connection pool size (`ActiveRecord::Base.connection_pool.size`)
139
134
 
135
+ ### Migrating to GoodJob from a different ActiveJob backend
136
+
137
+ If your application is already using an ActiveJob backend, you will need to install GoodJob to enqueue and perform newly created jobs _and_ finish performing pre-existing jobs on the previous backend.
138
+
139
+ 1. Enqueue newly created jobs on GoodJob either entirely by setting `ActiveJob::Base.queue_adapter = :good_job` or progressively via individual job classes:
140
+
141
+ ```ruby
142
+ # jobs/specific_job.rb
143
+ class SpecificJob < ApplicationJob
144
+ self.queue_adapter = :good_job
145
+ # ...
146
+ end
147
+ ```
148
+
149
+ 1. Continue running executors for both backends. For example, on Heroku it's possible to run [two processes](https://help.heroku.com/CTFS2TJK/how-do-i-run-multiple-processes-on-a-dyno) within the same dyno:
150
+ ```procfile
151
+ # Procfile
152
+ # ...
153
+ worker: bundle exec que ./config/environment.rb & bundle exec good_job & wait -n
154
+ ```
155
+
156
+ 1. Once you are confident that no unperformed jobs remain in the previous ActiveJob backend, code and configuration for that backend can be completely removed.
157
+
158
+ ### Monitoring and preserving worked jobs
159
+
160
+ GoodJob is fully instrumented with [`ActiveSupport::Notifications`](https://edgeguides.rubyonrails.org/active_support_instrumentation.html#introduction-to-instrumentation).
161
+
162
+ By default, GoodJob will delete job records after they are run, regardless of whether they succeed or not (raising a kind of `StandardError`), unless they are interrupted (raising a kind of `Exception`).
163
+
164
+ To preserve job records for later inspection, set an initializer:
165
+
166
+ ```ruby
167
+ # config/initializers/good_job.rb
168
+ GoodJob.preserve_job_records = true
169
+ ```
170
+
171
+ It is also necessary to delete these preserved jobs from the database after a certain time period:
172
+
173
+ - For example, in a Rake task:
174
+
175
+ ```ruby
176
+ # GoodJob::Job.finished(1.day.ago).delete_all
177
+ ```
178
+ - For example, using the `good_job` command-line utility:
179
+
180
+ ```bash
181
+ $ bundle exec good_job cleanup_preserved_jobs --before-seconds-ago=86400
182
+ ```
183
+
140
184
  ## Development
141
185
 
142
186
  To run tests:
@@ -1,12 +1,18 @@
1
1
  module ActiveJob
2
2
  module QueueAdapters
3
3
  class GoodJobAdapter < GoodJob::Adapter
4
- def initialize
5
- if Rails.env.development? || Rails.env.test?
6
- super(inline: true)
7
- else
8
- super(inline: false)
9
- end
4
+ def initialize(execution_mode: nil)
5
+ execution_mode = if execution_mode
6
+ execution_mode
7
+ elsif ENV['GOOD_JOB_EXECUTION_MODE'].present?
8
+ ENV['GOOD_JOB_EXECUTION_MODE'].to_sym
9
+ elsif Rails.env.development? || Rails.env.test?
10
+ :inline
11
+ else
12
+ :external
13
+ end
14
+
15
+ super(execution_mode: execution_mode)
10
16
  end
11
17
  end
12
18
  end
@@ -4,13 +4,15 @@ require 'good_job/railtie'
4
4
  require 'good_job/logging'
5
5
  require 'good_job/lockable'
6
6
  require 'good_job/job'
7
- require "good_job/scheduler"
7
+ require 'good_job/scheduler'
8
8
  require 'good_job/adapter'
9
9
  require 'good_job/pg_locks'
10
+ require 'good_job/performer'
10
11
 
11
12
  require 'active_job/queue_adapters/good_job_adapter'
12
13
 
13
14
  module GoodJob
15
+ mattr_accessor :preserve_job_records, default: false
14
16
  include Logging
15
17
 
16
18
  ActiveSupport.run_load_hooks(:good_job, self)
@@ -1,7 +1,18 @@
1
1
  module GoodJob
2
2
  class Adapter
3
- def initialize(inline: false)
4
- @inline = inline
3
+ EXECUTION_MODES = [:inline, :external].freeze # TODO: async
4
+
5
+ def initialize(execution_mode: nil, inline: false)
6
+ if inline
7
+ ActiveSupport::Deprecation.warn('GoodJob::Adapter#new(inline: true) is deprecated; use GoodJob::Adapter.new(execution_mode: :inline) instead')
8
+ @execution_mode = :inline
9
+ elsif execution_mode
10
+ raise ArgumentError, "execution_mode: must be one of #{EXECUTION_MODES.join(', ')}." unless EXECUTION_MODES.include?(execution_mode)
11
+
12
+ @execution_mode = execution_mode
13
+ else
14
+ @execution_mode = :external
15
+ end
5
16
  end
6
17
 
7
18
  def enqueue(active_job)
@@ -12,10 +23,10 @@ module GoodJob
12
23
  good_job = GoodJob::Job.enqueue(
13
24
  active_job,
14
25
  scheduled_at: timestamp ? Time.zone.at(timestamp) : nil,
15
- create_with_advisory_lock: inline?
26
+ create_with_advisory_lock: execute_inline?
16
27
  )
17
28
 
18
- if inline?
29
+ if execute_inline?
19
30
  begin
20
31
  good_job.perform
21
32
  ensure
@@ -30,8 +41,17 @@ module GoodJob
30
41
  nil
31
42
  end
32
43
 
44
+ def execute_inline?
45
+ @execution_mode == :inline
46
+ end
47
+
33
48
  def inline?
34
- @inline
49
+ ActiveSupport::Deprecation.warn('GoodJob::Adapter::inline? is deprecated; use GoodJob::Adapter::execute_inline? instead')
50
+ execute_inline?
51
+ end
52
+
53
+ def execute_externally?
54
+ @execution_mode == :external
35
55
  end
36
56
  end
37
57
  end
@@ -36,11 +36,16 @@ module GoodJob
36
36
  ENV['GOOD_JOB_POLL_INTERVAL']
37
37
  ).to_i
38
38
 
39
- job_query = GoodJob::Job.all
39
+ job_query = GoodJob::Job.all.priority_ordered
40
40
  queue_names_without_all = queue_names.reject { |q| q == '*' }
41
41
  job_query = job_query.where(queue_name: queue_names_without_all) unless queue_names_without_all.size.zero?
42
42
 
43
- job_performer = job_query.only_scheduled.priority_ordered.to_performer
43
+ performer_method = if GoodJob.preserve_job_records
44
+ :perform_with_advisory_lock_and_preserve_job_records
45
+ else
46
+ :perform_with_advisory_lock_and_destroy_job_records
47
+ end
48
+ job_performer = GoodJob::Performer.new(job_query, performer_method)
44
49
 
45
50
  $stdout.puts "GoodJob worker starting with max_threads=#{max_threads} on queues=#{queue_names.join(',')}"
46
51
 
@@ -68,6 +73,19 @@ module GoodJob
68
73
  $stdout.puts "GoodJob's jobs finished, exiting..."
69
74
  end
70
75
 
76
+ desc :cleanup_preserved_jobs, "Delete preserved job records"
77
+ method_option :before_seconds_ago,
78
+ type: :numeric,
79
+ default: 24 * 60 * 60,
80
+ desc: "Delete records finished more than this many seconds ago"
81
+ def cleanup_preserved_jobs
82
+ require RAILS_ENVIRONMENT_RB
83
+
84
+ timestamp = Time.current - options[:before_seconds_ago]
85
+ result = GoodJob::Job.finished(timestamp).delete_all
86
+ $stdout.puts "Deleted #{result} preserved #{'job'.pluralize(result)} finished before #{timestamp}."
87
+ end
88
+
71
89
  default_task :start
72
90
  end
73
91
  end
@@ -2,32 +2,46 @@ module GoodJob
2
2
  class Job < ActiveRecord::Base
3
3
  include Lockable
4
4
 
5
+ PreviouslyPerformedError = Class.new(StandardError)
6
+
5
7
  DEFAULT_QUEUE_NAME = 'default'.freeze
6
8
  DEFAULT_PRIORITY = 0
7
9
 
8
10
  self.table_name = 'good_jobs'.freeze
9
11
 
12
+ scope :unfinished, (lambda do
13
+ if column_names.include?('finished_at')
14
+ where(finished_at: nil)
15
+ else
16
+ ActiveSupport::Deprecation.warn('GoodJob expects a good_jobs.finished_at column to exist. Please see the GoodJob README.md for migration instructions.')
17
+ nil
18
+ end
19
+ end)
10
20
  scope :only_scheduled, -> { where(arel_table['scheduled_at'].lteq(Time.current)).or(where(scheduled_at: nil)) }
11
21
  scope :priority_ordered, -> { order(priority: :desc) }
12
- scope :to_performer, -> { Performer.new(self) }
22
+ scope :finished, ->(timestamp = nil) { timestamp ? where(arel_table['finished_at'].lteq(timestamp)) : where.not(finished_at: nil) }
13
23
 
14
- class Performer
15
- def initialize(query)
16
- @query = query
17
- end
24
+ def self.perform_with_advisory_lock(destroy_after: !GoodJob.preserve_job_records)
25
+ good_job = nil
26
+ result = nil
27
+ error = nil
28
+
29
+ unfinished.only_scheduled.limit(1).with_advisory_lock do |good_jobs|
30
+ good_job = good_jobs.first
31
+ break unless good_job
18
32
 
19
- def next
20
- good_job = nil
33
+ result, error = good_job.perform(destroy_after: destroy_after)
34
+ end
21
35
 
22
- @query.limit(1).with_advisory_lock do |good_jobs|
23
- good_job = good_jobs.first
24
- break unless good_job
36
+ [good_job, result, error] if good_job
37
+ end
25
38
 
26
- good_job.perform
27
- end
39
+ def self.perform_with_advisory_lock_and_preserve_job_records
40
+ perform_with_advisory_lock(destroy_after: false)
41
+ end
28
42
 
29
- good_job
30
- end
43
+ def self.perform_with_advisory_lock_and_destroy_job_records
44
+ perform_with_advisory_lock(destroy_after: true)
31
45
  end
32
46
 
33
47
  def self.enqueue(active_job, scheduled_at: nil, create_with_advisory_lock: false)
@@ -50,16 +64,43 @@ module GoodJob
50
64
  good_job
51
65
  end
52
66
 
53
- def perform
67
+ def perform(destroy_after: true)
68
+ raise PreviouslyPerformedError, 'Cannot perform a job that has already been performed' if finished_at
69
+
70
+ result = nil
71
+ error = nil
72
+
54
73
  ActiveSupport::Notifications.instrument("before_perform_job.good_job", { good_job: self })
74
+ self.performed_at = Time.current
75
+ save! unless destroy_after
76
+
55
77
  ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self }) do
56
78
  params = serialized_params.merge(
57
79
  "provider_job_id" => id
58
80
  )
59
- ActiveJob::Base.execute(params)
81
+ begin
82
+ result = ActiveJob::Base.execute(params)
83
+ rescue StandardError => e
84
+ error = e
85
+ end
86
+ end
60
87
 
88
+ if error.nil? && result.is_a?(Exception)
89
+ error = result
90
+ result = nil
91
+ end
92
+
93
+ error_message = "#{error.class}: #{error.message}" if error
94
+ self.error = error_message
95
+ self.finished_at = Time.current
96
+
97
+ if destroy_after
61
98
  destroy!
99
+ else
100
+ save!
62
101
  end
102
+
103
+ [result, error]
63
104
  end
64
105
  end
65
106
  end
@@ -0,0 +1,12 @@
1
+ module GoodJob
2
+ class Performer
3
+ def initialize(target, method_name)
4
+ @target = target
5
+ @method_name = method_name
6
+ end
7
+
8
+ def next
9
+ @target.public_send(@method_name)
10
+ end
11
+ end
12
+ end
@@ -59,21 +59,21 @@ module GoodJob
59
59
 
60
60
  def create_thread
61
61
  future = Concurrent::Future.new(args: [@performer], executor: @pool) do |performer|
62
- result = nil
63
- Rails.application.executor.wrap { result = performer.next }
64
- result
62
+ output = nil
63
+ Rails.application.executor.wrap { output = performer.next }
64
+ output
65
65
  end
66
66
  future.add_observer(self, :task_observer)
67
67
  future.execute
68
68
  end
69
69
 
70
- def timer_observer(time, executed_task, error)
71
- ActiveSupport::Notifications.instrument("finished_timer_task.good_job", { result: executed_task, error: error, time: time })
70
+ def timer_observer(time, executed_task, thread_error)
71
+ ActiveSupport::Notifications.instrument("finished_timer_task.good_job", { result: executed_task, error: thread_error, time: time })
72
72
  end
73
73
 
74
- def task_observer(time, result, error)
75
- ActiveSupport::Notifications.instrument("finished_job_task.good_job", { result: result, error: error, time: time })
76
- create_thread if result
74
+ def task_observer(time, output, thread_error)
75
+ ActiveSupport::Notifications.instrument("finished_job_task.good_job", { result: output, error: thread_error, time: time })
76
+ create_thread if output
77
77
  end
78
78
  end
79
79
  end
@@ -1,3 +1,3 @@
1
1
  module GoodJob
2
- VERSION = '0.7.0'.freeze
2
+ VERSION = '1.0.0'.freeze
3
3
  end
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: 0.7.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Sheldon
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-07-16 00:00:00.000000000 Z
11
+ date: 2020-07-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -242,18 +242,19 @@ files:
242
242
  - lib/good_job/job.rb
243
243
  - lib/good_job/lockable.rb
244
244
  - lib/good_job/logging.rb
245
+ - lib/good_job/performer.rb
245
246
  - lib/good_job/pg_locks.rb
246
247
  - lib/good_job/railtie.rb
247
248
  - lib/good_job/scheduler.rb
248
249
  - lib/good_job/version.rb
249
- homepage: https://github.com/benheldon/good_job
250
+ homepage: https://github.com/bensheldon/good_job
250
251
  licenses:
251
252
  - MIT
252
253
  metadata:
253
254
  bug_tracker_uri: https://github.com/bensheldon/good_job/issues
254
255
  changelog_uri: https://github.com/bensheldon/good_job/blob/master/CHANGELOG.md
255
256
  documentation_uri: https://rdoc.info/github/bensheldon/good_job
256
- homepage_uri: https://github.com/benheldon/good_job
257
+ homepage_uri: https://github.com/bensheldon/good_job
257
258
  source_code_uri: https://github.com/bensheldon/good_job
258
259
  post_install_message:
259
260
  rdoc_options: