sidekiq-robust-job 0.1.0

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