sidekiq-robust-job 0.1.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 +7 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.travis.yml +9 -0
- data/Changelog.md +4 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +127 -0
- data/LICENSE.txt +21 -0
- data/README.md +291 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/sidekiq-robust-job.rb +1 -0
- data/lib/sidekiq/robust/job.rb +1 -0
- data/lib/sidekiq/robust/job/version.rb +7 -0
- data/lib/sidekiq_robust_job.rb +64 -0
- data/lib/sidekiq_robust_job/configuration.rb +48 -0
- data/lib/sidekiq_robust_job/dependencies_container.rb +64 -0
- data/lib/sidekiq_robust_job/digest_generator.rb +14 -0
- data/lib/sidekiq_robust_job/enqueue_conflict_resolution_strategy.rb +58 -0
- data/lib/sidekiq_robust_job/enqueue_conflict_resolution_strategy/base.rb +17 -0
- data/lib/sidekiq_robust_job/enqueue_conflict_resolution_strategy/do_nothing.rb +8 -0
- data/lib/sidekiq_robust_job/enqueue_conflict_resolution_strategy/drop_self.rb +12 -0
- data/lib/sidekiq_robust_job/enqueue_conflict_resolution_strategy/replace.rb +13 -0
- data/lib/sidekiq_robust_job/missed_jobs.rb +26 -0
- data/lib/sidekiq_robust_job/missed_jobs_scheduler.rb +56 -0
- data/lib/sidekiq_robust_job/model.rb +90 -0
- data/lib/sidekiq_robust_job/perform_missed_jobs_job.rb +9 -0
- data/lib/sidekiq_robust_job/repository.rb +50 -0
- data/lib/sidekiq_robust_job/setter_proxy_job.rb +70 -0
- data/lib/sidekiq_robust_job/sidekiq_job_extensions.rb +34 -0
- data/lib/sidekiq_robust_job/sidekiq_job_manager.rb +80 -0
- data/lib/sidekiq_robust_job/support/matchers/enqueue_sidekiq_robust_job.rb +71 -0
- data/lib/sidekiq_robust_job/uniqueness_strategy.rb +66 -0
- data/lib/sidekiq_robust_job/uniqueness_strategy/base.rb +46 -0
- data/lib/sidekiq_robust_job/uniqueness_strategy/no_uniqueness.rb +9 -0
- data/lib/sidekiq_robust_job/uniqueness_strategy/until_executed.rb +16 -0
- data/lib/sidekiq_robust_job/uniqueness_strategy/until_executing.rb +16 -0
- data/sidekiq-robust-job.gemspec +42 -0
- metadata +238 -0
@@ -0,0 +1,50 @@
|
|
1
|
+
class SidekiqRobustJob
|
2
|
+
class Repository
|
3
|
+
attr_reader :jobs_database, :clock
|
4
|
+
private :jobs_database, :clock
|
5
|
+
|
6
|
+
def initialize(jobs_database:, clock:)
|
7
|
+
@jobs_database = jobs_database
|
8
|
+
@clock = clock
|
9
|
+
end
|
10
|
+
|
11
|
+
def transaction
|
12
|
+
jobs_database.transaction { yield }
|
13
|
+
end
|
14
|
+
|
15
|
+
def find(id)
|
16
|
+
jobs_database.find(id)
|
17
|
+
end
|
18
|
+
|
19
|
+
def save(record)
|
20
|
+
record.save!
|
21
|
+
end
|
22
|
+
|
23
|
+
def create(attributes)
|
24
|
+
jobs_database.create!(attributes)
|
25
|
+
end
|
26
|
+
|
27
|
+
def missed_jobs(missed_job_policy:)
|
28
|
+
jobs_database
|
29
|
+
.where(completed_at: nil, dropped_at: nil, failed_at: nil)
|
30
|
+
.select { |potentially_missed_job| missed_job_policy.call(potentially_missed_job) }
|
31
|
+
end
|
32
|
+
|
33
|
+
def unprocessed_for_digest(digest, exclude_id:)
|
34
|
+
jobs_database
|
35
|
+
.where(digest: digest)
|
36
|
+
.where(completed_at: nil)
|
37
|
+
.where(dropped_at: nil)
|
38
|
+
.where.not(id: exclude_id)
|
39
|
+
end
|
40
|
+
|
41
|
+
def drop_unprocessed_jobs_by_digest(dropped_by_job_id:, digest:, exclude_id:)
|
42
|
+
transaction do
|
43
|
+
unprocessed_for_digest(digest, exclude_id: exclude_id).lock!.find_each do |job|
|
44
|
+
job.drop(dropped_by_job_id: dropped_by_job_id)
|
45
|
+
save(job)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
class SidekiqRobustJob
|
2
|
+
class SetterProxyJob
|
3
|
+
def build(job_class, options)
|
4
|
+
Class.new(SimpleDelegator) do
|
5
|
+
attr_reader :job_class, :custom_options
|
6
|
+
private :job_class, :custom_options
|
7
|
+
|
8
|
+
def initialize(job_class, custom_options = {})
|
9
|
+
super(job_class)
|
10
|
+
|
11
|
+
@job_class = job_class
|
12
|
+
@custom_options = custom_options
|
13
|
+
end
|
14
|
+
|
15
|
+
# rubocop:disable Naming/AccessorMethodName
|
16
|
+
def get_sidekiq_options
|
17
|
+
job_class.get_sidekiq_options.merge(custom_options.stringify_keys)
|
18
|
+
end
|
19
|
+
# rubocop:enable Naming/AccessorMethodName
|
20
|
+
|
21
|
+
def perform_async(*arguments)
|
22
|
+
SidekiqRobustJob.perform_async(self, *arguments)
|
23
|
+
end
|
24
|
+
|
25
|
+
def perform_in(interval, *arguments)
|
26
|
+
SidekiqRobustJob.perform_in(self, interval, *arguments)
|
27
|
+
end
|
28
|
+
|
29
|
+
def perform_at(time, *arguments)
|
30
|
+
SidekiqRobustJob.perform_at(self, time, *arguments)
|
31
|
+
end
|
32
|
+
|
33
|
+
def set(options = {})
|
34
|
+
SidekiqRobustJob.set(self, options)
|
35
|
+
end
|
36
|
+
|
37
|
+
def original_perform_in(*args)
|
38
|
+
call_sidekiq_method(:perform_in, *args)
|
39
|
+
end
|
40
|
+
|
41
|
+
def original_perform_at(*args)
|
42
|
+
call_sidekiq_method(:perform_at, *args)
|
43
|
+
end
|
44
|
+
|
45
|
+
def original_perform_async(*args)
|
46
|
+
call_sidekiq_method(:perform_async, *args)
|
47
|
+
end
|
48
|
+
|
49
|
+
def original_set(*args)
|
50
|
+
call_sidekiq_method(:set, *args)
|
51
|
+
end
|
52
|
+
|
53
|
+
# override to not fail on Sidekiq internal validation
|
54
|
+
def is_a?(val)
|
55
|
+
if val == Class
|
56
|
+
true
|
57
|
+
else
|
58
|
+
super
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def call_sidekiq_method(name, *args)
|
65
|
+
Sidekiq::Worker::ClassMethods.instance_method(name).bind(self).call(*args)
|
66
|
+
end
|
67
|
+
end.new(job_class, options)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class SidekiqRobustJob
|
2
|
+
module SidekiqJobExtensions
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
class << self
|
7
|
+
alias_method :original_perform_async, :perform_async
|
8
|
+
alias_method :original_perform_in, :perform_in
|
9
|
+
alias_method :original_perform_at, :perform_at
|
10
|
+
alias_method :original_set, :set
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.perform_async(*arguments)
|
14
|
+
SidekiqRobustJob.perform_async(self, *arguments)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.perform_in(interval, *arguments)
|
18
|
+
SidekiqRobustJob.perform_in(self, interval, *arguments)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.perform_at(time, *arguments)
|
22
|
+
SidekiqRobustJob.perform_at(self, time, *arguments)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.set(options = {})
|
26
|
+
SidekiqRobustJob.set(self, options)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def perform(job_id)
|
31
|
+
SidekiqRobustJob.perform(job_id)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
class SidekiqRobustJob
|
2
|
+
class SidekiqJobManager
|
3
|
+
attr_reader :jobs_repository, :clock, :digest_generator, :memory_monitor
|
4
|
+
private :jobs_repository, :clock, :digest_generator, :memory_monitor
|
5
|
+
|
6
|
+
def initialize(jobs_repository:, clock:, digest_generator:, memory_monitor:)
|
7
|
+
@jobs_repository = jobs_repository
|
8
|
+
@clock = clock
|
9
|
+
@digest_generator = digest_generator
|
10
|
+
@memory_monitor = memory_monitor
|
11
|
+
end
|
12
|
+
|
13
|
+
def perform_async(job_class, *arguments)
|
14
|
+
job = create_job(job_class, *arguments)
|
15
|
+
return if job.unprocessable?
|
16
|
+
job_class.original_perform_async(job.id).tap do |sidekiq_jid|
|
17
|
+
job.assign_sidekiq_data(execute_at: clock.now, sidekiq_jid: sidekiq_jid)
|
18
|
+
jobs_repository.save(job)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def perform_in(job_class, interval, *arguments)
|
23
|
+
job = create_job(job_class, *arguments)
|
24
|
+
return if job.unprocessable?
|
25
|
+
job_class.original_perform_in(interval, job.id).tap do |sidekiq_jid|
|
26
|
+
job.assign_sidekiq_data(execute_at: clock.now + interval, sidekiq_jid: sidekiq_jid)
|
27
|
+
jobs_repository.save(job)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def perform_at(job_class, time, *arguments)
|
32
|
+
job = create_job(job_class, *arguments)
|
33
|
+
return if job.unprocessable?
|
34
|
+
job_class.original_perform_at(time, job.id).tap do |sidekiq_jid|
|
35
|
+
job.assign_sidekiq_data(execute_at: time, sidekiq_jid: sidekiq_jid)
|
36
|
+
jobs_repository.save(job)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def set(job_class, options = {})
|
41
|
+
SidekiqRobustJob::DependenciesContainer["setter_proxy_job"].build(job_class, options)
|
42
|
+
end
|
43
|
+
|
44
|
+
def perform(job_id)
|
45
|
+
job = jobs_repository.find(job_id)
|
46
|
+
return if job.unprocessable?
|
47
|
+
|
48
|
+
job.started(memory_monitor: memory_monitor)
|
49
|
+
jobs_repository.save(job)
|
50
|
+
job.execute
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def create_job(job_class, *arguments)
|
56
|
+
jobs_repository.create(
|
57
|
+
job_class: job_class,
|
58
|
+
arguments: Array.wrap(arguments),
|
59
|
+
enqueued_at: clock.now,
|
60
|
+
digest: digest_generator.generate(job_class, *arguments),
|
61
|
+
queue: job_class.get_sidekiq_options.fetch("queue", "default"),
|
62
|
+
uniqueness_strategy: job_class.get_sidekiq_options.fetch("uniqueness_strategy",
|
63
|
+
SidekiqRobustJob::UniquenessStrategy.no_uniqueness),
|
64
|
+
enqueue_conflict_resolution_strategy: job_class.get_sidekiq_options.fetch("enqueue_conflict_resolution_strategy",
|
65
|
+
SidekiqRobustJob::EnqueueConflictResolutionStrategy.do_nothing)
|
66
|
+
).tap do |job|
|
67
|
+
jobs_repository.transaction do
|
68
|
+
resolve_potential_conflict_for_enqueueing(job)
|
69
|
+
jobs_repository.save(job)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def resolve_potential_conflict_for_enqueueing(job)
|
75
|
+
SidekiqRobustJob::DependenciesContainer["enqueue_conflict_resolution_resolver"]
|
76
|
+
.resolve(job.enqueue_conflict_resolution_strategy)
|
77
|
+
.execute(job)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
if Object.const_defined?("RSpec")
|
2
|
+
RSpec::Matchers.define :enqueue_sidekiq_robust_job do |job_class|
|
3
|
+
supports_block_expectations
|
4
|
+
|
5
|
+
match do |block|
|
6
|
+
robust_jobs_before = robust_jobs
|
7
|
+
|
8
|
+
block.call
|
9
|
+
|
10
|
+
robust_jobs_before.empty? && expected_job_after_execution_exists?
|
11
|
+
end
|
12
|
+
|
13
|
+
failure_message do
|
14
|
+
"expected #{job_class} to have enqueued job with #{@args} to be executed at: #{execution_time}. " +
|
15
|
+
"All enqueued jobs of this type are: #{formatted_sidekiq_jobs}. Perhaps the job was enqueued before or execution time didn't match?"
|
16
|
+
end
|
17
|
+
|
18
|
+
define_method :jobs do
|
19
|
+
SidekiqRobustJob.configuration.sidekiq_job_model.all.to_a
|
20
|
+
end
|
21
|
+
|
22
|
+
define_method :sidekiq_jobs do
|
23
|
+
job_class.jobs
|
24
|
+
end
|
25
|
+
|
26
|
+
define_method :formatted_sidekiq_jobs do
|
27
|
+
sidekiq_jobs.map do |job|
|
28
|
+
arguments = SidekiqRobustJob.configuration.sidekiq_job_model.find(job["args"].first).arguments
|
29
|
+
"arguments: #{arguments}, at: #{job['at']}"
|
30
|
+
end.join(";")
|
31
|
+
end
|
32
|
+
|
33
|
+
define_method :execution_time do
|
34
|
+
raise "cannot use both :in and :at!" if @interval && @at
|
35
|
+
|
36
|
+
if @interval
|
37
|
+
Time.now.to_i + @interval
|
38
|
+
elsif @at
|
39
|
+
@at.to_time.to_i
|
40
|
+
else
|
41
|
+
Time.now.to_i
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
define_method :expected_job_after_execution_exists? do
|
46
|
+
robust_jobs.any? do |robust_job|
|
47
|
+
sidekiq_jobs.any? do |sidekiq_job|
|
48
|
+
sidekiq_job_at = sidekiq_job["at"]
|
49
|
+
sidekiq_job["args"] == [robust_job.id] && (sidekiq_job_at.nil? || execution_time.to_i == Time.at(sidekiq_job_at).to_i)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
define_method :robust_jobs do
|
55
|
+
jobs.select { |job| job.job_class == job_class.to_s && job.arguments == @args }
|
56
|
+
end
|
57
|
+
|
58
|
+
chain :with do |*args|
|
59
|
+
@args = Array.wrap(args)
|
60
|
+
end
|
61
|
+
|
62
|
+
chain :in do |interval|
|
63
|
+
@interval = interval
|
64
|
+
end
|
65
|
+
|
66
|
+
chain :at do |at_time|
|
67
|
+
@at = at_time
|
68
|
+
end
|
69
|
+
end
|
70
|
+
RSpec::Matchers.define_negated_matcher :not_enqueue_sidekiq_robust_job, :enqueue_sidekiq_robust_job
|
71
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
class SidekiqRobustJob
|
2
|
+
class UniquenessStrategy
|
3
|
+
def self.no_uniqueness
|
4
|
+
:no_uniqueness
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.until_executing
|
8
|
+
:until_executing
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.until_executed
|
12
|
+
:until_executed
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :locker, :lock_ttl_proc, :jobs_repository, :memory_monitor
|
16
|
+
private :locker, :lock_ttl_proc, :jobs_repository, :memory_monitor
|
17
|
+
|
18
|
+
def initialize(locker:, lock_ttl_proc:, jobs_repository:, memory_monitor:)
|
19
|
+
@locker = locker
|
20
|
+
@lock_ttl_proc = lock_ttl_proc
|
21
|
+
@jobs_repository = jobs_repository
|
22
|
+
@memory_monitor = memory_monitor
|
23
|
+
end
|
24
|
+
|
25
|
+
def resolve(strategy)
|
26
|
+
case strategy.to_sym
|
27
|
+
when SidekiqRobustJob::UniquenessStrategy.no_uniqueness
|
28
|
+
SidekiqRobustJob::UniquenessStrategy::NoUniqueness.new(
|
29
|
+
locker: locker,
|
30
|
+
lock_ttl_proc: lock_ttl_proc,
|
31
|
+
jobs_repository: jobs_repository,
|
32
|
+
memory_monitor: memory_monitor
|
33
|
+
)
|
34
|
+
when SidekiqRobustJob::UniquenessStrategy.until_executing
|
35
|
+
SidekiqRobustJob::UniquenessStrategy::UntilExecuting.new(
|
36
|
+
locker: locker,
|
37
|
+
lock_ttl_proc: lock_ttl_proc,
|
38
|
+
jobs_repository: jobs_repository,
|
39
|
+
memory_monitor: memory_monitor
|
40
|
+
)
|
41
|
+
when SidekiqRobustJob::UniquenessStrategy.until_executed
|
42
|
+
SidekiqRobustJob::UniquenessStrategy::UntilExecuted.new(
|
43
|
+
locker: locker,
|
44
|
+
lock_ttl_proc: lock_ttl_proc,
|
45
|
+
jobs_repository: jobs_repository,
|
46
|
+
memory_monitor: memory_monitor
|
47
|
+
)
|
48
|
+
else
|
49
|
+
raise UnknownStrategyError.new(strategy)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class UnknownStrategyError < StandardError
|
54
|
+
attr_reader :strategy
|
55
|
+
private :strategy
|
56
|
+
|
57
|
+
def initialize(strategy)
|
58
|
+
@strategy = strategy
|
59
|
+
end
|
60
|
+
|
61
|
+
def message
|
62
|
+
"unknown uniqueness strategy: #{strategy}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class SidekiqRobustJob
|
2
|
+
class UniquenessStrategy
|
3
|
+
class Base
|
4
|
+
attr_reader :locker, :lock_ttl_proc, :jobs_repository, :memory_monitor
|
5
|
+
private :locker, :lock_ttl_proc, :jobs_repository, :memory_monitor
|
6
|
+
|
7
|
+
def initialize(locker:, lock_ttl_proc:, jobs_repository:, memory_monitor:)
|
8
|
+
@locker = locker
|
9
|
+
@lock_ttl_proc = lock_ttl_proc
|
10
|
+
@jobs_repository = jobs_repository
|
11
|
+
@memory_monitor = memory_monitor
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute(_job)
|
15
|
+
raise "implement me"
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def perform_job_and_finalize(job)
|
21
|
+
begin
|
22
|
+
job.call
|
23
|
+
rescue StandardError => error
|
24
|
+
job.failed(error)
|
25
|
+
jobs_repository.save(job)
|
26
|
+
raise
|
27
|
+
end
|
28
|
+
|
29
|
+
job.completed(memory_monitor: memory_monitor)
|
30
|
+
jobs_repository.save(job)
|
31
|
+
end
|
32
|
+
|
33
|
+
def drop_unprocessed_jobs(job)
|
34
|
+
jobs_repository.drop_unprocessed_jobs_by_digest(
|
35
|
+
dropped_by_job_id: job.id,
|
36
|
+
digest: job.digest,
|
37
|
+
exclude_id: job.id
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
def lock(job)
|
42
|
+
locker.lock(job.digest, lock_ttl_proc.call(job)) { |locked| yield locked }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class SidekiqRobustJob
|
2
|
+
class UniquenessStrategy
|
3
|
+
class UntilExecuted < SidekiqRobustJob::UniquenessStrategy::Base
|
4
|
+
def execute(job)
|
5
|
+
lock(job) do |locked|
|
6
|
+
if locked
|
7
|
+
perform_job_and_finalize(job)
|
8
|
+
drop_unprocessed_jobs(job)
|
9
|
+
else
|
10
|
+
job.reschedule and return
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|