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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +9 -0
  5. data/Changelog.md +4 -0
  6. data/Gemfile +4 -0
  7. data/Gemfile.lock +127 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +291 -0
  10. data/Rakefile +6 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +8 -0
  13. data/lib/sidekiq-robust-job.rb +1 -0
  14. data/lib/sidekiq/robust/job.rb +1 -0
  15. data/lib/sidekiq/robust/job/version.rb +7 -0
  16. data/lib/sidekiq_robust_job.rb +64 -0
  17. data/lib/sidekiq_robust_job/configuration.rb +48 -0
  18. data/lib/sidekiq_robust_job/dependencies_container.rb +64 -0
  19. data/lib/sidekiq_robust_job/digest_generator.rb +14 -0
  20. data/lib/sidekiq_robust_job/enqueue_conflict_resolution_strategy.rb +58 -0
  21. data/lib/sidekiq_robust_job/enqueue_conflict_resolution_strategy/base.rb +17 -0
  22. data/lib/sidekiq_robust_job/enqueue_conflict_resolution_strategy/do_nothing.rb +8 -0
  23. data/lib/sidekiq_robust_job/enqueue_conflict_resolution_strategy/drop_self.rb +12 -0
  24. data/lib/sidekiq_robust_job/enqueue_conflict_resolution_strategy/replace.rb +13 -0
  25. data/lib/sidekiq_robust_job/missed_jobs.rb +26 -0
  26. data/lib/sidekiq_robust_job/missed_jobs_scheduler.rb +56 -0
  27. data/lib/sidekiq_robust_job/model.rb +90 -0
  28. data/lib/sidekiq_robust_job/perform_missed_jobs_job.rb +9 -0
  29. data/lib/sidekiq_robust_job/repository.rb +50 -0
  30. data/lib/sidekiq_robust_job/setter_proxy_job.rb +70 -0
  31. data/lib/sidekiq_robust_job/sidekiq_job_extensions.rb +34 -0
  32. data/lib/sidekiq_robust_job/sidekiq_job_manager.rb +80 -0
  33. data/lib/sidekiq_robust_job/support/matchers/enqueue_sidekiq_robust_job.rb +71 -0
  34. data/lib/sidekiq_robust_job/uniqueness_strategy.rb +66 -0
  35. data/lib/sidekiq_robust_job/uniqueness_strategy/base.rb +46 -0
  36. data/lib/sidekiq_robust_job/uniqueness_strategy/no_uniqueness.rb +9 -0
  37. data/lib/sidekiq_robust_job/uniqueness_strategy/until_executed.rb +16 -0
  38. data/lib/sidekiq_robust_job/uniqueness_strategy/until_executing.rb +16 -0
  39. data/sidekiq-robust-job.gemspec +42 -0
  40. metadata +238 -0
@@ -0,0 +1,9 @@
1
+ class SidekiqRobustJob
2
+ class PerformMissedJobsJob
3
+ include Sidekiq::Worker
4
+
5
+ def perform
6
+ SidekiqRobustJob::DependenciesContainer["missed_jobs"].invoke
7
+ end
8
+ end
9
+ end
@@ -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,9 @@
1
+ class SidekiqRobustJob
2
+ class UniquenessStrategy
3
+ class NoUniqueness < SidekiqRobustJob::UniquenessStrategy::Base
4
+ def execute(job)
5
+ perform_job_and_finalize(job)
6
+ end
7
+ end
8
+ end
9
+ 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