sidekiq-unique-jobs 6.0.24 → 7.0.4

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 (122) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +707 -25
  3. data/README.md +516 -105
  4. data/lib/sidekiq_unique_jobs.rb +48 -7
  5. data/lib/sidekiq_unique_jobs/batch_delete.rb +123 -0
  6. data/lib/sidekiq_unique_jobs/changelog.rb +78 -0
  7. data/lib/sidekiq_unique_jobs/cli.rb +34 -31
  8. data/lib/sidekiq_unique_jobs/config.rb +263 -0
  9. data/lib/sidekiq_unique_jobs/connection.rb +6 -5
  10. data/lib/sidekiq_unique_jobs/constants.rb +46 -24
  11. data/lib/sidekiq_unique_jobs/core_ext.rb +80 -0
  12. data/lib/sidekiq_unique_jobs/digests.rb +71 -100
  13. data/lib/sidekiq_unique_jobs/exceptions.rb +78 -12
  14. data/lib/sidekiq_unique_jobs/job.rb +41 -12
  15. data/lib/sidekiq_unique_jobs/json.rb +40 -0
  16. data/lib/sidekiq_unique_jobs/key.rb +93 -0
  17. data/lib/sidekiq_unique_jobs/lock.rb +325 -0
  18. data/lib/sidekiq_unique_jobs/lock/base_lock.rb +66 -50
  19. data/lib/sidekiq_unique_jobs/lock/client_validator.rb +28 -0
  20. data/lib/sidekiq_unique_jobs/lock/server_validator.rb +27 -0
  21. data/lib/sidekiq_unique_jobs/lock/until_and_while_executing.rb +7 -10
  22. data/lib/sidekiq_unique_jobs/lock/until_executed.rb +6 -6
  23. data/lib/sidekiq_unique_jobs/lock/until_executing.rb +1 -1
  24. data/lib/sidekiq_unique_jobs/lock/until_expired.rb +4 -21
  25. data/lib/sidekiq_unique_jobs/lock/validator.rb +96 -0
  26. data/lib/sidekiq_unique_jobs/lock/while_executing.rb +13 -9
  27. data/lib/sidekiq_unique_jobs/lock/while_executing_reject.rb +3 -3
  28. data/lib/sidekiq_unique_jobs/lock_args.rb +123 -0
  29. data/lib/sidekiq_unique_jobs/lock_config.rb +122 -0
  30. data/lib/sidekiq_unique_jobs/lock_digest.rb +79 -0
  31. data/lib/sidekiq_unique_jobs/lock_info.rb +68 -0
  32. data/lib/sidekiq_unique_jobs/lock_timeout.rb +62 -0
  33. data/lib/sidekiq_unique_jobs/lock_ttl.rb +77 -0
  34. data/lib/sidekiq_unique_jobs/locksmith.rb +261 -101
  35. data/lib/sidekiq_unique_jobs/logging.rb +149 -23
  36. data/lib/sidekiq_unique_jobs/logging/middleware_context.rb +44 -0
  37. data/lib/sidekiq_unique_jobs/lua/delete.lua +51 -0
  38. data/lib/sidekiq_unique_jobs/lua/delete_by_digest.lua +42 -0
  39. data/lib/sidekiq_unique_jobs/lua/delete_job_by_digest.lua +38 -0
  40. data/lib/sidekiq_unique_jobs/lua/find_digest_in_queues.lua +26 -0
  41. data/lib/sidekiq_unique_jobs/lua/lock.lua +93 -0
  42. data/lib/sidekiq_unique_jobs/lua/locked.lua +35 -0
  43. data/lib/sidekiq_unique_jobs/lua/queue.lua +87 -0
  44. data/lib/sidekiq_unique_jobs/lua/reap_orphans.lua +94 -0
  45. data/lib/sidekiq_unique_jobs/lua/shared/_common.lua +40 -0
  46. data/lib/sidekiq_unique_jobs/lua/shared/_current_time.lua +8 -0
  47. data/lib/sidekiq_unique_jobs/lua/shared/_delete_from_queue.lua +22 -0
  48. data/lib/sidekiq_unique_jobs/lua/shared/_delete_from_sorted_set.lua +18 -0
  49. data/lib/sidekiq_unique_jobs/lua/shared/_find_digest_in_process_set.lua +53 -0
  50. data/lib/sidekiq_unique_jobs/lua/shared/_find_digest_in_queues.lua +43 -0
  51. data/lib/sidekiq_unique_jobs/lua/shared/_find_digest_in_sorted_set.lua +24 -0
  52. data/lib/sidekiq_unique_jobs/lua/shared/_hgetall.lua +13 -0
  53. data/lib/sidekiq_unique_jobs/lua/shared/_upgrades.lua +3 -0
  54. data/lib/sidekiq_unique_jobs/lua/unlock.lua +95 -0
  55. data/lib/sidekiq_unique_jobs/lua/update_version.lua +40 -0
  56. data/lib/sidekiq_unique_jobs/lua/upgrade.lua +68 -0
  57. data/lib/sidekiq_unique_jobs/middleware.rb +29 -31
  58. data/lib/sidekiq_unique_jobs/middleware/client.rb +42 -0
  59. data/lib/sidekiq_unique_jobs/middleware/server.rb +27 -0
  60. data/lib/sidekiq_unique_jobs/normalizer.rb +4 -4
  61. data/lib/sidekiq_unique_jobs/on_conflict.rb +23 -10
  62. data/lib/sidekiq_unique_jobs/on_conflict/log.rb +9 -5
  63. data/lib/sidekiq_unique_jobs/on_conflict/null_strategy.rb +1 -1
  64. data/lib/sidekiq_unique_jobs/on_conflict/raise.rb +1 -1
  65. data/lib/sidekiq_unique_jobs/on_conflict/reject.rb +61 -15
  66. data/lib/sidekiq_unique_jobs/on_conflict/replace.rb +54 -14
  67. data/lib/sidekiq_unique_jobs/on_conflict/reschedule.rb +12 -5
  68. data/lib/sidekiq_unique_jobs/on_conflict/strategy.rb +25 -6
  69. data/lib/sidekiq_unique_jobs/options_with_fallback.rb +41 -27
  70. data/lib/sidekiq_unique_jobs/orphans/lua_reaper.rb +29 -0
  71. data/lib/sidekiq_unique_jobs/orphans/manager.rb +212 -0
  72. data/lib/sidekiq_unique_jobs/orphans/null_reaper.rb +24 -0
  73. data/lib/sidekiq_unique_jobs/orphans/observer.rb +42 -0
  74. data/lib/sidekiq_unique_jobs/orphans/reaper.rb +114 -0
  75. data/lib/sidekiq_unique_jobs/orphans/ruby_reaper.rb +201 -0
  76. data/lib/sidekiq_unique_jobs/redis.rb +11 -0
  77. data/lib/sidekiq_unique_jobs/redis/entity.rb +106 -0
  78. data/lib/sidekiq_unique_jobs/redis/hash.rb +56 -0
  79. data/lib/sidekiq_unique_jobs/redis/list.rb +32 -0
  80. data/lib/sidekiq_unique_jobs/redis/set.rb +32 -0
  81. data/lib/sidekiq_unique_jobs/redis/sorted_set.rb +86 -0
  82. data/lib/sidekiq_unique_jobs/redis/string.rb +49 -0
  83. data/lib/sidekiq_unique_jobs/rspec/matchers.rb +26 -0
  84. data/lib/sidekiq_unique_jobs/rspec/matchers/have_valid_sidekiq_options.rb +51 -0
  85. data/lib/sidekiq_unique_jobs/script.rb +15 -0
  86. data/lib/sidekiq_unique_jobs/script/caller.rb +125 -0
  87. data/lib/sidekiq_unique_jobs/server.rb +48 -0
  88. data/lib/sidekiq_unique_jobs/sidekiq_unique_ext.rb +92 -65
  89. data/lib/sidekiq_unique_jobs/sidekiq_unique_jobs.rb +185 -34
  90. data/lib/sidekiq_unique_jobs/sidekiq_worker_methods.rb +11 -5
  91. data/lib/sidekiq_unique_jobs/testing.rb +62 -21
  92. data/lib/sidekiq_unique_jobs/timer_task.rb +78 -0
  93. data/lib/sidekiq_unique_jobs/timing.rb +58 -0
  94. data/lib/sidekiq_unique_jobs/unlockable.rb +20 -4
  95. data/lib/sidekiq_unique_jobs/update_version.rb +25 -0
  96. data/lib/sidekiq_unique_jobs/upgrade_locks.rb +155 -0
  97. data/lib/sidekiq_unique_jobs/version.rb +3 -1
  98. data/lib/sidekiq_unique_jobs/version_check.rb +23 -4
  99. data/lib/sidekiq_unique_jobs/web.rb +50 -27
  100. data/lib/sidekiq_unique_jobs/web/helpers.rb +125 -10
  101. data/lib/sidekiq_unique_jobs/web/views/changelogs.erb +54 -0
  102. data/lib/sidekiq_unique_jobs/web/views/lock.erb +108 -0
  103. data/lib/sidekiq_unique_jobs/web/views/locks.erb +52 -0
  104. data/lib/tasks/changelog.rake +5 -5
  105. metadata +117 -177
  106. data/lib/sidekiq_unique_jobs/client/middleware.rb +0 -56
  107. data/lib/sidekiq_unique_jobs/scripts.rb +0 -118
  108. data/lib/sidekiq_unique_jobs/server/middleware.rb +0 -46
  109. data/lib/sidekiq_unique_jobs/timeout.rb +0 -8
  110. data/lib/sidekiq_unique_jobs/timeout/calculator.rb +0 -63
  111. data/lib/sidekiq_unique_jobs/unique_args.rb +0 -150
  112. data/lib/sidekiq_unique_jobs/util.rb +0 -103
  113. data/lib/sidekiq_unique_jobs/web/views/unique_digest.erb +0 -28
  114. data/lib/sidekiq_unique_jobs/web/views/unique_digests.erb +0 -46
  115. data/redis/acquire_lock.lua +0 -21
  116. data/redis/convert_legacy_lock.lua +0 -13
  117. data/redis/delete.lua +0 -14
  118. data/redis/delete_by_digest.lua +0 -23
  119. data/redis/delete_job_by_digest.lua +0 -60
  120. data/redis/lock.lua +0 -62
  121. data/redis/release_stale_locks.lua +0 -90
  122. data/redis/unlock.lua +0 -35
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SidekiqUniqueJobs
4
+ # Calculates timeout and expiration
5
+ #
6
+ # @author Mikael Henriksson <mikael@mhenrixon.com>
7
+ class LockTimeout
8
+ # includes "SidekiqUniqueJobs::SidekiqWorkerMethods"
9
+ # @!parse include SidekiqUniqueJobs::SidekiqWorkerMethods
10
+ include SidekiqUniqueJobs::SidekiqWorkerMethods
11
+
12
+ #
13
+ # Calculates the timeout for a Sidekiq job
14
+ #
15
+ # @param [Hash] item sidekiq job hash
16
+ #
17
+ # @return [Integer] timeout in seconds
18
+ #
19
+ def self.calculate(item)
20
+ new(item).calculate
21
+ end
22
+
23
+ # @!attribute [r] item
24
+ # @return [Hash] the Sidekiq job hash
25
+ attr_reader :item
26
+
27
+ # @param [Hash] item the Sidekiq job hash
28
+ # @option item [Integer, nil] :lock_ttl the configured lock expiration
29
+ # @option item [Integer, nil] :lock_timeout the configured lock timeout
30
+ # @option item [String] :class the class of the sidekiq worker
31
+ # @option item [Float] :at the unix time the job is scheduled at
32
+ def initialize(item)
33
+ @item = item
34
+ @worker_class = item[CLASS]
35
+ end
36
+
37
+ #
38
+ # Finds a lock timeout in either of
39
+ # default worker options, {default_lock_timeout} or provided worker_options
40
+ #
41
+ #
42
+ # @return [Integer, nil]
43
+ #
44
+ def calculate
45
+ timeout = default_worker_options[LOCK_TIMEOUT]
46
+ timeout = default_lock_timeout if default_lock_timeout
47
+ timeout = worker_options[LOCK_TIMEOUT] if worker_options.key?(LOCK_TIMEOUT)
48
+ timeout
49
+ end
50
+
51
+ #
52
+ # The configured default_lock_timeout
53
+ # @see SidekiqUniqueJobs::Config#lock_timeout
54
+ #
55
+ #
56
+ # @return [Integer, nil]
57
+ #
58
+ def default_lock_timeout
59
+ SidekiqUniqueJobs.config.lock_timeout
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SidekiqUniqueJobs
4
+ # Calculates timeout and expiration
5
+ #
6
+ # @author Mikael Henriksson <mikael@mhenrixon.com>
7
+ class LockTTL
8
+ # includes "SidekiqUniqueJobs::SidekiqWorkerMethods"
9
+ # @!parse include SidekiqUniqueJobs::SidekiqWorkerMethods
10
+ include SidekiqUniqueJobs::SidekiqWorkerMethods
11
+
12
+ #
13
+ # Computes lock ttl from job arguments, sidekiq_options.
14
+ # Falls back to {SidekiqUniqueJobs::Config#lock_ttl}
15
+ #
16
+ # @note this method takes into consideration the time
17
+ # until a job is scheduled
18
+ #
19
+ #
20
+ # @return [Integer] the number of seconds to live
21
+ #
22
+ def self.calculate(item)
23
+ new(item).calculate
24
+ end
25
+
26
+ # @!attribute [r] item
27
+ # @return [Hash] the Sidekiq job hash
28
+ attr_reader :item
29
+
30
+ # @param [Hash] item the Sidekiq job hash
31
+ # @option item [Integer, nil] :lock_ttl the configured lock expiration
32
+ # @option item [Integer, nil] :lock_timeout the configured lock timeout
33
+ # @option item [String] :class the class of the sidekiq worker
34
+ # @option item [Float] :at the unix time the job is scheduled at
35
+ def initialize(item)
36
+ @item = item
37
+ @worker_class = item[CLASS]
38
+ end
39
+
40
+ #
41
+ # Calculates the time until the job is scheduled starting from now
42
+ #
43
+ #
44
+ # @return [Integer] the number of seconds until job is scheduled
45
+ #
46
+ def time_until_scheduled
47
+ return 0 unless scheduled_at
48
+
49
+ scheduled_at.to_i - Time.now.utc.to_i
50
+ end
51
+
52
+ # The time a job is scheduled
53
+ # @return [Float] the exact unix time the job is scheduled at
54
+ def scheduled_at
55
+ @scheduled_at ||= item[AT]
56
+ end
57
+
58
+ #
59
+ # Computes lock ttl from job arguments, sidekiq_options.
60
+ # Falls back to {SidekiqUniqueJobs::Config#lock_ttl}
61
+ #
62
+ # @note this method takes into consideration the time
63
+ # until a job is scheduled
64
+ #
65
+ #
66
+ # @return [Integer] the number of seconds to live
67
+ #
68
+ def calculate
69
+ ttl = item[LOCK_TTL]
70
+ ttl ||= worker_options[LOCK_TTL]
71
+ ttl ||= item[LOCK_EXPIRATION] # TODO: Deprecate at some point
72
+ ttl ||= worker_options[LOCK_EXPIRATION] # TODO: Deprecate at some point
73
+ ttl ||= SidekiqUniqueJobs.config.lock_ttl
74
+ ttl && ttl.to_i + time_until_scheduled
75
+ end
76
+ end
77
+ end
@@ -3,64 +3,97 @@
3
3
  module SidekiqUniqueJobs
4
4
  # Lock manager class that handles all the various locks
5
5
  #
6
- # @author Mikael Henriksson <mikael@zoolutions.se>
7
- # rubocop:disable Metrics/ClassLength
8
- class Locksmith
6
+ # @author Mikael Henriksson <mikael@mhenrixon.com>
7
+ class Locksmith # rubocop:disable Metrics/ClassLength
8
+ # includes "SidekiqUniqueJobs::Connection"
9
+ # @!parse include SidekiqUniqueJobs::Connection
9
10
  include SidekiqUniqueJobs::Connection
10
11
 
12
+ # includes "SidekiqUniqueJobs::Logging"
13
+ # @!parse include SidekiqUniqueJobs::Logging
14
+ include SidekiqUniqueJobs::Logging
15
+
16
+ # includes "SidekiqUniqueJobs::Timing"
17
+ # @!parse include SidekiqUniqueJobs::Timing
18
+ include SidekiqUniqueJobs::Timing
19
+
20
+ # includes "SidekiqUniqueJobs::Script::Caller"
21
+ # @!parse include SidekiqUniqueJobs::Script::Caller
22
+ include SidekiqUniqueJobs::Script::Caller
23
+
24
+ # includes "SidekiqUniqueJobs::JSON"
25
+ # @!parse include SidekiqUniqueJobs::JSON
26
+ include SidekiqUniqueJobs::JSON
27
+
28
+ #
29
+ # @return [Float] used to take into consideration the inaccuracy of redis timestamps
30
+ CLOCK_DRIFT_FACTOR = 0.01
31
+
32
+ #
33
+ # @!attribute [r] key
34
+ # @return [Key] the key used for locking
35
+ attr_reader :key
36
+ #
37
+ # @!attribute [r] job_id
38
+ # @return [String] a sidekiq JID
39
+ attr_reader :job_id
40
+ #
41
+ # @!attribute [r] config
42
+ # @return [LockConfig] the configuration for this lock
43
+ attr_reader :config
44
+ #
45
+ # @!attribute [r] item
46
+ # @return [Hash] a sidekiq job hash
47
+ attr_reader :item
48
+
49
+ #
50
+ # Initialize a new Locksmith instance
51
+ #
11
52
  # @param [Hash] item a Sidekiq job hash
12
- # @option item [Integer] :lock_expiration the configured expiration
53
+ # @option item [Integer] :lock_ttl the configured expiration
13
54
  # @option item [String] :jid the sidekiq job id
14
- # @option item [String] :unique_digest the unique digest (See: {UniqueArgs#unique_digest})
55
+ # @option item [String] :unique_digest the unique digest (See: {LockDigest#lock_digest})
15
56
  # @param [Sidekiq::RedisConnection, ConnectionPool] redis_pool the redis connection
57
+ #
16
58
  def initialize(item, redis_pool = nil)
17
- # @concurrency = 1 # removed in a0cff5bc42edbe7190d6ede7e7f845074d2d7af6
18
- @ttl = item[LOCK_EXPIRATION_KEY] || item[LOCK_TTL_KEY]
19
- @jid = item[JID_KEY]
20
- @unique_digest = item[UNIQUE_DIGEST_KEY] || item[LOCK_DIGEST_KEY]
21
- @lock_type = item[LOCK_KEY] || item[UNIQUE_KEY]
22
- @lock_type &&= @lock_type.to_sym
23
- @redis_pool = redis_pool
59
+ @item = item
60
+ @key = Key.new(item[LOCK_DIGEST] || item[UNIQUE_DIGEST]) # fallback until can be removed
61
+ @job_id = item[JID]
62
+ @config = LockConfig.new(item)
63
+ @redis_pool = redis_pool
24
64
  end
25
65
 
26
66
  #
27
- # Deletes the lock unless it has a ttl set
67
+ # Deletes the lock unless it has a pttl set
28
68
  #
29
69
  #
30
70
  def delete
31
- return if ttl
71
+ return if config.pttl.positive?
32
72
 
33
73
  delete!
34
74
  end
35
75
 
36
- # Deletes the lock regardless of if it has a ttl set
76
+ #
77
+ # Deletes the lock regardless of if it has a pttl set
78
+ #
37
79
  def delete!
38
- Scripts.call(
39
- :delete,
40
- redis_pool,
41
- keys: [exists_key, grabbed_key, available_key, version_key, UNIQUE_SET, unique_digest],
42
- )
80
+ call_script(:delete, key.to_a, [job_id, config.pttl, config.type, config.limit]).positive?
43
81
  end
44
82
 
45
83
  #
46
- # Create a lock for the item
84
+ # Create a lock for the Sidekiq job
47
85
  #
48
- # @param [Integer] timeout the number of seconds to wait for a lock.
86
+ # @return [String] the Sidekiq job_id that was locked/queued
49
87
  #
50
- # @return [String] the Sidekiq job_id (jid)
51
- #
52
- #
53
- def lock(timeout = nil, &block)
54
- Scripts.call(:lock, redis_pool,
55
- keys: [exists_key, grabbed_key, available_key, UNIQUE_SET, unique_digest],
56
- argv: [jid, ttl, lock_type])
88
+ def lock(&block)
89
+ redis(redis_pool) do |conn|
90
+ return lock_async(conn, &block) if block
57
91
 
58
- grab_token(timeout) do |token|
59
- touch_grabbed_token(token)
60
- return_token_or_block_value(token, &block)
92
+ lock_sync(conn) do
93
+ return job_id
94
+ end
61
95
  end
62
96
  end
63
- alias wait lock
64
97
 
65
98
  #
66
99
  # Removes the lock keys from Redis if locked by the provided jid/token
@@ -68,122 +101,249 @@ module SidekiqUniqueJobs
68
101
  # @return [false] unless locked?
69
102
  # @return [String] Sidekiq job_id (jid) if successful
70
103
  #
71
- def unlock(token = nil)
72
- token ||= jid
73
- return false unless locked?(token)
104
+ def unlock(conn = nil)
105
+ return false unless locked?(conn)
74
106
 
75
- unlock!(token)
107
+ unlock!(conn)
76
108
  end
77
109
 
78
110
  #
79
111
  # Removes the lock keys from Redis
80
112
  #
81
- # @param [String] token the token to unlock (defaults to jid)
82
- #
83
113
  # @return [false] unless locked?
84
114
  # @return [String] Sidekiq job_id (jid) if successful
85
115
  #
86
- def unlock!(token = nil)
87
- token ||= jid
116
+ def unlock!(conn = nil)
117
+ call_script(:unlock, key.to_a, argv, conn)
118
+ end
88
119
 
89
- Scripts.call(
90
- :unlock,
91
- redis_pool,
92
- keys: [exists_key, grabbed_key, available_key, version_key, UNIQUE_SET, unique_digest],
93
- argv: [token, ttl, lock_type],
94
- )
120
+ # Checks if this instance is considered locked
121
+ #
122
+ # @return [true, false] true when the :LOCKED hash contains the job_id
123
+ #
124
+ def locked?(conn = nil)
125
+ return taken?(conn) if conn
126
+
127
+ redis { |rcon| taken?(rcon) }
95
128
  end
96
129
 
97
130
  #
98
- # @param [String] token the unique token to check for a lock.
99
- # nil will default to the jid provided in the initializer
100
- # @return [true, false]
131
+ # Nicely formatted string with information about self
101
132
  #
102
- # Checks if this instance is considered locked
103
133
  #
104
- # @param [<type>] token <description>
134
+ # @return [String]
135
+ #
136
+ def to_s
137
+ "Locksmith##{object_id}(digest=#{key} job_id=#{job_id}, locked=#{locked?})"
138
+ end
139
+
105
140
  #
106
- # @return [<type>] <description>
141
+ # @see to_s
107
142
  #
108
- def locked?(token = nil)
109
- token ||= jid
143
+ def inspect
144
+ to_s
145
+ end
110
146
 
111
- convert_legacy_lock(token)
112
- redis(redis_pool) { |conn| conn.hexists(grabbed_key, token) }
147
+ #
148
+ # Compare this locksmith with another
149
+ #
150
+ # @param [Locksmith] other the locksmith to compare with
151
+ #
152
+ # @return [true, false]
153
+ #
154
+ def ==(other)
155
+ key == other.key && job_id == other.job_id
113
156
  end
114
157
 
115
158
  private
116
159
 
117
- attr_reader :unique_digest, :ttl, :jid, :redis_pool, :lock_type
160
+ attr_reader :redis_pool
118
161
 
119
- def convert_legacy_lock(token)
120
- Scripts.call(
121
- :convert_legacy_lock,
122
- redis_pool,
123
- keys: [grabbed_key, unique_digest],
124
- argv: [token, current_time.to_f],
125
- )
162
+ def argv
163
+ [job_id, config.pttl, config.type, config.limit]
126
164
  end
127
165
 
128
- def grab_token(timeout = nil)
129
- redis(redis_pool) do |conn|
130
- if timeout.nil? || timeout.positive?
131
- # passing timeout 0 to blpop causes it to block
132
- _key, token = conn.blpop(available_key, timeout || 0)
133
- else
134
- token = conn.lpop(available_key)
135
- end
166
+ #
167
+ # Used for runtime locks that need automatic unlock after yielding
168
+ #
169
+ # @param [Redis] conn a redis connection
170
+ #
171
+ # @return [nil] when lock was not possible
172
+ # @return [Object] whatever the block returns when lock was acquired
173
+ #
174
+ # @yieldparam [String] job_id a Sidekiq JID
175
+ #
176
+ def lock_async(conn)
177
+ return yield job_id if locked?(conn)
136
178
 
137
- return yield jid if token
179
+ enqueue(conn) do
180
+ primed_async(conn) do
181
+ locked_token = call_script(:lock, key.to_a, argv, conn)
182
+ return yield if locked_token == job_id
183
+ end
138
184
  end
185
+ ensure
186
+ unlock!(conn)
139
187
  end
140
188
 
141
- def touch_grabbed_token(token)
142
- redis(redis_pool) do |conn|
143
- conn.hset(grabbed_key, token, current_time.to_f)
144
- conn.expire(grabbed_key, ttl) if ttl && lock_type == :until_expired
189
+ #
190
+ # Pops an enqueued token
191
+ # @note Used for runtime locks to avoid problems with blocking commands
192
+ # in current thread
193
+ #
194
+ # @param [Redis] conn a redis connection
195
+ #
196
+ # @return [nil] when lock was not possible
197
+ # @return [Object] whatever the block returns when lock was acquired
198
+ #
199
+ def primed_async(conn)
200
+ return yield if Concurrent::Promises
201
+ .future(conn) { |red_con| pop_queued(red_con) }
202
+ .value(drift(config.ttl))
203
+
204
+ warn_about_timeout
205
+ end
206
+
207
+ #
208
+ # Used for non-runtime locks (no block was given)
209
+ #
210
+ # @param [Redis] conn a redis connection
211
+ #
212
+ # @return [nil] when lock was not possible
213
+ # @return [Object] whatever the block returns when lock was acquired
214
+ #
215
+ # @yieldparam [String] job_id a Sidekiq JID
216
+ #
217
+ def lock_sync(conn)
218
+ return yield if locked?(conn)
219
+
220
+ enqueue(conn) do
221
+ primed_sync(conn) do
222
+ locked_token = call_script(:lock, key.to_a, argv, conn)
223
+ return yield if locked_token
224
+ end
145
225
  end
146
226
  end
147
227
 
148
- def return_token_or_block_value(token)
149
- return token unless block_given?
228
+ #
229
+ # Pops an enqueued token
230
+ # @note Used for non-runtime locks
231
+ #
232
+ # @param [Redis] conn a redis connection
233
+ #
234
+ # @return [nil] when lock was not possible
235
+ # @return [Object] whatever the block returns when lock was acquired
236
+ #
237
+ def primed_sync(conn)
238
+ return yield if pop_queued(conn)
239
+
240
+ warn_about_timeout
241
+ end
150
242
 
151
- # The reason for begin is to only signal when we have a block
152
- begin
153
- yield token
154
- ensure
155
- unlock(token)
243
+ #
244
+ # Does the actual popping of the enqueued token
245
+ #
246
+ # @param [Redis] conn a redis connection
247
+ #
248
+ # @return [String] a previously enqueued token (now taken off the queue)
249
+ #
250
+ def pop_queued(conn)
251
+ if config.wait_for_lock?
252
+ brpoplpush(conn)
253
+ else
254
+ rpoplpush(conn)
156
255
  end
157
256
  end
158
257
 
159
- def available_key
160
- @available_key ||= namespaced_key("AVAILABLE")
258
+ #
259
+ # @api private
260
+ #
261
+ def brpoplpush(conn)
262
+ # passing timeout 0 to brpoplpush causes it to block indefinitely
263
+ conn.brpoplpush(key.queued, key.primed, timeout: config.timeout || 0)
161
264
  end
162
265
 
163
- def exists_key
164
- @exists_key ||= namespaced_key("EXISTS")
266
+ #
267
+ # @api private
268
+ #
269
+ def rpoplpush(conn)
270
+ conn.rpoplpush(key.queued, key.primed)
165
271
  end
166
272
 
167
- def grabbed_key
168
- @grabbed_key ||= namespaced_key("GRABBED")
273
+ #
274
+ # Prepares all the various lock data
275
+ #
276
+ # @param [Redis] conn a redis connection
277
+ #
278
+ # @return [nil] when redis was already prepared for this lock
279
+ # @return [yield<String>] when successfully enqueued
280
+ #
281
+ def enqueue(conn)
282
+ queued_token, elapsed = timed do
283
+ call_script(:queue, key.to_a, argv, conn)
284
+ end
285
+
286
+ validity = config.pttl - elapsed - drift(config.pttl)
287
+
288
+ return unless queued_token && (validity >= 0 || config.pttl.zero?)
289
+
290
+ write_lock_info(conn)
291
+ yield
169
292
  end
170
293
 
171
- def version_key
172
- @version_key ||= namespaced_key("VERSION")
294
+ #
295
+ # Writes lock information to redis.
296
+ # The lock information contains information about worker, queue, limit etc.
297
+ #
298
+ #
299
+ # @return [void]
300
+ #
301
+ def write_lock_info(conn)
302
+ return unless config.lock_info
303
+
304
+ conn.set(key.info, lock_info)
173
305
  end
174
306
 
175
- def namespaced_key(variable)
176
- "#{unique_digest}:#{variable}"
307
+ #
308
+ # Used to combat redis imprecision with ttl/pttl
309
+ #
310
+ # @param [Integer] val the value to compute drift for
311
+ #
312
+ # @return [Integer] a computed drift value
313
+ #
314
+ def drift(val)
315
+ # Add 2 milliseconds to the drift to account for Redis expires
316
+ # precision, which is 1 millisecond, plus 1 millisecond min drift
317
+ # for small TTLs.
318
+ (val.to_i * CLOCK_DRIFT_FACTOR).to_i + 2
319
+ end
320
+
321
+ #
322
+ # Checks if the lock has been taken
323
+ #
324
+ # @param [Redis] conn a redis connection
325
+ #
326
+ # @return [true, false]
327
+ #
328
+ def taken?(conn)
329
+ conn.hexists(key.locked, job_id)
177
330
  end
178
331
 
179
- def current_time
180
- seconds, microseconds_with_frac = redis_time
181
- Time.at(seconds, microseconds_with_frac)
332
+ def warn_about_timeout
333
+ log_warn("Timed out after #{config.timeout}s while waiting for primed token (digest: #{key}, job_id: #{job_id})")
182
334
  end
183
335
 
184
- def redis_time
185
- redis(&:time)
336
+ def lock_info
337
+ @lock_info ||= dump_json(
338
+ WORKER => item[CLASS],
339
+ QUEUE => item[QUEUE],
340
+ LIMIT => item[LOCK_LIMIT],
341
+ TIMEOUT => item[LOCK_TIMEOUT],
342
+ TTL => item[LOCK_TTL],
343
+ LOCK => config.type,
344
+ LOCK_ARGS => item[LOCK_ARGS],
345
+ TIME => now_f,
346
+ )
186
347
  end
187
348
  end
188
- # rubocop:enable Metrics/ClassLength
189
349
  end