dynamodb-sidekiq-scheduler 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,80 @@
1
+ module SidekiqScheduler
2
+ class Config
3
+ # We have to set the default as nil because the scheduler could be instantiated without
4
+ # passing the sidekiq config, and in those scenarios we don't want to fail
5
+ def initialize(sidekiq_config: nil, without_defaults: false)
6
+ @sidekiq_config = sidekiq_config
7
+ @scheduler_config = fetch_scheduler_config(sidekiq_config, without_defaults)
8
+ end
9
+
10
+ def enabled?
11
+ scheduler_config[:enabled]
12
+ end
13
+
14
+ def enabled=(value)
15
+ scheduler_config[:enabled] = value
16
+ end
17
+
18
+ def dynamic?
19
+ scheduler_config[:dynamic]
20
+ end
21
+
22
+ def dynamic=(value)
23
+ scheduler_config[:dynamic] = value
24
+ end
25
+
26
+ def dynamic_every?
27
+ scheduler_config[:dynamic_every]
28
+ end
29
+
30
+ def dynamic_every=(value)
31
+ scheduler_config[:dynamic_every] = value
32
+ end
33
+
34
+ def schedule
35
+ scheduler_config[:schedule]
36
+ end
37
+
38
+ def schedule=(value)
39
+ scheduler_config[:schedule] = value
40
+ end
41
+
42
+ def listened_queues_only?
43
+ scheduler_config[:listened_queues_only]
44
+ end
45
+
46
+ def listened_queues_only=(value)
47
+ scheduler_config[:listened_queues_only] = value
48
+ end
49
+
50
+ def rufus_scheduler_options
51
+ scheduler_config[:rufus_scheduler_options]
52
+ end
53
+
54
+ def rufus_scheduler_options=(value)
55
+ scheduler_config[:rufus_scheduler_options] = value
56
+ end
57
+
58
+ def sidekiq_queues
59
+ SidekiqScheduler::SidekiqAdapter.sidekiq_queues(sidekiq_config)
60
+ end
61
+
62
+ private
63
+
64
+ attr_reader :scheduler_config
65
+ attr_reader :sidekiq_config
66
+
67
+ DEFAULT_OPTIONS = {
68
+ enabled: true,
69
+ dynamic: false,
70
+ dynamic_every: '5s',
71
+ schedule: {},
72
+ rufus_scheduler_options: {}
73
+ }.freeze
74
+
75
+ def fetch_scheduler_config(sidekiq_config, without_defaults)
76
+ conf = SidekiqScheduler::SidekiqAdapter.fetch_scheduler_config_from_sidekiq(sidekiq_config)
77
+ without_defaults ? conf : DEFAULT_OPTIONS.merge(conf)
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,4 @@
1
+ require 'sidekiq'
2
+ require_relative '../schedule'
3
+
4
+ Sidekiq.extend SidekiqScheduler::Schedule
@@ -0,0 +1,14 @@
1
+ require 'sidekiq/web' unless defined?(Sidekiq::Web)
2
+
3
+ ASSETS_PATH = File.expand_path('../../../web/assets', __dir__)
4
+
5
+ Sidekiq::Web.register(SidekiqScheduler::Web)
6
+ Sidekiq::Web.tabs['recurring_jobs'] = 'recurring-jobs'
7
+ Sidekiq::Web.locales << File.expand_path("#{File.dirname(__FILE__)}/../../../web/locales")
8
+
9
+ if Sidekiq::VERSION >= '6.0.0'
10
+ Sidekiq::Web.use Rack::Static, urls: ['/stylesheets-scheduler'],
11
+ root: ASSETS_PATH,
12
+ cascade: true,
13
+ header_rules: [[:all, { 'Cache-Control' => 'public, max-age=86400' }]]
14
+ end
@@ -0,0 +1,74 @@
1
+ begin
2
+ require 'sidekiq/web/helpers'
3
+ rescue LoadError
4
+ require 'sidekiq/web_helpers'
5
+ end
6
+ require 'sidekiq-scheduler/redis_manager'
7
+
8
+ module SidekiqScheduler
9
+ class JobPresenter
10
+ attr_reader :name
11
+
12
+ include Sidekiq::WebHelpers
13
+
14
+ def initialize(name, attributes)
15
+ @name = name
16
+ @attributes = attributes
17
+ end
18
+
19
+ # Returns the next time execution for the job
20
+ #
21
+ # @return [String] with the job's next time
22
+ def next_time
23
+ execution_time = SidekiqScheduler::RedisManager.get_job_next_time(name)
24
+
25
+ relative_time(Time.parse(execution_time)) if execution_time
26
+ end
27
+
28
+ # Returns the last execution time for the job
29
+ #
30
+ # @return [String] with the job's last time
31
+ def last_time
32
+ execution_time = SidekiqScheduler::RedisManager.get_job_last_time(name)
33
+
34
+ relative_time(Time.parse(execution_time)) if execution_time
35
+ end
36
+
37
+ # Returns the interval for the job
38
+ #
39
+ # @return [String] with the job's interval
40
+ def interval
41
+ @attributes['cron'] || @attributes['interval'] || @attributes['every']
42
+ end
43
+
44
+ # Returns the queue of the job
45
+ #
46
+ # @return [String] with the job's queue
47
+ def queue
48
+ @attributes.fetch('queue', 'default')
49
+ end
50
+
51
+ # Delegates the :[] method to the attributes' hash
52
+ #
53
+ # @return [String] with the value for that key
54
+ def [](key)
55
+ @attributes[key]
56
+ end
57
+
58
+ def enabled?
59
+ SidekiqScheduler::Scheduler.job_enabled?(@name)
60
+ end
61
+
62
+ # Builds the presenter instances for the schedule hash
63
+ #
64
+ # @param schedule_hash [Hash] with the redis schedule
65
+ # @return [Array<JobPresenter>] an array with the instances of presenters
66
+ def self.build_collection(schedule_hash)
67
+ schedule_hash ||= {}
68
+
69
+ schedule_hash.sort.map do |name, job_spec|
70
+ new(name, job_spec)
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,44 @@
1
+ require 'sidekiq-scheduler/schedule'
2
+ require 'sidekiq-scheduler/scheduler'
3
+
4
+ module SidekiqScheduler
5
+
6
+ # The delayed job router in the system. This
7
+ # manages the scheduled jobs pushed messages
8
+ # from Redis onto the work queues
9
+ #
10
+ class Manager
11
+ def initialize(config)
12
+ set_current_scheduler_options(config)
13
+
14
+ @scheduler_instance = SidekiqScheduler::Scheduler.new(config)
15
+ SidekiqScheduler::Scheduler.instance = @scheduler_instance
16
+ Sidekiq.schedule = config.schedule if @scheduler_instance.enabled
17
+ end
18
+
19
+ def stop
20
+ @scheduler_instance.clear_schedule!
21
+ end
22
+
23
+ def start
24
+ @scheduler_instance.load_schedule!
25
+ end
26
+
27
+ private
28
+
29
+ def set_current_scheduler_options(config)
30
+ enabled = SidekiqScheduler::Scheduler.enabled
31
+ dynamic = SidekiqScheduler::Scheduler.dynamic
32
+ dynamic_every = SidekiqScheduler::Scheduler.dynamic_every
33
+ listened_queues_only = SidekiqScheduler::Scheduler.listened_queues_only
34
+
35
+ config.enabled = enabled unless enabled.nil?
36
+ config.dynamic = dynamic unless dynamic.nil?
37
+ config.dynamic_every = dynamic_every unless dynamic_every.nil?
38
+ unless Sidekiq.schedule.nil? || (Sidekiq.schedule.respond_to?(:empty?) && Sidekiq.schedule.empty?)
39
+ config.schedule = Sidekiq.schedule
40
+ end
41
+ config.listened_queues_only = listened_queues_only unless listened_queues_only.nil?
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,241 @@
1
+ module SidekiqScheduler
2
+ module RedisManager
3
+
4
+ REGISTERED_JOBS_THRESHOLD_IN_SECONDS = 24 * 60 * 60
5
+
6
+ # Returns the schedule of a given job
7
+ #
8
+ # @param [String] name The name of the job
9
+ #
10
+ # @return [String] schedule in JSON format
11
+ def self.get_job_schedule(name)
12
+ hget(schedules_key, name)
13
+ end
14
+
15
+ # Returns the state of a given job
16
+ #
17
+ # @param [String] name The name of the job
18
+ #
19
+ # @return [String] state in JSON format
20
+ def self.get_job_state(name)
21
+ hget(schedules_state_key, name)
22
+ end
23
+
24
+ # Returns the next execution time for a given job
25
+ #
26
+ # @param [String] name The name of the job
27
+ #
28
+ # @return [String] next time the job has to be executed
29
+ def self.get_job_next_time(name)
30
+ hget(next_times_key, name)
31
+ end
32
+
33
+ # Returns the last execution time of a given job
34
+ #
35
+ # @param [String] name The name of the job
36
+ #
37
+ # @return [String] last time the job was executed
38
+ def self.get_job_last_time(name)
39
+ hget(last_times_key, name)
40
+ end
41
+
42
+ # Sets the schedule for a given job
43
+ #
44
+ # @param [String] name The name of the job
45
+ # @param [Hash] config The new schedule for the job
46
+ def self.set_job_schedule(name, config)
47
+ hset(schedules_key, name, JSON.generate(config))
48
+ end
49
+
50
+ # Sets the state for a given job
51
+ #
52
+ # @param [String] name The name of the job
53
+ # @param [Hash] state The new state for the job
54
+ def self.set_job_state(name, state)
55
+ hset(schedules_state_key, name, JSON.generate(state))
56
+ end
57
+
58
+ # Sets the next execution time for a given job
59
+ #
60
+ # @param [String] name The name of the job
61
+ # @param [String] next_time The next time the job has to be executed
62
+ def self.set_job_next_time(name, next_time)
63
+ hset(next_times_key, name, String(next_time))
64
+ end
65
+
66
+ # Sets the last execution time for a given job
67
+ #
68
+ # @param [String] name The name of the job
69
+ # @param [String] last_time The last time the job was executed
70
+ def self.set_job_last_time(name, last_time)
71
+ hset(last_times_key, name, String(last_time))
72
+ end
73
+
74
+ # Removes the schedule for a given job
75
+ #
76
+ # @param [String] name The name of the job
77
+ def self.remove_job_schedule(name)
78
+ hdel(schedules_key, name)
79
+ end
80
+
81
+ # Removes the next execution time for a given job
82
+ #
83
+ # @param [String] name The name of the job
84
+ def self.remove_job_next_time(name)
85
+ hdel(next_times_key, name)
86
+ end
87
+
88
+ # Returns the schedules of all the jobs
89
+ #
90
+ # @return [Hash] hash with all the job schedules
91
+ def self.get_all_schedules
92
+ Sidekiq.redis { |r| r.hgetall(schedules_key) }
93
+ end
94
+
95
+ # Returns boolean value that indicates if the schedules value exists
96
+ #
97
+ # @return [Boolean] true if the schedules key is set, false otherwise
98
+ def self.schedule_exist?
99
+ SidekiqScheduler::SidekiqAdapter.redis_key_exists?(schedules_key)
100
+ end
101
+
102
+ # Returns all the schedule changes for a given time range.
103
+ #
104
+ # @param [Float] from The minimum value in the range
105
+ # @param [Float] to The maximum value in the range
106
+ #
107
+ # @return [Array] array with all the changed job names
108
+ def self.get_schedule_changes(from, to)
109
+ Sidekiq.redis { |r| r.zrangebyscore(schedules_changed_key, from, "(#{to}") }
110
+ end
111
+
112
+ # Register a schedule change for a given job
113
+ #
114
+ # @param [String] name The name of the job
115
+ def self.add_schedule_change(name)
116
+ Sidekiq.redis { |r| r.zadd(schedules_changed_key, Time.now.to_f, name) }
117
+ end
118
+
119
+ # Remove all the schedule changes records
120
+ def self.clean_schedules_changed
121
+ Sidekiq.redis { |r| r.del(schedules_changed_key) unless r.type(schedules_changed_key) == 'zset' }
122
+ end
123
+
124
+ # Removes a queued job instance
125
+ #
126
+ # @param [String] job_name The name of the job
127
+ # @param [Time] time The time at which the job was cleared by the scheduler
128
+ #
129
+ # @return [Boolean] true if the job was registered, false otherwise
130
+ def self.register_job_instance(job_name, time)
131
+ job_key = pushed_job_key(job_name)
132
+ registered, _ = Sidekiq.redis do |r|
133
+ r.pipelined do |pipeline|
134
+ pipeline.zadd(job_key, time.to_i, time.to_i)
135
+ pipeline.expire(job_key, REGISTERED_JOBS_THRESHOLD_IN_SECONDS)
136
+ end
137
+ end
138
+
139
+ registered.instance_of?(Integer) ? (registered > 0) : registered
140
+ end
141
+
142
+ # Removes instances of the job older than 24 hours
143
+ #
144
+ # @param [String] job_name The name of the job
145
+ def self.remove_elder_job_instances(job_name)
146
+ seconds_ago = Time.now.to_i - REGISTERED_JOBS_THRESHOLD_IN_SECONDS
147
+
148
+ Sidekiq.redis do |r|
149
+ r.zremrangebyscore(pushed_job_key(job_name), 0, seconds_ago)
150
+ end
151
+ end
152
+
153
+ # Returns the key of the Redis sorted set used to store job enqueues
154
+ #
155
+ # @param [String] job_name The name of the job
156
+ #
157
+ # @return [String] the pushed job key
158
+ def self.pushed_job_key(job_name)
159
+ "#{key_prefix}sidekiq-scheduler:pushed:#{job_name}"
160
+ end
161
+
162
+ # Returns the key of the Redis hash for job's execution times hash
163
+ #
164
+ # @return [String] with the key
165
+ def self.next_times_key
166
+ "#{key_prefix}sidekiq-scheduler:next_times"
167
+ end
168
+
169
+ # Returns the key of the Redis hash for job's last execution times hash
170
+ #
171
+ # @return [String] with the key
172
+ def self.last_times_key
173
+ "#{key_prefix}sidekiq-scheduler:last_times"
174
+ end
175
+
176
+ # Returns the Redis's key for saving schedule states.
177
+ #
178
+ # @return [String] with the key
179
+ def self.schedules_state_key
180
+ "#{key_prefix}sidekiq-scheduler:states"
181
+ end
182
+
183
+ # Returns the Redis's key for saving schedules.
184
+ #
185
+ # @return [String] with the key
186
+ def self.schedules_key
187
+ "#{key_prefix}schedules"
188
+ end
189
+
190
+ # Returns the Redis's key for saving schedule changes.
191
+ #
192
+ # @return [String] with the key
193
+ def self.schedules_changed_key
194
+ "#{key_prefix}schedules_changed"
195
+ end
196
+
197
+ # Returns the key prefix used to generate all scheduler keys
198
+ #
199
+ # @return [String] with the key prefix
200
+ def self.key_prefix
201
+ @key_prefix
202
+ end
203
+
204
+ # Sets the key prefix used to scope all scheduler keys
205
+ #
206
+ # @param [String] value The string to use as the prefix. A ":" will be appended as a delimiter if needed.
207
+ def self.key_prefix=(value)
208
+ value = "#{value}:" if value && !%w[. :].include?(value[-1])
209
+ @key_prefix = value
210
+ end
211
+
212
+ private
213
+
214
+ # Returns the value of a Redis stored hash field
215
+ #
216
+ # @param [String] hash_key The key name of the hash
217
+ # @param [String] field_key The key name of the field
218
+ #
219
+ # @return [String]
220
+ def self.hget(hash_key, field_key)
221
+ Sidekiq.redis { |r| r.hget(hash_key, field_key) }
222
+ end
223
+
224
+ # Sets the value of a Redis stored hash field
225
+ #
226
+ # @param [String] hash_key The key name of the hash
227
+ # @param [String] field_key The key name of the field
228
+ # @param [String] value The new value name for the field
229
+ def self.hset(hash_key, field_key, value)
230
+ Sidekiq.redis { |r| r.hset(hash_key, field_key, value) }
231
+ end
232
+
233
+ # Removes the value of a Redis stored hash field
234
+ #
235
+ # @param [String] hash_key The key name of the hash
236
+ # @param [String] field_key The key name of the field
237
+ def self.hdel(hash_key, field_key)
238
+ Sidekiq.redis { |r| r.hdel(hash_key, field_key) }
239
+ end
240
+ end
241
+ end
@@ -0,0 +1,29 @@
1
+ require 'sidekiq-scheduler/utils'
2
+
3
+ module SidekiqScheduler
4
+ class RufusUtils
5
+
6
+ # Normalizes schedule options to rufus scheduler options
7
+ #
8
+ # @param options [String, Array]
9
+ #
10
+ # @return [Array]
11
+ #
12
+ # @example
13
+ # normalize_schedule_options('15m') => ['15m', {}]
14
+ # normalize_schedule_options(['15m']) => ['15m', {}]
15
+ # normalize_schedule_options(['15m', first_in: '5m']) => ['15m', { first_in: '5m' }]
16
+ def self.normalize_schedule_options(options)
17
+ schedule, opts = options
18
+
19
+ if !opts.is_a?(Hash)
20
+ opts = {}
21
+ end
22
+
23
+ opts = SidekiqScheduler::Utils.symbolize_keys(opts)
24
+
25
+ return schedule, opts
26
+ end
27
+ end
28
+ end
29
+
@@ -0,0 +1,154 @@
1
+ require 'json'
2
+
3
+ require 'sidekiq-scheduler/utils'
4
+ require_relative 'redis_manager'
5
+
6
+ module SidekiqScheduler
7
+ module Schedule
8
+
9
+ # Accepts a new schedule configuration of the form:
10
+ #
11
+ # {
12
+ # "MakeTea" => {
13
+ # "every" => "1m" },
14
+ # "some_name" => {
15
+ # "cron" => "5/* * * *",
16
+ # "class" => "DoSomeWork",
17
+ # "args" => "work on this string",
18
+ # "description" => "this thing works it"s butter off" },
19
+ # ...
20
+ # }
21
+ #
22
+ # Hash keys can be anything and are used to describe and reference
23
+ # the scheduled job. If the "class" argument is missing, the key
24
+ # is used implicitly as "class" argument - in the "MakeTea" example,
25
+ # "MakeTea" is used both as job name and sidekiq worker class.
26
+ #
27
+ # :cron can be any cron scheduling string
28
+ #
29
+ # :every can be used in lieu of :cron. see rufus-scheduler's 'every' usage
30
+ # for valid syntax. If :cron is present it will take precedence over :every.
31
+ #
32
+ # :class must be a sidekiq worker class. If it is missing, the job name (hash key)
33
+ # will be used as :class.
34
+ #
35
+ # :args can be any yaml which will be converted to a ruby literal and
36
+ # passed in a params. (optional)
37
+ #
38
+ # :description is just that, a description of the job (optional). If
39
+ # params is an array, each element in the array is passed as a separate
40
+ # param, otherwise params is passed in as the only parameter to perform.
41
+ def schedule=(schedule_hash)
42
+ schedule_hash = prepare_schedule(schedule_hash)
43
+ to_remove = get_all_schedules.keys - schedule_hash.keys.map(&:to_s)
44
+
45
+ schedule_hash.each do |name, job_spec|
46
+ set_schedule(name, job_spec)
47
+ end
48
+
49
+ to_remove.each do |name|
50
+ remove_schedule(name)
51
+ end
52
+
53
+ @schedule = schedule_hash
54
+ end
55
+
56
+ def schedule
57
+ @schedule
58
+ end
59
+
60
+ # Reloads the schedule from Redis and return it.
61
+ #
62
+ # @return Hash
63
+ def reload_schedule!
64
+ @schedule = get_schedule
65
+ end
66
+ alias_method :schedule!, :reload_schedule!
67
+
68
+ # Retrive the schedule configuration for the given name
69
+ # if the name is nil it returns a hash with all the
70
+ # names end their schedules.
71
+ def get_schedule(name = nil)
72
+ if name.nil?
73
+ get_all_schedules
74
+ else
75
+ encoded_schedule = SidekiqScheduler::RedisManager.get_job_schedule(name)
76
+ encoded_schedule.nil? ? nil : JSON.parse(encoded_schedule)
77
+ end
78
+ end
79
+
80
+ # gets the schedule as it exists in redis
81
+ def get_all_schedules
82
+ schedules = {}
83
+
84
+ if SidekiqScheduler::RedisManager.schedule_exist?
85
+ SidekiqScheduler::RedisManager.get_all_schedules.tap do |h|
86
+ h.each do |name, config|
87
+ schedules[name] = JSON.parse(config)
88
+ end
89
+ end
90
+ end
91
+
92
+ schedules
93
+ end
94
+
95
+ # Create or update a schedule with the provided name and configuration.
96
+ #
97
+ # Note: values for class and custom_job_class need to be strings,
98
+ # not constants.
99
+ #
100
+ # Sidekiq.set_schedule('some_job', { :class => 'SomeJob',
101
+ # :every => '15mins',
102
+ # :queue => 'high',
103
+ # :args => '/tmp/poop' })
104
+ def set_schedule(name, config)
105
+ existing_config = get_schedule(name)
106
+ unless existing_config && existing_config == config
107
+ SidekiqScheduler::RedisManager.set_job_schedule(name, config)
108
+ SidekiqScheduler::RedisManager.add_schedule_change(name)
109
+ end
110
+ config
111
+ end
112
+
113
+ # remove a given schedule by name
114
+ def remove_schedule(name)
115
+ SidekiqScheduler::RedisManager.remove_job_schedule(name)
116
+ SidekiqScheduler::RedisManager.add_schedule_change(name)
117
+ end
118
+
119
+ private
120
+
121
+ def prepare_schedule(schedule_hash)
122
+ schedule_hash = SidekiqScheduler::Utils.stringify_keys(schedule_hash)
123
+
124
+ prepared_hash = {}
125
+
126
+ schedule_hash.each do |name, job_spec|
127
+ job_spec = job_spec.dup
128
+
129
+ job_class = job_spec.fetch('class', name)
130
+ inferred_queue = infer_queue(job_class)
131
+
132
+ job_spec['class'] ||= job_class
133
+ job_spec['queue'] ||= inferred_queue unless inferred_queue.nil?
134
+
135
+ prepared_hash[name] = job_spec
136
+ end
137
+ prepared_hash
138
+ end
139
+
140
+ def infer_queue(klass)
141
+ klass = try_to_constantize(klass)
142
+
143
+ # ActiveJob uses queue_as when the job is created
144
+ # to determine the queue
145
+ if klass.respond_to?(:sidekiq_options) && !SidekiqScheduler::Utils.active_job_enqueue?(klass)
146
+ klass.sidekiq_options['queue']
147
+ end
148
+ end
149
+
150
+ def try_to_constantize(klass)
151
+ SidekiqScheduler::Utils.try_to_constantize(klass)
152
+ end
153
+ end
154
+ end