sidekiq-cron 1.9.1 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +98 -3
  3. data/Gemfile +6 -0
  4. data/README.md +320 -44
  5. data/lib/sidekiq/cron/job.rb +336 -176
  6. data/lib/sidekiq/cron/launcher.rb +4 -6
  7. data/lib/sidekiq/cron/locales/de.yml +15 -6
  8. data/lib/sidekiq/cron/locales/en.yml +14 -14
  9. data/lib/sidekiq/cron/locales/es.yml +22 -0
  10. data/lib/sidekiq/cron/locales/id.yml +22 -0
  11. data/lib/sidekiq/cron/locales/it.yml +15 -16
  12. data/lib/sidekiq/cron/locales/ja.yml +14 -10
  13. data/lib/sidekiq/cron/locales/pt.yml +14 -14
  14. data/lib/sidekiq/cron/locales/ru.yml +15 -7
  15. data/lib/sidekiq/cron/locales/zh-CN.yml +14 -11
  16. data/lib/sidekiq/cron/namespace.rb +43 -0
  17. data/lib/sidekiq/cron/poller.rb +16 -13
  18. data/lib/sidekiq/cron/schedule_loader.rb +56 -16
  19. data/lib/sidekiq/cron/support.rb +4 -30
  20. data/lib/sidekiq/cron/version.rb +1 -1
  21. data/lib/sidekiq/cron/views/cron.erb +98 -92
  22. data/lib/sidekiq/cron/views/cron_show.erb +87 -82
  23. data/lib/sidekiq/cron/views/legacy/cron.erb +114 -0
  24. data/lib/sidekiq/cron/views/legacy/cron_show.erb +92 -0
  25. data/lib/sidekiq/cron/web.rb +21 -2
  26. data/lib/sidekiq/cron/web_extension.rb +99 -31
  27. data/lib/sidekiq/cron.rb +95 -5
  28. data/lib/sidekiq/options.rb +9 -7
  29. data/lib/sidekiq-cron.rb +6 -0
  30. data/sidekiq-cron.gemspec +10 -7
  31. metadata +65 -23
  32. data/test/integration/performance_test.rb +0 -46
  33. data/test/test_helper.rb +0 -87
  34. data/test/unit/fixtures/schedule_array.yml +0 -13
  35. data/test/unit/fixtures/schedule_erb.yml +0 -6
  36. data/test/unit/fixtures/schedule_hash.yml +0 -12
  37. data/test/unit/fixtures/schedule_string.yml +0 -1
  38. data/test/unit/job_test.rb +0 -1258
  39. data/test/unit/launcher_test.rb +0 -33
  40. data/test/unit/poller_test.rb +0 -144
  41. data/test/unit/schedule_loader_test.rb +0 -58
  42. data/test/unit/web_extension_test.rb +0 -155
@@ -1,58 +1,138 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'fugit'
2
- require 'sidekiq'
4
+ require 'cronex'
5
+ require 'globalid'
3
6
  require 'sidekiq/cron/support'
4
- require 'sidekiq/options'
5
7
 
6
8
  module Sidekiq
7
9
  module Cron
8
10
  class Job
9
- # How long we would like to store informations about previous enqueues.
11
+ # How long we would like to store information about previous enqueues.
10
12
  REMEMBER_THRESHOLD = 24 * 60 * 60
11
13
 
12
14
  # Time format for enqueued jobs.
13
15
  LAST_ENQUEUE_TIME_FORMAT = '%Y-%m-%d %H:%M:%S %z'
14
16
 
15
- # Use the exists? method if we're on a newer version of Redis.
16
- REDIS_EXISTS_METHOD = Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new("7.0.0") || Gem.loaded_specs['redis'].version < Gem::Version.new('4.2') ? :exists : :exists?
17
+ # Use serialize/deserialize key of GlobalID.
18
+ GLOBALID_KEY = "_sc_globalid"
19
+
20
+ attr_accessor :name, :namespace, :cron, :description, :klass, :message
21
+ attr_reader :last_enqueue_time, :fetch_missing_args, :source, :args
22
+
23
+ def initialize input_args = {}
24
+ args = Hash[input_args.map{ |k, v| [k.to_s, v] }]
25
+ @fetch_missing_args = args.delete('fetch_missing_args')
26
+ @fetch_missing_args = true if @fetch_missing_args.nil?
27
+
28
+ @name = args["name"]
29
+ @cron = args["cron"]
30
+ @description = args["description"] if args["description"]
31
+ @source = args["source"] == "schedule" ? "schedule" : "dynamic"
32
+
33
+ default_namespace = Sidekiq::Cron.configuration.default_namespace
34
+ @namespace = args["namespace"] || default_namespace
35
+ if Sidekiq::Cron::Namespace.available_namespaces_provided? && !Sidekiq::Cron::Namespace.all.include?(@namespace)
36
+ Sidekiq.logger.warn { "Cron Jobs - unexpected namespace #{@namespace} encountered. Assigning to default namespace." }
37
+ @namespace = default_namespace
38
+ end
39
+
40
+ # Get class from klass or class.
41
+ @klass = args["klass"] || args["class"]
42
+
43
+ # Set status of job.
44
+ @status = args['status'] || status_from_redis
45
+
46
+ # Set last enqueue time - from args or from existing job.
47
+ if args['last_enqueue_time'] && !args['last_enqueue_time'].empty?
48
+ @last_enqueue_time = parse_enqueue_time(args['last_enqueue_time'])
49
+ else
50
+ @last_enqueue_time = last_enqueue_time_from_redis
51
+ end
52
+
53
+ # Get right arguments for job.
54
+ @symbolize_args = args["symbolize_args"] == true || ("#{args["symbolize_args"]}" =~ (/^(true|t|yes|y|1)$/i)) == 0 || false
55
+ @args = parse_args(args["args"])
56
+
57
+ @date_as_argument = args["date_as_argument"] == true || ("#{args["date_as_argument"]}" =~ (/^(true|t|yes|y|1)$/i)) == 0 || false
58
+
59
+ @active_job = args["active_job"] == true || ("#{args["active_job"]}" =~ (/^(true|t|yes|y|1)$/i)) == 0 || false
60
+ @active_job_queue_name_prefix = args["queue_name_prefix"]
61
+ @active_job_queue_name_delimiter = args["queue_name_delimiter"]
62
+
63
+ # symbolize_args is only used when active_job is true
64
+ Sidekiq.logger.warn { "Cron Jobs - 'symbolize_args' is gonna be ignored, as it is only used when 'active_job' is true" } if @symbolize_args && !@active_job
65
+
66
+ if args["message"]
67
+ @message = args["message"]
68
+ message_data = Sidekiq.load_json(@message) || {}
69
+ @queue = message_data['queue'] || "default"
70
+ @retry = message_data['retry']
71
+ elsif @klass
72
+ message_data = {
73
+ "class" => @klass.to_s,
74
+ "args" => @args,
75
+ }
76
+
77
+ # Get right data for message,
78
+ # only if message wasn't specified before.
79
+ klass_data = get_job_options(@klass, @args)
80
+ message_data = klass_data.merge(message_data)
81
+
82
+ # Override queue and retry if set in config,
83
+ # only if message is hash - can be string (dumped JSON).
84
+ if args['queue']
85
+ @queue = message_data['queue'] = args['queue']
86
+ else
87
+ @queue = message_data['queue'] || "default"
88
+ end
89
+
90
+ if args['retry'] != nil
91
+ @retry = message_data['retry'] = args['retry']
92
+ else
93
+ @retry = message_data['retry']
94
+ end
95
+
96
+ @message = message_data
97
+ end
98
+
99
+ @queue_name_with_prefix = queue_name_with_prefix
100
+ end
17
101
 
18
102
  # Crucial part of whole enqueuing job.
19
- def should_enque? time
103
+ def should_enqueue? time
104
+ return false unless status == "enabled"
105
+ return false if past_scheduled_time?(time)
106
+ return false if enqueued_after?(time)
107
+
20
108
  enqueue = Sidekiq.redis do |conn|
21
- status == "enabled" &&
22
- not_past_scheduled_time?(time) &&
23
- not_enqueued_after?(time) &&
24
- conn.zadd(job_enqueued_key, formatted_enqueue_time(time), formatted_last_time(time))
109
+ conn.zadd(job_enqueued_key, formatted_enqueue_time(time), formatted_last_time(time))
25
110
  end
26
111
  enqueue == true || enqueue == 1
27
112
  end
28
113
 
29
114
  # Remove previous information about run times,
30
115
  # this will clear Redis and make sure that Redis will not overflow with memory.
31
- def remove_previous_enques time
116
+ def remove_previous_enqueues time
32
117
  Sidekiq.redis do |conn|
33
118
  conn.zremrangebyscore(job_enqueued_key, 0, "(#{(time.to_f - REMEMBER_THRESHOLD).to_s}")
34
119
  end
35
120
  end
36
121
 
37
122
  # Test if job should be enqueued.
38
- def test_and_enque_for_time! time
39
- if should_enque?(time)
40
- enque!
123
+ def test_and_enqueue_for_time! time
124
+ if should_enqueue?(time)
125
+ enqueue!
41
126
 
42
- remove_previous_enques(time)
127
+ remove_previous_enqueues(time)
43
128
  end
44
129
  end
45
130
 
46
131
  # Enqueue cron job to queue.
47
- def enque! time = Time.now.utc
48
- @last_enqueue_time = time.strftime(LAST_ENQUEUE_TIME_FORMAT)
49
-
50
- klass_const =
51
- begin
52
- Sidekiq::Cron::Support.constantize(@klass.to_s)
53
- rescue NameError
54
- nil
55
- end
132
+ def enqueue! time = Time.now.utc
133
+ @last_enqueue_time = time
134
+
135
+ klass_const = Sidekiq::Cron::Support.safe_constantize(@klass.to_s)
56
136
 
57
137
  jid =
58
138
  if klass_const
@@ -75,9 +155,10 @@ module Sidekiq
75
155
  end
76
156
 
77
157
  def is_active_job?(klass = nil)
78
- @active_job || defined?(ActiveJob::Base) && (klass || Sidekiq::Cron::Support.constantize(@klass.to_s)) < ActiveJob::Base
79
- rescue NameError
80
- false
158
+ @active_job || defined?(::ActiveJob::Base) && begin
159
+ klass ||= Sidekiq::Cron::Support.safe_constantize(@klass.to_s)
160
+ klass ? klass < ::ActiveJob::Base : false
161
+ end
81
162
  end
82
163
 
83
164
  def date_as_argument?
@@ -85,7 +166,8 @@ module Sidekiq
85
166
  end
86
167
 
87
168
  def enqueue_args
88
- date_as_argument? ? @args + [Time.now.to_f] : @args
169
+ args = date_as_argument? ? @args + [Time.now.to_f] : @args
170
+ deserialize_argument(args)
89
171
  end
90
172
 
91
173
  def enqueue_active_job(klass_const)
@@ -93,7 +175,7 @@ module Sidekiq
93
175
  end
94
176
 
95
177
  def enqueue_sidekiq_worker(klass_const)
96
- klass_const.set(queue: queue_name_with_prefix).perform_async(*enqueue_args)
178
+ klass_const.set(queue: queue_name_with_prefix, retry: @retry).perform_async(*enqueue_args)
97
179
  end
98
180
 
99
181
  # Sidekiq worker message.
@@ -108,16 +190,16 @@ module Sidekiq
108
190
 
109
191
  if !"#{@active_job_queue_name_delimiter}".empty?
110
192
  queue_name_delimiter = @active_job_queue_name_delimiter
111
- elsif defined?(ActiveJob::Base) && defined?(ActiveJob::Base.queue_name_delimiter) && !ActiveJob::Base.queue_name_delimiter.empty?
112
- queue_name_delimiter = ActiveJob::Base.queue_name_delimiter
193
+ elsif defined?(::ActiveJob::Base) && defined?(::ActiveJob::Base.queue_name_delimiter) && !::ActiveJob::Base.queue_name_delimiter.empty?
194
+ queue_name_delimiter = ::ActiveJob::Base.queue_name_delimiter
113
195
  else
114
196
  queue_name_delimiter = '_'
115
197
  end
116
198
 
117
199
  if !"#{@active_job_queue_name_prefix}".empty?
118
200
  queue_name = "#{@active_job_queue_name_prefix}#{queue_name_delimiter}#{@queue}"
119
- elsif defined?(ActiveJob::Base) && defined?(ActiveJob::Base.queue_name_prefix) && !"#{ActiveJob::Base.queue_name_prefix}".empty?
120
- queue_name = "#{ActiveJob::Base.queue_name_prefix}#{queue_name_delimiter}#{@queue}"
201
+ elsif defined?(::ActiveJob::Base) && defined?(::ActiveJob::Base.queue_name_prefix) && !"#{::ActiveJob::Base.queue_name_prefix}".empty?
202
+ queue_name = "#{::ActiveJob::Base.queue_name_prefix}#{queue_name_delimiter}#{@queue}"
121
203
  else
122
204
  queue_name = @queue
123
205
  end
@@ -146,6 +228,7 @@ module Sidekiq
146
228
  # Input structure should look like:
147
229
  # {
148
230
  # 'name_of_job' => {
231
+ # 'namespace' => 'MyNamespace',
149
232
  # 'class' => 'MyClass',
150
233
  # 'cron' => '1 * * * *',
151
234
  # 'args' => '(OPTIONAL) [Array or Hash]',
@@ -157,25 +240,26 @@ module Sidekiq
157
240
  # }
158
241
  # }
159
242
  #
160
- def self.load_from_hash hash
243
+ def self.load_from_hash(hash, options = {})
161
244
  array = hash.map do |key, job|
162
245
  job['name'] = key
163
246
  job
164
247
  end
165
- load_from_array array
248
+ load_from_array(array, options)
166
249
  end
167
250
 
168
251
  # Like #load_from_hash.
169
252
  # If exists old jobs in Redis but removed from args, destroy old jobs.
170
- def self.load_from_hash! hash
253
+ def self.load_from_hash!(hash, options = {})
171
254
  destroy_removed_jobs(hash.keys)
172
- load_from_hash(hash)
255
+ load_from_hash(hash, options)
173
256
  end
174
257
 
175
258
  # Load cron jobs from Array.
176
259
  # Input structure should look like:
177
260
  # [
178
261
  # {
262
+ # 'namespace' => 'MyNamespace',
179
263
  # 'name' => 'name_of_job',
180
264
  # 'class' => 'MyClass',
181
265
  # 'cron' => '1 * * * *',
@@ -189,10 +273,10 @@ module Sidekiq
189
273
  # }
190
274
  # ]
191
275
  #
192
- def self.load_from_array array
276
+ def self.load_from_array(array, options = {})
193
277
  errors = {}
194
278
  array.each do |job_data|
195
- job = new(job_data)
279
+ job = new(job_data.merge(options))
196
280
  errors[job.name] = job.errors unless job.save
197
281
  end
198
282
  errors
@@ -200,20 +284,21 @@ module Sidekiq
200
284
 
201
285
  # Like #load_from_array.
202
286
  # If exists old jobs in Redis but removed from args, destroy old jobs.
203
- def self.load_from_array! array
204
- job_names = array.map { |job| job["name"] }
287
+ def self.load_from_array!(array, options = {})
288
+ job_names = array.map { |job| job["name"] || job[:name] }
205
289
  destroy_removed_jobs(job_names)
206
- load_from_array(array)
290
+ load_from_array(array, options)
207
291
  end
208
292
 
209
293
  # Get all cron jobs.
210
- def self.all
294
+ def self.all(namespace = Sidekiq::Cron.configuration.default_namespace)
211
295
  job_hashes = nil
212
296
  Sidekiq.redis do |conn|
213
- set_members = conn.smembers(jobs_key)
297
+ job_keys = job_keys_from_namespace(namespace)
298
+
214
299
  job_hashes = conn.pipelined do |pipeline|
215
- set_members.each do |key|
216
- pipeline.hgetall(key)
300
+ job_keys.each do |job_key|
301
+ pipeline.hgetall(job_key)
217
302
  end
218
303
  end
219
304
  end
@@ -223,22 +308,25 @@ module Sidekiq
223
308
  end
224
309
  end
225
310
 
226
- def self.count
227
- out = 0
228
- Sidekiq.redis do |conn|
229
- out = conn.scard(jobs_key)
311
+ def self.count(namespace = Sidekiq::Cron.configuration.default_namespace)
312
+ if namespace == '*'
313
+ Namespace.all_with_count.reduce(0) do |memo, namespace_count|
314
+ memo + namespace_count[:count]
315
+ end
316
+ else
317
+ Sidekiq.redis { |conn| conn.scard(jobs_key(namespace)) }
230
318
  end
231
- out
232
319
  end
233
320
 
234
- def self.find name
321
+ def self.find(name, namespace = Sidekiq::Cron.configuration.default_namespace)
235
322
  # If name is hash try to get name from it.
236
323
  name = name[:name] || name['name'] if name.is_a?(Hash)
324
+ return unless exists? name, namespace
237
325
 
238
326
  output = nil
239
327
  Sidekiq.redis do |conn|
240
- if exists? name
241
- output = Job.new conn.hgetall( redis_key(name) )
328
+ if exists? name, namespace
329
+ output = Job.new conn.hgetall(redis_key(name, namespace))
242
330
  end
243
331
  end
244
332
  output if output && output.valid?
@@ -250,92 +338,17 @@ module Sidekiq
250
338
  end
251
339
 
252
340
  # Destroy job by name.
253
- def self.destroy name
341
+ def self.destroy(name, namespace = Sidekiq::Cron.configuration.default_namespace)
254
342
  # If name is hash try to get name from it.
255
343
  name = name[:name] || name['name'] if name.is_a?(Hash)
256
344
 
257
- if job = find(name)
345
+ if (job = find(name, namespace))
258
346
  job.destroy
259
347
  else
260
348
  false
261
349
  end
262
350
  end
263
351
 
264
- attr_accessor :name, :cron, :description, :klass, :args, :message
265
- attr_reader :last_enqueue_time, :fetch_missing_args
266
-
267
- def initialize input_args = {}
268
- args = Hash[input_args.map{ |k, v| [k.to_s, v] }]
269
- @fetch_missing_args = args.delete('fetch_missing_args')
270
- @fetch_missing_args = true if @fetch_missing_args.nil?
271
-
272
- @name = args["name"]
273
- @cron = args["cron"]
274
- @description = args["description"] if args["description"]
275
-
276
- # Get class from klass or class.
277
- @klass = args["klass"] || args["class"]
278
-
279
- # Set status of job.
280
- @status = args['status'] || status_from_redis
281
-
282
- # Set last enqueue time - from args or from existing job.
283
- if args['last_enqueue_time'] && !args['last_enqueue_time'].empty?
284
- @last_enqueue_time = parse_enqueue_time(args['last_enqueue_time'])
285
- else
286
- @last_enqueue_time = last_enqueue_time_from_redis
287
- end
288
-
289
- # Get right arguments for job.
290
- @symbolize_args = args["symbolize_args"] == true || ("#{args["symbolize_args"]}" =~ (/^(true|t|yes|y|1)$/i)) == 0 || false
291
- @args = args["args"].nil? ? [] : parse_args( args["args"] )
292
-
293
- @date_as_argument = args["date_as_argument"] == true || ("#{args["date_as_argument"]}" =~ (/^(true|t|yes|y|1)$/i)) == 0 || false
294
-
295
- @active_job = args["active_job"] == true || ("#{args["active_job"]}" =~ (/^(true|t|yes|y|1)$/i)) == 0 || false
296
- @active_job_queue_name_prefix = args["queue_name_prefix"]
297
- @active_job_queue_name_delimiter = args["queue_name_delimiter"]
298
-
299
- if args["message"]
300
- @message = args["message"]
301
- message_data = Sidekiq.load_json(@message) || {}
302
- @queue = message_data['queue'] || "default"
303
- elsif @klass
304
- message_data = {
305
- "class" => @klass.to_s,
306
- "args" => @args,
307
- }
308
-
309
- # Get right data for message,
310
- # only if message wasn't specified before.
311
- klass_data = case @klass
312
- when Class
313
- @klass.get_sidekiq_options
314
- when String
315
- begin
316
- Sidekiq::Cron::Support.constantize(@klass).get_sidekiq_options
317
- rescue Exception => e
318
- # Unknown class
319
- {"queue"=>"default"}
320
- end
321
- end
322
-
323
- message_data = klass_data.merge(message_data)
324
-
325
- # Override queue if setted in config,
326
- # only if message is hash - can be string (dumped JSON).
327
- if args['queue']
328
- @queue = message_data['queue'] = args['queue']
329
- else
330
- @queue = message_data['queue'] || "default"
331
- end
332
-
333
- @message = message_data
334
- end
335
-
336
- @queue_name_with_prefix = queue_name_with_prefix
337
- end
338
-
339
352
  def status
340
353
  @status
341
354
  end
@@ -364,6 +377,12 @@ module Sidekiq
364
377
  message
365
378
  end
366
379
 
380
+ def human_cron
381
+ Cronex::ExpressionDescriptor.new(cron).description
382
+ rescue
383
+ cron
384
+ end
385
+
367
386
  def status_from_redis
368
387
  out = "enabled"
369
388
  if fetch_missing_args
@@ -400,9 +419,11 @@ module Sidekiq
400
419
  def to_hash
401
420
  {
402
421
  name: @name,
422
+ namespace: @namespace,
403
423
  klass: @klass.to_s,
404
424
  cron: @cron,
405
425
  description: @description,
426
+ source: @source,
406
427
  args: @args.is_a?(String) ? @args : Sidekiq.dump_json(@args || []),
407
428
  date_as_argument: date_as_argument? ? "1" : "0",
408
429
  message: @message.is_a?(String) ? @message : Sidekiq.dump_json(@message || {}),
@@ -410,7 +431,8 @@ module Sidekiq
410
431
  active_job: @active_job ? "1" : "0",
411
432
  queue_name_prefix: @active_job_queue_name_prefix,
412
433
  queue_name_delimiter: @active_job_queue_name_delimiter,
413
- last_enqueue_time: @last_enqueue_time.to_s,
434
+ retry: @retry.nil? || @retry.is_a?(Numeric) ? @retry : @retry.to_s,
435
+ last_enqueue_time: serialized_last_enqueue_time,
414
436
  symbolize_args: symbolize_args? ? "1" : "0",
415
437
  }
416
438
  end
@@ -424,11 +446,14 @@ module Sidekiq
424
446
  @errors = []
425
447
 
426
448
  errors << "'name' must be set" if @name.nil? || @name.size == 0
449
+ errors << "'namespace' must be set" if @namespace.nil? || @namespace.size == 0
450
+ errors << "'namespace' cannot be '*'" if @namespace == "*"
451
+
427
452
  if @cron.nil? || @cron.size == 0
428
453
  errors << "'cron' must be set"
429
454
  else
430
455
  begin
431
- @parsed_cron = Fugit.do_parse_cronish(@cron)
456
+ @parsed_cron = do_parse_cron(@cron)
432
457
  rescue => e
433
458
  errors << "'cron' -> #{@cron.inspect} -> #{e.class}: #{e.message}"
434
459
  end
@@ -454,25 +479,29 @@ module Sidekiq
454
479
  return false unless valid?
455
480
 
456
481
  Sidekiq.redis do |conn|
457
-
458
482
  # Add to set of all jobs
459
- conn.sadd self.class.jobs_key, [redis_key]
483
+ conn.sadd self.class.jobs_key(@namespace), [redis_key]
460
484
 
461
- # Add informations for this job!
462
- conn.hmset redis_key, *hash_to_redis(to_hash)
485
+ # Add information for this job!
486
+ conn.hset redis_key, to_hash.transform_values! { |v| v || '' }.flatten
463
487
 
464
- # Add information about last time! - don't enque right after scheduler poller starts!
488
+ # Add information about last time! - don't enqueue right after scheduler poller starts!
465
489
  time = Time.now.utc
466
- exists = conn.public_send(REDIS_EXISTS_METHOD, job_enqueued_key)
467
- conn.zadd(job_enqueued_key, time.to_f.to_s, formatted_last_time(time).to_s) unless exists == true || exists == 1
490
+ exists = conn.exists(job_enqueued_key)
491
+
492
+ unless exists == true || exists == 1
493
+ conn.zadd(job_enqueued_key, time.to_f.to_s, formatted_last_time(time).to_s)
494
+ Sidekiq.logger.info { "Cron Jobs - added job with name #{@name} in the namespace #{@namespace}" }
495
+ end
468
496
  end
469
- Sidekiq.logger.info { "Cron Jobs - added job with name: #{@name}" }
497
+
498
+ true
470
499
  end
471
500
 
472
501
  def save_last_enqueue_time
473
502
  Sidekiq.redis do |conn|
474
503
  # Update last enqueue time.
475
- conn.hset redis_key, 'last_enqueue_time', @last_enqueue_time
504
+ conn.hset redis_key, 'last_enqueue_time', serialized_last_enqueue_time
476
505
  end
477
506
  end
478
507
 
@@ -482,7 +511,7 @@ module Sidekiq
482
511
  enqueued: @last_enqueue_time
483
512
  }
484
513
 
485
- @history_size ||= (Sidekiq::Options[:cron_history_size] || 10).to_i - 1
514
+ @history_size ||= Sidekiq::Cron.configuration.cron_history_size.to_i - 1
486
515
  Sidekiq.redis do |conn|
487
516
  conn.lpush jid_history_key,
488
517
  Sidekiq.dump_json(jid_history)
@@ -494,9 +523,9 @@ module Sidekiq
494
523
  def destroy
495
524
  Sidekiq.redis do |conn|
496
525
  # Delete from set.
497
- conn.srem self.class.jobs_key, [redis_key]
526
+ conn.srem self.class.jobs_key(@namespace), [redis_key]
498
527
 
499
- # Delete runned timestamps.
528
+ # Delete ran timestamps.
500
529
  conn.del job_enqueued_key
501
530
 
502
531
  # Delete jid_history.
@@ -506,7 +535,7 @@ module Sidekiq
506
535
  conn.del redis_key
507
536
  end
508
537
 
509
- Sidekiq.logger.info { "Cron Jobs - deleted job with name: #{@name}" }
538
+ Sidekiq.logger.info { "Cron Jobs - deleted job with name #{@name} from namespace #{@namespace}" }
510
539
  end
511
540
 
512
541
  # Remove all job from cron.
@@ -519,9 +548,17 @@ module Sidekiq
519
548
 
520
549
  # Remove "removed jobs" between current jobs and new jobs
521
550
  def self.destroy_removed_jobs new_job_names
522
- current_job_names = Sidekiq::Cron::Job.all.map(&:name)
551
+ current_jobs = Sidekiq::Cron::Job.all("*").filter_map { |j| j if j.source == "schedule" }
552
+ current_job_names = current_jobs.map(&:name)
523
553
  removed_job_names = current_job_names - new_job_names
524
- removed_job_names.each { |j| Sidekiq::Cron::Job.destroy(j) }
554
+ removed_job_names.each do |j|
555
+ job_to_destroy = current_jobs.detect { |job| job.name == j }
556
+
557
+ Sidekiq::Cron::Job.destroy(
558
+ job_to_destroy.name,
559
+ job_to_destroy.namespace
560
+ )
561
+ end
525
562
  removed_job_names
526
563
  end
527
564
 
@@ -539,29 +576,52 @@ module Sidekiq
539
576
  last_time(now).getutc.iso8601
540
577
  end
541
578
 
542
- def self.exists? name
579
+ def self.exists?(name, namespace = Sidekiq::Cron.configuration.default_namespace)
543
580
  out = Sidekiq.redis do |conn|
544
- conn.public_send(REDIS_EXISTS_METHOD, redis_key(name))
581
+ conn.exists(redis_key(name, namespace))
545
582
  end
546
- out == true || out == 1
583
+
584
+ [true, 1].include?(out)
547
585
  end
548
586
 
549
587
  def exists?
550
- self.class.exists? @name
588
+ self.class.exists? @name, @namespace
551
589
  end
552
590
 
553
591
  def sort_name
554
592
  "#{status == "enabled" ? 0 : 1}_#{name}".downcase
555
593
  end
556
594
 
595
+ def args=(args)
596
+ @args = parse_args(args)
597
+ end
598
+
599
+ def cron_expression_string
600
+ parsed_cron.to_cron_s
601
+ end
602
+
557
603
  private
558
604
 
559
605
  def parsed_cron
560
- @parsed_cron ||= Fugit.parse_cronish(@cron)
606
+ @parsed_cron ||= do_parse_cron(@cron)
561
607
  end
562
608
 
563
- def not_enqueued_after?(time)
564
- @last_enqueue_time.nil? || @last_enqueue_time.to_i < last_time(time).to_i
609
+ def do_parse_cron(cron)
610
+ case Sidekiq::Cron.configuration.natural_cron_parsing_mode
611
+ when :single
612
+ Fugit.do_parse_cronish(cron)
613
+ when :strict
614
+ Fugit.parse_cron(cron) || # Ex. '11 1 * * 1'
615
+ Fugit.parse_nat(cron, :multi => :fail) || # Ex. 'every Monday at 01:11'
616
+ fail(ArgumentError.new("invalid cron string #{cron.inspect}"))
617
+ else
618
+ mode = Sidekiq::Cron.configuration.natural_cron_parsing_mode
619
+ raise ArgumentError, "Unknown natural cron parsing mode: #{mode.inspect}"
620
+ end
621
+ end
622
+
623
+ def enqueued_after?(time)
624
+ @last_enqueue_time && @last_enqueue_time.to_i >= last_time(time).to_i
565
625
  end
566
626
 
567
627
  # Try parsing inbound args into an array.
@@ -569,6 +629,8 @@ module Sidekiq
569
629
  # try to load JSON, then failover to string array.
570
630
  def parse_args(args)
571
631
  case args
632
+ when GlobalID::Identification
633
+ [convert_to_global_id_hash(args)]
572
634
  when String
573
635
  begin
574
636
  parsed_args = Sidekiq.load_json(args)
@@ -577,8 +639,10 @@ module Sidekiq
577
639
  [*args]
578
640
  end
579
641
  when Hash
642
+ args = serialize_argument(args)
580
643
  symbolize_args? ? [symbolize_args(args)] : [args]
581
644
  when Array
645
+ args = serialize_argument(args)
582
646
  symbolize_args? ? symbolize_args(args) : args
583
647
  else
584
648
  [*args]
@@ -611,47 +675,143 @@ module Sidekiq
611
675
  DateTime.parse(timestamp).to_time.utc
612
676
  end
613
677
 
614
- def not_past_scheduled_time?(current_time)
678
+ def past_scheduled_time?(current_time)
615
679
  last_cron_time = parsed_cron.previous_time(current_time).utc
616
- return false if (current_time.to_i - last_cron_time.to_i) > 60
617
- true
680
+ period = Sidekiq::Cron.configuration.reschedule_grace_period
681
+
682
+ current_time.to_i - last_cron_time.to_i > period
618
683
  end
619
684
 
620
- # Redis key for set of all cron jobs.
621
- def self.jobs_key
622
- "cron_jobs"
685
+ def self.default_if_blank(namespace)
686
+ if namespace.nil? || namespace == ''
687
+ Sidekiq::Cron.configuration.default_namespace
688
+ else
689
+ namespace
690
+ end
623
691
  end
624
692
 
625
- # Redis key for storing one cron job.
626
- def self.redis_key name
627
- "cron_job:#{name}"
693
+ def self.job_keys_from_namespace(namespace = Sidekiq::Cron.configuration.default_namespace)
694
+ Sidekiq.redis do |conn|
695
+ if namespace == '*'
696
+ namespaces = Sidekiq::Cron::Namespace.all.map { jobs_key(_1) }
697
+ namespaces.flat_map { |name| conn.smembers(name) }
698
+ else
699
+ conn.smembers(jobs_key(namespace))
700
+ end
701
+ end
628
702
  end
629
703
 
630
- # Redis key for storing one cron job.
704
+ def self.migrate_old_jobs_if_needed!
705
+ Sidekiq.redis do |conn|
706
+ old_job_keys = conn.smembers('cron_jobs')
707
+ old_job_keys.each do |old_job|
708
+ old_job_hash = conn.hgetall(old_job)
709
+ old_job_hash[:namespace] = Sidekiq::Cron.configuration.default_namespace
710
+ create(old_job_hash)
711
+ conn.srem('cron_jobs', old_job)
712
+ end
713
+ end
714
+ end
715
+
716
+ # Redis key for set of all cron jobs
717
+ def self.jobs_key(namespace = Sidekiq::Cron.configuration.default_namespace)
718
+ "cron_jobs:#{default_if_blank(namespace)}"
719
+ end
720
+
721
+ # Redis key for storing one cron job
722
+ def self.redis_key(name, namespace = Sidekiq::Cron.configuration.default_namespace)
723
+ "cron_job:#{default_if_blank(namespace)}:#{name}"
724
+ end
725
+
726
+ # Redis key for storing one cron job
631
727
  def redis_key
632
- self.class.redis_key @name
728
+ self.class.redis_key @name, @namespace
633
729
  end
634
730
 
635
- # Redis key for storing one cron job run times (when poller added job to queue)
636
- def self.job_enqueued_key name
637
- "cron_job:#{name}:enqueued"
731
+ # Redis key for storing one cron job run times
732
+ # (when poller added job to queue)
733
+ def self.job_enqueued_key(name, namespace = Sidekiq::Cron.configuration.default_namespace)
734
+ "cron_job:#{default_if_blank(namespace)}:#{name}:enqueued"
638
735
  end
639
736
 
640
- def self.jid_history_key name
641
- "cron_job:#{name}:jid_history"
737
+ def self.jid_history_key(name, namespace = Sidekiq::Cron.configuration.default_namespace)
738
+ "cron_job:#{default_if_blank(namespace)}:#{name}:jid_history"
642
739
  end
643
740
 
741
+ # Redis key for storing one cron job run times
742
+ # (when poller added job to queue)
644
743
  def job_enqueued_key
645
- self.class.job_enqueued_key @name
744
+ self.class.job_enqueued_key @name, @namespace
646
745
  end
647
746
 
648
747
  def jid_history_key
649
- self.class.jid_history_key @name
748
+ self.class.jid_history_key @name, @namespace
749
+ end
750
+
751
+ def serialized_last_enqueue_time
752
+ @last_enqueue_time&.strftime(LAST_ENQUEUE_TIME_FORMAT)
753
+ end
754
+
755
+ def convert_to_global_id_hash(argument)
756
+ { GLOBALID_KEY => argument.to_global_id.to_s }
757
+ rescue URI::GID::MissingModelIdError
758
+ raise "Unable to serialize #{argument.class} " \
759
+ "without an id. (Maybe you forgot to call save?)"
650
760
  end
651
761
 
652
- # Give Hash returns array for using it for redis.hmset
653
- def hash_to_redis hash
654
- hash.flat_map{ |key, value| [key, value || ""] }
762
+ def deserialize_argument(argument)
763
+ case argument
764
+ when String
765
+ argument
766
+ when Array
767
+ argument.map { |arg| deserialize_argument(arg) }
768
+ when Hash
769
+ if serialized_global_id?(argument)
770
+ deserialize_global_id argument
771
+ else
772
+ argument.transform_values { |v| deserialize_argument(v) }
773
+ end
774
+ else
775
+ argument
776
+ end
777
+ end
778
+
779
+ def serialized_global_id?(hash)
780
+ hash.size == 1 && hash.include?(GLOBALID_KEY)
781
+ end
782
+
783
+ def deserialize_global_id(hash)
784
+ GlobalID::Locator.locate hash[GLOBALID_KEY]
785
+ end
786
+
787
+ def serialize_argument(argument)
788
+ case argument
789
+ when GlobalID::Identification
790
+ convert_to_global_id_hash(argument)
791
+ when Array
792
+ argument.map { |arg| serialize_argument(arg) }
793
+ when Hash
794
+ argument.each_with_object({}) do |(key, value), hash|
795
+ hash[key] = serialize_argument(value)
796
+ end
797
+ else
798
+ argument
799
+ end
800
+ end
801
+
802
+ def get_job_options(klass, args)
803
+ klass = klass.is_a?(Class) ? klass : Sidekiq::Cron::Support.safe_constantize(klass)
804
+
805
+ if klass.nil?
806
+ # Unknown class
807
+ {"queue"=>"default"}
808
+ elsif is_active_job?(klass)
809
+ job = klass.new(args)
810
+
811
+ {"queue"=>job.queue_name}
812
+ else
813
+ klass.get_sidekiq_options
814
+ end
655
815
  end
656
816
  end
657
817
  end