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,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "sidekiq/robust/job"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1 @@
1
+ require "sidekiq_robust_job"
@@ -0,0 +1 @@
1
+ require_relative "../../sidekiq-robust-job.rb"
@@ -0,0 +1,7 @@
1
+ module Sidekiq
2
+ module Robust
3
+ module Job
4
+ VERSION = "0.1.0"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,64 @@
1
+ require "sidekiq/robust/job/version"
2
+ require "sidekiq_robust_job/configuration"
3
+ require "sidekiq_robust_job/dependencies_container"
4
+ require "sidekiq_robust_job/digest_generator"
5
+ require "sidekiq_robust_job/enqueue_conflict_resolution_strategy"
6
+ require "sidekiq_robust_job/enqueue_conflict_resolution_strategy/base"
7
+ require "sidekiq_robust_job/enqueue_conflict_resolution_strategy/do_nothing"
8
+ require "sidekiq_robust_job/enqueue_conflict_resolution_strategy/drop_self"
9
+ require "sidekiq_robust_job/enqueue_conflict_resolution_strategy/replace"
10
+ require "sidekiq_robust_job/missed_jobs"
11
+ require "sidekiq_robust_job/missed_jobs_scheduler"
12
+ require "sidekiq_robust_job/model"
13
+ require "sidekiq_robust_job/perform_missed_jobs_job"
14
+ require "sidekiq_robust_job/repository"
15
+ require "sidekiq_robust_job/setter_proxy_job"
16
+ require "sidekiq_robust_job/sidekiq_job_extensions"
17
+ require "sidekiq_robust_job/sidekiq_job_manager"
18
+ require "sidekiq_robust_job/uniqueness_strategy"
19
+ require "sidekiq_robust_job/uniqueness_strategy/base"
20
+ require "sidekiq_robust_job/uniqueness_strategy/no_uniqueness"
21
+ require "sidekiq_robust_job/uniqueness_strategy/until_executed"
22
+ require "sidekiq_robust_job/uniqueness_strategy/until_executing"
23
+ require "sidekiq/cron/job"
24
+ require "sidekiq"
25
+ require "active_support/concern"
26
+
27
+ class SidekiqRobustJob
28
+ def self.configuration
29
+ @configuration ||= SidekiqRobustJob::Configuration.new
30
+ end
31
+
32
+ def self.configure
33
+ yield configuration
34
+ end
35
+
36
+ def self.perform_async(job_class, *arguments)
37
+ sidekiq_job_manager.perform_async(job_class, *arguments)
38
+ end
39
+
40
+ def self.perform_in(job_class, interval, *arguments)
41
+ sidekiq_job_manager.perform_in(job_class, interval, *arguments)
42
+ end
43
+
44
+ def self.perform_at(job_class, interval, *arguments)
45
+ sidekiq_job_manager.perform_at(job_class, interval, *arguments)
46
+ end
47
+
48
+ def self.set(job_class, options)
49
+ sidekiq_job_manager.set(job_class, options)
50
+ end
51
+
52
+ def self.perform(job_id)
53
+ sidekiq_job_manager.perform(job_id)
54
+ end
55
+
56
+ def self.schedule_missed_jobs_handling
57
+ SidekiqRobustJob::DependenciesContainer["missed_jobs_scheduler"].schedule
58
+ end
59
+
60
+ def self.sidekiq_job_manager
61
+ SidekiqRobustJob::DependenciesContainer["sidekiq_job_manager"]
62
+ end
63
+ private_class_method :sidekiq_job_manager
64
+ end
@@ -0,0 +1,48 @@
1
+ class SidekiqRobustJob
2
+ class Configuration
3
+ DEFAULT_MISSED_JOB_CRON_EVERY_THREE_HOURS = "0 */3 * * *".freeze
4
+
5
+ attr_accessor :locker, :lock_ttl_proc, :memory_monitor, :clock, :digest_generator_backend, :sidekiq_job_model,
6
+ :missed_job_policy, :missed_job_cron
7
+
8
+ def lock_ttl_proc=(val)
9
+ raise ArgumentError.new("must be lambda-like object!") if !val.respond_to?(:call)
10
+ @lock_ttl_proc = val
11
+ end
12
+
13
+ def lock_ttl_proc
14
+ @lock_ttl_proc ||= ->(_job) { 120_000 }
15
+ end
16
+
17
+ def clock
18
+ @clock ||= Time
19
+ end
20
+
21
+ def digest_generator_backend
22
+ @digest_generator_backend ||= Digest::MD5
23
+ end
24
+
25
+ def sidekiq_job_model
26
+ @sidekiq_job_model
27
+ end
28
+
29
+ def missed_job_policy=(val)
30
+ raise ArgumentError.new("must be lambda-like object!") if !val.respond_to?(:call)
31
+ @missed_job_policy = val
32
+ end
33
+
34
+ def missed_job_policy
35
+ @missed_job_policy || ->(job) { Time.current > (job.created_at + 3.hours) }
36
+ end
37
+
38
+ def missed_job_cron=(val)
39
+ Fugit.do_parse_cron(val)
40
+
41
+ @missed_job_cron = val
42
+ end
43
+
44
+ def missed_job_cron
45
+ @missed_job_cron || DEFAULT_MISSED_JOB_CRON_EVERY_THREE_HOURS
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,64 @@
1
+ class SidekiqRobustJob
2
+ class DependenciesContainer
3
+ def self.[](method_name)
4
+ public_send(method_name)
5
+ end
6
+
7
+ def self.sidekiq_job_manager
8
+ SidekiqRobustJob::SidekiqJobManager.new(
9
+ jobs_repository: SidekiqRobustJob::DependenciesContainer["jobs_repository"],
10
+ clock: SidekiqRobustJob.configuration.clock,
11
+ digest_generator: SidekiqRobustJob::DependenciesContainer["digest_generator"],
12
+ memory_monitor: SidekiqRobustJob.configuration.memory_monitor
13
+ )
14
+ end
15
+
16
+ def self.jobs_repository
17
+ SidekiqRobustJob::Repository.new(
18
+ jobs_database: SidekiqRobustJob.configuration.sidekiq_job_model,
19
+ clock: SidekiqRobustJob.configuration.clock
20
+ )
21
+ end
22
+
23
+ def self.uniqueness_strategy_resolver
24
+ SidekiqRobustJob::UniquenessStrategy.new(
25
+ locker: SidekiqRobustJob.configuration.locker,
26
+ lock_ttl_proc: SidekiqRobustJob.configuration.lock_ttl_proc,
27
+ jobs_repository: SidekiqRobustJob::DependenciesContainer["jobs_repository"],
28
+ memory_monitor: SidekiqRobustJob.configuration.memory_monitor
29
+ )
30
+ end
31
+
32
+ def self.digest_generator
33
+ SidekiqRobustJob::DigestGenerator.new(
34
+ backend: SidekiqRobustJob.configuration.digest_generator_backend
35
+ )
36
+ end
37
+
38
+ def self.enqueue_conflict_resolution_resolver
39
+ SidekiqRobustJob::EnqueueConflictResolutionStrategy.new(
40
+ jobs_repository: SidekiqRobustJob::DependenciesContainer["jobs_repository"],
41
+ clock: SidekiqRobustJob.configuration.clock
42
+ )
43
+ end
44
+
45
+ def self.setter_proxy_job
46
+ SidekiqRobustJob::SetterProxyJob.new
47
+ end
48
+
49
+ def self.missed_jobs
50
+ SidekiqRobustJob::MissedJobs.new(
51
+ jobs_repository: SidekiqRobustJob::DependenciesContainer["jobs_repository"],
52
+ missed_job_policy: SidekiqRobustJob.configuration.missed_job_policy
53
+ )
54
+ end
55
+
56
+ def self.missed_jobs_scheduler
57
+ SidekiqRobustJob::MissedJobsScheduler.new(
58
+ cron: SidekiqRobustJob.configuration.missed_job_cron,
59
+ scheduled_jobs_repository: Sidekiq::Cron::Job,
60
+ job_class: SidekiqRobustJob::PerformMissedJobsJob
61
+ )
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,14 @@
1
+ class SidekiqRobustJob
2
+ class DigestGenerator
3
+ attr_reader :backend
4
+ private :backend
5
+
6
+ def initialize(backend:)
7
+ @backend = backend
8
+ end
9
+
10
+ def generate(job_class, *arguments)
11
+ backend.hexdigest("#{job_class}-#{Array(arguments).join('-')}")
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,58 @@
1
+ class SidekiqRobustJob
2
+ class EnqueueConflictResolutionStrategy
3
+ def self.do_nothing
4
+ :do_nothing
5
+ end
6
+
7
+ def self.drop_self
8
+ :drop_self
9
+ end
10
+
11
+ def self.replace
12
+ :replace
13
+ end
14
+
15
+ attr_reader :jobs_repository, :clock
16
+ private :jobs_repository, :clock
17
+
18
+ def initialize(jobs_repository:, clock:)
19
+ @jobs_repository = jobs_repository
20
+ @clock = clock
21
+ end
22
+
23
+ def resolve(strategy)
24
+ case strategy.to_sym
25
+ when SidekiqRobustJob::EnqueueConflictResolutionStrategy.do_nothing
26
+ SidekiqRobustJob::EnqueueConflictResolutionStrategy::DoNothing.new(
27
+ jobs_repository: jobs_repository,
28
+ clock: clock
29
+ )
30
+ when SidekiqRobustJob::EnqueueConflictResolutionStrategy.drop_self
31
+ SidekiqRobustJob::EnqueueConflictResolutionStrategy::DropSelf.new(
32
+ jobs_repository: jobs_repository,
33
+ clock: clock
34
+ )
35
+ when SidekiqRobustJob::EnqueueConflictResolutionStrategy.replace
36
+ SidekiqRobustJob::EnqueueConflictResolutionStrategy::Replace.new(
37
+ jobs_repository: jobs_repository,
38
+ clock: clock
39
+ )
40
+ else
41
+ raise UnknownStrategyError.new(strategy)
42
+ end
43
+ end
44
+
45
+ class UnknownStrategyError < StandardError
46
+ attr_reader :strategy
47
+ private :strategy
48
+
49
+ def initialize(strategy)
50
+ @strategy = strategy
51
+ end
52
+
53
+ def message
54
+ "unknown enqueue conflict resolution strategy: #{strategy}"
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,17 @@
1
+ class SidekiqRobustJob
2
+ class EnqueueConflictResolutionStrategy
3
+ class Base
4
+ attr_reader :jobs_repository, :clock
5
+ private :jobs_repository, :clock
6
+
7
+ def initialize(jobs_repository:, clock:)
8
+ @jobs_repository = jobs_repository
9
+ @clock = clock
10
+ end
11
+
12
+ def execute(_job)
13
+ raise "implement me"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,8 @@
1
+ class SidekiqRobustJob
2
+ class EnqueueConflictResolutionStrategy
3
+ class DoNothing < SidekiqRobustJob::EnqueueConflictResolutionStrategy::Base
4
+ def execute(_job)
5
+ end
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,12 @@
1
+ class SidekiqRobustJob
2
+ class EnqueueConflictResolutionStrategy
3
+ class DropSelf < SidekiqRobustJob::EnqueueConflictResolutionStrategy::Base
4
+ def execute(job)
5
+ if jobs_repository.unprocessed_for_digest(job.digest, exclude_id: job.id).any?
6
+ job.drop(dropped_by_job_id: job.id, clock: clock)
7
+ jobs_repository.save(job)
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ class SidekiqRobustJob
2
+ class EnqueueConflictResolutionStrategy
3
+ class Replace < SidekiqRobustJob::EnqueueConflictResolutionStrategy::Base
4
+ def execute(job)
5
+ jobs_repository.drop_unprocessed_jobs_by_digest(
6
+ dropped_by_job_id: job.id,
7
+ digest: job.digest,
8
+ exclude_id: job.id
9
+ )
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,26 @@
1
+ require "forwardable"
2
+
3
+ class SidekiqRobustJob
4
+ class MissedJobs
5
+ include Enumerable
6
+ extend Forwardable
7
+
8
+ def_delegator :all, :each
9
+
10
+ attr_reader :jobs_repository, :missed_job_policy
11
+ private :jobs_repository, :missed_job_policy
12
+
13
+ def initialize(jobs_repository:, missed_job_policy:)
14
+ @jobs_repository = jobs_repository
15
+ @missed_job_policy = missed_job_policy
16
+ end
17
+
18
+ def all
19
+ @all ||= jobs_repository.missed_jobs(missed_job_policy: missed_job_policy)
20
+ end
21
+
22
+ def invoke
23
+ each(&:reschedule)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,56 @@
1
+ class SidekiqRobustJob
2
+ class MissedJobsScheduler
3
+ attr_reader :serializer, :scheduled_jobs_repository
4
+ private :serializer, :scheduled_jobs_repository
5
+
6
+ def initialize(cron:, scheduled_jobs_repository:, job_class:)
7
+ @serializer = MissedJobSerializer.new(cron, job_class)
8
+ @scheduled_jobs_repository = scheduled_jobs_repository
9
+ end
10
+
11
+ def schedule
12
+ scheduled_jobs_repository.new(serializer.serialize).tap do |job|
13
+ if job.valid?
14
+ job.save
15
+ else
16
+ raise_invalid_job(job)
17
+ end
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def raise_invalid_job(job)
24
+ errors = job.errors.join(",")
25
+ raise "could not save job: #{errors}"
26
+ end
27
+
28
+ class MissedJobSerializer
29
+ NAME = "SidekiqRobustJob - MissedJobsScheduler".freeze
30
+ private_constant :NAME
31
+
32
+ attr_reader :cron, :job_class
33
+ private :cron, :job_class
34
+
35
+ def initialize(cron, job_class)
36
+ @cron = cron
37
+ @job_class = job_class
38
+ end
39
+
40
+ def serialize
41
+ {
42
+ name: name,
43
+ cron: cron,
44
+ class: job_class,
45
+ }
46
+ end
47
+
48
+ private
49
+
50
+ def name
51
+ NAME
52
+ end
53
+ end
54
+ private_constant :MissedJobSerializer
55
+ end
56
+ end
@@ -0,0 +1,90 @@
1
+ class SidekiqRobustJob
2
+ module Model
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ validates :job_class, :enqueued_at, :digest, :queue, presence: :true
7
+ validates :uniqueness_strategy, inclusion: {
8
+ in: [
9
+ SidekiqRobustJob::UniquenessStrategy.no_uniqueness,
10
+ SidekiqRobustJob::UniquenessStrategy.until_executing,
11
+ SidekiqRobustJob::UniquenessStrategy.until_executed,
12
+ ].map(&:to_s)
13
+ }, on: :create
14
+
15
+ validates :enqueue_conflict_resolution_strategy, inclusion: {
16
+ in: [
17
+ SidekiqRobustJob::EnqueueConflictResolutionStrategy.do_nothing,
18
+ SidekiqRobustJob::EnqueueConflictResolutionStrategy.drop_self,
19
+ SidekiqRobustJob::EnqueueConflictResolutionStrategy.replace,
20
+ ].map(&:to_s)
21
+ }, on: :create
22
+
23
+ def self.save(job)
24
+ job.save!
25
+ end
26
+ end
27
+
28
+ def unprocessable?
29
+ completed? || dropped?
30
+ end
31
+
32
+ def completed?
33
+ completed_at.present?
34
+ end
35
+
36
+ def dropped?
37
+ dropped_at.present?
38
+ end
39
+
40
+ def started(memory_monitor:, clock: SidekiqRobustJob.configuration.clock)
41
+ self.memory_usage_before_processing_in_megabytes = memory_monitor.mb
42
+ self.attempts += 1
43
+ self.started_at = clock.now
44
+ end
45
+
46
+ def completed(memory_monitor:, clock: SidekiqRobustJob.configuration.clock)
47
+ self.memory_usage_after_processing_in_megabytes = memory_monitor.mb
48
+ self.memory_usage_change_in_megabytes = memory_usage_after_processing_in_megabytes - memory_usage_before_processing_in_megabytes
49
+ self.completed_at = clock.now
50
+ self.dropped_at = nil
51
+ self.dropped_by_job_id = nil
52
+ self.error_type = nil
53
+ self.error_message = nil
54
+ self.failed_at = nil
55
+ end
56
+
57
+ def failed(error, clock: SidekiqRobustJob.configuration.clock)
58
+ self.error_type = error.class
59
+ self.error_message = error.message
60
+ self.failed_at = clock.now
61
+ end
62
+
63
+ def reschedule(job_class_resolver: Object)
64
+ sidekiq_job = job_class_resolver.const_get(job_class)
65
+ interval_in_seconds = sidekiq_job.sidekiq_options.fetch("reschedule_interval_in_seconds", 5)
66
+
67
+ sidekiq_job.original_perform_in(interval_in_seconds.seconds, id)
68
+ end
69
+
70
+ def drop(dropped_by_job_id:, clock: SidekiqRobustJob.configuration.clock)
71
+ self.dropped_at = clock.now
72
+ self.dropped_by_job_id = dropped_by_job_id
73
+ end
74
+
75
+ def execute
76
+ SidekiqRobustJob::DependenciesContainer["uniqueness_strategy_resolver"]
77
+ .resolve(uniqueness_strategy)
78
+ .execute(self)
79
+ end
80
+
81
+ def call(job_class_resolver: Object)
82
+ job_class_resolver.const_get(job_class).new.call(*arguments)
83
+ end
84
+
85
+ def assign_sidekiq_data(execute_at:, sidekiq_jid:)
86
+ self.execute_at = execute_at
87
+ self.sidekiq_jid = sidekiq_jid
88
+ end
89
+ end
90
+ end