good_job 0.8.2 → 0.9.0

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: 6c630bdd77afa672158924fa33dd200506354db2df4c1e795c85b3e02becc71b
4
- data.tar.gz: 39ac5362e8832e62f50ba9703060af5907dcbacd64f8765b9cafaa98f10dbbb6
3
+ metadata.gz: bddc60fa297293af01b1334d4adf51981bfb8b314ba4577a7b3bff1d7385bd5a
4
+ data.tar.gz: 3533f21ffee4dba606558fa0fadd1f37a943aa285b4f6bebed4bb3b89045b61e
5
5
  SHA512:
6
- metadata.gz: 86a6d5613d68428276244373b2c718a327538fe30c0461b1eaec0156ca51f72b2863d6e268e96e59f4c40e8096059c05758cc1ce83d02b680916f09a57bff428
7
- data.tar.gz: 50749d69aa79ea96e3a9081b2a0cd02bdbeb4537d3da0bba748ffc3c43fb251149f84ea8cc1620fbc211794ab499fc713de8d57f8b74f49cead98fb1b8d4c1bd
6
+ metadata.gz: 74c0d3df2df08e78d06baa8982401d633af536277231a17d87687ff3c7ba1553d4123f7c1ce49ae23094bd781db5ee23eb9584eeba94aae49fb8cc83da0c68e0
7
+ data.tar.gz: 7df20f03621cb66b864a2c2950e4a11f60199cd4954ad5a622a1631254fa04bab1243ee376567600a2aed294a8c14fa9802fd1024855dc7d473a7ff8f7166515
@@ -1,8 +1,8 @@
1
1
  # Changelog
2
2
 
3
- ## [v0.8.2](https://github.com/bensheldon/good_job/tree/v0.8.2) (2020-07-18)
3
+ ## [v0.9.0](https://github.com/bensheldon/good_job/tree/v0.9.0) (2020-07-20)
4
4
 
5
- [Full Changelog](https://github.com/bensheldon/good_job/compare/v0.6.0...v0.8.2)
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v0.6.0...v0.9.0)
6
6
 
7
7
  **Closed issues:**
8
8
 
@@ -11,6 +11,7 @@
11
11
 
12
12
  **Merged pull requests:**
13
13
 
14
+ - Allow preservation of finished job records [\#46](https://github.com/bensheldon/good_job/pull/46) ([bensheldon](https://github.com/bensheldon))
14
15
  - Run Github Action tests on PRs from forks [\#44](https://github.com/bensheldon/good_job/pull/44) ([bensheldon](https://github.com/bensheldon))
15
16
  - Fix Rubygems homepage URL [\#43](https://github.com/bensheldon/good_job/pull/43) ([joshmn](https://github.com/joshmn))
16
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))
data/README.md CHANGED
@@ -43,9 +43,12 @@ $ bundle install
43
43
  t.integer :priority
44
44
  t.jsonb :serialized_params
45
45
  t.timestamp :scheduled_at
46
-
47
- t.index :scheduled_at
48
- 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)"
49
52
  end
50
53
  end
51
54
  end
@@ -152,6 +155,32 @@ If your application is already using an ActiveJob backend, you will need to inst
152
155
 
153
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.
154
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
+
155
184
  ## Development
156
185
 
157
186
  To run tests:
@@ -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
@@ -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.8.2'.freeze
2
+ VERSION = '0.9.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.8.2
4
+ version: 0.9.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-18 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,6 +242,7 @@ 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