dynamodb-sidekiq-scheduler 0.0.1

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.
@@ -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