resque-scheduler 2.2.0 → 4.10.2

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 (68) hide show
  1. checksums.yaml +7 -0
  2. data/.github/dependabot.yml +12 -0
  3. data/.github/funding.yml +4 -0
  4. data/.github/workflows/codeql-analysis.yml +59 -0
  5. data/.github/workflows/rubocop.yml +27 -0
  6. data/.github/workflows/ruby.yml +81 -0
  7. data/AUTHORS.md +31 -0
  8. data/CHANGELOG.md +539 -0
  9. data/CODE_OF_CONDUCT.md +74 -0
  10. data/Gemfile +26 -2
  11. data/LICENSE +1 -1
  12. data/README.md +423 -91
  13. data/Rakefile +12 -35
  14. data/exe/resque-scheduler +5 -0
  15. data/lib/resque/scheduler/cli.rb +147 -0
  16. data/lib/resque/scheduler/configuration.rb +102 -0
  17. data/lib/resque/scheduler/delaying_extensions.rb +371 -0
  18. data/lib/resque/scheduler/env.rb +85 -0
  19. data/lib/resque/scheduler/extension.rb +13 -0
  20. data/lib/resque/scheduler/failure_handler.rb +11 -0
  21. data/lib/resque/scheduler/lock/base.rb +12 -3
  22. data/lib/resque/scheduler/lock/basic.rb +4 -5
  23. data/lib/resque/scheduler/lock/resilient.rb +52 -43
  24. data/lib/resque/scheduler/lock.rb +2 -1
  25. data/lib/resque/scheduler/locking.rb +104 -0
  26. data/lib/resque/scheduler/logger_builder.rb +83 -0
  27. data/lib/resque/scheduler/plugin.rb +31 -0
  28. data/lib/resque/scheduler/scheduling_extensions.rb +142 -0
  29. data/lib/{resque_scheduler → resque/scheduler}/server/views/delayed.erb +23 -8
  30. data/lib/resque/scheduler/server/views/delayed_schedules.erb +20 -0
  31. data/lib/{resque_scheduler → resque/scheduler}/server/views/delayed_timestamp.erb +1 -1
  32. data/lib/resque/scheduler/server/views/scheduler.erb +58 -0
  33. data/lib/resque/scheduler/server/views/search.erb +69 -0
  34. data/lib/resque/scheduler/server/views/search_form.erb +4 -0
  35. data/lib/resque/scheduler/server.rb +268 -0
  36. data/lib/resque/scheduler/signal_handling.rb +40 -0
  37. data/lib/resque/scheduler/tasks.rb +25 -0
  38. data/lib/resque/scheduler/util.rb +39 -0
  39. data/lib/resque/scheduler/version.rb +7 -0
  40. data/lib/resque/scheduler.rb +333 -149
  41. data/lib/resque-scheduler.rb +4 -0
  42. data/resque-scheduler.gemspec +56 -20
  43. metadata +205 -104
  44. data/.gitignore +0 -8
  45. data/.rubocop.yml +0 -120
  46. data/.travis.yml +0 -10
  47. data/HISTORY.md +0 -180
  48. data/lib/resque/scheduler_locking.rb +0 -90
  49. data/lib/resque_scheduler/logger_builder.rb +0 -51
  50. data/lib/resque_scheduler/plugin.rb +0 -25
  51. data/lib/resque_scheduler/server/views/scheduler.erb +0 -44
  52. data/lib/resque_scheduler/server.rb +0 -92
  53. data/lib/resque_scheduler/tasks.rb +0 -40
  54. data/lib/resque_scheduler/version.rb +0 -3
  55. data/lib/resque_scheduler.rb +0 -355
  56. data/script/migrate_to_timestamps_set.rb +0 -14
  57. data/tasks/resque_scheduler.rake +0 -2
  58. data/test/delayed_queue_test.rb +0 -383
  59. data/test/redis-test.conf +0 -108
  60. data/test/resque-web_test.rb +0 -116
  61. data/test/scheduler_args_test.rb +0 -156
  62. data/test/scheduler_hooks_test.rb +0 -23
  63. data/test/scheduler_locking_test.rb +0 -180
  64. data/test/scheduler_setup_test.rb +0 -59
  65. data/test/scheduler_test.rb +0 -256
  66. data/test/support/redis_instance.rb +0 -129
  67. data/test/test_helper.rb +0 -92
  68. /data/lib/{resque_scheduler → resque/scheduler}/server/views/requeue-params.erb +0 -0
@@ -0,0 +1,85 @@
1
+ # vim:fileencoding=utf-8
2
+
3
+ require 'English' # $PROCESS_ID
4
+
5
+ module Resque
6
+ module Scheduler
7
+ class Env
8
+ def initialize(options)
9
+ @options = options
10
+ @pidfile_path = nil
11
+ end
12
+
13
+ def setup
14
+ require 'resque'
15
+ require 'resque/scheduler'
16
+
17
+ setup_backgrounding
18
+ setup_pid_file
19
+ setup_scheduler_configuration
20
+ end
21
+
22
+ def cleanup
23
+ cleanup_pid_file
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :options, :pidfile_path
29
+
30
+ def setup_backgrounding
31
+ return unless options[:background]
32
+
33
+ # Need to set this here for conditional Process.daemon redirect of
34
+ # stderr/stdout to /dev/null
35
+ Resque::Scheduler.quiet = if options.key?(:quiet)
36
+ !!options[:quiet]
37
+ else
38
+ true
39
+ end
40
+
41
+ Process.daemon(true, !Resque::Scheduler.quiet)
42
+ Resque.redis.reconnect
43
+ end
44
+
45
+ def setup_pid_file
46
+ return unless options[:pidfile]
47
+
48
+ @pidfile_path = File.expand_path(options[:pidfile])
49
+
50
+ File.open(pidfile_path, 'w') do |f|
51
+ f.puts $PROCESS_ID
52
+ end
53
+
54
+ at_exit { cleanup_pid_file }
55
+ end
56
+
57
+ def setup_scheduler_configuration
58
+ Resque::Scheduler.configure do |c|
59
+ c.app_name = options[:app_name] if options.key?(:app_name)
60
+
61
+ c.dynamic = !!options[:dynamic] if options.key?(:dynamic)
62
+
63
+ c.env = options[:env] if options.key?(:env)
64
+
65
+ c.logfile = options[:logfile] if options.key?(:logfile)
66
+
67
+ c.logformat = options[:logformat] if options.key?(:logformat)
68
+
69
+ if (psleep = options[:poll_sleep_amount]) && !psleep.nil?
70
+ c.poll_sleep_amount = Float(psleep)
71
+ end
72
+
73
+ c.verbose = !!options[:verbose] if options.key?(:verbose)
74
+ end
75
+ end
76
+
77
+ def cleanup_pid_file
78
+ return unless pidfile_path
79
+
80
+ File.delete(pidfile_path) if File.exist?(pidfile_path)
81
+ @pidfile_path = nil
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,13 @@
1
+ # vim:fileencoding=utf-8
2
+
3
+ require_relative 'scheduling_extensions'
4
+ require_relative 'delaying_extensions'
5
+
6
+ module Resque
7
+ module Scheduler
8
+ module Extension
9
+ include SchedulingExtensions
10
+ include DelayingExtensions
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ module Resque
2
+ module Scheduler
3
+ class FailureHandler
4
+ def self.on_enqueue_failure(_, e)
5
+ Resque::Scheduler.log_error(
6
+ "#{e.class.name}: #{e.message} #{e.backtrace.inspect}"
7
+ )
8
+ end
9
+ end
10
+ end
11
+ end
@@ -1,5 +1,7 @@
1
+ # vim:fileencoding=utf-8
2
+
1
3
  module Resque
2
- class Scheduler
4
+ module Scheduler
3
5
  module Lock
4
6
  class Base
5
7
  attr_reader :key
@@ -31,7 +33,12 @@ module Resque
31
33
  Resque.redis.del(key) == 1
32
34
  end
33
35
 
34
- private
36
+ # Releases the lock iff we own it
37
+ def release
38
+ locked? && release!
39
+ end
40
+
41
+ private
35
42
 
36
43
  # Extends the lock by `timeout` seconds.
37
44
  def extend_lock!
@@ -40,7 +47,9 @@ module Resque
40
47
 
41
48
  def hostname
42
49
  local_hostname = Socket.gethostname
43
- Socket.gethostbyname(local_hostname).first rescue local_hostname
50
+ Addrinfo.getaddrinfo(local_hostname, 'http').first.getnameinfo.first
51
+ rescue
52
+ local_hostname
44
53
  end
45
54
 
46
55
  def process_id
@@ -1,7 +1,8 @@
1
- require 'resque/scheduler/lock/base'
1
+ # vim:fileencoding=utf-8
2
+ require_relative 'base'
2
3
 
3
4
  module Resque
4
- class Scheduler
5
+ module Scheduler
5
6
  module Lock
6
7
  class Basic < Base
7
8
  def acquire!
@@ -15,9 +16,7 @@ module Resque
15
16
  if Resque.redis.get(key) == value
16
17
  extend_lock!
17
18
 
18
- if Resque.redis.get(key) == value
19
- return true
20
- end
19
+ return true if Resque.redis.get(key) == value
21
20
  end
22
21
 
23
22
  false
@@ -1,67 +1,76 @@
1
- require 'resque/scheduler/lock/base'
1
+ # vim:fileencoding=utf-8
2
+ require_relative 'base'
2
3
 
3
4
  module Resque
4
- class Scheduler
5
+ module Scheduler
5
6
  module Lock
6
7
  class Resilient < Base
7
8
  def acquire!
8
- Resque.redis.evalsha(
9
- acquire_sha,
10
- :keys => [key],
11
- :argv => [value]
12
- ).to_i == 1
9
+ evalsha(:acquire, [key], [value]).to_i == 1
13
10
  end
14
11
 
15
12
  def locked?
16
- Resque.redis.evalsha(
17
- locked_sha,
18
- :keys => [key],
19
- :argv => [value]
20
- ).to_i == 1
13
+ evalsha(:locked, [key], [value]).to_i == 1
21
14
  end
22
15
 
23
- private
16
+ def timeout=(seconds)
17
+ if locked?
18
+ @timeout = seconds
19
+ @locked_sha = nil
20
+ @acquire_sha = nil
21
+ end
22
+ @timeout
23
+ end
24
+
25
+ private
26
+
27
+ def evalsha(script, keys, argv, refresh: false)
28
+ sha_method_name = "#{script}_sha"
29
+ Resque.redis.evalsha(
30
+ send(sha_method_name, refresh),
31
+ keys: keys,
32
+ argv: argv
33
+ )
34
+ rescue Redis::CommandError => e
35
+ if e.message =~ /NOSCRIPT/
36
+ refresh = true
37
+ retry
38
+ end
39
+ raise
40
+ end
24
41
 
25
42
  def locked_sha(refresh = false)
26
43
  @locked_sha = nil if refresh
27
44
 
28
- @locked_sha ||= begin
29
- Resque.redis.script(
30
- :load,
31
- <<-EOF
32
- if redis.call('GET', KEYS[1]) == ARGV[1]
33
- then
34
- redis.call('EXPIRE', KEYS[1], #{timeout})
45
+ @locked_sha ||=
46
+ Resque.data_store.redis.script(:load, <<-EOF.gsub(/^ {14}/, ''))
47
+ if redis.call('GET', KEYS[1]) == ARGV[1]
48
+ then
49
+ redis.call('EXPIRE', KEYS[1], #{timeout})
35
50
 
36
- if redis.call('GET', KEYS[1]) == ARGV[1]
37
- then
38
- return 1
39
- end
40
- end
51
+ if redis.call('GET', KEYS[1]) == ARGV[1]
52
+ then
53
+ return 1
54
+ end
55
+ end
41
56
 
42
- return 0
43
- EOF
44
- )
45
- end
57
+ return 0
58
+ EOF
46
59
  end
47
60
 
48
61
  def acquire_sha(refresh = false)
49
62
  @acquire_sha = nil if refresh
50
63
 
51
- @acquire_sha ||= begin
52
- Resque.redis.script(
53
- :load,
54
- <<-EOF
55
- if redis.call('SETNX', KEYS[1], ARGV[1]) == 1
56
- then
57
- redis.call('EXPIRE', KEYS[1], #{timeout})
58
- return 1
59
- else
60
- return 0
61
- end
62
- EOF
63
- )
64
- end
64
+ @acquire_sha ||=
65
+ Resque.data_store.redis.script(:load, <<-EOF.gsub(/^ {14}/, ''))
66
+ if redis.call('SETNX', KEYS[1], ARGV[1]) == 1
67
+ then
68
+ redis.call('EXPIRE', KEYS[1], #{timeout})
69
+ return 1
70
+ else
71
+ return 0
72
+ end
73
+ EOF
65
74
  end
66
75
  end
67
76
  end
@@ -1,3 +1,4 @@
1
- %w[base basic resilient].each do |file|
1
+ # vim:fileencoding=utf-8
2
+ %w(base basic resilient).each do |file|
2
3
  require "resque/scheduler/lock/#{file}"
3
4
  end
@@ -0,0 +1,104 @@
1
+ # vim:fileencoding=utf-8
2
+
3
+ # ### Locking the scheduler process
4
+ #
5
+ # There are two places in resque-scheduler that need to be synchronized in order
6
+ # to be able to run redundant scheduler processes while ensuring jobs don't get
7
+ # queued multiple times when the master process changes.
8
+ #
9
+ # 1) Processing the delayed queues (jobs that are created from
10
+ # enqueue_at/enqueue_in, etc) 2) Processing the scheduled (cron-like) jobs from
11
+ # rufus-scheduler
12
+ #
13
+ # Protecting the delayed queues (#1) is relatively easy. A simple SETNX in
14
+ # redis would suffice. However, protecting the scheduled jobs is trickier
15
+ # because the clocks on machines could be slightly off or actual firing times
16
+ # could vary slightly due to load. If scheduler A's clock is slightly ahead of
17
+ # scheduler B's clock (since they are on different machines), when scheduler A
18
+ # dies, we need to ensure that scheduler B doesn't queue jobs that A already
19
+ # queued before it's death. (This all assumes that it is better to miss a few
20
+ # scheduled jobs than it is to run them multiple times for the same iteration.)
21
+ #
22
+ # To avoid queuing multiple jobs in the case of master fail-over, the master
23
+ # should remain the master as long as it can rather than a simple SETNX which
24
+ # would result in the master roll being passed around frequently.
25
+ #
26
+ # Locking Scheme: Each resque-scheduler process attempts to get the master lock
27
+ # via SETNX. Once obtained, it sets the expiration for 3 minutes
28
+ # (configurable). The master process continually updates the timeout on the
29
+ # lock key to be 3 minutes in the future in it's loop(s) (see `run`) and when
30
+ # jobs come out of rufus-scheduler (see `load_schedule_job`). That ensures
31
+ # that a minimum of 3 minutes must pass since the last queuing operation before
32
+ # a new master is chosen. If, for whatever reason, the master fails to update
33
+ # the expiration for 3 minutes, the key expires and the lock is up for grabs.
34
+ # If miraculously the original master comes back to life, it will realize it is
35
+ # no longer the master and stop processing jobs.
36
+ #
37
+ # The clocks on the scheduler machines can then be up to 3 minutes off from
38
+ # each other without the risk of queueing the same scheduled job twice during a
39
+ # master change. The catch is, in the event of a master change, no scheduled
40
+ # jobs will be queued during those 3 minutes. So, there is a trade off: the
41
+ # higher the timeout, the less likely scheduled jobs will be fired twice but
42
+ # greater chances of missing scheduled jobs. The lower the timeout, less
43
+ # likely jobs will be missed, greater the chances of jobs firing twice. If you
44
+ # don't care about jobs firing twice or are certain your machines' clocks are
45
+ # well in sync, a lower timeout is preferable. One thing to keep in mind: this
46
+ # only effects *scheduled* jobs - delayed jobs will never be lost or skipped
47
+ # since eventually a master will come online and it will process everything
48
+ # that is ready (no matter how old it is). Scheduled jobs work like cron - if
49
+ # you stop cron, no jobs fire while it's stopped and it doesn't fire jobs that
50
+ # were missed when it starts up again.
51
+
52
+ require_relative 'lock'
53
+
54
+ module Resque
55
+ module Scheduler
56
+ module Locking
57
+ def master_lock
58
+ @master_lock ||= build_master_lock
59
+ end
60
+
61
+ def supports_lua?
62
+ redis_master_version >= 2.5
63
+ end
64
+
65
+ def master?
66
+ master_lock.acquire! || master_lock.locked?
67
+ end
68
+
69
+ def release_master_lock!
70
+ warn "#{self}\#release_master_lock! is deprecated because it does " \
71
+ "not respect lock ownership. Use #{self}\#release_master_lock " \
72
+ "instead (at #{caller.first}"
73
+
74
+ master_lock.release!
75
+ end
76
+
77
+ def release_master_lock
78
+ master_lock.release
79
+ rescue *INTERMITTENT_ERRORS
80
+ @master_lock = nil
81
+ end
82
+
83
+ private
84
+
85
+ def build_master_lock
86
+ if supports_lua?
87
+ Resque::Scheduler::Lock::Resilient.new(master_lock_key)
88
+ else
89
+ Resque::Scheduler::Lock::Basic.new(master_lock_key)
90
+ end
91
+ end
92
+
93
+ def master_lock_key
94
+ lock_prefix = ENV['RESQUE_SCHEDULER_MASTER_LOCK_PREFIX'] || ''
95
+ lock_prefix += ':' if lock_prefix != ''
96
+ "#{Resque.redis.namespace}:#{lock_prefix}resque_scheduler_master_lock"
97
+ end
98
+
99
+ def redis_master_version
100
+ Resque.data_store.redis.info['redis_version'].to_f
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,83 @@
1
+ # vim:fileencoding=utf-8
2
+
3
+ require 'mono_logger'
4
+
5
+ module Resque
6
+ module Scheduler
7
+ # Just builds a logger, with specified verbosity and destination.
8
+ # The simplest example:
9
+ #
10
+ # Resque::Scheduler::LoggerBuilder.new.build
11
+ class LoggerBuilder
12
+ # Initializes new instance of the builder
13
+ #
14
+ # Pass :opts Hash with
15
+ # - :quiet if logger needs to be silent for all levels. Default - false
16
+ # - :verbose if there is a need in debug messages. Default - false
17
+ # - :log_dev to output logs into a desired file. Default - STDOUT
18
+ # - :format log format, either 'text', 'json' or 'logfmt'. Default - 'text'
19
+ #
20
+ # Example:
21
+ #
22
+ # LoggerBuilder.new(
23
+ # :quiet => false, :verbose => true, :log_dev => 'log/scheduler.log'
24
+ # )
25
+ def initialize(opts = {})
26
+ @quiet = !!opts[:quiet]
27
+ @verbose = !!opts[:verbose]
28
+ @log_dev = opts[:log_dev] || $stdout
29
+ @format = opts[:format] || 'text'
30
+ end
31
+
32
+ # Returns an instance of MonoLogger
33
+ def build
34
+ logger = MonoLogger.new(@log_dev)
35
+ logger.progname = 'resque-scheduler'.freeze
36
+ logger.level = level
37
+ logger.formatter = send(:"#{@format}_formatter")
38
+ logger
39
+ end
40
+
41
+ private
42
+
43
+ def level
44
+ if @verbose && !@quiet
45
+ MonoLogger::DEBUG
46
+ elsif !@quiet
47
+ MonoLogger::INFO
48
+ else
49
+ MonoLogger::FATAL
50
+ end
51
+ end
52
+
53
+ def text_formatter
54
+ proc do |severity, datetime, progname, msg|
55
+ "#{progname}: [#{severity}] #{datetime.iso8601}: #{msg}\n"
56
+ end
57
+ end
58
+
59
+ def json_formatter
60
+ proc do |severity, datetime, progname, msg|
61
+ require 'json'
62
+ log_data = {
63
+ name: progname,
64
+ progname: progname,
65
+ level: severity,
66
+ timestamp: datetime.iso8601,
67
+ msg: msg
68
+ }
69
+ JSON.dump(log_data) + "\n"
70
+ end
71
+ end
72
+
73
+ def logfmt_formatter
74
+ proc do |severity, datetime, progname, msg|
75
+ "timestamp=\"#{datetime.iso8601}\" " \
76
+ "level=\"#{severity}\" " \
77
+ "progname=\"#{progname}\" " \
78
+ "msg=\"#{msg}\"\n"
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,31 @@
1
+ # vim:fileencoding=utf-8
2
+
3
+ module Resque
4
+ module Scheduler
5
+ module Plugin
6
+ def self.hooks(job, pattern)
7
+ job.methods.grep(/^#{pattern}/).sort
8
+ end
9
+
10
+ def self.run_hooks(job, pattern, *args)
11
+ results = hooks(job, pattern).map do |hook|
12
+ job.send(hook, *args)
13
+ end
14
+
15
+ results.all? { |result| result != false }
16
+ end
17
+
18
+ def self.run_before_delayed_enqueue_hooks(klass, *args)
19
+ run_hooks(klass, 'before_delayed_enqueue', *args)
20
+ end
21
+
22
+ def self.run_before_schedule_hooks(klass, *args)
23
+ run_hooks(klass, 'before_schedule', *args)
24
+ end
25
+
26
+ def self.run_after_schedule_hooks(klass, *args)
27
+ run_hooks(klass, 'after_schedule', *args)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,142 @@
1
+ # vim:fileencoding=utf-8
2
+
3
+ module Resque
4
+ module Scheduler
5
+ module SchedulingExtensions
6
+ # Accepts a new schedule configuration of the form:
7
+ #
8
+ # {
9
+ # "MakeTea" => {
10
+ # "every" => "1m" },
11
+ # "some_name" => {
12
+ # "cron" => "5/* * * *",
13
+ # "class" => "DoSomeWork",
14
+ # "args" => "work on this string",
15
+ # "description" => "this thing works it"s butter off" },
16
+ # ...
17
+ # }
18
+ #
19
+ # Hash keys can be anything and are used to describe and reference
20
+ # the scheduled job. If the "class" argument is missing, the key
21
+ # is used implicitly as "class" argument - in the "MakeTea" example,
22
+ # "MakeTea" is used both as job name and resque worker class.
23
+ #
24
+ # Any jobs that were in the old schedule, but are not
25
+ # present in the new schedule, will be removed.
26
+ #
27
+ # :cron can be any cron scheduling string
28
+ #
29
+ # :every can be used in lieu of :cron. see rufus-scheduler's 'every'
30
+ # usage for valid syntax. If :cron is present it will take precedence
31
+ # over :every.
32
+ #
33
+ # :class must be a resque worker class. If it is missing, the job name
34
+ # (hash key) will be used as :class.
35
+ #
36
+ # :args can be any yaml which will be converted to a ruby literal and
37
+ # passed in a params. (optional)
38
+ #
39
+ # :rails_env is the list of envs where the job gets loaded. Envs are
40
+ # comma separated (optional)
41
+ #
42
+ # :description is just that, a description of the job (optional). If
43
+ # params is an array, each element in the array is passed as a separate
44
+ # param, otherwise params is passed in as the only parameter to
45
+ # perform.
46
+ def schedule=(schedule_hash)
47
+ @non_persistent_schedules = nil
48
+ prepared_schedules = prepare_schedules(schedule_hash)
49
+
50
+ prepared_schedules.each do |schedule, config|
51
+ set_schedule(schedule, config, false)
52
+ end
53
+
54
+ # ensure only return the successfully saved data!
55
+ reload_schedule!
56
+ end
57
+
58
+ # Returns the schedule hash
59
+ def schedule
60
+ @schedule ||= all_schedules
61
+ @schedule || {}
62
+ end
63
+
64
+ # reloads the schedule from redis and memory
65
+ def reload_schedule!
66
+ @schedule = all_schedules
67
+ end
68
+
69
+ # gets the schedules as it exists in redis
70
+ def all_schedules
71
+ non_persistent_schedules.merge(persistent_schedules)
72
+ end
73
+
74
+ # Create or update a schedule with the provided name and configuration.
75
+ #
76
+ # Note: values for class and custom_job_class need to be strings,
77
+ # not constants.
78
+ #
79
+ # Resque.set_schedule('some_job', {:class => 'SomeJob',
80
+ # :every => '15mins',
81
+ # :queue => 'high',
82
+ # :args => '/tmp/poop'})
83
+ #
84
+ # Preventing a reload is optional and available to batch operations
85
+ def set_schedule(name, config, reload = true)
86
+ persist = config.delete(:persist) || config.delete('persist')
87
+
88
+ if persist
89
+ redis.hset(:persistent_schedules, name, encode(config))
90
+ else
91
+ non_persistent_schedules[name] = decode(encode(config))
92
+ end
93
+
94
+ redis.sadd(:schedules_changed, [name])
95
+ reload_schedule! if reload
96
+ end
97
+
98
+ # retrive the schedule configuration for the given name
99
+ def fetch_schedule(name)
100
+ schedule[name]
101
+ end
102
+
103
+ # remove a given schedule by name
104
+ # Preventing a reload is optional and available to batch operations
105
+ def remove_schedule(name, reload = true)
106
+ non_persistent_schedules.delete(name)
107
+ redis.hdel(:persistent_schedules, name)
108
+ redis.sadd(:schedules_changed, [name])
109
+
110
+ reload_schedule! if reload
111
+ end
112
+
113
+ private
114
+
115
+ # we store our non-persistent schedules in this hash
116
+ def non_persistent_schedules
117
+ @non_persistent_schedules ||= {}
118
+ end
119
+
120
+ # reads the persistent schedules from redis
121
+ def persistent_schedules
122
+ redis.hgetall(:persistent_schedules).tap do |h|
123
+ h.each do |name, config|
124
+ h[name] = decode(config)
125
+ end
126
+ end
127
+ end
128
+
129
+ def prepare_schedules(schedule_hash)
130
+ prepared_hash = {}
131
+ schedule_hash.each do |name, job_spec|
132
+ job_spec = job_spec.dup
133
+ unless job_spec.key?('class') || job_spec.key?(:class)
134
+ job_spec['class'] = name
135
+ end
136
+ prepared_hash[name] = job_spec
137
+ end
138
+ prepared_hash
139
+ end
140
+ end
141
+ end
142
+ end