Compare commits

...

12 Commits

Author SHA1 Message Date
Dzmitry Plashchynski
d31aaaffbe v8.8.0.pre 2015-04-13 15:54:00 +03:00
Dzmitry Plashchynski
6d41a19212 Handle a few jobs scheduled at the same time 2015-04-13 15:53:20 +03:00
Dzmitry Plashchynski
a28ec7b276 Add specs for bug with jobs scheduled at same time without at 2015-04-13 14:19:34 +03:00
Dzmitry Plashchynski
e8c7400caa Do not freeze time for period specs 2015-04-13 14:18:55 +03:00
Dzmitry Plashchynski
260cf14e95 Fix Cli specs 2015-04-13 14:18:32 +03:00
Dzmitry Plashchynski
1900a06582 Add Crono::Cronotab to process cronotab 2015-04-13 12:43:52 +03:00
Dzmitry Plashchynski
8174f86407 Fix crono:clean task 2015-04-13 12:29:27 +03:00
Dzmitry Plashchynski
c8f9ff4e34 v0.8.7.pre 2015-04-12 17:04:43 +03:00
Dzmitry Plashchynski
84ac08e5d4 Fix error when next time in the past 2015-04-12 17:04:07 +03:00
Dzmitry Plashchynski
e8812b1329 v0.8.6.pre 2015-04-12 16:52:37 +03:00
Dzmitry Plashchynski
f6b393ad6b Period should only return future time 2015-04-12 16:48:57 +03:00
Dzmitry Plashchynski
55e3956618 Clean up 2015-04-12 16:34:28 +03:00
13 changed files with 93 additions and 62 deletions

View File

@@ -1,7 +1,7 @@
PATH
remote: .
specs:
crono (0.8.5.pre)
crono (0.8.8.pre)
activejob (~> 4.0)
activerecord (~> 4.0)
activesupport (~> 4.0)
@@ -31,7 +31,7 @@ GEM
columnize (= 0.9.0)
columnize (0.9.0)
diff-lcs (1.2.5)
globalid (0.3.3)
globalid (0.3.5)
activesupport (>= 4.1.0)
haml (4.0.6)
tilt

View File

@@ -10,6 +10,7 @@ require 'crono/job'
require 'crono/scheduler'
require 'crono/config'
require 'crono/performer_proxy'
require 'crono/cronotab'
require 'crono/orm/active_record/crono_job'
require 'crono/railtie' if defined?(Rails)

View File

@@ -21,6 +21,7 @@ module Crono
write_pid
load_rails
Cronotab.process(File.expand_path(config.cronotab))
print_banner
check_jobs
@@ -62,7 +63,7 @@ module Crono
logger.info 'Jobs:'
Crono.scheduler.jobs.each do |job|
logger.info "'#{job.performer}' with rule '#{job.period.description}'"\
"next time will perform at #{job.next}"
" next time will perform at #{job.next}"
end
end
@@ -71,7 +72,6 @@ module Crono
require 'rails'
require File.expand_path('config/environment.rb')
::Rails.application.eager_load!
require File.expand_path(config.cronotab)
end
def check_jobs
@@ -80,9 +80,9 @@ module Crono
end
def start_working_loop
while true
loop do
next_time, jobs = Crono.scheduler.next_jobs
sleep(next_time - Time.now)
sleep(next_time - Time.now) if next_time > Time.now
jobs.each(&:perform)
end
end

10
lib/crono/cronotab.rb Normal file
View File

@@ -0,0 +1,10 @@
module Crono
class Cronotab
def self.process(cronotab_path = nil)
cronotab_path ||= ENV['CRONOTAB'] || (defined?(Rails) &&
File.join(Rails.root, Config::CRONOTAB))
fail 'No cronotab defined' unless cronotab_path
require cronotab_path
end
end
end

View File

@@ -6,18 +6,20 @@ module Crono
class Job
include Logging
attr_accessor :performer, :period, :last_performed_at, :job_log,
:job_logger, :healthy
attr_accessor :performer, :period, :last_performed_at,
:next_performed_at, :job_log, :job_logger, :healthy
def initialize(performer, period)
self.performer, self.period = performer, period
self.job_log = StringIO.new
self.job_logger = Logger.new(job_log)
self.next_performed_at = period.next
@semaphore = Mutex.new
end
def next
period.next(since: last_performed_at)
return next_performed_at if next_performed_at.future?
Time.now
end
def description
@@ -31,6 +33,7 @@ module Crono
def perform
log "Perform #{performer}"
self.last_performed_at = Time.now
self.next_performed_at = period.next(since: last_performed_at)
Thread.new { perform_job }
end
@@ -44,6 +47,7 @@ module Crono
def load
self.last_performed_at = model.last_performed_at
self.next_performed_at = period.next(since: last_performed_at)
end
private
@@ -60,26 +64,25 @@ module Crono
end
def perform_job
performer_instance = performer.new
performer_instance.instance_variable_set(:@_crono_job, self)
performer_instance.perform
finished_time_sec = format('%.2f', Time.now - last_performed_at)
performer.new.perform
rescue StandardError => e
handle_job_fail(e, finished_time_sec)
handle_job_fail(e)
else
handle_job_success(finished_time_sec)
handle_job_success
ensure
save
end
def handle_job_fail(exception, finished_time_sec)
def handle_job_fail(exception)
finished_time_sec = format('%.2f', Time.now - last_performed_at)
self.healthy = false
log_error "Finished #{performer} in #{finished_time_sec} seconds"\
"with error: #{exception.message}"
" with error: #{exception.message}"
log_error exception.backtrace.join("\n")
end
def handle_job_success(finished_time_sec)
def handle_job_success
finished_time_sec = format('%.2f', Time.now - last_performed_at)
self.healthy = true
log "Finished #{performer} in #{finished_time_sec} seconds"
end

View File

@@ -1,5 +1,5 @@
module Crono
# Period describe frequency of performing a task
# Period describe frequency of jobs
class Period
DAYS = [:monday, :tuesday, :wednesday, :thursday, :friday, :saturday,
:sunday]
@@ -14,7 +14,9 @@ module Crono
return initial_next unless since
@next = @period.since(since)
@next = @next.beginning_of_week.advance(days: @on) if @on
@next.change(time_atts)
@next = @next.change(time_atts)
return @next if @next.future?
Time.now
end
def description

View File

@@ -1,3 +1,3 @@
module Crono
VERSION = '0.8.5.pre'
VERSION = '0.8.8.pre'
end

View File

@@ -1,18 +1,8 @@
module Crono
def self.load_cronotab
cronotab_path = ENV['CRONOTAB'] || (defined?(Rails) &&
File.join(Rails.root, cronotab_path))
fail 'No cronotab defined' unless cronotab_path
puts "Load cronotab #{cronotab_path}"
require cronotab_path
end
end
namespace :crono do
desc 'Clean unused job stats from DB'
task clean: :environment do
Crono.scheduler = Crono::Scheduler.new
Crono.load_cronotab
Crono::Cronotab.process
current_job_ids = Crono.scheduler.jobs.map(&:job_id)
Crono::CronoJob.where.not(job_id: current_job_ids).destroy_all
end
@@ -20,7 +10,7 @@ namespace :crono do
desc 'Check cronotab.rb syntax'
task check: :environment do
Crono.scheduler = Crono::Scheduler.new
Crono.load_cronotab
Crono::Cronotab.process
puts 'Syntax ok'
end
end

View File

@@ -10,6 +10,7 @@ describe Crono::CLI do
expect(cli).to receive(:start_working_loop)
expect(cli).to receive(:parse_options)
expect(cli).to receive(:write_pid)
expect(Crono::Cronotab).to receive(:process)
cli.run
end
end

20
spec/cronotab_spec.rb Normal file
View File

@@ -0,0 +1,20 @@
require 'spec_helper'
describe Crono::Cronotab do
describe '#process' do
it 'should load cronotab file' do
cronotab_path = File.expand_path('../assets/good_cronotab.rb', __FILE__)
expect(Crono.scheduler).to receive(:add_job).with(kind_of(Crono::Job))
expect {
Crono::Cronotab.process(cronotab_path)
}.to_not raise_error
end
it 'should raise error when cronotab is invalid' do
cronotab_path = File.expand_path('../assets/bad_cronotab.rb', __FILE__)
expect {
Crono::Cronotab.process(cronotab_path)
}.to raise_error
end
end
end

View File

@@ -32,23 +32,10 @@ describe Crono::Job do
expect(saved_log).to include 'Some error'
end
xit 'should set Job#healthy to true if perform ok' do
class TestJob
def perform
@_crono_job
end
end
job.perform.join
end
it 'should set Job#healthy to false if perform with error' do
failing_job.perform.join
expect(failing_job.healthy).to be false
end
xit 'should set @_crono_job variable to instance' do
job.perform
end
end
describe '#description' do

View File

@@ -1,12 +1,6 @@
require 'spec_helper'
describe Crono::Period do
around(:each) do |example|
Timecop.freeze do
example.run
end
end
describe '#description' do
it 'should return period description' do
@period = Crono::Period.new(1.week, on: :monday, at: '15:20')
@@ -53,28 +47,33 @@ describe Crono::Period do
end
context 'in daily basis' do
it "should return Time.now if the next time in past" do
@period = Crono::Period.new(1.day, at: '06:00')
expect(@period.next(since: 2.days.ago).to_s).to be_eql(Time.now.to_s)
end
it 'should return the time 2 days from now' do
@period = Crono::Period.new(2.day)
expect(@period.next).to be_eql(2.day.from_now)
expect(@period.next.to_s).to be_eql(2.days.from_now.to_s)
end
it "should set time to 'at' time as a string" do
time = 10.minutes.ago
at = [time.hour, time.min].join(':')
@period = Crono::Period.new(2.day, at: at)
expect(@period.next).to be_eql(2.day.from_now.change(hour: time.hour, min: time.min))
@period = Crono::Period.new(2.days, at: at)
expect(@period.next).to be_eql(2.days.from_now.change(hour: time.hour, min: time.min))
end
it "should set time to 'at' time as a hash" do
time = 10.minutes.ago
at = { hour: time.hour, min: time.min }
@period = Crono::Period.new(2.day, at: at)
expect(@period.next).to be_eql(2.day.from_now.change(at))
@period = Crono::Period.new(2.days, at: at)
expect(@period.next).to be_eql(2.days.from_now.change(at))
end
it "should raise error when 'at' is wrong" do
expect {
Crono::Period.new(2.day, at: 1)
Crono::Period.new(2.days, at: 1)
}.to raise_error("Unknown 'at' format")
end
@@ -85,14 +84,14 @@ describe Crono::Period do
end
it 'should return time in relation to last time' do
@period = Crono::Period.new(2.day)
expect(@period.next(since: 1.day.ago)).to be_eql(1.day.from_now)
@period = Crono::Period.new(2.days)
expect(@period.next(since: 1.day.ago).to_s).to be_eql(1.day.from_now.to_s)
end
it 'should return today time if it is first run and not too late' do
time = 10.minutes.from_now
at = { hour: time.hour, min: time.min }
@period = Crono::Period.new(2.day, at: at)
@period = Crono::Period.new(2.days, at: at)
expect(@period.next.utc.to_s).to be_eql(Time.now.change(at).utc.to_s)
end
end

View File

@@ -23,7 +23,7 @@ describe Crono::Scheduler do
expect(jobs).to be_eql [jobs[0]]
end
it 'should return an array of jobs scheduled at same time' do
it 'should return an array of jobs scheduled at same time with `at`' do
time = 5.minutes.from_now
scheduler.jobs = jobs = [
Crono::Period.new(1.day, at: time.strftime('%H:%M')),
@@ -34,5 +34,23 @@ describe Crono::Scheduler do
time, jobs = scheduler.next_jobs
expect(jobs).to be_eql [jobs[0], jobs[1]]
end
it 'should handle a few jobs scheduled at same time without `at`' do
scheduler.jobs = jobs = [
Crono::Period.new(10.seconds),
Crono::Period.new(10.seconds),
Crono::Period.new(1.day, at: 10.minutes.from_now.strftime('%H:%M'))
].map { |period| Crono::Job.new(TestJob, period) }
_, next_jobs = scheduler.next_jobs
expect(next_jobs).to be_eql [jobs[0]]
Timecop.travel(4.seconds.from_now)
expect(Thread).to receive(:new)
jobs[0].perform
_, next_jobs = scheduler.next_jobs
expect(next_jobs).to be_eql [jobs[1]]
end
end
end