sidekiq 5.2.7 → 8.0.5
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 +4 -4
- data/Changes.md +845 -8
- data/LICENSE.txt +9 -0
- data/README.md +54 -54
- data/bin/multi_queue_bench +271 -0
- data/bin/sidekiq +22 -3
- data/bin/sidekiqload +219 -112
- data/bin/sidekiqmon +11 -0
- data/bin/webload +69 -0
- data/lib/active_job/queue_adapters/sidekiq_adapter.rb +120 -0
- data/lib/generators/sidekiq/job_generator.rb +59 -0
- data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
- data/lib/generators/sidekiq/templates/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
- data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
- data/lib/sidekiq/api.rb +757 -373
- data/lib/sidekiq/capsule.rb +132 -0
- data/lib/sidekiq/cli.rb +210 -233
- data/lib/sidekiq/client.rb +145 -103
- data/lib/sidekiq/component.rb +128 -0
- data/lib/sidekiq/config.rb +315 -0
- data/lib/sidekiq/deploy.rb +64 -0
- data/lib/sidekiq/embedded.rb +64 -0
- data/lib/sidekiq/fetch.rb +49 -42
- data/lib/sidekiq/iterable_job.rb +56 -0
- data/lib/sidekiq/job/interrupt_handler.rb +24 -0
- data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
- data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
- data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
- data/lib/sidekiq/job/iterable.rb +306 -0
- data/lib/sidekiq/job.rb +385 -0
- data/lib/sidekiq/job_logger.rb +34 -7
- data/lib/sidekiq/job_retry.rb +164 -109
- data/lib/sidekiq/job_util.rb +113 -0
- data/lib/sidekiq/launcher.rb +208 -107
- data/lib/sidekiq/logger.rb +80 -0
- data/lib/sidekiq/manager.rb +42 -46
- data/lib/sidekiq/metrics/query.rb +184 -0
- data/lib/sidekiq/metrics/shared.rb +109 -0
- data/lib/sidekiq/metrics/tracking.rb +150 -0
- data/lib/sidekiq/middleware/chain.rb +113 -56
- data/lib/sidekiq/middleware/current_attributes.rb +119 -0
- data/lib/sidekiq/middleware/i18n.rb +7 -7
- data/lib/sidekiq/middleware/modules.rb +23 -0
- data/lib/sidekiq/monitor.rb +147 -0
- data/lib/sidekiq/paginator.rb +41 -16
- data/lib/sidekiq/processor.rb +146 -127
- data/lib/sidekiq/profiler.rb +72 -0
- data/lib/sidekiq/rails.rb +46 -43
- data/lib/sidekiq/redis_client_adapter.rb +113 -0
- data/lib/sidekiq/redis_connection.rb +79 -108
- data/lib/sidekiq/ring_buffer.rb +31 -0
- data/lib/sidekiq/scheduled.rb +112 -50
- data/lib/sidekiq/sd_notify.rb +149 -0
- data/lib/sidekiq/systemd.rb +26 -0
- data/lib/sidekiq/testing/inline.rb +6 -5
- data/lib/sidekiq/testing.rb +91 -90
- data/lib/sidekiq/transaction_aware_client.rb +51 -0
- data/lib/sidekiq/version.rb +7 -1
- data/lib/sidekiq/web/action.rb +125 -60
- data/lib/sidekiq/web/application.rb +363 -259
- data/lib/sidekiq/web/config.rb +120 -0
- data/lib/sidekiq/web/csrf_protection.rb +183 -0
- data/lib/sidekiq/web/helpers.rb +241 -120
- data/lib/sidekiq/web/router.rb +62 -71
- data/lib/sidekiq/web.rb +69 -161
- data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
- data/lib/sidekiq.rb +94 -182
- data/sidekiq.gemspec +26 -16
- data/web/assets/images/apple-touch-icon.png +0 -0
- data/web/assets/javascripts/application.js +150 -61
- data/web/assets/javascripts/base-charts.js +120 -0
- data/web/assets/javascripts/chart.min.js +13 -0
- data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
- data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
- data/web/assets/javascripts/dashboard-charts.js +194 -0
- data/web/assets/javascripts/dashboard.js +41 -293
- data/web/assets/javascripts/metrics.js +280 -0
- data/web/assets/stylesheets/style.css +766 -0
- data/web/locales/ar.yml +72 -65
- data/web/locales/cs.yml +63 -62
- data/web/locales/da.yml +61 -53
- data/web/locales/de.yml +66 -53
- data/web/locales/el.yml +44 -24
- data/web/locales/en.yml +94 -66
- data/web/locales/es.yml +92 -54
- data/web/locales/fa.yml +66 -65
- data/web/locales/fr.yml +83 -62
- data/web/locales/gd.yml +99 -0
- data/web/locales/he.yml +66 -64
- data/web/locales/hi.yml +60 -59
- data/web/locales/it.yml +93 -54
- data/web/locales/ja.yml +75 -64
- data/web/locales/ko.yml +53 -52
- data/web/locales/lt.yml +84 -0
- data/web/locales/nb.yml +62 -61
- data/web/locales/nl.yml +53 -52
- data/web/locales/pl.yml +46 -45
- data/web/locales/{pt-br.yml → pt-BR.yml} +84 -56
- data/web/locales/pt.yml +52 -51
- data/web/locales/ru.yml +69 -63
- data/web/locales/sv.yml +54 -53
- data/web/locales/ta.yml +61 -60
- data/web/locales/tr.yml +101 -0
- data/web/locales/uk.yml +86 -61
- data/web/locales/ur.yml +65 -64
- data/web/locales/vi.yml +84 -0
- data/web/locales/zh-CN.yml +106 -0
- data/web/locales/{zh-tw.yml → zh-TW.yml} +43 -9
- data/web/views/_footer.erb +31 -19
- data/web/views/_job_info.erb +94 -75
- data/web/views/_metrics_period_select.erb +15 -0
- data/web/views/_nav.erb +14 -21
- data/web/views/_paging.erb +23 -19
- data/web/views/_poll_link.erb +3 -6
- data/web/views/_summary.erb +23 -23
- data/web/views/busy.erb +139 -87
- data/web/views/dashboard.erb +82 -53
- data/web/views/dead.erb +31 -27
- data/web/views/filtering.erb +6 -0
- data/web/views/layout.erb +15 -29
- data/web/views/metrics.erb +84 -0
- data/web/views/metrics_for_job.erb +58 -0
- data/web/views/morgue.erb +60 -70
- data/web/views/profiles.erb +43 -0
- data/web/views/queue.erb +50 -39
- data/web/views/queues.erb +45 -29
- data/web/views/retries.erb +65 -75
- data/web/views/retry.erb +32 -27
- data/web/views/scheduled.erb +58 -52
- data/web/views/scheduled_job_info.erb +1 -1
- metadata +96 -76
- data/.circleci/config.yml +0 -61
- data/.github/contributing.md +0 -32
- data/.github/issue_template.md +0 -11
- data/.gitignore +0 -15
- data/.travis.yml +0 -11
- data/3.0-Upgrade.md +0 -70
- data/4.0-Upgrade.md +0 -53
- data/5.0-Upgrade.md +0 -56
- data/COMM-LICENSE +0 -97
- data/Ent-Changes.md +0 -238
- data/Gemfile +0 -23
- data/LICENSE +0 -9
- data/Pro-2.0-Upgrade.md +0 -138
- data/Pro-3.0-Upgrade.md +0 -44
- data/Pro-4.0-Upgrade.md +0 -35
- data/Pro-Changes.md +0 -759
- data/Rakefile +0 -9
- data/bin/sidekiqctl +0 -20
- data/code_of_conduct.md +0 -50
- data/lib/generators/sidekiq/worker_generator.rb +0 -49
- data/lib/sidekiq/core_ext.rb +0 -1
- data/lib/sidekiq/ctl.rb +0 -221
- data/lib/sidekiq/delay.rb +0 -42
- data/lib/sidekiq/exception_handler.rb +0 -29
- data/lib/sidekiq/extensions/action_mailer.rb +0 -57
- data/lib/sidekiq/extensions/active_record.rb +0 -40
- data/lib/sidekiq/extensions/class_methods.rb +0 -40
- data/lib/sidekiq/extensions/generic_proxy.rb +0 -31
- data/lib/sidekiq/logging.rb +0 -122
- data/lib/sidekiq/middleware/server/active_record.rb +0 -23
- data/lib/sidekiq/util.rb +0 -66
- data/lib/sidekiq/worker.rb +0 -220
- data/web/assets/stylesheets/application-rtl.css +0 -246
- data/web/assets/stylesheets/application.css +0 -1144
- data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
- data/web/assets/stylesheets/bootstrap.css +0 -5
- data/web/locales/zh-cn.yml +0 -68
- data/web/views/_status.erb +0 -4
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sidekiq
|
4
|
+
module Job
|
5
|
+
module Iterable
|
6
|
+
# @api private
|
7
|
+
class ActiveRecordEnumerator
|
8
|
+
def initialize(relation, cursor: nil, **options)
|
9
|
+
@relation = relation
|
10
|
+
@cursor = cursor
|
11
|
+
@options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
def records
|
15
|
+
Enumerator.new(-> { @relation.count }) do |yielder|
|
16
|
+
@relation.find_each(**@options, start: @cursor) do |record|
|
17
|
+
yielder.yield(record, record.id)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def batches
|
23
|
+
Enumerator.new(-> { @relation.count }) do |yielder|
|
24
|
+
@relation.find_in_batches(**@options, start: @cursor) do |batch|
|
25
|
+
yielder.yield(batch, batch.first.id)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def relations
|
31
|
+
Enumerator.new(-> { relations_size }) do |yielder|
|
32
|
+
# Convenience to use :batch_size for all the
|
33
|
+
# ActiveRecord batching methods.
|
34
|
+
options = @options.dup
|
35
|
+
options[:of] ||= options.delete(:batch_size)
|
36
|
+
|
37
|
+
@relation.in_batches(**options, start: @cursor) do |relation|
|
38
|
+
first_record = relation.first
|
39
|
+
yielder.yield(relation, first_record.id)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def relations_size
|
47
|
+
batch_size = @options[:batch_size] || 1000
|
48
|
+
(@relation.count + batch_size - 1) / batch_size # ceiling division
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sidekiq
|
4
|
+
module Job
|
5
|
+
module Iterable
|
6
|
+
# @api private
|
7
|
+
class CsvEnumerator
|
8
|
+
def initialize(csv)
|
9
|
+
unless defined?(CSV) && csv.instance_of?(CSV)
|
10
|
+
raise ArgumentError, "CsvEnumerator.new takes CSV object"
|
11
|
+
end
|
12
|
+
|
13
|
+
@csv = csv
|
14
|
+
end
|
15
|
+
|
16
|
+
def rows(cursor:)
|
17
|
+
@csv.lazy
|
18
|
+
.each_with_index
|
19
|
+
.drop(cursor || 0)
|
20
|
+
.to_enum { count_of_rows_in_file }
|
21
|
+
end
|
22
|
+
|
23
|
+
def batches(cursor:, batch_size: 100)
|
24
|
+
@csv.lazy
|
25
|
+
.each_slice(batch_size)
|
26
|
+
.with_index
|
27
|
+
.drop(cursor || 0)
|
28
|
+
.to_enum { (count_of_rows_in_file.to_f / batch_size).ceil }
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def count_of_rows_in_file
|
34
|
+
filepath = @csv.path
|
35
|
+
return unless filepath
|
36
|
+
|
37
|
+
count = IO.popen(["wc", "-l", filepath]) do |out|
|
38
|
+
out.read.strip.to_i
|
39
|
+
end
|
40
|
+
|
41
|
+
count -= 1 if @csv.headers
|
42
|
+
count
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "active_record_enumerator"
|
4
|
+
require_relative "csv_enumerator"
|
5
|
+
|
6
|
+
module Sidekiq
|
7
|
+
module Job
|
8
|
+
module Iterable
|
9
|
+
module Enumerators
|
10
|
+
# Builds Enumerator object from a given array, using +cursor+ as an offset.
|
11
|
+
#
|
12
|
+
# @param array [Array]
|
13
|
+
# @param cursor [Integer] offset to start iteration from
|
14
|
+
#
|
15
|
+
# @return [Enumerator]
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
# array_enumerator(['build', 'enumerator', 'from', 'any', 'array'], cursor: cursor)
|
19
|
+
#
|
20
|
+
def array_enumerator(array, cursor:)
|
21
|
+
raise ArgumentError, "array must be an Array" unless array.is_a?(Array)
|
22
|
+
|
23
|
+
x = array.each_with_index.drop(cursor || 0)
|
24
|
+
x.to_enum { x.size }
|
25
|
+
end
|
26
|
+
|
27
|
+
# Builds Enumerator from `ActiveRecord::Relation`.
|
28
|
+
# Each Enumerator tick moves the cursor one row forward.
|
29
|
+
#
|
30
|
+
# @param relation [ActiveRecord::Relation] relation to iterate
|
31
|
+
# @param cursor [Object] offset id to start iteration from
|
32
|
+
# @param options [Hash] additional options that will be passed to relevant
|
33
|
+
# ActiveRecord batching methods
|
34
|
+
#
|
35
|
+
# @return [ActiveRecordEnumerator]
|
36
|
+
#
|
37
|
+
# @example
|
38
|
+
# def build_enumerator(cursor:)
|
39
|
+
# active_record_records_enumerator(User.all, cursor: cursor)
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# def each_iteration(user)
|
43
|
+
# user.notify_about_something
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
def active_record_records_enumerator(relation, cursor:, **options)
|
47
|
+
ActiveRecordEnumerator.new(relation, cursor: cursor, **options).records
|
48
|
+
end
|
49
|
+
|
50
|
+
# Builds Enumerator from `ActiveRecord::Relation` and enumerates on batches of records.
|
51
|
+
# Each Enumerator tick moves the cursor `:batch_size` rows forward.
|
52
|
+
# @see #active_record_records_enumerator
|
53
|
+
#
|
54
|
+
# @example
|
55
|
+
# def build_enumerator(product_id, cursor:)
|
56
|
+
# active_record_batches_enumerator(
|
57
|
+
# Comment.where(product_id: product_id).select(:id),
|
58
|
+
# cursor: cursor,
|
59
|
+
# batch_size: 100
|
60
|
+
# )
|
61
|
+
# end
|
62
|
+
#
|
63
|
+
# def each_iteration(batch_of_comments, product_id)
|
64
|
+
# comment_ids = batch_of_comments.map(&:id)
|
65
|
+
# CommentService.call(comment_ids: comment_ids)
|
66
|
+
# end
|
67
|
+
#
|
68
|
+
def active_record_batches_enumerator(relation, cursor:, **options)
|
69
|
+
ActiveRecordEnumerator.new(relation, cursor: cursor, **options).batches
|
70
|
+
end
|
71
|
+
|
72
|
+
# Builds Enumerator from `ActiveRecord::Relation` and enumerates on batches,
|
73
|
+
# yielding `ActiveRecord::Relation`s.
|
74
|
+
# @see #active_record_records_enumerator
|
75
|
+
#
|
76
|
+
# @example
|
77
|
+
# def build_enumerator(product_id, cursor:)
|
78
|
+
# active_record_relations_enumerator(
|
79
|
+
# Product.find(product_id).comments,
|
80
|
+
# cursor: cursor,
|
81
|
+
# batch_size: 100,
|
82
|
+
# )
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
# def each_iteration(batch_of_comments, product_id)
|
86
|
+
# # batch_of_comments will be a Comment::ActiveRecord_Relation
|
87
|
+
# batch_of_comments.update_all(deleted: true)
|
88
|
+
# end
|
89
|
+
#
|
90
|
+
def active_record_relations_enumerator(relation, cursor:, **options)
|
91
|
+
ActiveRecordEnumerator.new(relation, cursor: cursor, **options).relations
|
92
|
+
end
|
93
|
+
|
94
|
+
# Builds Enumerator from a CSV file.
|
95
|
+
#
|
96
|
+
# @param csv [CSV] an instance of CSV object
|
97
|
+
# @param cursor [Integer] offset to start iteration from
|
98
|
+
#
|
99
|
+
# @example
|
100
|
+
# def build_enumerator(import_id, cursor:)
|
101
|
+
# import = Import.find(import_id)
|
102
|
+
# csv_enumerator(import.csv, cursor: cursor)
|
103
|
+
# end
|
104
|
+
#
|
105
|
+
# def each_iteration(csv_row)
|
106
|
+
# # insert csv_row into database
|
107
|
+
# end
|
108
|
+
#
|
109
|
+
def csv_enumerator(csv, cursor:)
|
110
|
+
CsvEnumerator.new(csv).rows(cursor: cursor)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Builds Enumerator from a CSV file and enumerates on batches of records.
|
114
|
+
#
|
115
|
+
# @param csv [CSV] an instance of CSV object
|
116
|
+
# @param cursor [Integer] offset to start iteration from
|
117
|
+
# @option options :batch_size [Integer] (100) size of the batch
|
118
|
+
#
|
119
|
+
# @example
|
120
|
+
# def build_enumerator(import_id, cursor:)
|
121
|
+
# import = Import.find(import_id)
|
122
|
+
# csv_batches_enumerator(import.csv, cursor: cursor)
|
123
|
+
# end
|
124
|
+
#
|
125
|
+
# def each_iteration(batch_of_csv_rows)
|
126
|
+
# # ...
|
127
|
+
# end
|
128
|
+
#
|
129
|
+
def csv_batches_enumerator(csv, cursor:, **options)
|
130
|
+
CsvEnumerator.new(csv).batches(cursor: cursor, **options)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,306 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "iterable/enumerators"
|
4
|
+
|
5
|
+
module Sidekiq
|
6
|
+
module Job
|
7
|
+
class Interrupted < ::RuntimeError; end
|
8
|
+
|
9
|
+
module Iterable
|
10
|
+
include Enumerators
|
11
|
+
|
12
|
+
# @api private
|
13
|
+
def self.included(base)
|
14
|
+
base.extend(ClassMethods)
|
15
|
+
end
|
16
|
+
|
17
|
+
# @api private
|
18
|
+
module ClassMethods
|
19
|
+
def method_added(method_name)
|
20
|
+
raise "#{self} is an iterable job and must not define #perform" if method_name == :perform
|
21
|
+
super
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# @api private
|
26
|
+
def initialize
|
27
|
+
super
|
28
|
+
|
29
|
+
@_executions = 0
|
30
|
+
@_cursor = nil
|
31
|
+
@_start_time = nil
|
32
|
+
@_runtime = 0
|
33
|
+
@_args = nil
|
34
|
+
@_cancelled = nil
|
35
|
+
end
|
36
|
+
|
37
|
+
def arguments
|
38
|
+
@_args
|
39
|
+
end
|
40
|
+
|
41
|
+
# Three days is the longest period you generally need to wait for a retry to
|
42
|
+
# execute when using the default retry scheme. We don't want to "forget" the job
|
43
|
+
# is cancelled before it has a chance to execute and cancel itself.
|
44
|
+
CANCELLATION_PERIOD = (3 * 86_400).to_s
|
45
|
+
|
46
|
+
# Set a flag in Redis to mark this job as cancelled.
|
47
|
+
# Cancellation is asynchronous and is checked at the start of iteration
|
48
|
+
# and every 5 seconds thereafter as part of the recurring state flush.
|
49
|
+
def cancel!
|
50
|
+
return @_cancelled if cancelled?
|
51
|
+
|
52
|
+
key = "it-#{jid}"
|
53
|
+
_, result, _ = Sidekiq.redis do |c|
|
54
|
+
c.pipelined do |p|
|
55
|
+
p.hsetnx(key, "cancelled", Time.now.to_i)
|
56
|
+
p.hget(key, "cancelled")
|
57
|
+
p.expire(key, Sidekiq::Job::Iterable::STATE_TTL, "nx")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
@_cancelled = result.to_i
|
61
|
+
end
|
62
|
+
|
63
|
+
def cancelled?
|
64
|
+
@_cancelled
|
65
|
+
end
|
66
|
+
|
67
|
+
def cursor
|
68
|
+
@_cursor.freeze
|
69
|
+
end
|
70
|
+
|
71
|
+
# A hook to override that will be called when the job starts iterating.
|
72
|
+
#
|
73
|
+
# It is called only once, for the first time.
|
74
|
+
#
|
75
|
+
def on_start
|
76
|
+
end
|
77
|
+
|
78
|
+
# A hook to override that will be called around each iteration.
|
79
|
+
#
|
80
|
+
# Can be useful for some metrics collection, performance tracking etc.
|
81
|
+
#
|
82
|
+
def around_iteration
|
83
|
+
yield
|
84
|
+
end
|
85
|
+
|
86
|
+
# A hook to override that will be called when the job resumes iterating.
|
87
|
+
#
|
88
|
+
def on_resume
|
89
|
+
end
|
90
|
+
|
91
|
+
# A hook to override that will be called each time the job is interrupted.
|
92
|
+
#
|
93
|
+
# This can be due to interruption or sidekiq stopping.
|
94
|
+
#
|
95
|
+
def on_stop
|
96
|
+
end
|
97
|
+
|
98
|
+
# A hook to override that will be called when the job is cancelled.
|
99
|
+
#
|
100
|
+
def on_cancel
|
101
|
+
end
|
102
|
+
|
103
|
+
# A hook to override that will be called when the job finished iterating.
|
104
|
+
#
|
105
|
+
def on_complete
|
106
|
+
end
|
107
|
+
|
108
|
+
# The enumerator to be iterated over.
|
109
|
+
#
|
110
|
+
# @return [Enumerator]
|
111
|
+
#
|
112
|
+
# @raise [NotImplementedError] with a message advising subclasses to
|
113
|
+
# implement an override for this method.
|
114
|
+
#
|
115
|
+
def build_enumerator(*)
|
116
|
+
raise NotImplementedError, "#{self.class.name} must implement a '#build_enumerator' method"
|
117
|
+
end
|
118
|
+
|
119
|
+
# The action to be performed on each item from the enumerator.
|
120
|
+
#
|
121
|
+
# @return [void]
|
122
|
+
#
|
123
|
+
# @raise [NotImplementedError] with a message advising subclasses to
|
124
|
+
# implement an override for this method.
|
125
|
+
#
|
126
|
+
def each_iteration(*)
|
127
|
+
raise NotImplementedError, "#{self.class.name} must implement an '#each_iteration' method"
|
128
|
+
end
|
129
|
+
|
130
|
+
def iteration_key
|
131
|
+
"it-#{jid}"
|
132
|
+
end
|
133
|
+
|
134
|
+
# @api private
|
135
|
+
def perform(*args)
|
136
|
+
@_args = args.dup.freeze
|
137
|
+
fetch_previous_iteration_state
|
138
|
+
|
139
|
+
@_executions += 1
|
140
|
+
@_start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
141
|
+
|
142
|
+
enumerator = build_enumerator(*args, cursor: @_cursor)
|
143
|
+
unless enumerator
|
144
|
+
logger.info("'#build_enumerator' returned nil, skipping the job.")
|
145
|
+
return
|
146
|
+
end
|
147
|
+
|
148
|
+
assert_enumerator!(enumerator)
|
149
|
+
|
150
|
+
if @_executions == 1
|
151
|
+
on_start
|
152
|
+
else
|
153
|
+
on_resume
|
154
|
+
end
|
155
|
+
|
156
|
+
completed = catch(:abort) do
|
157
|
+
iterate_with_enumerator(enumerator, args)
|
158
|
+
end
|
159
|
+
|
160
|
+
on_stop
|
161
|
+
completed = handle_completed(completed)
|
162
|
+
|
163
|
+
if completed
|
164
|
+
on_complete
|
165
|
+
cleanup
|
166
|
+
else
|
167
|
+
reenqueue_iteration_job
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
private
|
172
|
+
|
173
|
+
def is_cancelled?
|
174
|
+
@_cancelled = Sidekiq.redis { |c| c.hget("it-#{jid}", "cancelled") }
|
175
|
+
end
|
176
|
+
|
177
|
+
def fetch_previous_iteration_state
|
178
|
+
state = Sidekiq.redis { |conn| conn.hgetall(iteration_key) }
|
179
|
+
|
180
|
+
unless state.empty?
|
181
|
+
@_executions = state["ex"].to_i
|
182
|
+
@_cursor = Sidekiq.load_json(state["c"])
|
183
|
+
@_runtime = state["rt"].to_f
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
STATE_FLUSH_INTERVAL = 5 # seconds
|
188
|
+
# we need to keep the state around as long as the job
|
189
|
+
# might be retrying
|
190
|
+
STATE_TTL = 30 * 24 * 60 * 60 # one month
|
191
|
+
|
192
|
+
def iterate_with_enumerator(enumerator, arguments)
|
193
|
+
if is_cancelled?
|
194
|
+
on_cancel
|
195
|
+
logger.info { "Job cancelled" }
|
196
|
+
return true
|
197
|
+
end
|
198
|
+
|
199
|
+
time_limit = Sidekiq.default_configuration[:timeout]
|
200
|
+
found_record = false
|
201
|
+
state_flushed_at = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
202
|
+
|
203
|
+
enumerator.each do |object, cursor|
|
204
|
+
found_record = true
|
205
|
+
@_cursor = cursor
|
206
|
+
|
207
|
+
is_interrupted = interrupted?
|
208
|
+
if ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - state_flushed_at >= STATE_FLUSH_INTERVAL || is_interrupted
|
209
|
+
_, _, cancelled = flush_state
|
210
|
+
state_flushed_at = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
211
|
+
if cancelled
|
212
|
+
@_cancelled = true
|
213
|
+
on_cancel
|
214
|
+
logger.info { "Job cancelled" }
|
215
|
+
return true
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
return false if is_interrupted
|
220
|
+
|
221
|
+
verify_iteration_time(time_limit, object) do
|
222
|
+
around_iteration do
|
223
|
+
each_iteration(object, *arguments)
|
224
|
+
rescue Exception
|
225
|
+
flush_state
|
226
|
+
raise
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
logger.debug("Enumerator found nothing to iterate!") unless found_record
|
232
|
+
true
|
233
|
+
ensure
|
234
|
+
@_runtime += (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - @_start_time)
|
235
|
+
end
|
236
|
+
|
237
|
+
def verify_iteration_time(time_limit, object)
|
238
|
+
start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
239
|
+
yield
|
240
|
+
finish = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
241
|
+
total = finish - start
|
242
|
+
if total > time_limit
|
243
|
+
logger.warn { "Iteration took longer (%.2f) than Sidekiq's shutdown timeout (%d) when processing `%s`. This can lead to job processing problems during deploys" % [total, time_limit, object] }
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def reenqueue_iteration_job
|
248
|
+
flush_state
|
249
|
+
logger.debug { "Interrupting job (cursor=#{@_cursor.inspect})" }
|
250
|
+
|
251
|
+
raise Interrupted
|
252
|
+
end
|
253
|
+
|
254
|
+
def assert_enumerator!(enum)
|
255
|
+
unless enum.is_a?(Enumerator)
|
256
|
+
raise ArgumentError, <<~MSG
|
257
|
+
#build_enumerator must return an Enumerator, but returned #{enum.class}.
|
258
|
+
Example:
|
259
|
+
def build_enumerator(params, cursor:)
|
260
|
+
active_record_records_enumerator(
|
261
|
+
Shop.find(params["shop_id"]).products,
|
262
|
+
cursor: cursor
|
263
|
+
)
|
264
|
+
end
|
265
|
+
MSG
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
def flush_state
|
270
|
+
key = iteration_key
|
271
|
+
state = {
|
272
|
+
"ex" => @_executions,
|
273
|
+
"c" => Sidekiq.dump_json(@_cursor),
|
274
|
+
"rt" => @_runtime
|
275
|
+
}
|
276
|
+
|
277
|
+
Sidekiq.redis do |conn|
|
278
|
+
conn.multi do |pipe|
|
279
|
+
pipe.hset(key, state)
|
280
|
+
pipe.expire(key, STATE_TTL, "nx")
|
281
|
+
pipe.hget(key, "cancelled")
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
def cleanup
|
287
|
+
logger.debug {
|
288
|
+
format("Completed iteration. executions=%d runtime=%.3f", @_executions, @_runtime)
|
289
|
+
}
|
290
|
+
Sidekiq.redis { |conn| conn.unlink(iteration_key) }
|
291
|
+
end
|
292
|
+
|
293
|
+
def handle_completed(completed)
|
294
|
+
case completed
|
295
|
+
when nil, # someone aborted the job but wants to call the on_complete callback
|
296
|
+
true
|
297
|
+
true
|
298
|
+
when false
|
299
|
+
false
|
300
|
+
else
|
301
|
+
raise "Unexpected thrown value: #{completed.inspect}"
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|