good_job 0.8.1 → 1.0.2

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: 9d3f717a6ef87a75232173cd29e90842edc441808036116ced1355d0c51957a2
4
- data.tar.gz: df7aaaab677397d52694697df2016bc9fa7c65db6bebe4f3a112432b5b9f3a96
3
+ metadata.gz: f7c64e8766a52e6bbbb5e55b409bb0100e54790d6d88340893e61f930cbf8b9e
4
+ data.tar.gz: 7e89b7bf2d3aaa45cc5b07deab36f5334a61cac44c0aec06206ddbd6eb0ebc22
5
5
  SHA512:
6
- metadata.gz: ec049cfa4441c5bfa61b70661ed6543cf22e9e3625036dd5ce824f528a2516347961dd95b1ecccc444587f5a31a353499f50a362005c43ea30263b9bb51d1524
7
- data.tar.gz: 4e0c0adbee22e2e8ae825015917cb52fe15475d409d48eec0a43df659de5fca95447f611bcacddcb98095888f8a809136890bed0d1fa9465b7bed38b5b38295b
6
+ metadata.gz: 944b64f713ad584b56fe386dc63aa6afc0538e498cff4fb46aec9098306b7e436a3d29cebd968157f34e62c9b1b0ce932e15967b4a631f9f04c60ca43d77f4c8
7
+ data.tar.gz: 5000ffc97bc381cb94999478c1fe90202b681d5cf87d697f4700cc60b94a7417fd67ca3f5428c4b50acb195bde7850a3a5d809e32c5baaf08ae3a58a2101624a
@@ -1,17 +1,81 @@
1
1
  # Changelog
2
2
 
3
- ## [v0.8.1](https://github.com/bensheldon/good_job/tree/v0.8.1) (2020-07-17)
3
+ ## [v1.0.2](https://github.com/bensheldon/good_job/tree/v1.0.2) (2020-07-24)
4
4
 
5
- [Full Changelog](https://github.com/bensheldon/good_job/compare/v0.6.0...v0.8.1)
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.0.1...v1.0.2)
6
+
7
+ **Fixed bugs:**
8
+
9
+ - Fix counting of available execution threads [\#58](https://github.com/bensheldon/good_job/pull/58) ([bensheldon](https://github.com/bensheldon))
6
10
 
7
11
  **Closed issues:**
8
12
 
9
- - Always store a default priority \(0\) and scheduled\_at\(Time.current\) [\#30](https://github.com/bensheldon/good_job/issues/30)
13
+ - repeating/recurring jobs [\#53](https://github.com/bensheldon/good_job/issues/53)
14
+
15
+ **Merged pull requests:**
16
+
17
+ - Add migration generator [\#56](https://github.com/bensheldon/good_job/pull/56) ([thedanbob](https://github.com/thedanbob))
18
+ - Fix migration script in readme [\#55](https://github.com/bensheldon/good_job/pull/55) ([thedanbob](https://github.com/thedanbob))
19
+
20
+ ## [v1.0.1](https://github.com/bensheldon/good_job/tree/v1.0.1) (2020-07-22)
21
+
22
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v1.0.0...v1.0.1)
23
+
24
+ **Merged pull requests:**
25
+
26
+ - Change threadpool idletime default to 60 seconds from 0 [\#49](https://github.com/bensheldon/good_job/pull/49) ([bensheldon](https://github.com/bensheldon))
27
+
28
+ ## [v1.0.0](https://github.com/bensheldon/good_job/tree/v1.0.0) (2020-07-20)
29
+
30
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v0.9.0...v1.0.0)
31
+
32
+ ## [v0.9.0](https://github.com/bensheldon/good_job/tree/v0.9.0) (2020-07-20)
33
+
34
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v0.8.2...v0.9.0)
35
+
36
+ **Merged pull requests:**
37
+
38
+ - Allow preservation of finished job records [\#46](https://github.com/bensheldon/good_job/pull/46) ([bensheldon](https://github.com/bensheldon))
39
+
40
+ ## [v0.8.2](https://github.com/bensheldon/good_job/tree/v0.8.2) (2020-07-18)
41
+
42
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v0.8.1...v0.8.2)
43
+
44
+ **Closed issues:**
45
+
46
+ - Add a job timeout configuration to time out jobs that have run too long [\#19](https://github.com/bensheldon/good_job/issues/19)
47
+
48
+ **Merged pull requests:**
49
+
50
+ - Run Github Action tests on PRs from forks [\#44](https://github.com/bensheldon/good_job/pull/44) ([bensheldon](https://github.com/bensheldon))
51
+ - Fix Rubygems homepage URL [\#43](https://github.com/bensheldon/good_job/pull/43) ([joshmn](https://github.com/joshmn))
52
+
53
+ ## [v0.8.1](https://github.com/bensheldon/good_job/tree/v0.8.1) (2020-07-18)
54
+
55
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v0.8.0...v0.8.1)
10
56
 
11
57
  **Merged pull requests:**
12
58
 
13
59
  - 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))
60
+
61
+ ## [v0.8.0](https://github.com/bensheldon/good_job/tree/v0.8.0) (2020-07-17)
62
+
63
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v0.7.0...v0.8.0)
64
+
65
+ **Merged pull requests:**
66
+
14
67
  - Replace Adapter inline boolean kwarg with execution\_mode instead [\#41](https://github.com/bensheldon/good_job/pull/41) ([bensheldon](https://github.com/bensheldon))
68
+
69
+ ## [v0.7.0](https://github.com/bensheldon/good_job/tree/v0.7.0) (2020-07-16)
70
+
71
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v0.6.0...v0.7.0)
72
+
73
+ **Closed issues:**
74
+
75
+ - Always store a default priority \(0\) and scheduled\_at\(Time.current\) [\#30](https://github.com/bensheldon/good_job/issues/30)
76
+
77
+ **Merged pull requests:**
78
+
15
79
  - Add more examples to Readme [\#39](https://github.com/bensheldon/good_job/pull/39) ([bensheldon](https://github.com/bensheldon))
16
80
  - Add additional Rubocops and lint [\#38](https://github.com/bensheldon/good_job/pull/38) ([bensheldon](https://github.com/bensheldon))
17
81
  - 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
@@ -9,6 +9,8 @@ GoodJob is a multithreaded, Postgres-based, ActiveJob backend for Ruby on Rails.
9
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
10
  - **For most workloads.** Targets full-stack teams, economy-minded solo developers, and applications that enqueue less than 1-million jobs/day.
11
11
 
12
+ For more of the story of GoodJob, read the [introductory blog post](https://island94.org/2020/07/introducing-goodjob-1-0).
13
+
12
14
  ## Installation
13
15
 
14
16
  Add this line to your application's Gemfile:
@@ -26,31 +28,9 @@ $ bundle install
26
28
 
27
29
  1. Create a database migration:
28
30
  ```bash
29
- $ bin/rails g migration CreateGoodJobs
31
+ $ bin/rails g good_job:install
30
32
  ```
31
33
 
32
- Add to the newly created migration file:
33
-
34
- ```ruby
35
- class CreateGoodJobs < ActiveRecord::Migration[6.0]
36
- def change
37
- enable_extension 'pgcrypto'
38
-
39
- create_table :good_jobs, id: :uuid do |t|
40
- t.timestamps
41
-
42
- t.text :queue_name
43
- t.integer :priority
44
- t.jsonb :serialized_params
45
- t.timestamp :scheduled_at
46
-
47
- t.index :scheduled_at
48
- t.index [:queue_name, :scheduled_at]
49
- end
50
- end
51
- end
52
- ```
53
-
54
34
  Run the migration:
55
35
 
56
36
  ```bash
@@ -152,6 +132,32 @@ If your application is already using an ActiveJob backend, you will need to inst
152
132
 
153
133
  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.
154
134
 
135
+ ### Monitoring and preserving worked jobs
136
+
137
+ GoodJob is fully instrumented with [`ActiveSupport::Notifications`](https://edgeguides.rubyonrails.org/active_support_instrumentation.html#introduction-to-instrumentation).
138
+
139
+ 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`).
140
+
141
+ To preserve job records for later inspection, set an initializer:
142
+
143
+ ```ruby
144
+ # config/initializers/good_job.rb
145
+ GoodJob.preserve_job_records = true
146
+ ```
147
+
148
+ It is also necessary to delete these preserved jobs from the database after a certain time period:
149
+
150
+ - For example, in a Rake task:
151
+
152
+ ```ruby
153
+ # GoodJob::Job.finished(1.day.ago).delete_all
154
+ ```
155
+ - For example, using the `good_job` command-line utility:
156
+
157
+ ```bash
158
+ $ bundle exec good_job cleanup_preserved_jobs --before-seconds-ago=86400
159
+ ```
160
+
155
161
  ## Development
156
162
 
157
163
  To run tests:
@@ -0,0 +1,24 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/active_record'
3
+
4
+ module GoodJob
5
+ class InstallGenerator < Rails::Generators::Base
6
+ include Rails::Generators::Migration
7
+
8
+ class << self
9
+ delegate :next_migration_number, to: ActiveRecord::Generators::Base
10
+ end
11
+
12
+ source_paths << File.join(File.dirname(__FILE__), "templates")
13
+
14
+ def create_migration_file
15
+ migration_template 'migration.rb.erb', 'db/migrate/create_good_jobs.rb', migration_version: migration_version
16
+ end
17
+
18
+ private
19
+
20
+ def migration_version
21
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,20 @@
1
+ class CreateGoodJobs < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ enable_extension 'pgcrypto'
4
+
5
+ create_table :good_jobs, id: :uuid do |t|
6
+ t.text :queue_name
7
+ t.integer :priority
8
+ t.jsonb :serialized_params
9
+ t.timestamp :scheduled_at
10
+ t.timestamp :performed_at
11
+ t.timestamp :finished_at
12
+ t.text :error
13
+
14
+ t.timestamps
15
+ end
16
+
17
+ add_index :good_jobs, :scheduled_at, where: "(finished_at IS NULL)"
18
+ add_index :good_jobs, [:queue_name, :scheduled_at], where: "(finished_at IS NULL)"
19
+ end
20
+ 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)
@@ -40,7 +40,12 @@ module GoodJob
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.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.only_scheduled.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
@@ -15,19 +15,18 @@ module GoodJob
15
15
  min_threads: 0,
16
16
  max_threads: Concurrent.processor_count,
17
17
  auto_terminate: true,
18
- idletime: 0,
19
- max_queue: 0,
20
- fallback_policy: :abort, # shouldn't matter -- 0 max queue
18
+ idletime: 60,
19
+ max_queue: -1,
20
+ fallback_policy: :discard,
21
21
  }.freeze
22
22
 
23
23
  def initialize(performer, timer_options: {}, pool_options: {})
24
24
  raise ArgumentError, "Performer argument must implement #next" unless performer.respond_to?(:next)
25
25
 
26
26
  @performer = performer
27
- @pool = Concurrent::ThreadPoolExecutor.new(DEFAULT_POOL_OPTIONS.merge(pool_options))
27
+ @pool = ThreadPoolExecutor.new(DEFAULT_POOL_OPTIONS.merge(pool_options))
28
28
  @timer = Concurrent::TimerTask.new(DEFAULT_TIMER_OPTIONS.merge(timer_options)) do
29
- idle_threads = @pool.max_length - @pool.length
30
- create_thread if idle_threads.positive?
29
+ create_thread
31
30
  end
32
31
  @timer.add_observer(self, :timer_observer)
33
32
  @timer.execute
@@ -58,22 +57,36 @@ module GoodJob
58
57
  end
59
58
 
60
59
  def create_thread
60
+ return false unless @pool.ready_worker_count.positive?
61
+
61
62
  future = Concurrent::Future.new(args: [@performer], executor: @pool) do |performer|
62
- result = nil
63
- Rails.application.executor.wrap { result = performer.next }
64
- result
63
+ output = nil
64
+ Rails.application.executor.wrap { output = performer.next }
65
+ output
65
66
  end
66
67
  future.add_observer(self, :task_observer)
67
68
  future.execute
68
69
  end
69
70
 
70
- def timer_observer(time, executed_task, error)
71
- ActiveSupport::Notifications.instrument("finished_timer_task.good_job", { result: executed_task, error: error, time: time })
71
+ def timer_observer(time, executed_task, thread_error)
72
+ ActiveSupport::Notifications.instrument("finished_timer_task.good_job", { result: executed_task, error: thread_error, time: time })
73
+ end
74
+
75
+ def task_observer(time, output, thread_error)
76
+ ActiveSupport::Notifications.instrument("finished_job_task.good_job", { result: output, error: thread_error, time: time })
77
+ create_thread if output
72
78
  end
73
79
 
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
80
+ class ThreadPoolExecutor < Concurrent::ThreadPoolExecutor
81
+ # https://github.com/ruby-concurrency/concurrent-ruby/issues/684#issuecomment-427594437
82
+ def ready_worker_count
83
+ synchronize do
84
+ workers_still_to_be_created = @max_length - @pool.length
85
+ workers_created_but_waiting = @ready.length
86
+
87
+ workers_still_to_be_created + workers_created_but_waiting
88
+ end
89
+ end
77
90
  end
78
91
  end
79
92
  end
@@ -1,3 +1,3 @@
1
1
  module GoodJob
2
- VERSION = '0.8.1'.freeze
2
+ VERSION = '1.0.2'.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.8.1
4
+ version: 1.0.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: 2020-07-18 00:00:00.000000000 Z
11
+ date: 2020-07-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -236,24 +236,27 @@ files:
236
236
  - README.md
237
237
  - exe/good_job
238
238
  - lib/active_job/queue_adapters/good_job_adapter.rb
239
+ - lib/generators/good_job/install_generator.rb
240
+ - lib/generators/good_job/templates/migration.rb.erb
239
241
  - lib/good_job.rb
240
242
  - lib/good_job/adapter.rb
241
243
  - lib/good_job/cli.rb
242
244
  - lib/good_job/job.rb
243
245
  - lib/good_job/lockable.rb
244
246
  - lib/good_job/logging.rb
247
+ - lib/good_job/performer.rb
245
248
  - lib/good_job/pg_locks.rb
246
249
  - lib/good_job/railtie.rb
247
250
  - lib/good_job/scheduler.rb
248
251
  - lib/good_job/version.rb
249
- homepage: https://github.com/benheldon/good_job
252
+ homepage: https://github.com/bensheldon/good_job
250
253
  licenses:
251
254
  - MIT
252
255
  metadata:
253
256
  bug_tracker_uri: https://github.com/bensheldon/good_job/issues
254
257
  changelog_uri: https://github.com/bensheldon/good_job/blob/master/CHANGELOG.md
255
258
  documentation_uri: https://rdoc.info/github/bensheldon/good_job
256
- homepage_uri: https://github.com/benheldon/good_job
259
+ homepage_uri: https://github.com/bensheldon/good_job
257
260
  source_code_uri: https://github.com/bensheldon/good_job
258
261
  post_install_message:
259
262
  rdoc_options: