Compare commits

..

38 Commits

Author SHA1 Message Date
Dzmitry Plashchynski
b4ad8fb953 Handle a few jobs scheduled at the same time 2015-04-08 20:08:58 +03:00
Dzmitry Plashchynski
d075a55f03 Merge branch 'master' of github.com:plashchynski/crono 2015-03-24 23:25:49 +02:00
Dzmitry Plashchynski
2d72020ac4 Fix spec 2015-03-24 23:25:38 +02:00
Dzmitry Plashchynski
cde8a2d214 Update README.md 2015-03-24 22:46:21 +02:00
Dzmitry Plashchynski
2ec9cfa829 Update Gemfile.lock 2015-03-24 22:24:08 +02:00
Dzmitry Plashchynski
63c86c8cd9 Bump version 2015-03-23 01:40:03 +02:00
Dzmitry Plashchynski
e10daec9c6 Fix typo 2015-03-23 01:38:53 +02:00
Dzmitry Plashchynski
f72c288ce8 Merge branch 'master' of github.com:plashchynski/crono 2015-03-22 23:54:25 +02:00
Dzmitry Plashchynski
78ce578484 Access to Crono::Job from performer class 2015-03-22 23:53:46 +02:00
Dzmitry Plashchynski
0c77c490bd Move TestJob to spec_helper.rb 2015-03-22 23:37:01 +02:00
Dzmitry Plashchynski
d889b9380d Update README.md 2015-03-20 22:45:18 +02:00
Dzmitry Plashchynski
f75bdf352b Update README.md 2015-03-19 21:12:40 +02:00
Dzmitry Plashchynski
fa97f573e0 Add rake task to check cronotab.rb syntax 2015-03-18 20:54:16 +02:00
Dzmitry Plashchynski
dc70212f9d Add crono rake tasks spec 2015-03-17 18:28:17 +02:00
Dzmitry Plashchynski
7328bea24c Add rake task to clean DB 2015-03-16 17:34:25 +02:00
Dzmitry Plashchynski
f57d440424 Bump 0.8.0 2015-03-15 09:57:18 +02:00
Dzmitry Plashchynski
95a237aeb5 Check period duration to be at least 1 day when using 'at' 2015-03-15 09:56:39 +02:00
Dzmitry Plashchynski
6508197f26 Remove unused spec 2015-03-15 09:35:16 +02:00
Dzmitry Plashchynski
a0c612fb27 Use on in a period description 2015-03-15 09:34:27 +02:00
Dzmitry Plashchynski
a3c4ec87f5 Added on to README.md 2015-03-15 09:31:46 +02:00
Dzmitry Plashchynski
9b85c8b8c3 Added 'on' option for Period 2015-03-15 09:29:02 +02:00
Dzmitry Plashchynski
1af691ef24 Lint web 2015-03-14 03:26:32 +02:00
Dzmitry Plashchynski
8a89a9a8eb CLI refactoring 2015-03-14 03:18:16 +02:00
Dzmitry Plashchynski
7e2e65e21b Job refactoring 2015-03-14 03:07:49 +02:00
Dzmitry Plashchynski
4205b12fe1 Fix installation instructions 2015-03-13 23:34:58 +02:00
Dzmitry Plashchynski
bb9f62a69b Fix loading issue 2015-03-13 23:28:10 +02:00
Dzmitry Plashchynski
a9798acb35 Lint the whole project 2015-03-13 20:46:54 +02:00
Dzmitry Plashchynski
1aa27baca8 Lint crono.gemspec 2015-03-13 19:51:32 +02:00
Dzmitry Plashchynski
af1e1959ac Update README.md 2015-03-12 18:21:13 +02:00
Dzmitry Plashchynski
7af68aa591 Update README.md 2015-03-11 00:14:28 +02:00
Dzmitry Plashchynski
6d2e3fbb75 Add job#log_error, log error with ERROR severity 2015-03-10 23:59:39 +02:00
Dzmitry Plashchynski
46fe2548d9 Refactoring 2015-03-10 23:49:38 +02:00
Dzmitry Plashchynski
6859d1f09a Add specs to test error marks 2015-03-10 23:37:15 +02:00
Dzmitry Plashchynski
6674bca0be Refactoring 2015-03-10 23:33:14 +02:00
Dzmitry Plashchynski
5a5c29c52a Mark job as error when it's not healthy 2015-03-10 23:26:58 +02:00
Dzmitry Plashchynski
a24389f6cc Add Job#healthy 2015-03-10 00:19:33 +02:00
Dzmitry Plashchynski
54a1b53af9 Add tests for Crono::Web 2015-03-09 23:43:17 +02:00
Dzmitry Plashchynski
eedff96d39 Autoload Crono::Web 2015-03-09 00:24:58 +02:00
37 changed files with 583 additions and 284 deletions

View File

@@ -1,24 +1,35 @@
0.5.0 0.8.0
----------- -----------
- Initial release! - Added `on` (day of week) option to cronotab.rb semantic
- Added job health check and job health indicator to the Web UI
0.5.1
0.7.0
----------- -----------
- Added -e/--environment ENV option to set the daemon rails environment. - Added simple Web UI
0.5.2
-----------
- Fix: Scheduled time now related to the last performing time.
0.6.1 0.6.1
----------- -----------
- Persist job state to your database. - Persist job state to your database.
0.7.0
0.5.2
----------- -----------
- Added simple Web UI - Fix: Scheduled time now related to the last performing time.
0.5.1
-----------
- Added -e/--environment ENV option to set the daemon rails environment.
0.5.0
-----------
- Initial release!

View File

@@ -1,7 +1,7 @@
PATH PATH
remote: . remote: .
specs: specs:
crono (0.7.0) crono (0.8.1)
activejob (~> 4.0) activejob (~> 4.0)
activerecord (~> 4.0) activerecord (~> 4.0)
activesupport (~> 4.0) activesupport (~> 4.0)
@@ -9,17 +9,17 @@ PATH
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
activejob (4.2.0) activejob (4.2.1)
activesupport (= 4.2.0) activesupport (= 4.2.1)
globalid (>= 0.3.0) globalid (>= 0.3.0)
activemodel (4.2.0) activemodel (4.2.1)
activesupport (= 4.2.0) activesupport (= 4.2.1)
builder (~> 3.1) builder (~> 3.1)
activerecord (4.2.0) activerecord (4.2.1)
activemodel (= 4.2.0) activemodel (= 4.2.1)
activesupport (= 4.2.0) activesupport (= 4.2.1)
arel (~> 6.0) arel (~> 6.0)
activesupport (4.2.0) activesupport (4.2.1)
i18n (~> 0.7) i18n (~> 0.7)
json (~> 1.7, >= 1.7.7) json (~> 1.7, >= 1.7.7)
minitest (~> 5.1) minitest (~> 5.1)
@@ -44,6 +44,8 @@ GEM
rack (1.6.0) rack (1.6.0)
rack-protection (1.5.3) rack-protection (1.5.3)
rack rack
rack-test (0.6.3)
rack (>= 1.0)
rake (10.4.2) rake (10.4.2)
rspec (3.2.0) rspec (3.2.0)
rspec-core (~> 3.2.0) rspec-core (~> 3.2.0)
@@ -64,7 +66,7 @@ GEM
tilt (~> 1.3, >= 1.3.4) tilt (~> 1.3, >= 1.3.4)
slop (3.6.0) slop (3.6.0)
sqlite3 (1.3.10) sqlite3 (1.3.10)
thread_safe (0.3.4) thread_safe (0.3.5)
tilt (1.4.1) tilt (1.4.1)
timecop (0.7.3) timecop (0.7.3)
tzinfo (1.2.2) tzinfo (1.2.2)
@@ -78,6 +80,7 @@ DEPENDENCIES
byebug byebug
crono! crono!
haml haml
rack-test
rake (~> 10.0) rake (~> 10.0)
rspec (~> 3.0) rspec (~> 3.0)
sinatra sinatra

View File

@@ -9,12 +9,13 @@ Crono — Job scheduler for Rails
Crono is a time-based background job scheduler daemon (just like Cron) for Ruby on Rails. Crono is a time-based background job scheduler daemon (just like Cron) for Ruby on Rails.
## The Idea ## The Purpose
Currently there is no such thing as Cron in Ruby for Rails. Well, there's [Whenever](https://github.com/javan/whenever) but it works on top of Unix Cron, so you have no total control of it from Ruby. Crono is pure Ruby. It doesn't use Unix Cron and other platform-dependent things. So you can use it on all platforms supported by Ruby. It persists job state to your database using Active Record. You have full control of jobs performing process. You have Ruby code, so you can understand and modify it to fit your needs. Currently there is no such thing as Ruby Cron for Rails. Well, there's [Whenever](https://github.com/javan/whenever) but it works on top of Unix Cron, so you haven't control of it from Ruby. Crono is pure Ruby. It doesn't use Unix Cron and other platform-dependent things. So you can use it on all platforms supported by Ruby. It persists job states to your database using Active Record. You have full control of jobs performing process. It's Ruby, so you can understand and modify it to fit your needs.
![Web UI](https://github.com/plashchynski/crono/raw/master/examples/crono_web_ui.png) ![Web UI](https://github.com/plashchynski/crono/raw/master/examples/crono_web_ui.png)
## Requirements ## Requirements
Tested with latest MRI Ruby (2.2, 2.1 and 2.0) and Rails 3.2+ Tested with latest MRI Ruby (2.2, 2.1 and 2.0) and Rails 3.2+
@@ -48,7 +49,7 @@ Now you are ready to move forward to create a job and schedule it.
Crono can use Active Job jobs from `app/jobs/`. The only requirements is that the `perform` method should take no arguments. Crono can use Active Job jobs from `app/jobs/`. The only requirements is that the `perform` method should take no arguments.
Here's an example of a test job: Here's an example of a job:
```ruby ```ruby
# app/jobs/test_job.rb # app/jobs/test_job.rb
@@ -71,20 +72,23 @@ class TestJob # This is not an Active Job job, but pretty legal Crono job.
end end
``` ```
_Please note that crono uses threads, so your code should be thread-safe_
#### Job Schedule #### Job Schedule
The schedule described in the configuration file `config/cronotab.rb`, that created using `crono:install` or manually. The semantic is pretty straightforward: Schedule list is defined in the file `config/cronotab.rb`, that created using `crono:install`. The semantic is pretty straightforward:
```ruby ```ruby
# config/cronotab.rb # config/cronotab.rb
Crono.perform(TestJob).every 2.days, at: "15:30" Crono.perform(TestJob).every 2.days, at: {hour: 15, min: 30}
Crono.perform(TestJob).every 1.week, on: :monday, at: "15:30"
``` ```
You can schedule one job a few times, if you want a job to be performed a few times a day: You can schedule one job a few times, if you want the job to be performed a few times a day or a week:
```ruby ```ruby
Crono.perform(TestJob).every 1.day, at: "00:00" Crono.perform(TestJob).every 1.week, on: :monday
Crono.perform(TestJob).every 1.day, at: "12:00" Crono.perform(TestJob).every 1.week, on: :thursday
``` ```
The `at` can be a Hash: The `at` can be a Hash:
@@ -109,6 +113,7 @@ Usage: crono [options]
-e, --environment ENV Application environment (Default: development) -e, --environment ENV Application environment (Default: development)
``` ```
## Web UI ## Web UI
Crono comes with a Sinatra application that can display the current state of Crono jobs. Crono comes with a Sinatra application that can display the current state of Crono jobs.
@@ -122,8 +127,6 @@ gem 'sinatra', require: nil
Add the following to your `config/routes.rb`: Add the following to your `config/routes.rb`:
```ruby ```ruby
require 'crono/web'
Rails.application.routes.draw do Rails.application.routes.draw do
mount Crono::Web, at: '/crono' mount Crono::Web, at: '/crono'
... ...
@@ -131,6 +134,7 @@ Rails.application.routes.draw do
Access management and other questions described in the [wiki](https://github.com/plashchynski/crono/wiki/Web-UI). Access management and other questions described in the [wiki](https://github.com/plashchynski/crono/wiki/Web-UI).
## Capistrano ## Capistrano
Use the `capistrano-crono` gem ([github](https://github.com/plashchynski/capistrano-crono/)). Use the `capistrano-crono` gem ([github](https://github.com/plashchynski/capistrano-crono/)).

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env ruby #!/usr/bin/env ruby
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
require "crono/cli" require 'crono/cli'
begin begin
Crono::CLI.instance.run Crono::CLI.instance.run

View File

@@ -2,30 +2,31 @@
require File.expand_path('../lib/crono/version', __FILE__) require File.expand_path('../lib/crono/version', __FILE__)
Gem::Specification.new do |s| Gem::Specification.new do |s|
s.name = "crono" s.name = 'crono'
s.version = Crono::VERSION s.version = Crono::VERSION
s.authors = ["Dzmitry Plashchynski"] s.authors = ['Dzmitry Plashchynski']
s.email = ["plashchynski@gmail.com"] s.email = ['plashchynski@gmail.com']
s.homepage = "https://github.com/plashchynski/crono" s.homepage = 'https://github.com/plashchynski/crono'
s.description = s.summary = "Job scheduler for Rails" s.description = s.summary = 'Job scheduler for Rails'
s.license = "Apache-2.0" s.license = 'Apache-2.0'
s.required_rubygems_version = ">= 1.3.6" s.required_rubygems_version = '>= 1.3.6'
s.rubyforge_project = "crono" s.rubyforge_project = 'crono'
s.add_runtime_dependency "activejob", "~> 4.0" s.add_runtime_dependency 'activejob', '~> 4.0'
s.add_runtime_dependency "activesupport", "~> 4.0" s.add_runtime_dependency 'activesupport', '~> 4.0'
s.add_runtime_dependency "activerecord", "~> 4.0" s.add_runtime_dependency 'activerecord', '~> 4.0'
s.add_development_dependency "rake", "~> 10.0" s.add_development_dependency 'rake', '~> 10.0'
s.add_development_dependency "bundler", ">= 1.0.0" s.add_development_dependency 'bundler', '>= 1.0.0'
s.add_development_dependency "rspec", "~> 3.0" s.add_development_dependency 'rspec', '~> 3.0'
s.add_development_dependency "timecop", "~> 0.7" s.add_development_dependency 'timecop', '~> 0.7'
s.add_development_dependency "sqlite3" s.add_development_dependency 'sqlite3'
s.add_development_dependency "byebug" s.add_development_dependency 'byebug'
s.add_development_dependency "sinatra" s.add_development_dependency 'sinatra'
s.add_development_dependency "haml" s.add_development_dependency 'haml'
s.add_development_dependency 'rack-test'
s.files = `git ls-files`.split("\n") s.files = `git ls-files`.split("\n")
s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact s.executables = ['crono']
s.require_path = 'lib' s.require_path = 'lib'
end end

View File

@@ -1,4 +1,4 @@
# cronotab.rb Crono configuration file # cronotab.rb - Crono configuration file
# #
# Here you can specify periodic jobs and schedule. # Here you can specify periodic jobs and schedule.
# You can use ActiveJob's jobs from `app/jobs/` # You can use ActiveJob's jobs from `app/jobs/`
@@ -7,9 +7,8 @@
# #
class TestJob class TestJob
def perform def perform
puts "Test!" puts 'Test!'
end end
end end
Crono.perform(TestJob).every 2.days, at: "15:30" Crono.perform(TestJob).every 2.days, at: '15:30'

View File

@@ -1,12 +1,16 @@
# Crono main module
module Crono module Crono
end end
require "active_support/all" require 'active_support/all'
require "crono/version.rb" require 'crono/version'
require "crono/logging.rb" require 'crono/logging'
require "crono/period.rb" require 'crono/period'
require "crono/job.rb" require 'crono/job'
require "crono/scheduler.rb" require 'crono/scheduler'
require "crono/config.rb" require 'crono/config'
require "crono/performer_proxy.rb" require 'crono/performer_proxy'
require "crono/orm/active_record/crono_job.rb" require 'crono/orm/active_record/crono_job'
require 'crono/railtie' if defined?(Rails)
Crono.autoload :Web, 'crono/web'

View File

@@ -2,8 +2,7 @@ require 'crono'
require 'optparse' require 'optparse'
module Crono module Crono
mattr_accessor :scheduler # Crono::CLI - The main class for the crono daemon exacutable `bin/crono`
class CLI class CLI
include Singleton include Singleton
include Logging include Logging
@@ -18,12 +17,7 @@ module Crono
def run def run
parse_options(ARGV) parse_options(ARGV)
if config.daemonize setup_log
set_log_to(config.logfile)
daemonize
else
set_log_to(STDOUT)
end
write_pid write_pid
load_rails load_rails
@@ -33,7 +27,17 @@ module Crono
start_working_loop start_working_loop
end end
private private
def setup_log
if config.daemonize
self.logfile = config.logfile
daemonize
else
self.logfile = STDOUT
end
end
def daemonize def daemonize
::Process.daemon(true, true) ::Process.daemon(true, true)
@@ -42,7 +46,7 @@ module Crono
io.sync = true io.sync = true
end end
$stdin.reopen("/dev/null") $stdin.reopen('/dev/null')
end end
def write_pid def write_pid
@@ -54,30 +58,31 @@ module Crono
logger.info "Loading Crono #{Crono::VERSION}" logger.info "Loading Crono #{Crono::VERSION}"
logger.info "Running in #{RUBY_DESCRIPTION}" logger.info "Running in #{RUBY_DESCRIPTION}"
logger.info "Jobs:" logger.info 'Jobs:'
Crono.scheduler.jobs.each do |job| Crono.scheduler.jobs.each do |job|
logger.info %{"#{job.performer}" with rule "#{job.period.description}" next time will perform at #{job.next}} logger.info "'#{job.performer}' with rule '#{job.period.description}'"\
"next time will perform at #{job.next}"
end end
end end
def load_rails def load_rails
ENV['RACK_ENV'] = ENV['RAILS_ENV'] = config.environment ENV['RACK_ENV'] = ENV['RAILS_ENV'] = config.environment
require 'rails' require 'rails'
require File.expand_path("config/environment.rb") require File.expand_path('config/environment.rb')
::Rails.application.eager_load! ::Rails.application.eager_load!
require File.expand_path(config.cronotab) require File.expand_path(config.cronotab)
end end
def check_jobs def check_jobs
if Crono.scheduler.jobs.empty? return if Crono.scheduler.jobs.present?
logger.error "You have no jobs defined in you cronotab file #{config.cronotab}" logger.error "You have no jobs in you cronotab file #{config.cronotab}"
end
end end
def start_working_loop def start_working_loop
while job = Crono.scheduler.next do while true
sleep(job.next - Time.now) next_time, jobs = Crono.scheduler.next_jobs
job.perform sleep(next_time - Time.now)
jobs.each(&:perform)
end end
end end

View File

@@ -1,21 +1,18 @@
module Crono module Crono
# Crono::Config stores Crono configuration
class Config class Config
CRONOTAB = "config/cronotab.rb" CRONOTAB = 'config/cronotab.rb'
LOGFILE = "log/crono.log" LOGFILE = 'log/crono.log'
PIDFILE = "tmp/pids/crono.pid" PIDFILE = 'tmp/pids/crono.pid'
attr_accessor :cronotab attr_accessor :cronotab, :logfile, :pidfile, :daemonize, :environment
attr_accessor :logfile
attr_accessor :pidfile
attr_accessor :daemonize
attr_accessor :environment
def initialize def initialize
self.cronotab = CRONOTAB self.cronotab = CRONOTAB
self.logfile = LOGFILE self.logfile = LOGFILE
self.pidfile = PIDFILE self.pidfile = PIDFILE
self.daemonize = false self.daemonize = false
self.environment = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "development" self.environment = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
end end
end end
end end

View File

@@ -2,14 +2,12 @@ require 'stringio'
require 'logger' require 'logger'
module Crono module Crono
# Crono::Job represents a Crono job
class Job class Job
include Logging include Logging
attr_accessor :performer attr_accessor :performer, :period, :last_performed_at, :job_log,
attr_accessor :period :job_logger, :healthy
attr_accessor :last_performed_at
attr_accessor :job_log
attr_accessor :job_logger
def initialize(performer, period) def initialize(performer, period)
self.performer, self.period = performer, period self.performer, self.period = performer, period
@@ -19,8 +17,7 @@ module Crono
end end
def next def next
next_time = period.next(since: last_performed_at) period.next(since: last_performed_at)
next_time.past? ? period.next : next_time
end end
def description def description
@@ -35,26 +32,13 @@ module Crono
log "Perform #{performer}" log "Perform #{performer}"
self.last_performed_at = Time.now self.last_performed_at = Time.now
Thread.new do Thread.new { perform_job }
begin
performer.new.perform
rescue Exception => e
log "Finished #{performer} in %.2f seconds with error: #{e.message}" % (Time.now - last_performed_at)
log e.backtrace.join("\n")
else
log "Finished #{performer} in %.2f seconds" % (Time.now - last_performed_at)
ensure
save
end
end
end end
def save def save
@semaphore.synchronize do @semaphore.synchronize do
log = model.reload.log || "" update_model
log << job_log.string clear_job_log
job_log.truncate(job_log.rewind)
model.update(last_performed_at: last_performed_at, log: log)
end end
end end
@@ -62,11 +46,52 @@ module Crono
self.last_performed_at = model.last_performed_at self.last_performed_at = model.last_performed_at
end end
private private
def log(message)
def clear_job_log
job_log.truncate(job_log.rewind)
end
def update_model
saved_log = model.reload.log || ''
log_to_save = saved_log + job_log.string
model.update(last_performed_at: last_performed_at, log: log_to_save,
healthy: healthy)
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)
rescue StandardError => e
handle_job_fail(e, finished_time_sec)
else
handle_job_success(finished_time_sec)
ensure
save
end
def handle_job_fail(exception, finished_time_sec)
self.healthy = false
log_error "Finished #{performer} in #{finished_time_sec} seconds"\
"with error: #{exception.message}"
log_error exception.backtrace.join("\n")
end
def handle_job_success(finished_time_sec)
self.healthy = true
log "Finished #{performer} in #{finished_time_sec} seconds"
end
def log_error(message)
log(message, Logger::ERROR)
end
def log(message, severity = Logger::INFO)
@semaphore.synchronize do @semaphore.synchronize do
logger.info message logger.log severity, message
job_logger.info message job_logger.log severity, message
end end
end end

View File

@@ -1,8 +1,9 @@
module Crono module Crono
mattr_accessor :logger mattr_accessor :logger
# Crono::Logging is a standart Ruby logger wrapper
module Logging module Logging
def set_log_to(logfile) def logfile=(logfile)
Crono.logger = Logger.new(logfile) Crono.logger = Logger.new(logfile)
end end

View File

@@ -1,8 +1,13 @@
require 'active_record' require 'active_record'
module Crono module Crono
# Crono::CronoJob is a ActiveRecord model to store job state
class CronoJob < ActiveRecord::Base class CronoJob < ActiveRecord::Base
self.table_name = "crono_jobs" self.table_name = 'crono_jobs'
validates :job_id, presence: true, uniqueness: true validates :job_id, presence: true, uniqueness: true
def self.outdated
self
end
end end
end end

View File

@@ -1,4 +1,5 @@
module Crono module Crono
# Crono::PerformerProxy is a proxy used in cronotab.rb semantic
class PerformerProxy class PerformerProxy
def initialize(performer, scheduler) def initialize(performer, scheduler)
@performer = performer @performer = performer

View File

@@ -1,27 +1,53 @@
module Crono module Crono
# Period describe frequency of performing a task
class Period class Period
def initialize(period, at: nil) DAYS = [:monday, :tuesday, :wednesday, :thursday, :friday, :saturday,
:sunday]
def initialize(period, at: nil, on: nil)
@period = period @period = period
@at_hour, @at_min = parse_at(at) if at @at_hour, @at_min = parse_at(at) if at
@on = parse_on(on) if on
end end
def next(since: nil) def next(since: nil)
if since.nil? return initial_next unless since
@next = Time.now.change(time_atts) @next = @period.since(since)
return @next if @next.future? @next = @next.beginning_of_week.advance(days: @on) if @on
since = Time.now @next.change(time_atts)
end
@period.since(since).change(time_atts)
end end
def description def description
desc = "every #{@period.inspect}" desc = "every #{@period.inspect}"
desc += " at %.2i:%.2i" % [@at_hour, @at_min] if @at_hour && @at_min desc += format(' at %.2i:%.2i', @at_hour, @at_min) if @at_hour && @at_min
desc += " on #{DAYS[@on].capitalize}" if @on
desc desc
end end
private
def initial_next
next_time = initial_day.change(time_atts)
return next_time if next_time.future?
@period.from_now.change(time_atts)
end
def initial_day
return Time.now unless @on
day = Time.now.beginning_of_week.advance(days: @on)
return day if day.future?
@period.from_now.beginning_of_week.advance(days: @on)
end
def parse_on(on)
day_number = DAYS.index(on)
fail "Wrong 'on' day" unless day_number
fail "period should be at least 1 week to use 'on'" if @period < 1.week
day_number
end
def parse_at(at) def parse_at(at)
fail "period should be at least 1 day to use 'at'" if @period < 1.day
case at case at
when String when String
time = Time.parse(at) time = Time.parse(at)
@@ -29,13 +55,12 @@ module Crono
when Hash when Hash
return at[:hour], at[:min] return at[:hour], at[:min]
else else
raise "Unknown 'at' format" fail "Unknown 'at' format"
end end
end end
private
def time_atts def time_atts
{hour: @at_hour, min: @at_min}.compact { hour: @at_hour, min: @at_min }.compact
end end
end end
end end

9
lib/crono/railtie.rb Normal file
View File

@@ -0,0 +1,9 @@
module Crono
class Railtie < ::Rails::Railtie
rake_tasks do
Dir[File.join(File.dirname(__FILE__), '../tasks/*.rake')].each do |file|
load file
end
end
end
end

View File

@@ -1,4 +1,5 @@
module Crono module Crono
# Scheduler is a container for job list and queue
class Scheduler class Scheduler
attr_accessor :jobs attr_accessor :jobs
@@ -11,13 +12,10 @@ module Crono
jobs << job jobs << job
end end
def next def next_jobs
queue.first jobs.group_by(&:next).sort_by {|time,_| time }.first
end
private
def queue
jobs.sort_by(&:next)
end end
end end
mattr_accessor :scheduler
end end

View File

@@ -1,3 +1,3 @@
module Crono module Crono
VERSION = '0.7.0' VERSION = '0.8.1'
end end

View File

@@ -1,18 +1,20 @@
require 'haml' require 'haml'
require 'sinatra/base' require 'sinatra/base'
require 'crono'
module Crono module Crono
# Web is a Web UI Sinatra app
class Web < Sinatra::Base class Web < Sinatra::Base
set :root, File.expand_path(File.dirname(__FILE__) + "/../../web") set :root, File.expand_path(File.dirname(__FILE__) + '/../../web')
set :public_folder, Proc.new { "#{root}/assets" } set :public_folder, proc { "#{root}/assets" }
set :views, Proc.new { "#{root}/views" } set :views, proc { "#{root}/views" }
get '/' do get '/' do
@jobs = Crono::CronoJob.all @jobs = Crono::CronoJob.all
haml :dashboard, format: :html5 haml :dashboard, format: :html5
end end
get '/jobs/:id' do get '/job/:id' do
@job = Crono::CronoJob.find(params[:id]) @job = Crono::CronoJob.find(params[:id])
haml :job haml :job
end end

View File

@@ -4,6 +4,7 @@ require 'rails/generators/active_record'
module Crono module Crono
module Generators module Generators
# rails generate crono:install
class InstallGenerator < ::Rails::Generators::Base class InstallGenerator < ::Rails::Generators::Base
include Rails::Generators::Migration include Rails::Generators::Migration
@@ -11,15 +12,16 @@ module Crono
ActiveRecord::Generators::Base.next_migration_number(path) ActiveRecord::Generators::Base.next_migration_number(path)
end end
desc "Installs crono and generates the necessary configuration files" desc 'Installs crono and generates the necessary configuration files'
source_root File.expand_path("../templates", __FILE__) source_root File.expand_path('../templates', __FILE__)
def copy_config def copy_config
template 'cronotab.rb.erb', 'config/cronotab.rb' template 'cronotab.rb.erb', 'config/cronotab.rb'
end end
def create_migrations def create_migrations
migration_template 'migrations/create_crono_jobs.rb', 'db/migrate/create_crono_jobs.rb' migration_template 'migrations/create_crono_jobs.rb',
'db/migrate/create_crono_jobs.rb'
end end
end end
end end

View File

@@ -7,9 +7,9 @@
# #
# class TestJob # class TestJob
# def perform # def perform
# puts "Test!" # puts 'Test!'
# end # end
# end # end
# #
# Crono.perform(TestJob).every 2.days, at: "15:30" # Crono.perform(TestJob).every 2.days, at: '15:30'
# #

View File

@@ -4,6 +4,7 @@ class CreateCronoJobs < ActiveRecord::Migration
t.string :job_id, null: false t.string :job_id, null: false
t.text :log t.text :log
t.datetime :last_performed_at t.datetime :last_performed_at
t.boolean :healthy
t.timestamps null: false t.timestamps null: false
end end
add_index :crono_jobs, [:job_id], unique: true add_index :crono_jobs, [:job_id], unique: true

View File

@@ -0,0 +1,26 @@
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
current_job_ids = Crono.scheduler.jobs.map(&:job_id)
Crono::CronoJob.where.not(job_id: current_job_ids).destroy_all
end
desc 'Check cronotab.rb syntax'
task check: :environment do
Crono.scheduler = Crono::Scheduler.new
Crono.load_cronotab
puts 'Syntax ok'
end
end

View File

@@ -0,0 +1,12 @@
# This is an example of a bad cronotab for tests
class TestJob
def perform
puts 'Test!'
end
end
# This is an error, because you can use `on` options with
# a period less than 7 days.
Crono.perform(TestJob).every 5.days, on: :sunday

View File

@@ -0,0 +1,9 @@
# This is an example of a good cronotab for tests
class TestJob
def perform
puts 'Test!'
end
end
Crono.perform(TestJob).every 5.seconds

View File

@@ -1,15 +1,11 @@
require "spec_helper" require 'spec_helper'
require 'crono/cli' require 'crono/cli'
class TestJob
def perform;end
end
describe Crono::CLI do describe Crono::CLI do
let(:cli) { Crono::CLI.instance } let(:cli) { Crono::CLI.instance }
describe "#run" do describe '#run' do
it "should try to initialize rails with #load_rails and start working loop" do it 'should initialize rails with #load_rails and start working loop' do
expect(cli).to receive(:load_rails) expect(cli).to receive(:load_rails)
expect(cli).to receive(:start_working_loop) expect(cli).to receive(:start_working_loop)
expect(cli).to receive(:parse_options) expect(cli).to receive(:parse_options)
@@ -18,34 +14,30 @@ describe Crono::CLI do
end end
end end
describe "#start_working_loop" do describe '#parse_options' do
it "should start working loop" it 'should set cronotab' do
end cli.send(:parse_options, ['--cronotab', '/tmp/cronotab.rb'])
expect(cli.config.cronotab).to be_eql '/tmp/cronotab.rb'
describe "#parse_options" do
it "should set cronotab" do
cli.send(:parse_options, ["--cronotab", "/tmp/cronotab.rb"])
expect(cli.config.cronotab).to be_eql "/tmp/cronotab.rb"
end end
it "should set logfile" do it 'should set logfile' do
cli.send(:parse_options, ["--logfile", "log/crono.log"]) cli.send(:parse_options, ['--logfile', 'log/crono.log'])
expect(cli.config.logfile).to be_eql "log/crono.log" expect(cli.config.logfile).to be_eql 'log/crono.log'
end end
it "should set pidfile" do it 'should set pidfile' do
cli.send(:parse_options, ["--pidfile", "tmp/pids/crono.0.log"]) cli.send(:parse_options, ['--pidfile', 'tmp/pids/crono.0.log'])
expect(cli.config.pidfile).to be_eql "tmp/pids/crono.0.log" expect(cli.config.pidfile).to be_eql 'tmp/pids/crono.0.log'
end end
it "should set daemonize" do it 'should set daemonize' do
cli.send(:parse_options, ["--daemonize"]) cli.send(:parse_options, ['--daemonize'])
expect(cli.config.daemonize).to be true expect(cli.config.daemonize).to be true
end end
it "should set environment" do it 'should set environment' do
cli.send(:parse_options, ["--environment", "production"]) cli.send(:parse_options, ['--environment', 'production'])
expect(cli.config.environment).to be_eql("production") expect(cli.config.environment).to be_eql('production')
end end
end end
end end

View File

@@ -1,15 +1,15 @@
require "spec_helper" require 'spec_helper'
describe Crono::Config do describe Crono::Config do
describe "#initialize" do describe '#initialize' do
it "should initialize with default configuration options" do it 'should initialize with default configuration options' do
ENV["RAILS_ENV"] = "test" ENV['RAILS_ENV'] = 'test'
@config = Crono::Config.new @config = Crono::Config.new
expect(@config.cronotab).to be Crono::Config::CRONOTAB expect(@config.cronotab).to be Crono::Config::CRONOTAB
expect(@config.logfile).to be Crono::Config::LOGFILE expect(@config.logfile).to be Crono::Config::LOGFILE
expect(@config.pidfile).to be Crono::Config::PIDFILE expect(@config.pidfile).to be Crono::Config::PIDFILE
expect(@config.daemonize).to be false expect(@config.daemonize).to be false
expect(@config.environment).to be_eql ENV["RAILS_ENV"] expect(@config.environment).to be_eql ENV['RAILS_ENV']
end end
end end
end end

View File

@@ -1,63 +1,80 @@
require "spec_helper" require 'spec_helper'
class TestJob
def perform;end
end
class TestFailingJob
def perform
raise "Some error"
end
end
describe Crono::Job do describe Crono::Job do
let(:period) { Crono::Period.new(2.day) } let(:period) { Crono::Period.new(2.day, at: '15:00') }
let(:job) { Crono::Job.new(TestJob, period) } let(:job) { Crono::Job.new(TestJob, period) }
let(:failing_job) { Crono::Job.new(TestFailingJob, period) } let(:failing_job) { Crono::Job.new(TestFailingJob, period) }
it "should contain performer and period" do it 'should contain performer and period' do
expect(job.performer).to be TestJob expect(job.performer).to be TestJob
expect(job.period).to be period expect(job.period).to be period
end end
describe "#perform" do describe '#next' do
it "should run performer in separate thread" do it 'should return next performing time according to period' do
expect(job.next).to be_eql period.next
end
end
describe '#perform' do
after { job.send(:model).destroy }
it 'should run performer in separate thread' do
expect(job).to receive(:save) expect(job).to receive(:save)
thread = job.perform.join thread = job.perform.join
expect(thread).to be_stop expect(thread).to be_stop
job.send(:model).destroy
end end
it "should save performin errors to log" do it 'should save performin errors to log' do
thread = failing_job.perform.join thread = failing_job.perform.join
expect(thread).to be_stop expect(thread).to be_stop
saved_log = Crono::CronoJob.find_by(job_id: failing_job.job_id).log saved_log = Crono::CronoJob.find_by(job_id: failing_job.job_id).log
expect(saved_log).to include "Some error" 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
end end
describe "#description" do describe '#description' do
it "should return job identificator" do it 'should return job identificator' do
expect(job.description).to be_eql("Perform TestJob every 2 days") expect(job.description).to be_eql('Perform TestJob every 2 days at 15:00')
end end
end end
describe "#save" do describe '#save' do
it "should save new job to DB" do it 'should save new job to DB' do
expect(Crono::CronoJob.where(job_id: job.job_id)).to_not exist expect(Crono::CronoJob.where(job_id: job.job_id)).to_not exist
job.save job.save
expect(Crono::CronoJob.where(job_id: job.job_id)).to exist expect(Crono::CronoJob.where(job_id: job.job_id)).to exist
end end
it "should update saved job" do it 'should update saved job' do
job.last_performed_at = Time.now job.last_performed_at = Time.now
job.healthy = true
job.save job.save
@crono_job = Crono::CronoJob.find_by(job_id: job.job_id) @crono_job = Crono::CronoJob.find_by(job_id: job.job_id)
expect(@crono_job.last_performed_at.utc.to_s).to be_eql job.last_performed_at.utc.to_s expect(@crono_job.last_performed_at.utc.to_s).to be_eql job.last_performed_at.utc.to_s
expect(@crono_job.healthy).to be true
end end
it "should save and truncate job log" do it 'should save and truncate job log' do
message = "test message" message = 'test message'
job.send(:log, message) job.send(:log, message)
job.save job.save
expect(job.send(:model).reload.log).to include message expect(job.send(:model).reload.log).to include message
@@ -65,31 +82,39 @@ describe Crono::Job do
end end
end end
describe "#load" do describe '#load' do
before do before do
@saved_last_performed_at = job.last_performed_at = Time.now @saved_last_performed_at = job.last_performed_at = Time.now
job.save job.save
end end
it "should load last_performed_at from DB" do it 'should load last_performed_at from DB' do
@job = Crono::Job.new(TestJob, period) @job = Crono::Job.new(TestJob, period)
@job.load @job.load
expect(@job.last_performed_at.utc.to_s).to be_eql @saved_last_performed_at.utc.to_s expect(@job.last_performed_at.utc.to_s).to be_eql @saved_last_performed_at.utc.to_s
end end
end end
describe "#log" do describe '#log' do
it "should write log messages to both common and job log" do it 'should write log messages to both common and job log' do
message = "Test message" message = 'Test message'
expect(job.logger).to receive(:info).with(message) expect(job.logger).to receive(:log).with(Logger::INFO, message)
expect(job.job_logger).to receive(:info).with(message) expect(job.job_logger).to receive(:log).with(Logger::INFO, message)
job.send(:log, message) job.send(:log, message)
end end
it "should write job log to Job#job_log" do it 'should write job log to Job#job_log' do
message = "Test message" message = 'Test message'
job.send(:log, message) job.send(:log, message)
expect(job.job_log.string).to include(message) expect(job.job_log.string).to include(message)
end end
end end
describe '#log_error' do
it 'should call log with ERROR severity' do
message = 'Test message'
expect(job).to receive(:log).with(message, Logger::ERROR)
job.send(:log_error, message)
end
end
end end

View File

@@ -1,26 +1,26 @@
require "spec_helper" require 'spec_helper'
describe Crono::CronoJob do describe Crono::CronoJob do
let(:valid_attrs) do let(:valid_attrs) do
{ {
job_id: "Perform TestJob every 3 days" job_id: 'Perform TestJob every 3 days'
} }
end end
it "should validate presence of job_id" do it 'should validate presence of job_id' do
@crono_job = Crono::CronoJob.new() @crono_job = Crono::CronoJob.new
expect(@crono_job).not_to be_valid expect(@crono_job).not_to be_valid
expect(@crono_job.errors.added?(:job_id, :blank)).to be true expect(@crono_job.errors.added?(:job_id, :blank)).to be true
end end
it "should validate uniqueness of job_id" do it 'should validate uniqueness of job_id' do
Crono::CronoJob.create!(job_id: "TestJob every 2 days") Crono::CronoJob.create!(job_id: 'TestJob every 2 days')
@crono_job = Crono::CronoJob.create(job_id: "TestJob every 2 days") @crono_job = Crono::CronoJob.create(job_id: 'TestJob every 2 days')
expect(@crono_job).not_to be_valid expect(@crono_job).not_to be_valid
expect(@crono_job.errors.added?(:job_id, :taken)).to be true expect(@crono_job.errors.added?(:job_id, :taken)).to be true
end end
it "should save job_id to DB" do it 'should save job_id to DB' do
Crono::CronoJob.create!(valid_attrs) Crono::CronoJob.create!(valid_attrs)
@crono_job = Crono::CronoJob.find_by(job_id: valid_attrs[:job_id]) @crono_job = Crono::CronoJob.find_by(job_id: valid_attrs[:job_id])
expect(@crono_job).to be_present expect(@crono_job).to be_present

View File

@@ -1,12 +1,8 @@
require "spec_helper" require 'spec_helper'
class TestJob
def perform;end
end
describe Crono::PerformerProxy do describe Crono::PerformerProxy do
it "should add job to schedule" do it 'should add job to schedule' do
expect(Crono.scheduler).to receive(:add_job).with(kind_of(Crono::Job)) expect(Crono.scheduler).to receive(:add_job).with(kind_of(Crono::Job))
Crono.perform(TestJob).every(2.days, at: "15:30") Crono.perform(TestJob).every(2.days, at: '15:30')
end end
end end

View File

@@ -1,4 +1,4 @@
require "spec_helper" require 'spec_helper'
describe Crono::Period do describe Crono::Period do
around(:each) do |example| around(:each) do |example|
@@ -7,49 +7,93 @@ describe Crono::Period do
end end
end end
describe "#description" do describe '#description' do
it "should return period description" do it 'should return period description' do
@period = Crono::Period.new(2.day, at: "15:20") @period = Crono::Period.new(1.week, on: :monday, at: '15:20')
expect(@period.description).to be_eql("every 2 days at 15:20") expect(@period.description).to be_eql('every 7 days at 15:20 on Monday')
end end
end end
describe "#next" do describe '#next' do
context "in daily basis" do context 'in weakly basis' do
it "should return the time 2 days from now" do it "should raise error if 'on' is wrong" do
expect { @period = Crono::Period.new(7.days, on: :bad_day) }
.to raise_error("Wrong 'on' day")
end
it 'should raise error when period is less than 1 week' do
expect { @period = Crono::Period.new(6.days, on: :monday) }
.to raise_error("period should be at least 1 week to use 'on'")
end
it "should return a 'on' day" do
@period = Crono::Period.new(1.week, on: :thursday, at: '15:30')
current_week = Time.now.beginning_of_week
last_run_time = current_week.advance(days: 1) # last run on the tuesday
next_run_at = Time.now.next_week.advance(days: 3)
.change(hour: 15, min: 30)
expect(@period.next(since: last_run_time)).to be_eql(next_run_at)
end
it "should return a next week day 'on'" do
@period = Crono::Period.new(1.week, on: :thursday)
Timecop.freeze(Time.now.beginning_of_week.advance(days: 4)) do
expect(@period.next).to be_eql(Time.now.next_week.advance(days: 3))
end
end
it 'should return a current week day on the first run if not too late' do
@period = Crono::Period.new(7.days, on: :tuesday)
beginning_of_the_week = Time.now.beginning_of_week
tuesday = beginning_of_the_week.advance(days: 1)
Timecop.freeze(beginning_of_the_week) do
expect(@period.next).to be_eql(tuesday)
end
end
end
context 'in daily basis' do
it 'should return the time 2 days from now' do
@period = Crono::Period.new(2.day) @period = Crono::Period.new(2.day)
expect(@period.next).to be_eql(2.day.from_now) expect(@period.next).to be_eql(2.day.from_now)
end end
it "should set time to 'at' time as a string" do it "should set time to 'at' time as a string" do
time = 10.minutes.ago time = 10.minutes.ago
@period = Crono::Period.new(2.day, at: [time.hour, time.min].join(':')) 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)) expect(@period.next).to be_eql(2.day.from_now.change(hour: time.hour, min: time.min))
end end
it "should set time to 'at' time as a hash" do it "should set time to 'at' time as a hash" do
time = 10.minutes.ago time = 10.minutes.ago
at = {hour: time.hour, min: time.min} at = { hour: time.hour, min: time.min }
@period = Crono::Period.new(2.day, at: at) @period = Crono::Period.new(2.day, at: at)
expect(@period.next).to be_eql(2.day.from_now.change(at)) expect(@period.next).to be_eql(2.day.from_now.change(at))
end end
it "should raise error when 'at' is wrong" do it "should raise error when 'at' is wrong" do
expect { expect {
@period = Crono::Period.new(2.day, at: 1) Crono::Period.new(2.day, at: 1)
}.to raise_error("Unknown 'at' format") }.to raise_error("Unknown 'at' format")
end end
it "should return time in relation to last time" do it 'should raise error when period is less than 1 day' do
expect {
Crono::Period.new(5.hours, at: '15:30')
}.to raise_error("period should be at least 1 day to use 'at'")
end
it 'should return time in relation to last time' do
@period = Crono::Period.new(2.day) @period = Crono::Period.new(2.day)
expect(@period.next(since: 1.day.ago)).to be_eql(1.day.from_now) expect(@period.next(since: 1.day.ago)).to be_eql(1.day.from_now)
end end
it "should return today time if it is first run and not too late" do it 'should return today time if it is first run and not too late' do
time = 10.minutes.from_now time = 10.minutes.from_now
at = {hour: time.hour, min: time.min} at = { hour: time.hour, min: time.min }
@period = Crono::Period.new(2.day, at: at) @period = Crono::Period.new(2.day, at: at)
expect(@period.next).to be_eql(Time.now.change(at)) expect(@period.next.utc.to_s).to be_eql(Time.now.change(at).utc.to_s)
end end
end end
end end

View File

@@ -1,31 +1,38 @@
require "spec_helper" require 'spec_helper'
class TestJob
def perform;end
end
describe Crono::Scheduler do describe Crono::Scheduler do
before(:each) do let(:scheduler) { Crono::Scheduler.new }
@scheduler = Crono::Scheduler.new
@jobs = [
Crono::Period.new(3.day, at: 10.minutes.from_now.strftime("%H:%M")),
Crono::Period.new(1.day, at: 20.minutes.from_now.strftime("%H:%M")),
Crono::Period.new(7.day, at: 40.minutes.from_now.strftime("%H:%M"))
].map { |period| Crono::Job.new(TestJob, period) }
@scheduler.jobs = @jobs
end
describe "#add_job" do describe '#add_job' do
it "should call Job#load on Job" do it 'should call Job#load on Job' do
@job = Crono::Job.new(TestJob, Crono::Period.new(10.day, at: "04:05")) @job = Crono::Job.new(TestJob, Crono::Period.new(10.day, at: '04:05'))
expect(@job).to receive(:load) expect(@job).to receive(:load)
@scheduler.add_job(@job) scheduler.add_job(@job)
end end
end end
describe "#next" do describe '#next_jobs' do
it "should return next job in schedule" do it 'should return next job in schedule' do
expect(@scheduler.next).to be @jobs[0] scheduler.jobs = jobs = [
Crono::Period.new(3.days, at: 10.minutes.from_now.strftime('%H:%M')),
Crono::Period.new(1.day, at: 20.minutes.from_now.strftime('%H:%M')),
Crono::Period.new(7.days, at: 40.minutes.from_now.strftime('%H:%M'))
].map { |period| Crono::Job.new(TestJob, period) }
time, jobs = scheduler.next_jobs
expect(jobs).to be_eql [jobs[0]]
end
it 'should return an array of jobs scheduled at same time' do
time = 5.minutes.from_now
scheduler.jobs = jobs = [
Crono::Period.new(1.day, at: time.strftime('%H:%M')),
Crono::Period.new(1.day, at: time.strftime('%H:%M')),
Crono::Period.new(1.day, at: 10.minutes.from_now.strftime('%H:%M'))
].map { |period| Crono::Job.new(TestJob, period) }
time, jobs = scheduler.next_jobs
expect(jobs).to be_eql [jobs[0], jobs[1]]
end end
end end
end end

View File

@@ -6,9 +6,21 @@ require 'byebug'
require 'crono' require 'crono'
require 'generators/crono/install/templates/migrations/create_crono_jobs.rb' require 'generators/crono/install/templates/migrations/create_crono_jobs.rb'
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: "file::memory:?cache=shared") ActiveRecord::Base.establish_connection(
adapter: 'sqlite3',
database: 'file::memory:?cache=shared'
)
ActiveRecord::Base.logger = Logger.new(STDOUT) ActiveRecord::Base.logger = Logger.new(STDOUT)
CreateCronoJobs.up CreateCronoJobs.up
RSpec.configure do |config| class TestJob
def perform
end
end
class TestFailingJob
def perform
fail 'Some error'
end
end end

View File

@@ -0,0 +1,23 @@
require 'spec_helper'
require 'rake'
load 'tasks/crono_tasks.rake'
Rake::Task.define_task(:environment)
describe 'rake' do
describe 'crono:clean' do
it 'should clean unused tasks from DB' do
Crono::CronoJob.create!(job_id: 'used_job')
ENV['CRONOTAB'] = File.expand_path('../../assets/good_cronotab.rb', __FILE__)
Rake::Task['crono:clean'].invoke
expect(Crono::CronoJob.where(job_id: 'used_job')).not_to exist
end
end
describe 'crono:check' do
it 'should check cronotab syntax' do
ENV['CRONOTAB'] = File.expand_path('../../assets/bad_cronotab.rb', __FILE__)
expect { Rake::Task['crono:check'].invoke }.to raise_error
end
end
end

49
spec/web_spec.rb Normal file
View File

@@ -0,0 +1,49 @@
require 'spec_helper'
require 'rack/test'
include Rack::Test::Methods
describe Crono::Web do
let(:app) { Crono::Web }
before do
Crono::CronoJob.destroy_all
@test_job_id = 'Perform TestJob every 5 seconds'
@test_job_log = 'All runs ok'
@test_job = Crono::CronoJob.create!(
job_id: @test_job_id,
log: @test_job_log
)
end
after { @test_job.destroy }
describe '/' do
it 'should show all jobs' do
get '/'
expect(last_response).to be_ok
expect(last_response.body).to include @test_job_id
end
it 'should show a error mark when a job is unhealthy' do
@test_job.update(healthy: false)
get '/'
expect(last_response.body).to include 'Error'
end
end
describe '/job/:id' do
it 'should show job log' do
get "/job/#{@test_job.id}"
expect(last_response).to be_ok
expect(last_response.body).to include @test_job_id
expect(last_response.body).to include @test_job_log
end
it 'should show a message about the unhealthy job' do
message = 'An error occurs during the last execution of this job'
@test_job.update(healthy: false)
get "/job/#{@test_job.id}"
expect(last_response.body).to include message
end
end
end

View File

@@ -7,11 +7,16 @@
%th Job %th Job
%th Last performed at %th Last performed at
%th %th
- for job in @jobs %th
- @jobs.each do |job|
%tr %tr
%td= job.job_id %td= job.job_id
%td= job.last_performed_at || "—" %td= job.last_performed_at || '-'
%td %td
%a{href: url("/jobs/#{job.id}")} - if job.healthy == false
%a{ href: url("/job/#{job.id}") }
%span.label.label-danger Error
%td
%a{ href: url("/job/#{job.id}") }
Log Log
%span.glyphicon.glyphicon-menu-right %span.glyphicon.glyphicon-menu-right

View File

@@ -1,8 +1,14 @@
%ol.breadcrumb %ol.breadcrumb
%li %li
%a{href: url("/")} Home %a{ href: url('/') } Home
%li.active= @job.job_id %li.active= @job.job_id
%h2 %h2
"#{@job.job_id}" Log: "#{@job.job_id}" Log:
- if @job.healthy == false
.alert.alert-danger{ role: 'alert' }
An error occurs during the last execution of this job.
Check the log below for details.
%pre= @job.log %pre= @job.log

View File

@@ -1,15 +1,15 @@
!!! 5 !!! 5
%html{lang: "en"} %html{ lang: 'en' }
%head %head
%meta{charset: "utf-8"} %meta{ charset: 'utf-8' }
%meta{"http-equiv" => "X-UA-Compatible", content: "IE=edge"} %meta{ 'http-equiv' => 'X-UA-Compatible', content: 'IE=edge' }
%meta{name: "viewport", content: "width=device-width, initial-scale=1"} %meta{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
%title Crono Dashboard %title Crono Dashboard
%link{href: "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css", rel: "stylesheet"} %link{ href: 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css', rel: 'stylesheet' }
%link{href: "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap-theme.min.css", rel: "stylesheet"} %link{ href: 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap-theme.min.css', rel: 'stylesheet' }
%link{href: "#{env['SCRIPT_NAME']}/custom.css", rel: "stylesheet"} %link{ href: "#{env['SCRIPT_NAME']}/custom.css", rel: 'stylesheet' }
%body %body
%br %br
@@ -21,5 +21,5 @@
%small Dashboard %small Dashboard
= yield = yield
%script{src: "https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"} %script{ src: 'https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js' }
%script{src: "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"} %script{ src: 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js' }