resque-scheduler 2.2.0 → 4.10.2

Sign up to get free protection for your applications and to get access to all the features.
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