sidekiq-unique-jobs 6.0.0.rc6 → 6.0.0.rc7

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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +6 -7
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
  4. data/.reek.yml +17 -48
  5. data/.rubocop.yml +3 -0
  6. data/.yardopts +7 -0
  7. data/CHANGELOG.md +2 -0
  8. data/README.md +65 -23
  9. data/assets/unique_digests_1.png +0 -0
  10. data/assets/unique_digests_2.png +0 -0
  11. data/examples/another_unique_job.rb +4 -2
  12. data/examples/custom_queue_job_with_filter_method.rb +1 -1
  13. data/examples/custom_queue_job_with_filter_proc.rb +1 -1
  14. data/examples/expiring_job.rb +1 -1
  15. data/examples/inline_worker.rb +1 -1
  16. data/examples/just_a_worker.rb +1 -1
  17. data/examples/long_running_job.rb +4 -2
  18. data/examples/main_job.rb +3 -2
  19. data/examples/my_unique_job.rb +4 -5
  20. data/examples/my_unique_job_with_filter_method.rb +3 -3
  21. data/examples/my_unique_job_with_filter_proc.rb +3 -3
  22. data/examples/notify_worker.rb +2 -2
  23. data/examples/simple_worker.rb +2 -2
  24. data/examples/unique_across_workers_job.rb +1 -1
  25. data/examples/unique_job_on_conflict_raise.rb +14 -0
  26. data/examples/unique_job_on_conflict_reject.rb +14 -0
  27. data/examples/unique_job_on_conflict_reschedule.rb +14 -0
  28. data/examples/unique_job_with_conditional_parameter.rb +3 -3
  29. data/examples/unique_job_with_filter_method.rb +5 -2
  30. data/examples/unique_job_with_nil_unique_args.rb +3 -3
  31. data/examples/unique_job_with_no_unique_args_method.rb +3 -3
  32. data/examples/unique_job_withthout_unique_args_parameter.rb +3 -3
  33. data/examples/unique_on_all_queues_job.rb +1 -1
  34. data/examples/until_and_while_executing_job.rb +4 -1
  35. data/examples/until_executed_2_job.rb +5 -5
  36. data/examples/until_executed_job.rb +5 -5
  37. data/examples/until_executing_job.rb +1 -1
  38. data/examples/until_expired_job.rb +1 -1
  39. data/examples/until_global_expired_job.rb +1 -1
  40. data/examples/while_executing_job.rb +2 -2
  41. data/examples/while_executing_reject_job.rb +2 -2
  42. data/examples/without_argument_job.rb +1 -1
  43. data/lib/sidekiq_unique_jobs.rb +30 -0
  44. data/lib/sidekiq_unique_jobs/client/middleware.rb +12 -1
  45. data/lib/sidekiq_unique_jobs/connection.rb +5 -1
  46. data/lib/sidekiq_unique_jobs/constants.rb +3 -0
  47. data/lib/sidekiq_unique_jobs/digests.rb +111 -0
  48. data/lib/sidekiq_unique_jobs/exceptions.rb +15 -16
  49. data/lib/sidekiq_unique_jobs/lock/base_lock.rb +44 -3
  50. data/lib/sidekiq_unique_jobs/lock/until_and_while_executing.rb +13 -3
  51. data/lib/sidekiq_unique_jobs/lock/until_executed.rb +8 -1
  52. data/lib/sidekiq_unique_jobs/lock/until_executing.rb +8 -1
  53. data/lib/sidekiq_unique_jobs/lock/until_expired.rb +14 -2
  54. data/lib/sidekiq_unique_jobs/lock/while_executing.rb +19 -5
  55. data/lib/sidekiq_unique_jobs/lock/while_executing_reject.rb +16 -63
  56. data/lib/sidekiq_unique_jobs/locksmith.rb +36 -13
  57. data/lib/sidekiq_unique_jobs/logging.rb +24 -1
  58. data/lib/sidekiq_unique_jobs/normalizer.rb +6 -0
  59. data/lib/sidekiq_unique_jobs/on_conflict.rb +24 -0
  60. data/lib/sidekiq_unique_jobs/on_conflict/log.rb +20 -0
  61. data/lib/sidekiq_unique_jobs/on_conflict/null_strategy.rb +16 -0
  62. data/lib/sidekiq_unique_jobs/on_conflict/raise.rb +17 -0
  63. data/lib/sidekiq_unique_jobs/on_conflict/reject.rb +72 -0
  64. data/lib/sidekiq_unique_jobs/on_conflict/reschedule.rb +24 -0
  65. data/lib/sidekiq_unique_jobs/on_conflict/strategy.rb +28 -0
  66. data/lib/sidekiq_unique_jobs/options_with_fallback.rb +19 -4
  67. data/lib/sidekiq_unique_jobs/scripts.rb +31 -0
  68. data/lib/sidekiq_unique_jobs/server/middleware.rb +10 -0
  69. data/lib/sidekiq_unique_jobs/sidekiq_worker_methods.rb +15 -1
  70. data/lib/sidekiq_unique_jobs/timeout/calculator.rb +17 -4
  71. data/lib/sidekiq_unique_jobs/unique_args.rb +47 -5
  72. data/lib/sidekiq_unique_jobs/unlockable.rb +10 -0
  73. data/lib/sidekiq_unique_jobs/util.rb +12 -7
  74. data/lib/sidekiq_unique_jobs/version.rb +1 -1
  75. data/lib/sidekiq_unique_jobs/web.rb +51 -0
  76. data/lib/sidekiq_unique_jobs/web/helpers.rb +37 -0
  77. data/lib/sidekiq_unique_jobs/web/views/unique_digest.erb +28 -0
  78. data/lib/sidekiq_unique_jobs/web/views/unique_digests.erb +42 -0
  79. data/redis/create.lua +4 -2
  80. data/redis/delete.lua +3 -1
  81. data/redis/delete_by_digest.lua +22 -0
  82. data/redis/signal.lua +3 -1
  83. data/sidekiq-unique-jobs.gemspec +2 -0
  84. metadata +49 -3
  85. data/lib/sidekiq_unique_jobs/lock/while_executing_requeue.rb +0 -21
@@ -2,74 +2,27 @@
2
2
 
3
3
  module SidekiqUniqueJobs
4
4
  class Lock
5
+ # Locks jobs while executing
6
+ # Locks from the server process
7
+ # Unlocks after the server is done processing
8
+ #
9
+ # See {#lock} for more information about the client.
10
+ # See {#execute} for more information about the server
11
+ #
12
+ # @author Mikael Henriksson <mikael@zoolutions.se>
5
13
  class WhileExecutingReject < WhileExecuting
14
+ # Executes in the Sidekiq server process
15
+ # @yield to the worker class perform method
6
16
  def execute
7
- return reject unless locksmith.lock(item[LOCK_TIMEOUT_KEY])
17
+ return strategy.call unless locksmith.lock(item[LOCK_TIMEOUT_KEY])
8
18
 
9
- with_cleanup { yield if block_given? }
19
+ with_cleanup { yield }
10
20
  end
11
21
 
12
- # Private below here, keeping public due to testing reasons
13
-
14
- def reject
15
- log_debug { "Rejecting job with jid: #{item[JID_KEY]} already running" }
16
- send_to_deadset
17
- end
18
-
19
- def send_to_deadset
20
- log_info { "Adding dead #{item[CLASS_KEY]} job #{item[JID_KEY]}" }
21
-
22
- if deadset_kill?
23
- deadset_kill
24
- else
25
- push_to_deadset
26
- end
27
- end
28
-
29
- def deadset_kill?
30
- deadset.respond_to?(:kill)
31
- end
32
-
33
- def deadset_kill
34
- if kill_with_options?
35
- kill_job_with_options
36
- else
37
- kill_job_without_options
38
- end
39
- end
40
-
41
- def kill_with_options?
42
- Sidekiq::DeadSet.instance_method(:kill).arity > 1
43
- end
44
-
45
- def kill_job_without_options
46
- deadset.kill(payload)
47
- end
48
-
49
- def kill_job_with_options
50
- deadset.kill(payload, notify_failure: false)
51
- end
52
-
53
- def deadset
54
- @deadset ||= Sidekiq::DeadSet.new
55
- end
56
-
57
- def push_to_deadset
58
- Sidekiq.redis do |conn|
59
- conn.multi do
60
- conn.zadd('dead', current_time, payload)
61
- conn.zremrangebyscore('dead', '-inf', current_time - Sidekiq::DeadSet.timeout)
62
- conn.zremrangebyrank('dead', 0, -Sidekiq::DeadSet.max_jobs)
63
- end
64
- end
65
- end
66
-
67
- def current_time
68
- @current_time ||= Time.now.to_f
69
- end
70
-
71
- def payload
72
- @payload ||= Sidekiq.dump_json(item)
22
+ # Overridden with a forced {OnConflict::Reject} strategy
23
+ # @return [OnConflict::Reject] a reject strategy
24
+ def strategy
25
+ @strategy ||= OnConflict.find_strategy(:reject).new(item)
73
26
  end
74
27
  end
75
28
  end
@@ -1,12 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SidekiqUniqueJobs
4
+ # Lock manager class that handles all the various locks
5
+ #
6
+ # @author Mikael Henriksson <mikael@zoolutions.se>
4
7
  class Locksmith # rubocop:disable ClassLength
5
8
  API_VERSION = '1'
6
9
  EXPIRES_IN = 10
7
10
 
8
11
  include SidekiqUniqueJobs::Connection
9
12
 
13
+ # @param [Hash] item a Sidekiq job hash
14
+ # @option item [Integer] :lock_expiration the configured expiration
15
+ # @option item [String] :jid the sidekiq job id
16
+ # @option item [String] :unique_digest the unique digest (See: {UniqueArgs#unique_digest})
17
+ # @param [Sidekiq::RedisConnection, ConnectionPool] redis_pool the redis connection
10
18
  def initialize(item, redis_pool = nil)
11
19
  @concurrency = 1 # removed in a0cff5bc42edbe7190d6ede7e7f845074d2d7af6
12
20
  @expiration = item[LOCK_EXPIRATION_KEY]
@@ -15,38 +23,51 @@ module SidekiqUniqueJobs
15
23
  @redis_pool = redis_pool
16
24
  end
17
25
 
26
+ # Creates the necessary keys in redis to attempt a lock
27
+ # @return [String] the Sidekiq job_id
18
28
  def create
19
29
  Scripts.call(
20
30
  :create,
21
31
  redis_pool,
22
- keys: [exists_key, grabbed_key, available_key, version_key, unique_digest],
32
+ keys: [exists_key, grabbed_key, available_key, version_key, UNIQUE_SET, unique_digest],
23
33
  argv: [jid, expiration, API_VERSION, concurrency],
24
34
  )
25
35
  end
26
36
 
37
+ # Checks if the exists key is created in redis
38
+ # @return [true, false]
27
39
  def exists?
28
40
  redis(redis_pool) { |conn| conn.exists(exists_key) }
29
41
  end
30
42
 
43
+ # The number of available resourced for this lock
44
+ # @return [Integer] the number of available resources
31
45
  def available_count
32
46
  return concurrency unless exists?
33
47
 
34
48
  redis(redis_pool) { |conn| conn.llen(available_key) }
35
49
  end
36
50
 
51
+ # Deletes the lock unless it has an expiration set
37
52
  def delete
38
- return unless expiration.nil?
53
+ return if expiration
39
54
  delete!
40
55
  end
41
56
 
57
+ # Deletes the lock regardless of if it has an expiration set
42
58
  def delete!
43
59
  Scripts.call(
44
60
  :delete,
45
61
  redis_pool,
46
- keys: [exists_key, grabbed_key, available_key, version_key, unique_digest],
62
+ keys: [exists_key, grabbed_key, available_key, version_key, UNIQUE_SET, unique_digest],
47
63
  )
48
64
  end
49
65
 
66
+ # Create a lock for the item
67
+ # @param [Integer] timeout the number of seconds to wait for a lock.
68
+ # nil means wait indefinitely
69
+ # @yield the block to execute if a lock is successful
70
+ # @return the Sidekiq job_id (jid)
50
71
  def lock(timeout = nil, &block)
51
72
  create
52
73
 
@@ -57,23 +78,34 @@ module SidekiqUniqueJobs
57
78
  end
58
79
  alias wait lock
59
80
 
81
+ # Removes the lock keys from Redis
82
+ # @return [false] unless locked?
83
+ # @return [String] Sidekiq job_id (jid) if successful
60
84
  def unlock
61
85
  return false unless locked?
62
86
  signal(jid)
63
87
  end
64
88
 
89
+ # Removes the lock keys from Redis
90
+ # @param [String] token the unique token to check for a lock.
91
+ # nil will default to the jid provided in the initializer
92
+ # @return [true, false]
65
93
  def locked?(token = nil)
66
94
  token ||= jid
67
95
  redis(redis_pool) { |conn| conn.hexists(grabbed_key, token) }
68
96
  end
69
97
 
98
+ # Signal that the token should be released
99
+ # @param [String] token the unique token to check for a lockk.
100
+ # nil will default to the jid provided in the initializer.
101
+ # @return [Integer] the number of available lock resources
70
102
  def signal(token = nil)
71
103
  token ||= jid
72
104
 
73
105
  Scripts.call(
74
106
  :signal,
75
107
  redis_pool,
76
- keys: [exists_key, grabbed_key, available_key, version_key, unique_digest],
108
+ keys: [exists_key, grabbed_key, available_key, version_key, UNIQUE_SET, unique_digest],
77
109
  argv: [token, expiration],
78
110
  )
79
111
  end
@@ -140,12 +172,3 @@ module SidekiqUniqueJobs
140
172
  end
141
173
  end
142
174
  end
143
-
144
- require 'sidekiq_unique_jobs/lock/base_lock'
145
- require 'sidekiq_unique_jobs/lock/until_executed'
146
- require 'sidekiq_unique_jobs/lock/until_executing'
147
- require 'sidekiq_unique_jobs/lock/until_expired'
148
- require 'sidekiq_unique_jobs/lock/while_executing'
149
- require 'sidekiq_unique_jobs/lock/while_executing_reject'
150
- require 'sidekiq_unique_jobs/lock/while_executing_requeue'
151
- require 'sidekiq_unique_jobs/lock/until_and_while_executing'
@@ -1,28 +1,51 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SidekiqUniqueJobs
4
+ # Utility module for reducing the number of uses of logger.
5
+ #
6
+ # @author Mikael Henriksson <mikael@zoolutions.se>
4
7
  module Logging
5
- # :reek:UtilityFunction { enabled: false }
8
+ # A convenience method for using the configured logger
6
9
  def logger
7
10
  SidekiqUniqueJobs.logger
8
11
  end
9
12
 
13
+ # Logs a message at debug level
14
+ # @param message_or_exception [String, Exception] the message or exception to log
15
+ # @yield the message or exception to use for log message
16
+ # Used for compatibility with logger
10
17
  def log_debug(message_or_exception = nil, &block)
11
18
  logger.debug(message_or_exception, &block)
12
19
  end
13
20
 
21
+ # Logs a message at info level
22
+ # @param message_or_exception [String, Exception] the message or exception to log
23
+ # @yield the message or exception to use for log message
24
+ # Used for compatibility with logger
14
25
  def log_info(message_or_exception = nil, &block)
15
26
  logger.info(message_or_exception, &block)
16
27
  end
17
28
 
29
+ # Logs a message at warn level
30
+ # @param message_or_exception [String, Exception] the message or exception to log
31
+ # @yield the message or exception to use for log message
32
+ # Used for compatibility with logger
18
33
  def log_warn(message_or_exception = nil, &block)
19
34
  logger.warn(message_or_exception, &block)
20
35
  end
21
36
 
37
+ # Logs a message at error level
38
+ # @param message_or_exception [String, Exception] the message or exception to log
39
+ # @yield the message or exception to use for log message
40
+ # Used for compatibility with logger
22
41
  def log_error(message_or_exception = nil, &block)
23
42
  logger.error(message_or_exception, &block)
24
43
  end
25
44
 
45
+ # Logs a message at fatal level
46
+ # @param message_or_exception [String, Exception] the message or exception to log
47
+ # @yield the message or exception to use for log message
48
+ # Used for compatibility with logger
26
49
  def log_fatal(message_or_exception = nil, &block)
27
50
  logger.fatal(message_or_exception, &block)
28
51
  end
@@ -3,7 +3,13 @@
3
3
  require 'json'
4
4
 
5
5
  module SidekiqUniqueJobs
6
+ # Normalizes hashes by dumping them to json and loading them from json
7
+ #
8
+ # @author Mikael Henriksson <mikael@zoolutions.se>
6
9
  module Normalizer
10
+ # Changes hash to a json compatible hash
11
+ # @param [Hash] args
12
+ # @return [Hash] a json compatible hash
7
13
  def self.jsonify(args)
8
14
  Sidekiq.load_json(Sidekiq.dump_json(args))
9
15
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'on_conflict/strategy'
4
+ require_relative 'on_conflict/null_strategy'
5
+ require_relative 'on_conflict/log'
6
+ require_relative 'on_conflict/raise'
7
+ require_relative 'on_conflict/reject'
8
+ require_relative 'on_conflict/reschedule'
9
+
10
+ module SidekiqUniqueJobs
11
+ module OnConflict
12
+ STRATEGIES = {
13
+ log: OnConflict::Log,
14
+ raise: OnConflict::Raise,
15
+ reject: OnConflict::Reject,
16
+ reschedule: OnConflict::Reschedule,
17
+ }.freeze
18
+
19
+ # returns OnConflict::NullStrategy when no other could be found
20
+ def self.find_strategy(strategy)
21
+ STRATEGIES.fetch(strategy.to_s.to_sym) { OnConflict::NullStrategy }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SidekiqUniqueJobs
4
+ module OnConflict
5
+ # Strategy to log information about conflict
6
+ #
7
+ # @author Mikael Henriksson <mikael@zoolutions.se>
8
+ class Log < OnConflict::Strategy
9
+ include SidekiqUniqueJobs::Logging
10
+
11
+ # Logs an informational message about that the job was not unique
12
+ def call
13
+ log_info(
14
+ "skipping job with id (#{item[JID_KEY]}) " \
15
+ "because unique_digest: (#{item[UNIQUE_DIGEST_KEY]}) already exists",
16
+ )
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SidekiqUniqueJobs
4
+ module OnConflict
5
+ # Default conflict strategy class that does nothing
6
+ #
7
+ # @author Mikael Henriksson <mikael@zoolutions.se>
8
+ class NullStrategy < OnConflict::Strategy
9
+ # Do nothing on conflict
10
+ # @return [nil]
11
+ def call
12
+ # NOOP
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SidekiqUniqueJobs
4
+ module OnConflict
5
+ # Strategy to raise an error on conflict
6
+ #
7
+ # @author Mikael Henriksson <mikael@zoolutions.se>
8
+ class Raise < OnConflict::Strategy
9
+ # Raise an error on conflict.
10
+ # This will cause Sidekiq to retry the job
11
+ # @raise [SidekiqUniqueJobs::Conflict]
12
+ def call
13
+ fail SidekiqUniqueJobs::Conflict, item
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SidekiqUniqueJobs
4
+ module OnConflict
5
+ # Strategy to send jobs to dead queue
6
+ #
7
+ # @author Mikael Henriksson <mikael@zoolutions.se>
8
+ class Reject < OnConflict::Strategy
9
+ # Send jobs to dead queue
10
+ def call
11
+ log_debug { "Rejecting job with jid: #{item[JID_KEY]}" }
12
+ send_to_deadset
13
+ end
14
+
15
+ def send_to_deadset
16
+ log_info { "Adding dead #{item[CLASS_KEY]} job #{item[JID_KEY]}" }
17
+
18
+ if deadset_kill?
19
+ deadset_kill
20
+ else
21
+ push_to_deadset
22
+ end
23
+ end
24
+
25
+ def deadset_kill?
26
+ deadset.respond_to?(:kill)
27
+ end
28
+
29
+ def deadset_kill
30
+ if kill_with_options?
31
+ kill_job_with_options
32
+ else
33
+ kill_job_without_options
34
+ end
35
+ end
36
+
37
+ def kill_with_options?
38
+ Sidekiq::DeadSet.instance_method(:kill).arity > 1
39
+ end
40
+
41
+ def kill_job_without_options
42
+ deadset.kill(payload)
43
+ end
44
+
45
+ def kill_job_with_options
46
+ deadset.kill(payload, notify_failure: false)
47
+ end
48
+
49
+ def deadset
50
+ @deadset ||= Sidekiq::DeadSet.new
51
+ end
52
+
53
+ def push_to_deadset
54
+ Sidekiq.redis do |conn|
55
+ conn.multi do
56
+ conn.zadd('dead', current_time, payload)
57
+ conn.zremrangebyscore('dead', '-inf', current_time - Sidekiq::DeadSet.timeout)
58
+ conn.zremrangebyrank('dead', 0, -Sidekiq::DeadSet.max_jobs)
59
+ end
60
+ end
61
+ end
62
+
63
+ def current_time
64
+ @current_time ||= Time.now.to_f
65
+ end
66
+
67
+ def payload
68
+ @payload ||= Sidekiq.dump_json(item)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SidekiqUniqueJobs
4
+ module OnConflict
5
+ # Strategy to reschedule job on conflict
6
+ #
7
+ # @author Mikael Henriksson <mikael@zoolutions.se>
8
+ class Reschedule < OnConflict::Strategy
9
+ include SidekiqUniqueJobs::SidekiqWorkerMethods
10
+
11
+ # @param [Hash] item sidekiq job hash
12
+ def initialize(item)
13
+ super
14
+ @worker_class = item[CLASS_KEY]
15
+ end
16
+
17
+ # Create a new job from the current one.
18
+ # This will mess up sidekiq stats because a new job is created
19
+ def call
20
+ worker_class&.perform_in(5, *item[ARGS_KEY]) if sidekiq_worker_class?
21
+ end
22
+ end
23
+ end
24
+ end