sidekiq-robust-job 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|