sidekiq-cron 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
@@ -0,0 +1,18 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - jruby-19mode
5
+ - rbx-19mode
6
+ - 2.0.0
7
+ branches:
8
+ only:
9
+ - master
10
+ notifications:
11
+ email:
12
+ recipients:
13
+ - ondrej@bartas.cz
14
+ matrix:
15
+ allow_failures:
16
+ - rvm: jruby-19mode
17
+ - rvm: rbx-19mode
18
+ - rvm: 2.0.0
@@ -0,0 +1,8 @@
1
+
2
+
3
+ v 0.1.1
4
+ -------
5
+
6
+ - add Web fontend with enabled/disable job, unqueue now, delete job
7
+ - add cron poller - enqueu cro jobs
8
+ - add cron job - save all needed data to redis
data/Gemfile ADDED
@@ -0,0 +1,30 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem "sidekiq", ">= 2.13.1"
4
+ gem 'parse-cron', '>= 0.1.2'
5
+
6
+ group :development do
7
+ gem "bundler"
8
+ gem "simplecov"
9
+
10
+ gem 'shoulda-context'
11
+ gem "turn"
12
+
13
+ gem 'rack'
14
+ gem 'rack-test'
15
+
16
+ gem "jeweler", "~> 1.8.3"
17
+
18
+ gem "sdoc" # sdoc -N .
19
+
20
+ gem "slim"
21
+ gem "sinatra"
22
+
23
+ gem 'mocha'
24
+ gem 'coveralls'
25
+
26
+ gem "shotgun"
27
+
28
+ # gem 'guard'
29
+ # gem 'guard-minitest'
30
+ end
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2013 Ondrej Bartas
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,152 @@
1
+ Sidekiq-Cron
2
+ ============
3
+
4
+ [![Build Status](https://travis-ci.org/ondrejbartas/sidekiq-cron.png?branch=master)](https://travis-ci.org/ondrejbartas/sidekiq-cron) [![Coverage Status](https://coveralls.io/repos/ondrejbartas/sidekiq-cron/badge.png?branch=master)](https://coveralls.io/r/ondrejbartas/sidekiq-cron?branch=master)
5
+
6
+ Add-on for [Sidekiq](http://sidekiq.org)
7
+
8
+ Allows you to schedule recurring jobs for sidekiq workers using cron notation _* * * * *_.
9
+
10
+ Requirements
11
+ -----------------
12
+
13
+ - Redis 2.4 or greater is required.
14
+ - Sidekiq 2.13.1 or grater is required.
15
+
16
+
17
+ Installation
18
+ ------------
19
+
20
+ $ gem install sidekiq-cron
21
+
22
+ or add to your Gemfile
23
+
24
+ gem "sidekiq-cron", "~> 0.1.0"
25
+
26
+
27
+ Getting Started
28
+ -----------------
29
+
30
+
31
+ If you are not using Rails you need to add `require 'sidekiq-cron'` somewhere after `require 'sidekiq'`.
32
+
33
+ _Job properties_:
34
+
35
+ ```ruby
36
+ {
37
+ 'name' => 'name_of_job', #must be uniq!
38
+ 'cron' => '1 * * * *',
39
+ 'klass' => 'MyClass',
40
+ #OPTIONAL
41
+ 'queue' => 'name of queue',
42
+ 'args' => '[Array or Hash] of arguments hich will be passed to perform method'
43
+ }
44
+ ```
45
+
46
+ #### Adding Cron job:
47
+ ```ruby
48
+
49
+ class HardWorker
50
+ include Sidekiq::Worker
51
+ def perform(name, count)
52
+ # do something
53
+ end
54
+ end
55
+
56
+ Sidekiq::Cron::Job.create( name: 'Hard worker - every 5min', cron: '*/5 * * * *', klass: 'HardWorker')
57
+ # => true
58
+ ```
59
+
60
+ `create` method will return only true/false if job was saved or not.
61
+
62
+ ```ruby
63
+ job = Sidekiq::Cron::Job.new( name: 'Hard worker - every 5min', cron: '*/5 * * * *', klass: 'HardWorker')
64
+
65
+ if job.valid?
66
+ job.save
67
+ else
68
+ puts job.errors
69
+ end
70
+
71
+ #or simple
72
+
73
+ unless job.save
74
+ puts job.errors #will return array of errors
75
+ end
76
+ ```
77
+
78
+ #### Finding jobs
79
+ ```ruby
80
+ #return array of all jobs
81
+ Sidekiq::Cron::Job.all
82
+
83
+ #return one job by its uniq name - case sensitive
84
+ Sidekiq::Cron::Job.find "Job Name"
85
+
86
+ #return one job by its uniq name - you can use hash with 'name' key
87
+ Sidekiq::Cron::Job.find name: "Job Name"
88
+
89
+ #if job can't be found nil is returned
90
+ ```
91
+
92
+ #### Destroy jobs:
93
+ ```ruby
94
+ #destroys all jobs
95
+ Sidekiq::Cron::Job.destroy_all!
96
+
97
+ #destroy job by its name
98
+ Sidekiq::Cron::Job.destroy "Job Name"
99
+
100
+ #destroy founded job
101
+ Sidekiq::Cron::Job.find('Job name').destroy
102
+ ```
103
+
104
+ #### Work with job:
105
+ ```ruby
106
+ job = Sidekiq::Cron::Job.find('Job name')
107
+
108
+ #disable cron scheduling
109
+ job.disable!
110
+
111
+ #enable cron scheduling
112
+ job.enable!
113
+
114
+ #get status of job:
115
+ job.status
116
+ # => enabled/disabled
117
+
118
+ #enqueue job right now!
119
+ job.enque!
120
+ ```
121
+
122
+ How to start scheduling?
123
+ Just start sidekiq workers by:
124
+
125
+ sidekiq
126
+
127
+ ### Web Ui for Cron Jobs
128
+
129
+ If you are using sidekiq web ui and you would like to add cron josb to web too,
130
+ add `require 'sidekiq-cron'` after `require 'sidekiq/web'`.
131
+ By this you will get:
132
+ ![Web UI](https://github.com/ondrejbartas/sidekiq-cron/raw/master/examples/web-cron-ui.png)
133
+
134
+
135
+
136
+ ## Contributing to sidekiq-cron
137
+
138
+
139
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
140
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
141
+ * Fork the project.
142
+ * Start a feature/bugfix branch.
143
+ * Commit and push until you are happy with your contribution.
144
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
145
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
146
+
147
+
148
+ ## Copyright
149
+
150
+ Copyright (c) 2013 Ondrej Bartas. See LICENSE.txt for
151
+ further details.
152
+
@@ -0,0 +1,61 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "sidekiq-cron"
18
+ gem.homepage = "http://github.com/ondrejbartas/sidekiq-cron"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{Sidekiq Cron helps to add repeated scheduled jobs}
21
+ gem.description = %Q{Enables to set jobs to be run in specified time (using CRON notation)}
22
+ gem.email = "ondrej@bartas.cz"
23
+ gem.authors = ["Ondrej Bartas"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ #TESTING
29
+
30
+ task :doc do
31
+ system 'sdoc -N .'
32
+ end
33
+
34
+ require 'rake/testtask'
35
+ task :default => :test
36
+
37
+ Rake::TestTask.new(:test) do |t|
38
+ t.test_files = FileList['test/functional/**/*_test.rb', 'test/unit/**/*_test.rb','test/integration/**/*_test.rb']
39
+ t.warning = false
40
+ t.verbose = false
41
+ end
42
+
43
+ namespace :test do
44
+ Rake::TestTask.new(:unit) do |t|
45
+ t.test_files = FileList['test/unit/**/*_test.rb']
46
+ t.warning = false
47
+ t.verbose = false
48
+ end
49
+
50
+ Rake::TestTask.new(:functional) do |t|
51
+ t.test_files = FileList['test/functional/**/*_test.rb']
52
+ t.warning = false
53
+ t.verbose = false
54
+ end
55
+
56
+ Rake::TestTask.new(:integration) do |t|
57
+ t.test_files = FileList['test/integration/**/*_test.rb']
58
+ t.warning = false
59
+ t.verbose = false
60
+ end
61
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,14 @@
1
+ require 'sidekiq'
2
+
3
+ Sidekiq.configure_client do |config|
4
+ config.redis = { :size => 1 }
5
+ end
6
+
7
+ require 'sidekiq/web'
8
+
9
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'lib'))
10
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
11
+
12
+ require 'sidekiq-cron'
13
+
14
+ run Sidekiq::Web
Binary file
@@ -0,0 +1,4 @@
1
+
2
+ require "sidekiq"
3
+
4
+ require "sidekiq/cron"
@@ -0,0 +1,29 @@
1
+ begin
2
+ require "sidekiq/web"
3
+ rescue LoadError
4
+ # client-only usage
5
+ end
6
+
7
+ require "sidekiq/cron/job"
8
+ require "sidekiq/cron/web_extension"
9
+
10
+ #require poller only if celluloid is defined
11
+ if defined?(Celluloid)
12
+ require "sidekiq/cron/poller"
13
+ end
14
+
15
+ module Sidekiq
16
+ module Cron
17
+ end
18
+ end
19
+
20
+ if defined?(Sidekiq::Web)
21
+ Sidekiq::Web.register Sidekiq::Cron::WebExtension
22
+
23
+ if Sidekiq::Web.tabs.is_a?(Array)
24
+ # For sidekiq < 2.5
25
+ Sidekiq::Web.tabs << "cron"
26
+ else
27
+ Sidekiq::Web.tabs["Cron"] = "cron"
28
+ end
29
+ end
@@ -0,0 +1,397 @@
1
+ require 'sidekiq'
2
+ require 'sidekiq/util'
3
+ require 'sidekiq/actor'
4
+ require 'parse-cron'
5
+
6
+ module Sidekiq
7
+ module Cron
8
+
9
+ class Job
10
+ include Util
11
+ extend Util
12
+
13
+ #how long we would like to store informations about previous enqueues
14
+ REMEMBER_THRESHOLD = 24 * 60 * 60
15
+
16
+ #crucial part of whole enquing job
17
+ def should_enque? time
18
+ out = false
19
+ Sidekiq.redis do |conn|
20
+ out = (
21
+ status == "enabled" &&
22
+ @last_run_time < last_time(time) &&
23
+ conn.zadd(job_enqueued_key, time.to_f.to_s, formated_last_time(time) )
24
+ )
25
+ end
26
+ out
27
+ end
28
+
29
+ # remove previous informations about run times
30
+ # this will clear redis and make sure that redis will
31
+ # not overflow with memory
32
+ def remove_previous_enques time
33
+ Sidekiq.redis do |conn|
34
+ conn.zremrangebyscore(job_enqueued_key, 0, "(#{(time.to_f - REMEMBER_THRESHOLD).to_s}")
35
+ end
36
+ end
37
+
38
+ #test if job should be enqued If yes add it to queue
39
+ def test_and_enque_for_time! time
40
+ #should this job be enqued?
41
+ if should_enque?(time)
42
+ enque!
43
+
44
+ remove_previous_enques(time)
45
+ end
46
+ end
47
+
48
+ #enque cron job to queue
49
+ def enque! time = Time.now
50
+ @last_run_time = time
51
+
52
+ Sidekiq::Client.push(@message.is_a?(String) ? Sidekiq.load_json(@message) : @message)
53
+
54
+ save
55
+ logger.debug { "enqueued #{@name}: #{@message}" }
56
+ end
57
+
58
+ # load cron jobs from Hash
59
+ # input structure should look like:
60
+ # {
61
+ # 'name_of_job' => {
62
+ # 'class' => 'MyClass',
63
+ # 'cron' => '1 * * * *',
64
+ # 'args' => '(OPTIONAL) [Array or Hash]'
65
+ # },
66
+ # 'My super iber cool job' => {
67
+ # 'class' => 'SecondClass',
68
+ # 'cron' => '*/5 * * * *'
69
+ # }
70
+ # }
71
+ #
72
+ def self.load_from_hash hash
73
+ array = hash.inject([]) do |out,(key, job)|
74
+ job['name'] = key
75
+ out << job
76
+ end
77
+ load_from_array array
78
+ end
79
+
80
+
81
+ # load cron jobs from Array
82
+ # input structure should look like:
83
+ # [
84
+ # {
85
+ # 'name' => 'name_of_job',
86
+ # 'class' => 'MyClass',
87
+ # 'cron' => '1 * * * *',
88
+ # 'args' => '(OPTIONAL) [Array or Hash]'
89
+ # },
90
+ # {
91
+ # 'name' => 'Cool Job for Second Class',
92
+ # 'class' => 'SecondClass',
93
+ # 'cron' => '*/5 * * * *'
94
+ # }
95
+ # ]
96
+ #
97
+ def self.load_from_array array
98
+ errors = {}
99
+ array.each do |job_data|
100
+ job = new(job_data)
101
+ errors[job.name] = job.errors unless job.save
102
+ end
103
+ errors
104
+ end
105
+
106
+
107
+ # get all cron jobs
108
+ def self.all
109
+ out = []
110
+ Sidekiq.redis do |conn|
111
+ out = conn.smembers(jobs_key).collect do |key|
112
+ if conn.exists key
113
+ Job.new conn.hgetall(key)
114
+ else
115
+ nil
116
+ end
117
+ end
118
+ end
119
+ out.select{|j| !j.nil? }
120
+ end
121
+
122
+ def self.count
123
+ out = 0
124
+ Sidekiq.redis do |conn|
125
+ out = conn.scard(jobs_key)
126
+ end
127
+ out
128
+ end
129
+
130
+ def self.find name
131
+ #if name is hash try to get name from it
132
+ name = name[:name] || name['name'] if name.is_a?(Hash)
133
+
134
+ output = nil
135
+ Sidekiq.redis do |conn|
136
+ if exists? name
137
+ output = Job.new conn.hgetall( redis_key(name) )
138
+ end
139
+ end
140
+ output
141
+ end
142
+
143
+ # create new instance of cron job
144
+ def self.create hash
145
+ new(hash).save
146
+ end
147
+
148
+ #destroy job by name
149
+ def self.destroy name
150
+ #if name is hash try to get name from it
151
+ name = name[:name] || name['name'] if name.is_a?(Hash)
152
+
153
+ if job = find(name)
154
+ job.destroy
155
+ else
156
+ false
157
+ end
158
+ end
159
+
160
+ attr_accessor :name, :cron, :klass, :args, :message
161
+ attr_reader :last_run_time
162
+
163
+ def initialize input_args = {}
164
+ args = input_args.stringify_keys
165
+
166
+ @name = args["name"]
167
+ @cron = args["cron"]
168
+
169
+ #get class from klass or class
170
+ @klass = args["klass"] || args["class"]
171
+
172
+ #set status of job
173
+ @status = args['status'] || status_from_redis
174
+
175
+ #set last run time
176
+ @last_run_time = Time.parse(args['last_run_time'].to_s) rescue Time.now
177
+
178
+ #get right arguments for job
179
+ @args = args["args"].nil? ? [] : (args["args"].is_a?(Array) ? args["args"] : [ args["args"] ])
180
+
181
+ if args["message"]
182
+ @message = args["message"]
183
+ elsif @klass
184
+ message_data = {
185
+ "class" => @klass.to_s,
186
+ "args" => @args,
187
+ }
188
+
189
+ #get right data for message
190
+ #only if message wasn't specified before
191
+ message_data = case @klass
192
+ when Class
193
+ @klass.get_sidekiq_options.merge(message_data)
194
+ when String
195
+ begin
196
+ @klass.constantize.get_sidekiq_options.merge(message_data)
197
+ rescue
198
+ #Unknown class
199
+ message_data.merge("queue"=>"default")
200
+ end
201
+
202
+ end
203
+
204
+ #override queue if setted in config
205
+ #only if message is hash - can be string (dumped JSON)
206
+ message_data['queue'] = args['queue'] if args['queue']
207
+
208
+ #dump message as json
209
+ @message = message_data
210
+ end
211
+
212
+ end
213
+
214
+ def status
215
+ @status
216
+ end
217
+
218
+ def disable!
219
+ @status = "disabled"
220
+ save
221
+ end
222
+
223
+ def enable!
224
+ @status = "enabled"
225
+ save
226
+ end
227
+
228
+ def status_from_redis
229
+ if exists?
230
+ out = "enabled"
231
+ Sidekiq.redis do |conn|
232
+ out = conn.hget redis_key, "status"
233
+ end
234
+ out
235
+ else
236
+ "enabled"
237
+ end
238
+ end
239
+
240
+ #export job data to hash
241
+ def to_hash
242
+ {
243
+ name: @name,
244
+ klass: @klass,
245
+ cron: @cron,
246
+ args: @args.is_a?(String) ? @args : Sidekiq.dump_json(@args || []),
247
+ message: @message.is_a?(String) ? @message : Sidekiq.dump_json(@message || {}),
248
+ status: @status,
249
+ last_run_time: @last_run_time,
250
+ }
251
+ end
252
+
253
+ def errors
254
+ @errors ||= []
255
+ end
256
+
257
+ def valid?
258
+ #clear previos errors
259
+ @errors = []
260
+
261
+ errors << "'name' must be set" if @name.nil? || @name.size == 0
262
+ if @cron.nil? || @cron.size == 0
263
+ errors << "'cron' must be set"
264
+ else
265
+ begin
266
+ cron = CronParser.new(@cron)
267
+ cron.next(Time.now)
268
+ rescue Exception => e
269
+ errors << "'cron' -> #{@cron}: #{e.message}"
270
+ end
271
+ end
272
+
273
+ errors << "'klass' (or class) must be set" if @klass.nil? || @klass.size == 0
274
+
275
+ !errors.any?
276
+ end
277
+
278
+ # add job to cron jobs
279
+ # input:
280
+ # name: (string) - name of job
281
+ # cron: (string: '* * * * *' - cron specification when to run job
282
+ # class: (string|class) - which class to perform
283
+ # optional input:
284
+ # queue: (string) - which queue to use for enquing (will override class queue)
285
+ # args: (array|hash|nil) - arguments for permorm method
286
+
287
+ def save
288
+ #if job is invalid return false
289
+ return false unless valid?
290
+
291
+ Sidekiq.redis do |conn|
292
+
293
+ #add to set of all jobs
294
+ conn.sadd self.class.jobs_key, redis_key
295
+
296
+ #add informations for this job!
297
+ conn.hmset redis_key, *hash_to_redis(to_hash)
298
+
299
+ #add information about last time! - don't enque right after scheduler poller starts!
300
+ time = Time.now
301
+ conn.zadd(job_enqueued_key, time.to_f.to_s, formated_last_time(time).to_s)
302
+ end
303
+ logger.info { "Cron Jobs - add job with name: #{@name}" }
304
+ end
305
+
306
+ # remove job from cron jobs by name
307
+ # input:
308
+ # first arg: name (string) - name of job (must be same - case sensitive)
309
+ def destroy
310
+ Sidekiq.redis do |conn|
311
+ #delete from set
312
+ conn.srem self.class.jobs_key, redis_key
313
+
314
+ #delete runned timestamps
315
+ conn.del job_enqueued_key
316
+
317
+ #delete main job
318
+ conn.del redis_key
319
+ end
320
+ logger.info { "Cron Jobs - deleted job with name: #{@name}" }
321
+ end
322
+
323
+ # remove all job from cron
324
+ def self.destroy_all!
325
+ all.each do |job|
326
+ job.destroy
327
+ end
328
+ logger.info { "Cron Jobs - deleted all jobs" }
329
+ end
330
+
331
+ # Parse cron specification '* * * * *' and returns
332
+ # time when last run should be performed
333
+ def last_time now = Time.now
334
+ # add 1 minute to Time now - Cron parser return last time after minute ends,
335
+ # so by adding 60 second we will get last time after the right time happens
336
+ # without any delay!
337
+ CronParser.new(@cron).last(now + 60)
338
+ end
339
+
340
+ def formated_last_time now = Time.now
341
+ last_time(now).getutc
342
+ end
343
+
344
+ def self.exists? name
345
+ out = false
346
+ Sidekiq.redis do |conn|
347
+ out = conn.exists redis_key name
348
+ end
349
+ out
350
+ end
351
+
352
+ def exists?
353
+ self.class.exists? @name
354
+ end
355
+
356
+ def sort_name
357
+ "#{status == "enabled" ? 0 : 1}_#{name}".downcase
358
+ end
359
+
360
+ private
361
+
362
+ # Redis key for set of all cron jobs
363
+ def self.jobs_key
364
+ "cron_jobs"
365
+ end
366
+
367
+ # Redis key for storing one cron job
368
+ def self.redis_key name
369
+ "cron_job:#{name}"
370
+ end
371
+
372
+ # Redis key for storing one cron job
373
+ def redis_key
374
+ self.class.redis_key @name
375
+ end
376
+
377
+ # Redis key for storing one cron job run times
378
+ # (when poller added job to queue)
379
+ def self.job_enqueued_key name
380
+ "cron_job:#{name}:enqueued"
381
+ end
382
+
383
+ # Redis key for storing one cron job run times
384
+ # (when poller added job to queue)
385
+ def job_enqueued_key
386
+ self.class.job_enqueued_key @name
387
+ end
388
+
389
+ # Give Hash
390
+ # returns array for using it for redis.hmset
391
+ def hash_to_redis hash
392
+ hash.inject([]){ |arr,kv| arr + [kv[0], kv[1]] }
393
+ end
394
+
395
+ end
396
+ end
397
+ end