sidekiq-unique-jobs 3.0.14 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sidekiq-unique-jobs might be problematic. Click here for more details.

Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +14 -0
  3. data/.gitignore +2 -0
  4. data/.rubocop.yml +8 -0
  5. data/.simplecov +12 -0
  6. data/.travis.yml +16 -8
  7. data/Appraisals +10 -10
  8. data/CHANGELOG.md +11 -0
  9. data/Gemfile +11 -1
  10. data/README.md +58 -4
  11. data/Rakefile +2 -1
  12. data/circle.yml +36 -0
  13. data/gemfiles/sidekiq_2.17.gemfile +8 -1
  14. data/gemfiles/sidekiq_3.0.gemfile +8 -1
  15. data/gemfiles/sidekiq_3.1.gemfile +8 -1
  16. data/gemfiles/sidekiq_3.2.gemfile +8 -1
  17. data/gemfiles/sidekiq_3.3.gemfile +8 -1
  18. data/gemfiles/sidekiq_develop.gemfile +8 -1
  19. data/lib/sidekiq-unique-jobs.rb +44 -9
  20. data/lib/sidekiq_unique_jobs/client/middleware.rb +47 -0
  21. data/lib/sidekiq_unique_jobs/config.rb +9 -33
  22. data/lib/sidekiq_unique_jobs/core_ext.rb +46 -0
  23. data/lib/sidekiq_unique_jobs/lock.rb +10 -0
  24. data/lib/sidekiq_unique_jobs/lock/time_calculator.rb +44 -0
  25. data/lib/sidekiq_unique_jobs/lock/until_executed.rb +56 -0
  26. data/lib/sidekiq_unique_jobs/lock/until_executing.rb +6 -0
  27. data/lib/sidekiq_unique_jobs/lock/until_timeout.rb +10 -0
  28. data/lib/sidekiq_unique_jobs/lock/while_executing.rb +31 -0
  29. data/lib/sidekiq_unique_jobs/middleware.rb +30 -14
  30. data/lib/sidekiq_unique_jobs/normalizer.rb +7 -0
  31. data/lib/sidekiq_unique_jobs/options_with_fallback.rb +36 -0
  32. data/lib/sidekiq_unique_jobs/run_lock_failed.rb +1 -0
  33. data/lib/sidekiq_unique_jobs/scripts.rb +50 -0
  34. data/lib/sidekiq_unique_jobs/server/middleware.rb +73 -0
  35. data/lib/sidekiq_unique_jobs/sidekiq_unique_ext.rb +71 -9
  36. data/lib/sidekiq_unique_jobs/testing.rb +34 -0
  37. data/lib/sidekiq_unique_jobs/testing/sidekiq_overrides.rb +63 -0
  38. data/lib/sidekiq_unique_jobs/unique_args.rb +132 -0
  39. data/lib/sidekiq_unique_jobs/unlockable.rb +26 -0
  40. data/lib/sidekiq_unique_jobs/version.rb +1 -1
  41. data/redis/aquire_lock.lua +9 -0
  42. data/redis/release_lock.lua +14 -0
  43. data/redis/synchronize.lua +15 -0
  44. data/sidekiq-unique-jobs.gemspec +2 -4
  45. data/spec/lib/sidekiq_unique_jobs/client/middleware_spec.rb +195 -0
  46. data/spec/lib/sidekiq_unique_jobs/core_ext_spec.rb +25 -0
  47. data/spec/lib/sidekiq_unique_jobs/lock/time_calculator_spec.rb +81 -0
  48. data/spec/lib/sidekiq_unique_jobs/lock/while_executing_spec.rb +48 -0
  49. data/spec/lib/sidekiq_unique_jobs/normalizer_spec.rb +21 -0
  50. data/spec/lib/sidekiq_unique_jobs/scripts_spec.rb +74 -0
  51. data/spec/lib/sidekiq_unique_jobs/server/middleware_spec.rb +100 -0
  52. data/spec/lib/{sidekiq_testing_enabled_spec.rb → sidekiq_unique_jobs/sidekiq_testing_enabled_spec.rb} +29 -68
  53. data/spec/lib/sidekiq_unique_jobs/sidekiq_unique_ext_spec.rb +79 -0
  54. data/spec/lib/sidekiq_unique_jobs/sidekiq_unique_jobs_spec.rb +36 -0
  55. data/spec/lib/sidekiq_unique_jobs/unique_args_spec.rb +106 -0
  56. data/spec/spec_helper.rb +40 -10
  57. data/spec/support/matchers/redis_matchers.rb +19 -0
  58. data/spec/support/ruby_meta.rb +10 -0
  59. data/spec/support/sidekiq_meta.rb +11 -2
  60. data/spec/support/unique_macros.rb +52 -0
  61. data/spec/workers/after_unlock_worker.rb +13 -0
  62. data/spec/{support → workers}/after_yield_worker.rb +6 -2
  63. data/spec/{support → workers}/another_unique_worker.rb +1 -1
  64. data/spec/workers/before_yield_worker.rb +9 -0
  65. data/spec/workers/expiring_worker.rb +4 -0
  66. data/spec/workers/inline_expiration_worker.rb +8 -0
  67. data/spec/workers/inline_unlock_order_worker.rb +8 -0
  68. data/spec/workers/inline_worker.rb +8 -0
  69. data/spec/workers/just_a_worker.rb +8 -0
  70. data/spec/workers/main_job.rb +8 -0
  71. data/spec/workers/my_unique_worker.rb +8 -0
  72. data/spec/{support → workers}/my_worker.rb +0 -0
  73. data/spec/workers/plain_class.rb +4 -0
  74. data/spec/workers/queue_worker.rb +6 -0
  75. data/spec/workers/queue_worker_with_filter_method.rb +7 -0
  76. data/spec/workers/queue_worker_with_filter_proc.rb +11 -0
  77. data/spec/workers/run_lock_with_retries_worker.rb +12 -0
  78. data/spec/workers/run_lock_worker.rb +7 -0
  79. data/spec/workers/test_class.rb +4 -0
  80. data/spec/workers/unique_job_with_filter_method.rb +18 -0
  81. data/spec/workers/unique_on_all_queues_worker.rb +13 -0
  82. data/spec/{support → workers}/unique_worker.rb +1 -1
  83. data/spec/workers/while_executing_worker.rb +13 -0
  84. metadata +65 -39
  85. data/lib/sidekiq_unique_jobs/connectors.rb +0 -16
  86. data/lib/sidekiq_unique_jobs/connectors/redis_pool.rb +0 -11
  87. data/lib/sidekiq_unique_jobs/connectors/sidekiq_redis.rb +0 -9
  88. data/lib/sidekiq_unique_jobs/connectors/testing.rb +0 -11
  89. data/lib/sidekiq_unique_jobs/inline_testing.rb +0 -12
  90. data/lib/sidekiq_unique_jobs/middleware/client/strategies/testing_inline.rb +0 -25
  91. data/lib/sidekiq_unique_jobs/middleware/client/strategies/unique.rb +0 -105
  92. data/lib/sidekiq_unique_jobs/middleware/client/unique_jobs.rb +0 -43
  93. data/lib/sidekiq_unique_jobs/middleware/server/unique_jobs.rb +0 -69
  94. data/lib/sidekiq_unique_jobs/payload_helper.rb +0 -42
  95. data/lib/sidekiq_unique_jobs/sidekiq_test_overrides.rb +0 -101
  96. data/spec/lib/client_spec.rb +0 -193
  97. data/spec/lib/middleware/server/unique_jobs_spec.rb +0 -112
  98. data/spec/lib/sidekiq_unique_ext_spec.rb +0 -70
  99. data/spec/lib/unlock_order_spec.rb +0 -64
@@ -0,0 +1,73 @@
1
+ require 'digest'
2
+ require 'forwardable'
3
+
4
+ module SidekiqUniqueJobs
5
+ module Server
6
+ class Middleware
7
+ extend Forwardable
8
+ def_delegators :Sidekiq, :logger
9
+ def_instance_delegator :@worker, :class, :worker_class
10
+
11
+ include OptionsWithFallback
12
+
13
+ def call(worker, item, queue, redis_pool = nil, &blk)
14
+ @worker = worker
15
+ @redis_pool = redis_pool
16
+ @queue = queue
17
+ @item = item
18
+
19
+ send(unique_lock, &blk)
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :redis_pool, :worker, :item, :worker_class
25
+
26
+ def until_executing
27
+ unlock
28
+ yield
29
+ end
30
+
31
+ def until_executed(&block)
32
+ operative = true
33
+ after_yield_yield(&block)
34
+ rescue Sidekiq::Shutdown
35
+ operative = false
36
+ raise
37
+ ensure
38
+ unlock if operative
39
+ end
40
+
41
+ def after_yield_yield
42
+ yield
43
+ end
44
+
45
+ def while_executing
46
+ lock.synchronize do
47
+ yield
48
+ end
49
+ rescue SidekiqUniqueJobs::RunLockFailed
50
+ return reschedule if reschedule_on_lock_fail
51
+ raise
52
+ end
53
+
54
+ def until_timeout
55
+ yield if block_given?
56
+ end
57
+
58
+ protected
59
+
60
+ def unlock
61
+ after_unlock_hook if lock.unlock(:server)
62
+ end
63
+
64
+ def after_unlock_hook
65
+ worker.after_unlock if worker.respond_to?(:after_unlock)
66
+ end
67
+
68
+ def reschedule
69
+ Sidekiq::Client.new(redis_pool).raw_push([item])
70
+ end
71
+ end
72
+ end
73
+ end
@@ -1,30 +1,81 @@
1
1
  require 'sidekiq/api'
2
2
 
3
3
  module Sidekiq
4
- class Job
4
+ module UnlockMethod
5
+ def unlock(item)
6
+ SidekiqUniqueJobs::Unlockable.unlock(item['unique_digest'.freeze], item['jid'.freeze])
7
+ end
8
+ end
9
+
10
+ class SortedEntry
5
11
  module UniqueExtension
6
12
  def self.included(base)
7
13
  base.class_eval do
14
+ include UnlockMethod
8
15
  alias_method :delete_orig, :delete
9
16
  alias_method :delete, :delete_ext
17
+ alias_method :remove_job_orig, :remove_job
18
+ alias_method :remove_job, :remove_job_ext
10
19
  end
11
20
  end
12
21
 
13
22
  def delete_ext
14
- unlock(payload_hash(item))
15
- delete_orig
23
+ unlock(item) if delete_orig
24
+ end
25
+
26
+ private
27
+
28
+ def remove_job_ext
29
+ remove_job_orig do |message|
30
+ unlock(Sidekiq.load_json(message))
31
+ yield message
32
+ end
33
+ end
34
+ end
35
+
36
+ include UniqueExtension if Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new('3.1')
37
+ end
38
+
39
+ class ScheduledSet
40
+ module UniqueExtension
41
+ def self.included(base)
42
+ base.class_eval do
43
+ include UnlockMethod
44
+ alias_method :delete_orig, :delete
45
+ alias_method :delete, :delete_ext
46
+ end
47
+ end
48
+
49
+ def delete_ext
50
+ unlock(item) if delete_orig
16
51
  end
17
52
 
18
- protected
53
+ def remove_job_ext
54
+ remove_job_orig do |message|
55
+ unlock(Sidekiq.load_json(message))
56
+ yield message
57
+ end
58
+ end
59
+ end
60
+ include UniqueExtension if Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new('3.1')
61
+ end
19
62
 
20
- def payload_hash(item)
21
- SidekiqUniqueJobs::PayloadHelper.get_payload(item['class'], item['queue'], item['args'])
63
+ class Job
64
+ module UniqueExtension
65
+ def self.included(base)
66
+ base.class_eval do
67
+ include UnlockMethod
68
+ alias_method :delete_orig, :delete
69
+ alias_method :delete, :delete_ext
70
+ end
22
71
  end
23
72
 
24
- def unlock(payload_hash)
25
- Sidekiq.redis { |conn| conn.del(payload_hash) }
73
+ def delete_ext
74
+ unlock(item)
75
+ delete_orig
26
76
  end
27
77
  end
78
+
28
79
  include UniqueExtension
29
80
  end
30
81
 
@@ -32,6 +83,7 @@ module Sidekiq
32
83
  module UniqueExtension
33
84
  def self.included(base)
34
85
  base.class_eval do
86
+ include UnlockMethod
35
87
  alias_method :clear_orig, :clear
36
88
  alias_method :clear, :clear_ext
37
89
  end
@@ -50,17 +102,27 @@ module Sidekiq
50
102
  module UniqueExtension
51
103
  def self.included(base)
52
104
  base.class_eval do
105
+ include UnlockMethod
53
106
  if base.method_defined?(:clear)
54
107
  alias_method :clear_orig, :clear
55
108
  alias_method :clear, :clear_ext
56
109
  end
110
+
111
+ if base.method_defined?(:delete_by_value)
112
+ alias_method :delete_by_value_orig, :delete_by_value
113
+ alias_method :delete_by_value, :delete_by_value_ext
114
+ end
57
115
  end
58
116
  end
59
117
 
60
118
  def clear_ext
61
- each(&:delete_ext)
119
+ each(&:delete)
62
120
  clear_orig
63
121
  end
122
+
123
+ def delete_by_value_ext(name, value)
124
+ unlock(JSON.parse(value)) if delete_by_value_orig(name, value)
125
+ end
64
126
  end
65
127
 
66
128
  include UniqueExtension
@@ -0,0 +1,34 @@
1
+ require 'sidekiq_unique_jobs/testing/sidekiq_overrides'
2
+
3
+ module SidekiqUniqueJobs
4
+ module Client
5
+ class Middleware
6
+ alias_method :call_real, :call
7
+ def call(worker_class, item, queue, redis_pool = nil)
8
+ worker_class = SidekiqUniqueJobs.worker_class_constantize(worker_class)
9
+
10
+ if Sidekiq::Testing.inline?
11
+ _server.call(worker_class.new, item, queue, redis_pool) do
12
+ call_real(worker_class, item, queue, redis_pool) do
13
+ yield
14
+ end
15
+ end
16
+ else
17
+ call_real(worker_class, item, queue, redis_pool) do
18
+ yield
19
+ end
20
+ end
21
+ end
22
+
23
+ def _server
24
+ SidekiqUniqueJobs::Server::Middleware.new
25
+ end
26
+ end
27
+ end
28
+
29
+ class Testing
30
+ def mocking!
31
+ require 'sidekiq_unique_jobs/testing/mocking'
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,63 @@
1
+ require 'sidekiq/testing'
2
+
3
+ module Sidekiq
4
+ module Worker
5
+ module ClassMethods
6
+ include SidekiqUniqueJobs::Unlockable
7
+
8
+ # Drain and run all jobs for this worker
9
+ def drain
10
+ while (job = jobs.shift)
11
+ worker = new
12
+ worker.jid = job['jid']
13
+ worker.bid = job['bid'] if worker.respond_to?(:bid=)
14
+ execute_job(worker, job['args'])
15
+ unlock(job['unique_digest'], job['jid']) if Sidekiq::Testing.fake?
16
+ end
17
+ end
18
+
19
+ # Pop out a single job and perform it
20
+ def perform_one
21
+ fail(EmptyQueueError, 'perform_one called with empty job queue') if jobs.empty?
22
+ job = jobs.shift
23
+ worker = new
24
+ worker.jid = job['jid']
25
+ worker.bid = job['bid'] if worker.respond_to?(:bid=)
26
+ execute_job(worker, job['args'])
27
+ unlock(job['unique_digest'], job['jid']) if Sidekiq::Testing.fake?
28
+ end
29
+
30
+ # Clear all jobs for this worker
31
+ def clear
32
+ jobs.each do |job|
33
+ unlock(job['unique_digest'], job['jid']) if Sidekiq::Testing.fake?
34
+ end
35
+ jobs.clear
36
+ end
37
+ end
38
+
39
+ module Overrides
40
+ def self.included(base)
41
+ base.extend Sidekiq::Worker::Overrides::ClassMethods
42
+ base.class_eval do
43
+ class << self
44
+ alias_method :clear_all_orig, :clear_all
45
+ alias_method :clear_all, :clear_all_ext
46
+ end
47
+ end
48
+ end
49
+
50
+ module ClassMethods
51
+ def clear_all_ext
52
+ Sidekiq.redis do |c|
53
+ unique_keys = c.keys("#{SidekiqUniqueJobs.config.unique_prefix}:*")
54
+ c.del(*unique_keys) unless unique_keys.empty?
55
+ end
56
+ clear_all_orig
57
+ end
58
+ end
59
+ end
60
+
61
+ include Overrides
62
+ end
63
+ end
@@ -0,0 +1,132 @@
1
+ require 'sidekiq_unique_jobs/normalizer'
2
+
3
+ module SidekiqUniqueJobs
4
+ # This class exists to be testable and the entire api should be considered private
5
+ # rubocop:disable ClassLength
6
+ class UniqueArgs
7
+ extend Forwardable
8
+ include Normalizer
9
+
10
+ def_delegators :SidekiqUniqueJobs, :config, :worker_class_constantize
11
+ def_delegators :'Sidekiq.logger', :logger, :debug, :warn, :error, :fatal
12
+
13
+ def self.digest(item)
14
+ new(item).unique_digest
15
+ end
16
+
17
+ def initialize(job)
18
+ Sidekiq::Logging.with_context(self.class.name) do
19
+ @item = job
20
+ @worker_class ||= worker_class_constantize(@item['class'.freeze])
21
+ @item['unique_prefix'.freeze] ||= unique_prefix
22
+ @item['unique_args'.freeze] ||= unique_args(@item['args'.freeze])
23
+ @item['unique_digest'.freeze] ||= unique_digest
24
+ end
25
+ end
26
+
27
+ def unique_digest
28
+ @unique_digest ||= begin
29
+ digest = Digest::MD5.hexdigest(Sidekiq.dump_json(digestable_hash))
30
+ digest = "#{unique_prefix}:#{digest}"
31
+ debug { "#{__method__} : #{digestable_hash} into #{digest}" }
32
+ digest
33
+ end
34
+ end
35
+
36
+ def unique_prefix
37
+ return config.unique_prefix unless sidekiq_worker_class?
38
+ @worker_class.get_sidekiq_options['unique_prefix'.freeze] || config.unique_prefix
39
+ end
40
+
41
+ def digestable_hash
42
+ hash = @item.slice('class', 'queue', 'unique_args')
43
+
44
+ if unique_on_all_queues?
45
+ debug { "uniqueness specified across all queues (deleting queue: #{@item['queue']} from hash)" }
46
+ hash.delete('queue')
47
+ end
48
+ hash
49
+ end
50
+
51
+ def unique_args(args)
52
+ if unique_args_enabled?
53
+ filtered_args(args)
54
+ else
55
+ debug { "#{__method__} : unique arguments disabled" }
56
+ args
57
+ end
58
+ rescue NameError
59
+ # fallback to not filtering args when class can't be instantiated
60
+ return args
61
+ end
62
+
63
+ def unique_on_all_queues?
64
+ return unless sidekiq_worker_class?
65
+ return unless unique_args_enabled?
66
+ @worker_class.get_sidekiq_options['unique_on_all_queues'.freeze]
67
+ end
68
+
69
+ def unique_args_enabled?
70
+ unique_args_enabled_in_worker? ||
71
+ config.unique_args_enabled
72
+ end
73
+
74
+ def unique_args_enabled_in_worker?
75
+ return unless sidekiq_worker_class?
76
+ @worker_class.get_sidekiq_options['unique_args_enabled'.freeze] ||
77
+ @worker_class.get_sidekiq_options['unique_args'.freeze]
78
+ end
79
+
80
+ def sidekiq_worker_class?
81
+ if @worker_class.respond_to?(:get_sidekiq_options)
82
+ true
83
+ else
84
+ debug { "#{@worker_class} does not respond to :get_sidekiq_options" }
85
+ nil
86
+ end
87
+ end
88
+
89
+ # Filters unique arguments by proc or symbol
90
+ # returns provided arguments for other configurations
91
+ def filtered_args(args)
92
+ return args if args.empty?
93
+ json_args = Normalizer.jsonify(args)
94
+ debug { "#filtered_args #{args} => #{json_args}" }
95
+
96
+ case unique_args_method
97
+ when Proc
98
+ filter_by_proc(json_args)
99
+ when Symbol
100
+ filter_by_symbol(json_args)
101
+ else
102
+ debug { 'arguments not filtered (the combined arguments count towards uniqueness)' }
103
+ json_args
104
+ end
105
+ end
106
+
107
+ def filter_by_proc(args)
108
+ filter_args = unique_args_method.call(args)
109
+ debug { "#{__method__} : #{args} -> #{filter_args}" }
110
+ filter_args
111
+ end
112
+
113
+ def filter_by_symbol(args)
114
+ unless @worker_class.respond_to?(unique_args_method)
115
+ warn do
116
+ "#{__method__} : #{unique_args_method}) not defined in #{@worker_class} " \
117
+ "returning #{args} unchanged"
118
+ end
119
+ return args
120
+ end
121
+
122
+ filter_args = @worker_class.send(unique_args_method, args)
123
+ debug { "#{__method__} : #{unique_args_method}(#{args}) => #{filter_args}" }
124
+ filter_args
125
+ end
126
+
127
+ def unique_args_method
128
+ @unique_args_method ||=
129
+ @worker_class.get_sidekiq_options['unique_args'.freeze] if sidekiq_worker_class?
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,26 @@
1
+ module SidekiqUniqueJobs
2
+ module Unlockable
3
+ module_function
4
+
5
+ # rubocop:disable MethodLength
6
+ def unlock(unique_key, jid, redis_pool = nil)
7
+ result = Scripts.call(:release_lock, redis_pool,
8
+ keys: [unique_key],
9
+ argv: [jid])
10
+ case result
11
+ when 1
12
+ Sidekiq.logger.debug { "successfully unlocked #{unique_key}" }
13
+ true
14
+ when 0
15
+ Sidekiq.logger.debug { "expiring lock #{unique_key} is not owned by #{jid}" }
16
+ false
17
+ when -1
18
+ Sidekiq.logger.debug { "#{unique_key} is not a known key" }
19
+ false
20
+ else
21
+ fail "#{__method__} returned an unexpected value (#{result})"
22
+ end
23
+ end
24
+ # rubocop:enable MethodLength
25
+ end
26
+ end