sidekiq-unique-jobs 5.0.11 → 6.0.0.rc1

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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +17 -9
  3. data/.gitignore +1 -3
  4. data/.reek +105 -0
  5. data/.rubocop.yml +36 -1
  6. data/.simplecov +7 -2
  7. data/.travis.yml +11 -10
  8. data/Appraisals +3 -7
  9. data/CHANGELOG.md +17 -0
  10. data/Gemfile +16 -13
  11. data/Guardfile +55 -0
  12. data/README.md +85 -73
  13. data/examples/another_unique_job.rb +13 -0
  14. data/examples/custom_queue_job.rb +12 -0
  15. data/examples/custom_queue_job_with_filter_method.rb +13 -0
  16. data/examples/custom_queue_job_with_filter_proc.rb +16 -0
  17. data/examples/expiring_job.rb +12 -0
  18. data/examples/inline_worker.rb +12 -0
  19. data/examples/just_a_worker.rb +13 -0
  20. data/examples/long_running_job.rb +12 -0
  21. data/examples/main_job.rb +13 -0
  22. data/examples/my_job.rb +12 -0
  23. data/examples/my_unique_job.rb +16 -0
  24. data/examples/my_unique_job_with_filter_method.rb +21 -0
  25. data/examples/my_unique_job_with_filter_proc.rb +19 -0
  26. data/examples/notify_worker.rb +14 -0
  27. data/examples/plain_class.rb +13 -0
  28. data/examples/simple_worker.rb +15 -0
  29. data/examples/spawn_simple_worker.rb +12 -0
  30. data/examples/test_class.rb +9 -0
  31. data/examples/unique_across_workers_job.rb +20 -0
  32. data/examples/unique_job_with_conditional_parameter.rb +18 -0
  33. data/examples/unique_job_with_filter_method.rb +18 -0
  34. data/examples/unique_job_with_nil_unique_args.rb +20 -0
  35. data/examples/unique_job_with_no_unique_args_method.rb +16 -0
  36. data/examples/unique_job_withthout_unique_args_parameter.rb +18 -0
  37. data/examples/unique_on_all_queues_job.rb +16 -0
  38. data/examples/until_and_while_executing_job.rb +13 -0
  39. data/examples/until_executed_2_job.rb +24 -0
  40. data/examples/until_executed_job.rb +25 -0
  41. data/examples/until_executing_job.rb +11 -0
  42. data/examples/until_expired_job.rb +12 -0
  43. data/examples/until_global_expired_job.rb +12 -0
  44. data/examples/while_executing_job.rb +15 -0
  45. data/examples/while_executing_reject_job.rb +14 -0
  46. data/examples/without_argument_job.rb +13 -0
  47. data/lib/sidekiq-unique-jobs.rb +1 -91
  48. data/lib/sidekiq/simulator.rb +15 -16
  49. data/lib/sidekiq_unique_jobs.rb +79 -0
  50. data/lib/sidekiq_unique_jobs/cli.rb +9 -18
  51. data/lib/sidekiq_unique_jobs/client/middleware.rb +16 -18
  52. data/lib/sidekiq_unique_jobs/connection.rb +18 -0
  53. data/lib/sidekiq_unique_jobs/constants.rb +13 -17
  54. data/lib/sidekiq_unique_jobs/core_ext.rb +28 -55
  55. data/lib/sidekiq_unique_jobs/exceptions.rb +30 -0
  56. data/lib/sidekiq_unique_jobs/lock/base_lock.rb +84 -0
  57. data/lib/sidekiq_unique_jobs/lock/until_and_while_executing.rb +11 -6
  58. data/lib/sidekiq_unique_jobs/lock/until_executed.rb +6 -58
  59. data/lib/sidekiq_unique_jobs/lock/until_executing.rb +6 -5
  60. data/lib/sidekiq_unique_jobs/lock/until_expired.rb +17 -0
  61. data/lib/sidekiq_unique_jobs/lock/while_executing.rb +20 -34
  62. data/lib/sidekiq_unique_jobs/lock/while_executing_reject.rb +78 -0
  63. data/lib/sidekiq_unique_jobs/lock/while_executing_requeue.rb +20 -0
  64. data/lib/sidekiq_unique_jobs/locksmith.rb +149 -0
  65. data/lib/sidekiq_unique_jobs/logging.rb +30 -0
  66. data/lib/sidekiq_unique_jobs/options_with_fallback.rb +27 -41
  67. data/lib/sidekiq_unique_jobs/scripts.rb +25 -24
  68. data/lib/sidekiq_unique_jobs/server/middleware.rb +12 -20
  69. data/lib/sidekiq_unique_jobs/sidekiq_unique_ext.rb +5 -5
  70. data/lib/sidekiq_unique_jobs/sidekiq_worker_methods.rb +42 -0
  71. data/lib/sidekiq_unique_jobs/testing.rb +40 -50
  72. data/lib/sidekiq_unique_jobs/timeout.rb +15 -0
  73. data/lib/sidekiq_unique_jobs/timeout/calculator.rb +49 -0
  74. data/lib/sidekiq_unique_jobs/unique_args.rb +33 -77
  75. data/lib/sidekiq_unique_jobs/unlockable.rb +5 -14
  76. data/lib/sidekiq_unique_jobs/util.rb +28 -90
  77. data/lib/sidekiq_unique_jobs/version.rb +1 -1
  78. data/redis/acquire_lock.lua +5 -3
  79. data/redis/create.lua +58 -0
  80. data/redis/delete.lua +11 -0
  81. data/redis/release_stale_locks.lua +90 -0
  82. data/redis/signal.lua +21 -0
  83. data/sidekiq-unique-jobs.gemspec +15 -8
  84. metadata +108 -32
  85. data/lib/sidekiq_unique_jobs/config.rb +0 -17
  86. data/lib/sidekiq_unique_jobs/lock.rb +0 -12
  87. data/lib/sidekiq_unique_jobs/lock/until_timeout.rb +0 -17
  88. data/lib/sidekiq_unique_jobs/run_lock_failed.rb +0 -3
  89. data/lib/sidekiq_unique_jobs/script_mock.rb +0 -66
  90. data/lib/sidekiq_unique_jobs/scripts/acquire_lock.rb +0 -47
  91. data/lib/sidekiq_unique_jobs/scripts/release_lock.rb +0 -49
  92. data/lib/sidekiq_unique_jobs/testing/sidekiq_overrides.rb +0 -50
  93. data/lib/sidekiq_unique_jobs/timeout_calculator.rb +0 -67
  94. data/redis/synchronize.lua +0 -16
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'timeout'
4
+
5
+ module SidekiqUniqueJobs
6
+ module Timeout
7
+ def using_timeout(value)
8
+ ::Timeout.timeout(value) do
9
+ yield
10
+ end
11
+ end
12
+ end
13
+ end
14
+
15
+ require 'sidekiq_unique_jobs/timeout/calculator'
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SidekiqUniqueJobs
4
+ module Timeout
5
+ class Calculator
6
+ include SidekiqUniqueJobs::SidekiqWorkerMethods
7
+ attr_reader :item
8
+
9
+ def initialize(item)
10
+ @item = item
11
+ @worker_class = item[CLASS_KEY]
12
+ end
13
+
14
+ def time_until_scheduled
15
+ return 0 unless scheduled_at
16
+ scheduled_at.to_i - Time.now.utc.to_i
17
+ end
18
+
19
+ def scheduled_at
20
+ @scheduled_at ||= item[AT_KEY]
21
+ end
22
+
23
+ def seconds
24
+ raise NotImplementedError, "##{__method__} needs to be implemented in #{self.class}"
25
+ end
26
+
27
+ def lock_expiration
28
+ @lock_expiration ||= begin
29
+ expiration = item[LOCK_EXPIRATION_KEY]
30
+ expiration ||= worker_options[LOCK_EXPIRATION_KEY]
31
+ expiration && expiration + time_until_scheduled
32
+ end
33
+ end
34
+
35
+ def lock_timeout
36
+ @lock_timeout = begin
37
+ timeout = default_worker_options[LOCK_TIMEOUT_KEY]
38
+ timeout = default_lock_timeout if default_lock_timeout
39
+ timeout = worker_options[LOCK_TIMEOUT_KEY] if worker_options.key?(LOCK_TIMEOUT_KEY)
40
+ timeout
41
+ end
42
+ end
43
+
44
+ def default_lock_timeout
45
+ SidekiqUniqueJobs.config.default_lock_timeout
46
+ end
47
+ end
48
+ end
49
+ end
@@ -5,101 +5,73 @@ require 'sidekiq_unique_jobs/normalizer'
5
5
 
6
6
  module SidekiqUniqueJobs
7
7
  # This class exists to be testable and the entire api should be considered private
8
- # rubocop:disable ClassLength
9
8
  class UniqueArgs
10
9
  CLASS_NAME = 'SidekiqUniqueJobs::UniqueArgs'
11
- extend Forwardable
12
10
 
13
- def_delegators :SidekiqUniqueJobs, :config, :worker_class_constantize
14
- def_delegators :Sidekiq, :logger
11
+ include SidekiqUniqueJobs::Logging
12
+ include SidekiqUniqueJobs::SidekiqWorkerMethods
15
13
 
16
14
  def self.digest(item)
17
15
  new(item).unique_digest
18
16
  end
19
17
 
18
+ attr_reader :item
19
+
20
20
  def initialize(item)
21
- Sidekiq::Logging.with_context(CLASS_NAME) do
22
- @item = item
23
- @worker_class ||= worker_class_constantize(@item[CLASS_KEY])
24
- @item[UNIQUE_PREFIX_KEY] ||= unique_prefix
25
- @item[UNIQUE_ARGS_KEY] = unique_args(@item[ARGS_KEY])
26
- @item[UNIQUE_DIGEST_KEY] = unique_digest
27
- end
21
+ @item = item
22
+ @worker_class = item[CLASS_KEY]
23
+
24
+ add_uniqueness_to_item
25
+ end
26
+
27
+ def add_uniqueness_to_item
28
+ item[UNIQUE_PREFIX_KEY] ||= unique_prefix
29
+ item[UNIQUE_ARGS_KEY] = unique_args(item[ARGS_KEY])
30
+ item[UNIQUE_DIGEST_KEY] = unique_digest
28
31
  end
29
32
 
30
33
  def unique_digest
31
- @unique_digest ||= begin
32
- digest = Digest::MD5.hexdigest(Sidekiq.dump_json(digestable_hash))
33
- digest = "#{unique_prefix}:#{digest}"
34
- logger.debug { "#{__method__} : #{digestable_hash} into #{digest}" }
35
- digest
36
- end
34
+ @unique_digest ||= create_digest
35
+ end
36
+
37
+ def create_digest
38
+ digest = Digest::MD5.hexdigest(Sidekiq.dump_json(digestable_hash))
39
+ "#{unique_prefix}:#{digest}"
37
40
  end
38
41
 
39
42
  def unique_prefix
40
- return config.unique_prefix unless sidekiq_worker_class?
41
- @worker_class.get_sidekiq_options[UNIQUE_PREFIX_KEY] || config.unique_prefix
43
+ worker_options[UNIQUE_PREFIX_KEY] || SidekiqUniqueJobs.config.unique_prefix
42
44
  end
43
45
 
44
46
  def digestable_hash
45
47
  @item.slice(CLASS_KEY, QUEUE_KEY, UNIQUE_ARGS_KEY).tap do |hash|
46
- if unique_on_all_queues?
47
- logger.debug { "#{__method__} deleting queue: #{@item[QUEUE_KEY]}" }
48
- hash.delete(QUEUE_KEY)
49
- end
50
- if unique_across_workers?
51
- logger.debug { "#{__method__} deleting class: #{@item[CLASS_KEY]}" }
52
- hash.delete(CLASS_KEY)
53
- end
48
+ hash.delete(QUEUE_KEY) if unique_on_all_queues?
49
+ hash.delete(CLASS_KEY) if unique_across_workers?
54
50
  end
55
51
  end
56
52
 
57
53
  def unique_args(args)
58
- if unique_args_enabled?
59
- filtered_args(args)
60
- else
61
- logger.debug { "#{__method__} : unique arguments disabled" }
62
- args
63
- end
64
- rescue NameError => ex
65
- logger.error "#{__method__}(#{args}) : failed with (#{ex.message})"
66
- logger.error ex
67
-
68
- raise if config.raise_unique_args_errors
69
-
54
+ return filtered_args(args) if unique_args_enabled?
70
55
  args
71
56
  end
72
57
 
73
58
  def unique_on_all_queues?
74
- return unless sidekiq_worker_class?
75
- @item[UNIQUE_ON_ALL_QUEUES_KEY] || @worker_class.get_sidekiq_options[UNIQUE_ON_ALL_QUEUES_KEY]
59
+ item[UNIQUE_ON_ALL_QUEUES_KEY] || worker_options[UNIQUE_ON_ALL_QUEUES_KEY]
76
60
  end
77
61
 
78
62
  def unique_across_workers?
79
- return unless sidekiq_worker_class?
80
- @item[UNIQUE_ACROSS_WORKERS_KEY] || @worker_class.get_sidekiq_options[UNIQUE_ACROSS_WORKERS_KEY]
63
+ item[UNIQUE_ACROSS_WORKERS_KEY] || worker_options[UNIQUE_ACROSS_WORKERS_KEY]
81
64
  end
82
65
 
83
66
  def unique_args_enabled?
84
- return unless sidekiq_worker_class?
85
67
  unique_args_method # && !unique_args_method.is_a?(Boolean)
86
68
  end
87
69
 
88
- def sidekiq_worker_class?
89
- if @worker_class.respond_to?(:get_sidekiq_options)
90
- true
91
- else
92
- logger.debug { "#{__method__} #{@worker_class} does not respond to :get_sidekiq_options" }
93
- nil
94
- end
95
- end
96
-
97
70
  # Filters unique arguments by proc or symbol
98
71
  # returns provided arguments for other configurations
99
72
  def filtered_args(args)
100
73
  return args if args.empty?
101
74
  json_args = Normalizer.jsonify(args)
102
- logger.debug { "#filtered_args #{args} => #{json_args}" }
103
75
 
104
76
  case unique_args_method
105
77
  when Proc
@@ -107,44 +79,28 @@ module SidekiqUniqueJobs
107
79
  when Symbol
108
80
  filter_by_symbol(json_args)
109
81
  else
110
- logger.debug { "#{__method__} arguments not filtered (using all arguments for uniqueness)" }
82
+ log_debug("#{__method__} arguments not filtered (using all arguments for uniqueness)")
111
83
  json_args
112
84
  end
113
85
  end
114
86
 
115
87
  def filter_by_proc(args)
116
- if unique_args_method.nil?
117
- logger.warn { "#{__method__} : unique_args_method is nil. Returning (#{args})" }
118
- return args
119
- end
120
-
121
- filter_args = unique_args_method.call(args)
122
- logger.debug { "#{__method__} : #{args} -> #{filter_args}" }
123
- filter_args
88
+ unique_args_method.call(args)
124
89
  end
125
90
 
126
91
  def filter_by_symbol(args)
127
- unless @worker_class.respond_to?(unique_args_method)
128
- logger.warn do
129
- "#{__method__} : #{@worker_class} does not respond to #{unique_args_method}). Returning (#{args})"
130
- end
131
- return args
132
- end
92
+ return args unless worker_method_defined?(unique_args_method)
133
93
 
134
- filter_args = @worker_class.send(unique_args_method, args)
135
- logger.debug { "#{__method__} : #{unique_args_method}(#{args}) => #{filter_args}" }
136
- filter_args
94
+ worker_class.send(unique_args_method, args)
137
95
  rescue ArgumentError => ex
138
- logger.fatal "#{__method__} : #{@worker_class}'s #{unique_args_method} needs at least one argument"
139
- logger.fatal ex
96
+ log_fatal(ex)
140
97
  args
141
98
  end
142
99
 
143
100
  def unique_args_method
144
- @unique_args_method ||= @worker_class.get_sidekiq_options[UNIQUE_ARGS_KEY] if sidekiq_worker_class?
145
- @unique_args_method ||= :unique_args if @worker_class.respond_to?(:unique_args)
101
+ @unique_args_method ||= worker_options[UNIQUE_ARGS_KEY]
102
+ @unique_args_method ||= :unique_args if worker_method_defined?(:unique_args)
146
103
  @unique_args_method ||= Sidekiq.default_worker_options.stringify_keys[UNIQUE_ARGS_KEY]
147
104
  end
148
105
  end
149
- # rubocop:enable ClassLength
150
106
  end
@@ -5,22 +5,13 @@ module SidekiqUniqueJobs
5
5
  module_function
6
6
 
7
7
  def unlock(item)
8
- return unless item[UNIQUE_DIGEST_KEY]
9
- unlock_by_key(item[UNIQUE_DIGEST_KEY], item[JID_KEY])
8
+ SidekiqUniqueJobs::UniqueArgs.digest(item)
9
+ SidekiqUniqueJobs::Locksmith.new(item).unlock
10
10
  end
11
11
 
12
- def unlock_by_key(unique_key, jid, redis_pool = nil)
13
- lock_released = Scripts::ReleaseLock.execute(redis_pool, unique_key, jid)
14
- ensure_job_id_removed(jid)
15
- lock_released
16
- end
17
-
18
- def ensure_job_id_removed(jid)
19
- Sidekiq.redis { |conn| conn.hdel(SidekiqUniqueJobs::HASH_KEY, jid) }
20
- end
21
-
22
- def logger
23
- SidekiqUniqueJobs.logger
12
+ def delete(item)
13
+ SidekiqUniqueJobs::UniqueArgs.digest(item)
14
+ SidekiqUniqueJobs::Locksmith.new(item).delete!
24
15
  end
25
16
  end
26
17
  end
@@ -1,85 +1,46 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SidekiqUniqueJobs
4
- module Util # rubocop:disable Metrics/ModuleLength
4
+ module Util
5
5
  COUNT = 'COUNT'
6
6
  DEFAULT_COUNT = 1_000
7
7
  EXPIRE_BATCH_SIZE = 100
8
- MATCH = 'MATCH'
9
- KEYS_METHOD = 'keys'
10
- SCAN_METHOD = 'scan'
8
+ SCAN_METHOD = 'SCAN'
11
9
  SCAN_PATTERN = '*'
12
10
 
11
+ include SidekiqUniqueJobs::Logging
12
+ include SidekiqUniqueJobs::Connection
13
13
  extend self # rubocop:disable Style/ModuleFunction
14
14
 
15
15
  def keys(pattern = SCAN_PATTERN, count = DEFAULT_COUNT)
16
- send("keys_by_#{redis_keys_method}", pattern, count)
16
+ return redis(&:keys) if pattern.nil?
17
+ redis { |conn| conn.scan_each(match: prefix(pattern), count: count).to_a }
17
18
  end
18
19
 
19
- def unique_key(jid)
20
- connection do |conn|
21
- conn.hget(SidekiqUniqueJobs::HASH_KEY, jid)
22
- end
23
- end
24
-
25
- def del(pattern = SCAN_PATTERN, count = 0, dry_run = true)
20
+ # Deletes unique keys from redis
21
+ #
22
+ #
23
+ # @param pattern [String] a pattern to scan for in redis
24
+ # @param count [Integer] the maximum number of keys to delete
25
+ # @return [Boolean] report success
26
+ def del(pattern = SCAN_PATTERN, count = 0)
26
27
  raise ArgumentError, 'Please provide a number of keys to delete greater than zero' if count.zero?
27
- logger.debug { "Deleting keys by: #{pattern}" }
28
- keys, time = timed { keys(pattern, count) }
29
- logger.debug { "#{keys.size} matching keys found in #{time} sec." }
30
- keys = dry_run(keys)
31
- logger.debug { "#{keys.size} matching keys after post-processing" }
32
- unless dry_run
33
- logger.debug { "deleting #{keys}..." }
34
- _, time = timed { batch_delete(keys) }
35
- logger.debug { "Deleted in #{time} sec." }
36
- end
37
- keys.size
38
- end
39
-
40
- def unique_hash
41
- connection do |conn|
42
- conn.hgetall(SidekiqUniqueJobs::HASH_KEY)
43
- end
44
- end
45
-
46
- def expire # rubocop:disable Metrics/MethodLength
47
- removed_keys = {}
48
- connection do |conn|
49
- cursor = '0'
50
- loop do
51
- cursor, jobs = get_jobs(conn, cursor)
52
- jobs.each do |job_array|
53
- jid, unique_key = job_array
54
-
55
- next if conn.get(unique_key)
56
- conn.hdel(SidekiqUniqueJobs::HASH_KEY, jid)
57
- removed_keys[jid] = unique_key
58
- end
28
+ pattern = "#{pattern}:*" unless pattern.end_with?(':*')
59
29
 
60
- break if cursor == '0'
61
- end
62
- end
30
+ log_debug { "Deleting keys by: #{pattern}" }
31
+ keys, time = timed { keys(pattern, count) }
32
+ key_size = keys.size
33
+ log_debug { "#{key_size} keys found in #{time} sec." }
34
+ _, time = timed { batch_delete(keys) }
35
+ log_debug { "Deleted #{key_size} keys in #{time} sec." }
63
36
 
64
- removed_keys
37
+ key_size
65
38
  end
66
39
 
67
40
  private
68
41
 
69
- def get_jobs(conn, cursor)
70
- conn.hscan(SidekiqUniqueJobs::HASH_KEY, [cursor, MATCH, SCAN_PATTERN, COUNT, EXPIRE_BATCH_SIZE])
71
- end
72
-
73
- def keys_by_scan(pattern, count)
74
- connection { |conn| conn.scan_each(match: prefix(pattern), count: count).to_a }
75
- end
76
-
77
- def keys_by_keys(pattern, _count)
78
- connection { |conn| conn.keys(prefix(pattern)).to_a }
79
- end
80
-
81
42
  def batch_delete(keys)
82
- connection do |conn|
43
+ redis do |conn|
83
44
  keys.each_slice(500) do |chunk|
84
45
  conn.pipelined do
85
46
  chunk.each do |key|
@@ -90,22 +51,15 @@ module SidekiqUniqueJobs
90
51
  end
91
52
  end
92
53
 
93
- def dry_run(keys, pattern = nil)
94
- return keys if pattern.nil?
95
- regex = Regexp.new(pattern)
96
- keys.select { |k| regex.match k }
97
- end
98
-
99
- def timed(&_block)
100
- start = Time.now
101
- result = yield
102
- elapsed = (Time.now - start).round(2)
54
+ def timed
55
+ start = current_time
56
+ result = yield
57
+ elapsed = (current_time - start).round(2)
103
58
  [result, elapsed]
104
59
  end
105
60
 
106
- def prefix_keys(keys)
107
- keys = Array(keys).flatten.compact
108
- keys.map { |key| prefix(key) }
61
+ def current_time
62
+ Time.now
109
63
  end
110
64
 
111
65
  def prefix(key)
@@ -117,21 +71,5 @@ module SidekiqUniqueJobs
117
71
  def unique_prefix
118
72
  SidekiqUniqueJobs.config.unique_prefix
119
73
  end
120
-
121
- def connection(&block)
122
- SidekiqUniqueJobs.connection(&block)
123
- end
124
-
125
- def redis_version
126
- SidekiqUniqueJobs.redis_version
127
- end
128
-
129
- def redis_keys_method
130
- (redis_version >= '2.8') ? SCAN_METHOD : KEYS_METHOD
131
- end
132
-
133
- def logger
134
- SidekiqUniqueJobs.logger
135
- end
136
74
  end
137
75
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SidekiqUniqueJobs
4
- VERSION = '5.0.11'
4
+ VERSION = '6.0.0.rc1'
5
5
  end
@@ -1,6 +1,6 @@
1
1
  local unique_key = KEYS[1]
2
2
  local job_id = ARGV[1]
3
- local expires = ARGV[2]
3
+ local expires = tonumber(ARGV[2])
4
4
  local stored_jid = redis.pcall('get', unique_key)
5
5
 
6
6
  if stored_jid then
@@ -11,8 +11,10 @@ if stored_jid then
11
11
  end
12
12
  end
13
13
 
14
- if redis.pcall('set', unique_key, job_id, 'nx', 'ex', expires) then
15
- -- redis.pcall('hsetnx', 'uniquejobs', job_id, unique_key)
14
+ if redis.call('SET', unique_key, job_id, 'nx') then
15
+ if expires then
16
+ redis.call('EXPIRE', unique_key, expires)
17
+ end
16
18
  return 1
17
19
  else
18
20
  return 0