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,47 @@
1
+ require 'sidekiq_unique_jobs/server/middleware'
2
+
3
+ module SidekiqUniqueJobs
4
+ module Client
5
+ class Middleware
6
+ extend Forwardable
7
+ def_delegators :SidekiqUniqueJobs, :connection, :config
8
+ def_delegators :Sidekiq, :logger
9
+
10
+ include OptionsWithFallback
11
+
12
+ def call(worker_class, item, queue, redis_pool = nil)
13
+ @worker_class = SidekiqUniqueJobs.worker_class_constantize(worker_class)
14
+ @item = item
15
+ @queue = queue
16
+ @redis_pool = redis_pool
17
+ yield if ordinary_or_locked?
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :item, :worker_class, :redis_pool, :queue
23
+
24
+ def ordinary_or_locked?
25
+ unique_disabled? || unlockable? || aquire_lock
26
+ end
27
+
28
+ def unlockable?
29
+ !lockable?
30
+ end
31
+
32
+ def lockable?
33
+ lock.respond_to?(:lock)
34
+ end
35
+
36
+ def aquire_lock
37
+ locked = lock.lock(:client)
38
+ warn_about_duplicate(item) unless locked
39
+ locked
40
+ end
41
+
42
+ def warn_about_duplicate(item)
43
+ logger.warn "payload is not unique #{item}" if log_duplicate_payload?
44
+ end
45
+ end
46
+ end
47
+ end
@@ -1,47 +1,23 @@
1
1
  module SidekiqUniqueJobs
2
2
  class Config < OpenStruct
3
+ TESTING_CONSTANT ||= 'Testing'.freeze
3
4
  CONFIG_ACCESSORS = [
4
5
  :unique_prefix,
5
- :unique_args_enabled,
6
6
  :default_expiration,
7
- :default_unlock_order,
8
- :unique_storage_method
9
- ]
10
-
11
- class << self
12
- CONFIG_ACCESSORS.each do |method|
13
- define_method(method) do
14
- warn("#{method} has been deprecated. See readme for information")
15
- config.send(method)
16
- end
17
-
18
- define_method("#{method}=") do |obj|
19
- warn("#{method} has been deprecated. See readme for information")
20
- config.send("#{method}=", obj)
21
- end
22
- end
23
-
24
- def unique_args_enabled?
25
- warn('unique_args_enabled has been deprecated. See readme for information')
26
- config.unique_args_enabled
27
- end
28
-
29
- def config
30
- SidekiqUniqueJobs.config
31
- end
32
- end
7
+ :default_lock,
8
+ :redis_mode
9
+ ].freeze
33
10
 
34
11
  def inline_testing_enabled?
35
- if testing_enabled? && Sidekiq::Testing.inline?
36
- require 'sidekiq_unique_jobs/inline_testing'
37
- return true
38
- end
12
+ testing_enabled? && Sidekiq::Testing.inline?
13
+ end
39
14
 
40
- false
15
+ def mocking?
16
+ inline_testing_enabled? && redis_test_mode.to_sym == :mock
41
17
  end
42
18
 
43
19
  def testing_enabled?
44
- Sidekiq.const_defined?('Testing') && Sidekiq::Testing.enabled?
20
+ Sidekiq.const_defined?(TESTING_CONSTANT) && Sidekiq::Testing.enabled?
45
21
  end
46
22
 
47
23
  def unique_args_enabled?
@@ -0,0 +1,46 @@
1
+ begin
2
+ require 'active_support/core_ext/hash/keys'
3
+ require 'active_support/core_ext/hash/deep_merge'
4
+ rescue LoadError
5
+ class Hash
6
+ def slice(*keys)
7
+ keys.map! { |key| convert_key(key) } if respond_to?(:convert_key, true)
8
+ keys.each_with_object(self.class.new) { |k, hash| hash[k] = self[k] if key?(k) }
9
+ end unless {}.respond_to?(:slice)
10
+
11
+ def slice!(*keys)
12
+ keys.map! { |key| convert_key(key) } if respond_to?(:convert_key, true)
13
+ omit = slice(*self.keys - keys)
14
+ hash = slice(*keys)
15
+ hash.default = default
16
+ hash.default_proc = default_proc if default_proc
17
+ replace(hash)
18
+ omit
19
+ end unless {}.respond_to?(:slice!)
20
+ end
21
+ end
22
+ begin
23
+ require 'active_support/core_ext/string/inflections'
24
+ rescue LoadError
25
+ class String
26
+ # File activesupport/lib/active_support/inflector/methods.rb, line 178
27
+ def classify
28
+ camelize(singularize(sub(/.*\./, '')))
29
+ end unless ''.respond_to?(:classify)
30
+
31
+ # File activesupport/lib/active_support/inflector/methods.rb, line 67
32
+ def camelize(uppercase_first_letter = true)
33
+ string = self
34
+ if uppercase_first_letter
35
+ string = string.sub(/^[a-z\d]*/) { $&.capitalize }
36
+ else
37
+ string = string.sub(/^(?:(?=\b|[A-Z_])|\w)/) { $&.downcase }
38
+ end
39
+ string.gsub!(%r{(?:_|(\/))([a-z\d]*)}i) do
40
+ "#{Regexp.last_match(1)}#{Regexp.last_match(2).capitalize}"
41
+ end
42
+ string.gsub!(%r{/}, '::')
43
+ string
44
+ end unless ''.respond_to?(:camelize)
45
+ end
46
+ end
@@ -0,0 +1,10 @@
1
+ require 'sidekiq_unique_jobs/lock/time_calculator'
2
+ require 'sidekiq_unique_jobs/lock/until_executed'
3
+ require 'sidekiq_unique_jobs/lock/until_executing'
4
+ require 'sidekiq_unique_jobs/lock/while_executing'
5
+ require 'sidekiq_unique_jobs/lock/until_timeout'
6
+
7
+ module SidekiqUniqueJobs
8
+ module Lock
9
+ end
10
+ end
@@ -0,0 +1,44 @@
1
+ module SidekiqUniqueJobs
2
+ module Lock
3
+ class TimeCalculator
4
+ def self.for_item(item)
5
+ new(item)
6
+ end
7
+
8
+ def initialize(item)
9
+ @item = item
10
+ end
11
+
12
+ def seconds
13
+ time_until_scheduled + unique_expiration
14
+ end
15
+
16
+ def time_until_scheduled
17
+ scheduled = item['at'.freeze]
18
+ return 0 unless scheduled
19
+ (Time.at(scheduled) - Time.now.utc).to_i
20
+ end
21
+
22
+ def unique_expiration
23
+ @unique_expiration ||=
24
+ (
25
+ worker_class_unique_expiration ||
26
+ SidekiqUniqueJobs.config.default_expiration
27
+ ).to_i
28
+ end
29
+
30
+ def worker_class_unique_expiration
31
+ return unless worker_class.respond_to?(:get_sidekiq_options)
32
+ worker_class.get_sidekiq_options['unique_expiration'.freeze]
33
+ end
34
+
35
+ def worker_class
36
+ @worker_class ||= SidekiqUniqueJobs.worker_class_constantize(item['class'.freeze])
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :item
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,56 @@
1
+ module SidekiqUniqueJobs
2
+ module Lock
3
+ class UntilExecuted
4
+ OK ||= 'OK'.freeze
5
+
6
+ extend Forwardable
7
+ def_delegators :Sidekiq, :logger
8
+
9
+ def initialize(item, redis_pool = nil)
10
+ @item = item
11
+ @redis_pool = redis_pool
12
+ end
13
+
14
+ def unlock(scope)
15
+ unless [:server, :api, :test].include?(scope)
16
+ fail ArgumentError, "#{scope} middleware can't #{__method__} #{unique_key}"
17
+ end
18
+ SidekiqUniqueJobs::Unlockable.unlock(unique_key, item['jid'.freeze], redis_pool)
19
+ end
20
+
21
+ # rubocop:disable MethodLength
22
+ def lock(scope)
23
+ if scope.to_sym != :client
24
+ fail ArgumentError, "#{scope} middleware can't #{__method__} #{unique_key}"
25
+ end
26
+
27
+ result = Scripts.call(:aquire_lock, redis_pool,
28
+ keys: [unique_key],
29
+ argv: [item['jid'.freeze], max_lock_time])
30
+ case result
31
+ when 1
32
+ logger.debug { "successfully locked #{unique_key} for #{max_lock_time} seconds" }
33
+ true
34
+ when 0
35
+ logger.debug { "failed to aquire lock for #{unique_key}" }
36
+ false
37
+ else
38
+ fail "#{__method__} returned an unexpected value (#{result})"
39
+ end
40
+ end
41
+ # rubocop:enable MethodLength
42
+
43
+ def unique_key
44
+ @unique_key ||= UniqueArgs.digest(item)
45
+ end
46
+
47
+ def max_lock_time
48
+ @max_lock_time ||= TimeCalculator.for_item(item).seconds
49
+ end
50
+
51
+ private
52
+
53
+ attr_reader :item, :redis_pool
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,6 @@
1
+ module SidekiqUniqueJobs
2
+ module Lock
3
+ class UntilExecuting < UntilExecuted
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,10 @@
1
+ module SidekiqUniqueJobs
2
+ module Lock
3
+ class UntilTimeout < UntilExecuted
4
+ def unlock(scope)
5
+ return true if scope.to_sym == :server
6
+ fail ArgumentError, "#{scope} middleware can't #{__method__} #{unique_key}"
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,31 @@
1
+ module SidekiqUniqueJobs
2
+ module Lock
3
+ class WhileExecuting
4
+ def self.synchronize(item, redis_pool = nil, &block)
5
+ new(item, redis_pool).synchronize(&block)
6
+ end
7
+
8
+ def initialize(item, redis_pool = nil)
9
+ @unique_digest = item['unique_digest'.freeze]
10
+ @run_key = "#{@unique_digest}:run"
11
+ @mutex = Mutex.new
12
+ @redis_pool = redis_pool
13
+ end
14
+
15
+ def synchronize(&_block)
16
+ @mutex.lock
17
+ sleep 0.001 until locked?
18
+
19
+ yield
20
+
21
+ ensure
22
+ SidekiqUniqueJobs.connection(@redis_pool) { |c| c.del @run_key }
23
+ @mutex.unlock
24
+ end
25
+
26
+ def locked?
27
+ Scripts.call(:synchronize, @redis_pool, keys: [@run_key], argv: [Time.now.to_i]) == 1
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,19 +1,35 @@
1
1
  require 'sidekiq'
2
2
 
3
- Sidekiq.configure_server do |config|
4
- config.server_middleware do |chain|
5
- require 'sidekiq_unique_jobs/middleware/server/unique_jobs'
6
- chain.add SidekiqUniqueJobs::Middleware::Server::UniqueJobs
7
- end
8
- config.client_middleware do |chain|
9
- require 'sidekiq_unique_jobs/middleware/client/unique_jobs'
10
- chain.add SidekiqUniqueJobs::Middleware::Client::UniqueJobs
11
- end
12
- end
3
+ module SidekiqUniqueJobs
4
+ module Middleware
5
+ def self.extended(base)
6
+ base.class_eval do
7
+ configure_middleware
8
+ end
9
+ end
10
+
11
+ def configure_middleware
12
+ configure_server_middleware
13
+ configure_client_middleware
14
+ end
15
+
16
+ def configure_server_middleware
17
+ Sidekiq.configure_server do |config|
18
+ config.server_middleware do |chain|
19
+ require 'sidekiq_unique_jobs/server/middleware'
20
+ chain.add SidekiqUniqueJobs::Server::Middleware
21
+ end
22
+ end
23
+ end
13
24
 
14
- Sidekiq.configure_client do |config|
15
- config.client_middleware do |chain|
16
- require 'sidekiq_unique_jobs/middleware/client/unique_jobs'
17
- chain.add SidekiqUniqueJobs::Middleware::Client::UniqueJobs
25
+ def configure_client_middleware
26
+ Sidekiq.configure_client do |config|
27
+ config.client_middleware do |chain|
28
+ require 'sidekiq_unique_jobs/client/middleware'
29
+ chain.add SidekiqUniqueJobs::Client::Middleware
30
+ end
31
+ end
32
+ end
18
33
  end
19
34
  end
35
+ SidekiqUniqueJobs.send(:extend, SidekiqUniqueJobs::Middleware)
@@ -0,0 +1,7 @@
1
+ module SidekiqUniqueJobs
2
+ module Normalizer
3
+ def self.jsonify(args)
4
+ JSON.parse(args.to_json)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,36 @@
1
+ module SidekiqUniqueJobs
2
+ module OptionsWithFallback
3
+ UNIQUE_KEY ||= 'unique'.freeze
4
+ UNIQUE_LOCK_KEY ||= 'unique_lock'.freeze
5
+ LOG_DUPLICATE_KEY ||= 'log_duplicate_payload'.freeze
6
+
7
+ def unique_enabled?
8
+ options[UNIQUE_KEY] || item[UNIQUE_KEY]
9
+ end
10
+
11
+ def unique_disabled?
12
+ !unique_enabled?
13
+ end
14
+
15
+ def log_duplicate_payload?
16
+ options[LOG_DUPLICATE_KEY] || item[LOG_DUPLICATE_KEY]
17
+ end
18
+
19
+ def lock
20
+ @lock = lock_class.new(item)
21
+ end
22
+
23
+ def lock_class
24
+ "SidekiqUniqueJobs::Lock::#{unique_lock.to_s.classify}".constantize
25
+ end
26
+
27
+ def unique_lock
28
+ options[UNIQUE_LOCK_KEY] || item[UNIQUE_LOCK_KEY] || SidekiqUniqueJobs.default_lock
29
+ end
30
+
31
+ def options
32
+ @options ||= worker_class.get_sidekiq_options if worker_class.respond_to?(:get_sidekiq_options)
33
+ @options ||= {}
34
+ end
35
+ end
36
+ end
@@ -0,0 +1 @@
1
+ SidekiqUniqueJobs::RunLockFailed = Class.new(StandardError)
@@ -0,0 +1,50 @@
1
+ require 'pathname'
2
+ require 'digest/sha1'
3
+
4
+ module SidekiqUniqueJobs
5
+ ScriptError = Class.new(StandardError)
6
+
7
+ module Scripts
8
+ extend Forwardable
9
+ LUA_PATHNAME ||= Pathname.new(__FILE__).dirname.join('../../redis').freeze
10
+ SOURCE_FILES ||= Dir[LUA_PATHNAME.join('**/*.lua')].compact.freeze
11
+ DEFINED_METHODS ||= []
12
+
13
+ module_function
14
+
15
+ def script_shas
16
+ @script_shas ||= {}
17
+ end
18
+
19
+ def logger
20
+ Sidekiq.logger
21
+ end
22
+
23
+ def call(file_name, redis_pool, options = {})
24
+ connection(redis_pool) do |redis|
25
+ script_shas[file_name] ||= redis.script(:load, script_source(file_name))
26
+ redis.evalsha(script_shas[file_name], options)
27
+ end
28
+ rescue Redis::CommandError => ex
29
+ raise ScriptError,
30
+ "#{file_name}.lua\n\n" +
31
+ ex.message + "\n\n" +
32
+ script_source(file_name) +
33
+ ex.backtrace.join("\n")
34
+ end
35
+
36
+ def connection(redis_pool, &_block)
37
+ SidekiqUniqueJobs.connection(redis_pool) do |conn|
38
+ yield conn
39
+ end
40
+ end
41
+
42
+ def script_source(file_name)
43
+ script_path(file_name).read
44
+ end
45
+
46
+ def script_path(file_name)
47
+ LUA_PATHNAME.join("#{file_name}.lua")
48
+ end
49
+ end
50
+ end