sidekiq-unique-jobs 6.0.6 → 6.0.25
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +768 -150
- data/README.md +123 -77
- data/bin/uniquejobs +2 -2
- data/lib/sidekiq-unique-jobs.rb +1 -1
- data/lib/sidekiq_unique_jobs/cli.rb +27 -12
- data/lib/sidekiq_unique_jobs/client/middleware.rb +8 -4
- data/lib/sidekiq_unique_jobs/constants.rb +29 -18
- data/lib/sidekiq_unique_jobs/digests.rb +34 -19
- data/lib/sidekiq_unique_jobs/job.rb +29 -0
- data/lib/sidekiq_unique_jobs/lock/base_lock.rb +14 -18
- data/lib/sidekiq_unique_jobs/lock/until_and_while_executing.rb +20 -4
- data/lib/sidekiq_unique_jobs/lock/until_executed.rb +7 -3
- data/lib/sidekiq_unique_jobs/lock/until_executing.rb +1 -1
- data/lib/sidekiq_unique_jobs/lock/until_expired.rb +1 -0
- data/lib/sidekiq_unique_jobs/lock/while_executing.rb +9 -3
- data/lib/sidekiq_unique_jobs/lock/while_executing_reject.rb +0 -8
- data/lib/sidekiq_unique_jobs/locksmith.rb +59 -32
- data/lib/sidekiq_unique_jobs/logging.rb +14 -0
- data/lib/sidekiq_unique_jobs/middleware.rb +26 -9
- data/lib/sidekiq_unique_jobs/normalizer.rb +1 -1
- data/lib/sidekiq_unique_jobs/on_conflict/raise.rb +1 -1
- data/lib/sidekiq_unique_jobs/on_conflict/reject.rb +3 -3
- data/lib/sidekiq_unique_jobs/on_conflict/replace.rb +7 -2
- data/lib/sidekiq_unique_jobs/on_conflict/strategy.rb +1 -1
- data/lib/sidekiq_unique_jobs/on_conflict.rb +12 -7
- data/lib/sidekiq_unique_jobs/options_with_fallback.rb +8 -8
- data/lib/sidekiq_unique_jobs/scripts.rb +38 -9
- data/lib/sidekiq_unique_jobs/server/middleware.rb +18 -6
- data/lib/sidekiq_unique_jobs/sidekiq_unique_ext.rb +1 -1
- data/lib/sidekiq_unique_jobs/sidekiq_unique_jobs.rb +88 -0
- data/lib/sidekiq_unique_jobs/sidekiq_worker_methods.rb +13 -4
- data/lib/sidekiq_unique_jobs/testing.rb +3 -3
- data/lib/sidekiq_unique_jobs/timeout/calculator.rb +2 -1
- data/lib/sidekiq_unique_jobs/timeout.rb +1 -1
- data/lib/sidekiq_unique_jobs/unique_args.rb +7 -4
- data/lib/sidekiq_unique_jobs/util.rb +8 -4
- data/lib/sidekiq_unique_jobs/version.rb +1 -1
- data/lib/sidekiq_unique_jobs/version_check.rb +95 -0
- data/lib/sidekiq_unique_jobs/web/helpers.rb +7 -6
- data/lib/sidekiq_unique_jobs/web/views/unique_digests.erb +4 -0
- data/lib/sidekiq_unique_jobs/web.rb +19 -12
- data/lib/sidekiq_unique_jobs.rb +33 -107
- data/lib/tasks/changelog.rake +23 -0
- data/redis/convert_legacy_lock.lua +13 -0
- data/redis/delete_by_digest.lua +10 -11
- data/redis/delete_job_by_digest.lua +6 -4
- data/redis/lock.lua +14 -10
- data/redis/unlock.lua +13 -11
- metadata +88 -108
- data/.codeclimate.yml +0 -35
- data/.csslintrc +0 -2
- data/.dockerignore +0 -4
- data/.editorconfig +0 -14
- data/.eslintignore +0 -1
- data/.eslintrc +0 -213
- data/.fasterer.yml +0 -23
- data/.github/ISSUE_TEMPLATE/bug_report.md +0 -31
- data/.github/ISSUE_TEMPLATE/feature_request.md +0 -17
- data/.gitignore +0 -26
- data/.reek.yml +0 -87
- data/.rspec +0 -2
- data/.rubocop.yml +0 -127
- data/.simplecov +0 -20
- data/.travis.yml +0 -49
- data/.yardopts +0 -7
- data/Appraisals +0 -25
- data/CODE_OF_CONDUCT.md +0 -74
- data/Gemfile +0 -24
- data/Guardfile +0 -55
- data/Rakefile +0 -12
- data/_config.yml +0 -1
- data/assets/unique_digests_1.png +0 -0
- data/assets/unique_digests_2.png +0 -0
- data/bin/bench +0 -20
- data/examples/another_unique_job.rb +0 -15
- data/examples/custom_queue_job.rb +0 -12
- data/examples/custom_queue_job_with_filter_method.rb +0 -13
- data/examples/custom_queue_job_with_filter_proc.rb +0 -16
- data/examples/expiring_job.rb +0 -12
- data/examples/inline_worker.rb +0 -12
- data/examples/just_a_worker.rb +0 -13
- data/examples/long_running_job.rb +0 -14
- data/examples/main_job.rb +0 -14
- data/examples/my_job.rb +0 -12
- data/examples/my_unique_job.rb +0 -15
- data/examples/my_unique_job_with_filter_method.rb +0 -21
- data/examples/my_unique_job_with_filter_proc.rb +0 -19
- data/examples/notify_worker.rb +0 -14
- data/examples/plain_class.rb +0 -13
- data/examples/simple_worker.rb +0 -15
- data/examples/spawn_simple_worker.rb +0 -12
- data/examples/test_class.rb +0 -9
- data/examples/unique_across_workers_job.rb +0 -20
- data/examples/unique_job_on_conflict_raise.rb +0 -14
- data/examples/unique_job_on_conflict_reject.rb +0 -14
- data/examples/unique_job_on_conflict_reschedule.rb +0 -14
- data/examples/unique_job_with_conditional_parameter.rb +0 -18
- data/examples/unique_job_with_filter_method.rb +0 -21
- data/examples/unique_job_with_nil_unique_args.rb +0 -20
- data/examples/unique_job_with_no_unique_args_method.rb +0 -16
- data/examples/unique_job_withthout_unique_args_parameter.rb +0 -18
- data/examples/unique_on_all_queues_job.rb +0 -16
- data/examples/until_and_while_executing_job.rb +0 -17
- data/examples/until_executed_2_job.rb +0 -24
- data/examples/until_executed_job.rb +0 -25
- data/examples/until_executing_job.rb +0 -11
- data/examples/until_expired_job.rb +0 -12
- data/examples/until_global_expired_job.rb +0 -12
- data/examples/while_executing_job.rb +0 -15
- data/examples/while_executing_reject_job.rb +0 -14
- data/examples/without_argument_job.rb +0 -13
- data/sidekiq-unique-jobs.gemspec +0 -42
@@ -1,29 +1,35 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
3
|
+
require "pathname"
|
4
|
+
require "digest/sha1"
|
5
|
+
require "concurrent/map"
|
6
6
|
|
7
7
|
module SidekiqUniqueJobs
|
8
8
|
# Interface to dealing with .lua files
|
9
9
|
#
|
10
10
|
# @author Mikael Henriksson <mikael@zoolutions.se>
|
11
11
|
module Scripts
|
12
|
-
LUA_PATHNAME ||= Pathname.new(__FILE__).dirname.join(
|
12
|
+
LUA_PATHNAME ||= Pathname.new(__FILE__).dirname.join("../../redis").freeze
|
13
13
|
SCRIPT_SHAS ||= Concurrent::Map.new
|
14
14
|
|
15
15
|
include SidekiqUniqueJobs::Connection
|
16
16
|
|
17
17
|
module_function
|
18
18
|
|
19
|
+
#
|
19
20
|
# Call a lua script with the provided file_name
|
21
|
+
#
|
22
|
+
# @note this method is recursive if we need to load a lua script
|
23
|
+
# that wasn't previously loaded.
|
24
|
+
#
|
20
25
|
# @param [Symbol] file_name the name of the lua script
|
21
26
|
# @param [Sidekiq::RedisConnection, ConnectionPool] redis_pool the redis connection
|
22
27
|
# @param [Hash] options arguments to pass to the script file
|
23
28
|
# @option options [Array] :keys the array of keys to pass to the script
|
24
29
|
# @option options [Array] :argv the array of arguments to pass to the script
|
25
|
-
#
|
26
|
-
#
|
30
|
+
#
|
31
|
+
# @return value from script
|
32
|
+
#
|
27
33
|
def call(file_name, redis_pool, options = {})
|
28
34
|
execute_script(file_name, redis_pool, options)
|
29
35
|
rescue Redis::CommandError => ex
|
@@ -32,12 +38,17 @@ module SidekiqUniqueJobs
|
|
32
38
|
end
|
33
39
|
end
|
34
40
|
|
41
|
+
#
|
35
42
|
# Execute the script file
|
43
|
+
#
|
36
44
|
# @param [Symbol] file_name the name of the lua script
|
37
45
|
# @param [Sidekiq::RedisConnection, ConnectionPool] redis_pool the redis connection
|
38
46
|
# @param [Hash] options arguments to pass to the script file
|
39
47
|
# @option options [Array] :keys the array of keys to pass to the script
|
40
48
|
# @option options [Array] :argv the array of arguments to pass to the script
|
49
|
+
#
|
50
|
+
# @return value from script (evalsha)
|
51
|
+
#
|
41
52
|
def execute_script(file_name, redis_pool, options = {})
|
42
53
|
redis(redis_pool) do |conn|
|
43
54
|
sha = script_sha(conn, file_name)
|
@@ -45,10 +56,15 @@ module SidekiqUniqueJobs
|
|
45
56
|
end
|
46
57
|
end
|
47
58
|
|
59
|
+
#
|
48
60
|
# Return sha of already loaded lua script or load it and return the sha
|
61
|
+
#
|
49
62
|
# @param [Sidekiq::RedisConnection] conn the redis connection
|
50
63
|
# @param [Symbol] file_name the name of the lua script
|
51
64
|
# @return [String] sha of the script file
|
65
|
+
#
|
66
|
+
# @return [String] the sha of the script
|
67
|
+
#
|
52
68
|
def script_sha(conn, file_name)
|
53
69
|
if (sha = SCRIPT_SHAS.get(file_name))
|
54
70
|
return sha
|
@@ -59,12 +75,17 @@ module SidekiqUniqueJobs
|
|
59
75
|
sha
|
60
76
|
end
|
61
77
|
|
62
|
-
#
|
78
|
+
#
|
79
|
+
# Handle errors to allow retrying errors that need retrying
|
80
|
+
#
|
63
81
|
# @param [Redis::CommandError] ex exception to handle
|
64
82
|
# @param [Symbol] file_name the name of the lua script
|
65
|
-
#
|
83
|
+
#
|
84
|
+
# @return [void]
|
85
|
+
#
|
86
|
+
# @yieldreturn [void] yields back to the caller when NOSCRIPT is raised
|
66
87
|
def handle_error(ex, file_name)
|
67
|
-
if ex.message ==
|
88
|
+
if ex.message == "NOSCRIPT No matching script. Please use EVAL."
|
68
89
|
SCRIPT_SHAS.delete(file_name)
|
69
90
|
return yield if block_given?
|
70
91
|
end
|
@@ -72,16 +93,24 @@ module SidekiqUniqueJobs
|
|
72
93
|
raise ScriptError, file_name: file_name, source_exception: ex
|
73
94
|
end
|
74
95
|
|
96
|
+
#
|
75
97
|
# Reads the lua file from disk
|
98
|
+
#
|
76
99
|
# @param [Symbol] file_name the name of the lua script
|
100
|
+
#
|
77
101
|
# @return [String] the content of the lua file
|
102
|
+
#
|
78
103
|
def script_source(file_name)
|
79
104
|
script_path(file_name).read
|
80
105
|
end
|
81
106
|
|
107
|
+
#
|
82
108
|
# Construct a Pathname to a lua script
|
109
|
+
#
|
83
110
|
# @param [Symbol] file_name the name of the lua script
|
111
|
+
#
|
84
112
|
# @return [Pathname] the full path to the gems lua script
|
113
|
+
#
|
85
114
|
def script_path(file_name)
|
86
115
|
LUA_PATHNAME.join("#{file_name}.lua")
|
87
116
|
end
|
@@ -6,28 +6,40 @@ module SidekiqUniqueJobs
|
|
6
6
|
#
|
7
7
|
# @author Mikael Henriksson <mikael@zoolutions.se>
|
8
8
|
class Middleware
|
9
|
+
include Logging
|
9
10
|
include OptionsWithFallback
|
10
11
|
|
11
|
-
#
|
12
|
-
#
|
12
|
+
#
|
13
|
+
#
|
14
|
+
# Runs the server middleware (used from Sidekiq::Processor#process)
|
15
|
+
#
|
13
16
|
# @param [Sidekiq::Worker] worker_class
|
14
17
|
# @param [Hash] item a sidekiq job hash
|
15
18
|
# @param [String] queue name of the queue
|
19
|
+
#
|
20
|
+
# @see https://github.com/mperham/sidekiq/wiki/Job-Format
|
21
|
+
# @see https://github.com/mperham/sidekiq/wiki/Middleware
|
22
|
+
#
|
16
23
|
# @yield when uniqueness is disabled
|
17
|
-
# @yield when the lock
|
24
|
+
# @yield when the lock is acquired
|
18
25
|
def call(worker_class, item, queue)
|
19
26
|
@worker_class = worker_class
|
20
27
|
@item = item
|
21
28
|
@queue = queue
|
22
29
|
return yield if unique_disabled?
|
23
30
|
|
24
|
-
|
25
|
-
|
31
|
+
SidekiqUniqueJobs::Job.add_uniqueness(item)
|
32
|
+
SidekiqUniqueJobs.with_context(logging_context(self.class, item)) do
|
33
|
+
lock.execute do
|
34
|
+
yield
|
35
|
+
end
|
26
36
|
end
|
27
37
|
end
|
28
38
|
|
29
|
-
|
39
|
+
private
|
30
40
|
|
41
|
+
# The sidekiq job hash
|
42
|
+
# @return [Hash] the Sidekiq job hash
|
31
43
|
attr_reader :item
|
32
44
|
end
|
33
45
|
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Contains configuration and utility methods that belongs top level
|
5
|
+
#
|
6
|
+
# @author Mikael Henriksson <mikael@zoolutions.se>
|
7
|
+
module SidekiqUniqueJobs
|
8
|
+
include SidekiqUniqueJobs::Connection
|
9
|
+
|
10
|
+
module_function
|
11
|
+
|
12
|
+
Config = Concurrent::MutableStruct.new(
|
13
|
+
:default_lock_timeout,
|
14
|
+
:enabled,
|
15
|
+
:unique_prefix,
|
16
|
+
:logger,
|
17
|
+
)
|
18
|
+
|
19
|
+
# The current configuration (See: {.configure} on how to configure)
|
20
|
+
def config
|
21
|
+
# Arguments here need to match the definition of the new class (see above)
|
22
|
+
@config ||= Config.new(
|
23
|
+
0,
|
24
|
+
true,
|
25
|
+
"uniquejobs",
|
26
|
+
Sidekiq.logger,
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
# The current logger
|
31
|
+
# @return [Logger] the configured logger
|
32
|
+
def logger
|
33
|
+
config.logger
|
34
|
+
end
|
35
|
+
|
36
|
+
# :reek:ManualDispatch
|
37
|
+
def with_context(context)
|
38
|
+
if logger.respond_to?(:with_context)
|
39
|
+
logger.with_context(context) { yield }
|
40
|
+
elsif defined?(Sidekiq::Context)
|
41
|
+
Sidekiq::Context.with(context) { yield }
|
42
|
+
elsif defined?(Sidekiq::Logging)
|
43
|
+
Sidekiq::Logging.with_context(context) { yield }
|
44
|
+
else
|
45
|
+
yield
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Set a new logger
|
50
|
+
# @param [Logger] other a new logger
|
51
|
+
def logger=(other)
|
52
|
+
config.logger = other
|
53
|
+
end
|
54
|
+
|
55
|
+
# Change global configuration while yielding
|
56
|
+
# @yield control to the caller
|
57
|
+
def use_config(tmp_config)
|
58
|
+
raise ::ArgumentError, "#{name}.#{__method__} needs a block" unless block_given?
|
59
|
+
|
60
|
+
old_config = config.to_h
|
61
|
+
configure(tmp_config)
|
62
|
+
yield
|
63
|
+
configure(old_config)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Configure the gem
|
67
|
+
#
|
68
|
+
# This is usually called once at startup of an application
|
69
|
+
# @param [Hash] options global gem options
|
70
|
+
# @option options [Integer] :default_lock_timeout (default is 0)
|
71
|
+
# @option options [true,false] :enabled (default is true)
|
72
|
+
# @option options [String] :unique_prefix (default is 'uniquejobs')
|
73
|
+
# @option options [Logger] :logger (default is Sidekiq.logger)
|
74
|
+
# @yield control to the caller when given block
|
75
|
+
def configure(options = {})
|
76
|
+
if block_given?
|
77
|
+
yield config
|
78
|
+
else
|
79
|
+
options.each do |key, val|
|
80
|
+
config.send("#{key}=", val)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def redis_version
|
86
|
+
@redis_version ||= redis { |conn| conn.info("server")["redis_version"] }
|
87
|
+
end
|
88
|
+
end
|
@@ -15,6 +15,7 @@ module SidekiqUniqueJobs
|
|
15
15
|
# @return [Hash] of the worker class sidekiq options
|
16
16
|
def worker_options
|
17
17
|
return {} unless sidekiq_worker_class?
|
18
|
+
|
18
19
|
worker_class.get_sidekiq_options.stringify_keys
|
19
20
|
end
|
20
21
|
|
@@ -28,21 +29,29 @@ module SidekiqUniqueJobs
|
|
28
29
|
# The Sidekiq::Worker implementation
|
29
30
|
# @return [Sidekiq::Worker]
|
30
31
|
def worker_class
|
31
|
-
@_worker_class ||= worker_class_constantize # rubocop:disable Naming/MemoizedInstanceVariableName
|
32
|
+
@_worker_class ||= worker_class_constantize(@worker_class) # rubocop:disable Naming/MemoizedInstanceVariableName
|
32
33
|
end
|
33
34
|
|
34
35
|
# The hook to call after a successful unlock
|
35
36
|
# @return [Proc]
|
36
37
|
def after_unlock_hook
|
37
|
-
|
38
|
+
lambda do
|
39
|
+
if @worker_class.respond_to?(:after_unlock)
|
40
|
+
@worker_class.after_unlock # instance method in sidekiq v6
|
41
|
+
elsif worker_class.respond_to?(:after_unlock)
|
42
|
+
worker_class.after_unlock # class method regardless of sidekiq version
|
43
|
+
end
|
44
|
+
end
|
38
45
|
end
|
39
46
|
|
40
47
|
# Attempt to constantize a string worker_class argument, always
|
41
48
|
# failing back to the original argument when the constant can't be found
|
42
49
|
#
|
43
50
|
# @return [Sidekiq::Worker]
|
44
|
-
def worker_class_constantize(klazz
|
45
|
-
return klazz
|
51
|
+
def worker_class_constantize(klazz)
|
52
|
+
return klazz.class if klazz.is_a?(Sidekiq::Worker) # sidekiq v6.x
|
53
|
+
return klazz unless klazz.is_a?(String)
|
54
|
+
|
46
55
|
Object.const_get(klazz)
|
47
56
|
rescue NameError => ex
|
48
57
|
case ex.message
|
@@ -2,8 +2,8 @@
|
|
2
2
|
|
3
3
|
# :nocov:
|
4
4
|
|
5
|
-
require
|
6
|
-
require
|
5
|
+
require "sidekiq"
|
6
|
+
require "sidekiq/testing"
|
7
7
|
|
8
8
|
module Sidekiq
|
9
9
|
def self.use_options(tmp_config = {})
|
@@ -52,7 +52,7 @@ module Sidekiq
|
|
52
52
|
module Testing
|
53
53
|
def clear_all_ext
|
54
54
|
clear_all_orig
|
55
|
-
SidekiqUniqueJobs::Util.del(
|
55
|
+
SidekiqUniqueJobs::Util.del("*", 1000)
|
56
56
|
end
|
57
57
|
end
|
58
58
|
end
|
@@ -25,6 +25,7 @@ module SidekiqUniqueJobs
|
|
25
25
|
# @return [Integer] the number of seconds until job is scheduled
|
26
26
|
def time_until_scheduled
|
27
27
|
return 0 unless scheduled_at
|
28
|
+
|
28
29
|
scheduled_at.to_i - Time.now.utc.to_i
|
29
30
|
end
|
30
31
|
|
@@ -39,7 +40,7 @@ module SidekiqUniqueJobs
|
|
39
40
|
@lock_expiration ||= begin
|
40
41
|
expiration = item[LOCK_EXPIRATION_KEY]
|
41
42
|
expiration ||= worker_options[LOCK_EXPIRATION_KEY]
|
42
|
-
expiration && expiration + time_until_scheduled
|
43
|
+
expiration && expiration.to_i + time_until_scheduled
|
43
44
|
end
|
44
45
|
end
|
45
46
|
|
@@ -1,7 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "digest"
|
4
|
+
require "openssl"
|
5
|
+
require "sidekiq_unique_jobs/normalizer"
|
5
6
|
|
6
7
|
module SidekiqUniqueJobs
|
7
8
|
# Handles uniqueness of sidekiq arguments
|
@@ -47,7 +48,7 @@ module SidekiqUniqueJobs
|
|
47
48
|
# Creates a namespaced unique digest based on the {#digestable_hash} and the {#unique_prefix}
|
48
49
|
# @return [String] a unique digest
|
49
50
|
def create_digest
|
50
|
-
digest = Digest::MD5.hexdigest(Sidekiq.dump_json(digestable_hash))
|
51
|
+
digest = OpenSSL::Digest::MD5.hexdigest(Sidekiq.dump_json(digestable_hash))
|
51
52
|
"#{unique_prefix}:#{digest}"
|
52
53
|
end
|
53
54
|
|
@@ -60,7 +61,7 @@ module SidekiqUniqueJobs
|
|
60
61
|
# Filter a hash to use for digest
|
61
62
|
# @return [Hash] to use for digest
|
62
63
|
def digestable_hash
|
63
|
-
@item.slice(CLASS_KEY, QUEUE_KEY, UNIQUE_ARGS_KEY).tap do |hash|
|
64
|
+
@item.slice(CLASS_KEY, QUEUE_KEY, UNIQUE_ARGS_KEY, APARTMENT).tap do |hash|
|
64
65
|
hash.delete(QUEUE_KEY) if unique_across_queues?
|
65
66
|
hash.delete(CLASS_KEY) if unique_across_workers?
|
66
67
|
end
|
@@ -70,6 +71,7 @@ module SidekiqUniqueJobs
|
|
70
71
|
# @return [Array] the arguments filters by the {#filtered_args} method if {#unique_args_enabled?}
|
71
72
|
def unique_args(args)
|
72
73
|
return filtered_args(args) if unique_args_enabled?
|
74
|
+
|
73
75
|
args
|
74
76
|
end
|
75
77
|
|
@@ -99,6 +101,7 @@ module SidekiqUniqueJobs
|
|
99
101
|
# @return [Array] args unfiltered when neither of the above
|
100
102
|
def filtered_args(args)
|
101
103
|
return args if args.empty?
|
104
|
+
|
102
105
|
json_args = Normalizer.jsonify(args)
|
103
106
|
|
104
107
|
case unique_args_method
|
@@ -7,11 +7,11 @@ module SidekiqUniqueJobs
|
|
7
7
|
# @author Mikael Henriksson <mikael@zoolutions.se>
|
8
8
|
module Util
|
9
9
|
DEFAULT_COUNT = 1_000
|
10
|
-
SCAN_PATTERN =
|
10
|
+
SCAN_PATTERN = "*"
|
11
11
|
|
12
12
|
include SidekiqUniqueJobs::Logging
|
13
13
|
include SidekiqUniqueJobs::Connection
|
14
|
-
extend self
|
14
|
+
extend self
|
15
15
|
|
16
16
|
# Find unique keys in redis
|
17
17
|
#
|
@@ -20,6 +20,7 @@ module SidekiqUniqueJobs
|
|
20
20
|
# @return [Array<String>] an array with active unique keys
|
21
21
|
def keys(pattern = SCAN_PATTERN, count = DEFAULT_COUNT)
|
22
22
|
return redis(&:keys) if pattern.nil?
|
23
|
+
|
23
24
|
redis { |conn| conn.scan_each(match: prefix(pattern), count: count).to_a }
|
24
25
|
end
|
25
26
|
|
@@ -43,7 +44,8 @@ module SidekiqUniqueJobs
|
|
43
44
|
# @param [Integer] count the maximum number of keys to delete
|
44
45
|
# @return [Integer] the number of keys deleted
|
45
46
|
def del(pattern = SCAN_PATTERN, count = 0)
|
46
|
-
raise ArgumentError,
|
47
|
+
raise ArgumentError, "Please provide a number of keys to delete greater than zero" if count.zero?
|
48
|
+
|
47
49
|
pattern = suffix(pattern)
|
48
50
|
|
49
51
|
log_debug { "Deleting keys by: #{pattern}" }
|
@@ -84,11 +86,13 @@ module SidekiqUniqueJobs
|
|
84
86
|
def prefix(key)
|
85
87
|
return key if unique_prefix.nil?
|
86
88
|
return key if key.start_with?("#{unique_prefix}:")
|
89
|
+
|
87
90
|
"#{unique_prefix}:#{key}"
|
88
91
|
end
|
89
92
|
|
90
93
|
def suffix(key)
|
91
|
-
return "#{key}*" unless key.end_with?(
|
94
|
+
return "#{key}*" unless key.end_with?(":*")
|
95
|
+
|
92
96
|
key
|
93
97
|
end
|
94
98
|
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SidekiqUniqueJobs
|
4
|
+
#
|
5
|
+
# Handles checking if a version is compliant with given constraint
|
6
|
+
#
|
7
|
+
# @author Mikael Henriksson <mikael@zoolutions.se>
|
8
|
+
#
|
9
|
+
class VersionCheck
|
10
|
+
PATTERN = /(?<operator1>[<>=]+)?\s?(?<version1>(\d+.?)+)(\s+&&\s+)?(?<operator2>[<>=]+)?\s?(?<version2>(\d+.?)+)?/m.freeze # rubocop:disable Layout/LineLength
|
11
|
+
|
12
|
+
#
|
13
|
+
# Checks if a version is consrtaint is satisfied
|
14
|
+
#
|
15
|
+
# @example A satisfied constraint
|
16
|
+
# VersionCheck.satisfied?("5.0.0", ">= 4.0.0") #=> true
|
17
|
+
#
|
18
|
+
# @example An unsatisfied constraint
|
19
|
+
# VersionCheck.satisfied?("5.0.0", "<= 4.0.0") #=> false
|
20
|
+
#
|
21
|
+
#
|
22
|
+
# @param [String] version a version string `5.0.0`
|
23
|
+
# @param [String] constraint a version constraint `>= 5.0.0 <= 5.1.1`
|
24
|
+
#
|
25
|
+
# @return [<type>] <description>
|
26
|
+
#
|
27
|
+
def self.satisfied?(version, constraint)
|
28
|
+
new(version, constraint).satisfied?
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# @!attribute [r] version
|
33
|
+
# @return [String] a version string `5.0.0`
|
34
|
+
attr_reader :version
|
35
|
+
#
|
36
|
+
# @!attribute [r] match
|
37
|
+
# @return [String] a version constraint `>= 5.0.0 <= 5.1.1`
|
38
|
+
attr_reader :match
|
39
|
+
|
40
|
+
#
|
41
|
+
# Initialize a new VersionCheck instance
|
42
|
+
#
|
43
|
+
# @param [String] version a version string `5.0.0`
|
44
|
+
# @param [String] constraint a version constraint `>= 5.0.0 <= 5.1.1`
|
45
|
+
#
|
46
|
+
def initialize(version, constraint)
|
47
|
+
@version = Gem::Version.new(version)
|
48
|
+
@match = PATTERN.match(constraint.to_s)
|
49
|
+
|
50
|
+
raise ArgumentError, "A version (eg. 5.0) is required to compare against" unless @version
|
51
|
+
raise ArgumentError, "At least one operator and version is required (eg. >= 5.1)" unless constraint
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# Checks if all constraints were met
|
56
|
+
#
|
57
|
+
#
|
58
|
+
# @return [true,false]
|
59
|
+
#
|
60
|
+
def satisfied?
|
61
|
+
constraints.all? do |expected, operator|
|
62
|
+
compare(expected, operator)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def compare(expected, operator)
|
69
|
+
Gem::Version.new(version).send(operator, Gem::Version.new(expected))
|
70
|
+
end
|
71
|
+
|
72
|
+
def constraints
|
73
|
+
result = { version_one => operator_one }
|
74
|
+
result[version_two] = operator_two if version_two
|
75
|
+
|
76
|
+
result
|
77
|
+
end
|
78
|
+
|
79
|
+
def version_one
|
80
|
+
match[:version1]
|
81
|
+
end
|
82
|
+
|
83
|
+
def operator_one
|
84
|
+
match[:operator1]
|
85
|
+
end
|
86
|
+
|
87
|
+
def version_two
|
88
|
+
match[:version2]
|
89
|
+
end
|
90
|
+
|
91
|
+
def operator_two
|
92
|
+
match[:operator2]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -3,7 +3,7 @@
|
|
3
3
|
module SidekiqUniqueJobs
|
4
4
|
module Web
|
5
5
|
module Helpers
|
6
|
-
VIEW_PATH = File.expand_path(
|
6
|
+
VIEW_PATH = File.expand_path("../web/views", __dir__)
|
7
7
|
|
8
8
|
def unique_template(name)
|
9
9
|
File.open(File.join(VIEW_PATH, "#{name}.erb")).read
|
@@ -19,8 +19,9 @@ module SidekiqUniqueJobs
|
|
19
19
|
|
20
20
|
params.merge(options).map do |key, value|
|
21
21
|
next unless SAFE_CPARAMS.include?(key)
|
22
|
+
|
22
23
|
"#{key}=#{CGI.escape(value.to_s)}"
|
23
|
-
end.compact.join(
|
24
|
+
end.compact.join("&")
|
24
25
|
end
|
25
26
|
|
26
27
|
def redirect_to(subpath)
|
@@ -35,10 +36,10 @@ module SidekiqUniqueJobs
|
|
35
36
|
|
36
37
|
def safe_relative_time(time)
|
37
38
|
time = if time.is_a?(Numeric)
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
39
|
+
Time.at(time)
|
40
|
+
else
|
41
|
+
Time.parse(time)
|
42
|
+
end
|
42
43
|
|
43
44
|
relative_time(time)
|
44
45
|
end
|
@@ -38,5 +38,9 @@
|
|
38
38
|
</tr>
|
39
39
|
<% end %>
|
40
40
|
</table>
|
41
|
+
|
42
|
+
<form action="<%= root_path %>unique_digests/delete_all" method="get">
|
43
|
+
<input class="btn btn-danger btn-xs" type="submit" name="delete_all" value="<%= t('DeleteAll') %>" data-confirm="<%= t('AreYouSure') %>" />
|
44
|
+
</form>
|
41
45
|
</div>
|
42
46
|
<% end %>
|
@@ -1,12 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
begin
|
4
|
-
require
|
5
|
-
|
4
|
+
require "delegate"
|
5
|
+
require "rack"
|
6
|
+
require "sidekiq/web"
|
7
|
+
rescue LoadError
|
6
8
|
# client-only usage
|
7
9
|
end
|
8
10
|
|
9
|
-
require_relative
|
11
|
+
require_relative "web/helpers"
|
10
12
|
|
11
13
|
module SidekiqUniqueJobs
|
12
14
|
# Utility module to help manage unique keys in redis.
|
@@ -19,27 +21,32 @@ module SidekiqUniqueJobs
|
|
19
21
|
include Web::Helpers
|
20
22
|
end
|
21
23
|
|
22
|
-
app.get
|
23
|
-
@filter = params[:filter] ||
|
24
|
-
@filter =
|
24
|
+
app.get "/unique_digests" do
|
25
|
+
@filter = params[:filter] || "*"
|
26
|
+
@filter = "*" if @filter == ""
|
25
27
|
@count = (params[:count] || 100).to_i
|
26
28
|
@current_cursor = params[:cursor]
|
27
29
|
@prev_cursor = params[:prev_cursor]
|
28
30
|
@total_size, @next_cursor, @unique_digests =
|
29
|
-
Digests.page(pattern: @filter, cursor: @current_cursor, page_size: @count)
|
31
|
+
SidekiqUniqueJobs::Digests.page(pattern: @filter, cursor: @current_cursor, page_size: @count)
|
30
32
|
|
31
33
|
erb(unique_template(:unique_digests))
|
32
34
|
end
|
33
35
|
|
34
|
-
app.get
|
36
|
+
app.get "/unique_digests/delete_all" do
|
37
|
+
SidekiqUniqueJobs::Digests.delete_by_pattern("*", count: SidekiqUniqueJobs::Digests.count)
|
38
|
+
redirect_to :unique_digests
|
39
|
+
end
|
40
|
+
|
41
|
+
app.get "/unique_digests/:digest" do
|
35
42
|
@digest = params[:digest]
|
36
|
-
@unique_keys = Util.keys("#{@digest}*", 1000)
|
43
|
+
@unique_keys = SidekiqUniqueJobs::Util.keys("#{@digest}*", 1000)
|
37
44
|
|
38
45
|
erb(unique_template(:unique_digest))
|
39
46
|
end
|
40
47
|
|
41
|
-
app.get
|
42
|
-
Digests.
|
48
|
+
app.get "/unique_digests/:digest/delete" do
|
49
|
+
SidekiqUniqueJobs::Digests.delete_by_digest(params[:digest])
|
43
50
|
redirect_to :unique_digests
|
44
51
|
end
|
45
52
|
end
|
@@ -48,6 +55,6 @@ end
|
|
48
55
|
|
49
56
|
if defined?(Sidekiq::Web)
|
50
57
|
Sidekiq::Web.register SidekiqUniqueJobs::Web
|
51
|
-
Sidekiq::Web.tabs[
|
58
|
+
Sidekiq::Web.tabs["Unique Digests"] = "unique_digests"
|
52
59
|
# Sidekiq::Web.settings.locales << File.join(File.dirname(__FILE__), 'locales')
|
53
60
|
end
|