Compare commits

...

92 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
Dzmitry Plashchynski
bb9ab77c1d Bump 0.7.0 2015-03-08 22:53:08 +02:00
Dzmitry Plashchynski
1b5479044f Add web ui screenshot 2015-03-08 22:43:36 +02:00
Dzmitry Plashchynski
9133a664bd Update screenshot 2015-03-08 22:42:19 +02:00
Dzmitry Plashchynski
a667c6ca24 Add crono UI example 2015-03-08 22:30:48 +02:00
Dzmitry Plashchynski
b95c480a8f Add background for web 2015-03-08 22:25:55 +02:00
Dzmitry Plashchynski
ba57834f68 Add layout for the web dashboard 2015-03-08 21:21:06 +02:00
Dzmitry Plashchynski
4fb45724d6 Able to see job log 2015-03-08 00:24:33 +02:00
Dzmitry Plashchynski
bb03a562cc Add simple web interface 2015-03-08 00:03:59 +02:00
Dzmitry Plashchynski
c0b25b2a7c Replace the file DB with shared cached memory DB 2015-03-07 18:50:34 +02:00
Dzmitry Plashchynski
f43ae4b1b1 Save performing errors to DB 2015-03-07 18:05:47 +02:00
Dzmitry Plashchynski
00e51604ae Use sqlite3 file database 2015-03-07 18:03:50 +02:00
Dzmitry Plashchynski
b4d15f7909 Job semaphore 2015-03-07 17:05:51 +02:00
Dzmitry Plashchynski
b3920fa2ee Use Mutex to synchronize access to Job's log 2015-03-07 16:38:11 +02:00
Dzmitry Plashchynski
5652d19e62 Write Job#job_log 2015-03-07 16:24:20 +02:00
Dzmitry Plashchynski
dc1f55e13b Add byebug 2015-03-07 16:24:01 +02:00
Dzmitry Plashchynski
98f058767f Fix spec 2015-03-07 16:06:40 +02:00
Dzmitry Plashchynski
5db94543d1 Fix specs 2015-03-07 04:23:58 +02:00
Dzmitry Plashchynski
3e5363f560 Fix specs 2015-03-07 04:18:54 +02:00
Dzmitry Plashchynski
6d90cb3233 Period#next should return today time if it is first run and not too late 2015-03-07 04:13:59 +02:00
Dzmitry Plashchynski
368cdde296 Fix Ruby 2.0.0 time comparsion issue 2015-03-07 03:54:35 +02:00
Dzmitry Plashchynski
84717493a8 Simplify text in examples 2015-03-07 03:49:29 +02:00
Dzmitry Plashchynski
186fe43fc0 Bump version 0.6.1 2015-03-07 01:39:32 +02:00
Dzmitry Plashchynski
0351079961 Handle situation when Job#next is in the past 2015-03-07 01:38:50 +02:00
Dzmitry Plashchynski
2b53dc7ea1 Error when no jobs 2015-03-07 01:35:45 +02:00
Dzmitry Plashchynski
b649594084 Fix specs 2015-03-07 01:29:16 +02:00
Dzmitry Plashchynski
9138826324 Bump 0.6.0 2015-03-07 01:24:26 +02:00
Dzmitry Plashchynski
3a3620d55c Fix migration 2015-03-07 00:52:50 +02:00
Dzmitry Plashchynski
fe24b435b3 Save job on every perform 2015-03-06 23:57:55 +02:00
Dzmitry Plashchynski
c8a4189fd4 Call Job#load on every Scheduler#add_job call 2015-03-06 23:48:34 +02:00
Dzmitry Plashchynski
78b1d8d6e1 Add Job#load to load info from DB 2015-03-06 23:44:30 +02:00
Dzmitry Plashchynski
c54f52a71d Add Job#save to save job info to DB 2015-03-06 23:41:03 +02:00
Dzmitry Plashchynski
d0b35aaa6e Validate uniqueness of CronoJob job_id 2015-03-06 23:27:02 +02:00
Dzmitry Plashchynski
05113b57ee Add an ActiveRecord model CronoJob 2015-03-06 23:16:43 +02:00
Dzmitry Plashchynski
20135b87ae Add migration to generator 2015-03-06 22:33:13 +02:00
Dzmitry Plashchynski
0f32f8a5a4 Refactoring 2015-03-05 15:29:31 +02:00
Dzmitry Plashchynski
59e71e89f3 Rename Schedule to Scheduler 2015-03-05 15:13:48 +02:00
Dzmitry Plashchynski
368d9ee0a9 Abort on exceptions in workers 2015-03-05 14:58:17 +02:00
Dzmitry Plashchynski
db6df90beb Add to log information about performing time 2015-03-05 14:53:26 +02:00
Dzmitry Plashchynski
2109be7bba Fix time formating 2015-03-05 14:52:56 +02:00
Dzmitry Plashchynski
9ca68b305f Replace loop by while 2015-03-05 14:14:35 +02:00
Dzmitry Plashchynski
750ecb98dd Mirroring logger to CLI class 2015-03-05 14:12:37 +02:00
Dzmitry Plashchynski
8ce3673368 Print schedule on load 2015-03-05 14:06:40 +02:00
Dzmitry Plashchynski
007989fa2c Add Job#description 2015-03-05 14:03:07 +02:00
Dzmitry Plashchynski
5b66e9049b Add Period#description 2015-03-05 14:02:44 +02:00
Dzmitry Plashchynski
527f4768bc Update Gemfile.lock 2015-03-04 23:41:57 +02:00
Dzmitry Plashchynski
dfae4015f8 Fix Changes.md formating 2015-03-04 18:53:38 +02:00
Dzmitry Plashchynski
27f949de10 Bump to 0.5.2 2015-03-04 18:50:53 +02:00
Dzmitry Plashchynski
78fa0f9dae Fix: Next performing time should be related to last performing time 2015-03-04 18:46:44 +02:00
Dzmitry Plashchynski
828488a6bc Add Job class 2015-03-04 18:31:59 +02:00
Dzmitry Plashchynski
36c35bce7d Add Support section to README 2015-03-04 16:07:40 +02:00
Dzmitry Plashchynski
ec53c8376f Bump to 0.5.1 2015-03-04 14:45:40 +02:00
Dzmitry Plashchynski
905b02a276 Note about capistrano-crono 2015-03-04 14:42:34 +02:00
Dzmitry Plashchynski
e96d71552e Add option -e to set environment 2015-03-04 14:26:44 +02:00
Dzmitry Plashchynski
0afdab02ac add monit example 2015-03-04 13:49:41 +02:00
44 changed files with 1041 additions and 195 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
pkg/*
*.gem
.bundle
tmp/*.sqlite3

View File

@@ -1,3 +1,34 @@
0.8.0
-----------
- Added `on` (day of week) option to cronotab.rb semantic
- Added job health check and job health indicator to the Web UI
0.7.0
-----------
- Added simple Web UI
0.6.1
-----------
- Persist job state to your database.
0.5.2
-----------
- 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
-----------

View File

@@ -1,28 +1,51 @@
PATH
remote: .
specs:
crono (0.5.0)
crono (0.8.1)
activejob (~> 4.0)
activerecord (~> 4.0)
activesupport (~> 4.0)
GEM
remote: https://rubygems.org/
specs:
activejob (4.2.0)
activesupport (= 4.2.0)
activejob (4.2.1)
activesupport (= 4.2.1)
globalid (>= 0.3.0)
activesupport (4.2.0)
activemodel (4.2.1)
activesupport (= 4.2.1)
builder (~> 3.1)
activerecord (4.2.1)
activemodel (= 4.2.1)
activesupport (= 4.2.1)
arel (~> 6.0)
activesupport (4.2.1)
i18n (~> 0.7)
json (~> 1.7, >= 1.7.7)
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
arel (6.0.0)
builder (3.2.2)
byebug (3.5.1)
columnize (~> 0.8)
debugger-linecache (~> 1.2)
slop (~> 3.6)
columnize (0.9.0)
debugger-linecache (1.2.0)
diff-lcs (1.2.5)
globalid (0.3.3)
activesupport (>= 4.1.0)
haml (4.0.6)
tilt
i18n (0.7.0)
json (1.8.2)
minitest (5.5.1)
rack (1.6.0)
rack-protection (1.5.3)
rack
rack-test (0.6.3)
rack (>= 1.0)
rake (10.4.2)
rspec (3.2.0)
rspec-core (~> 3.2.0)
@@ -37,7 +60,14 @@ GEM
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.2.0)
rspec-support (3.2.2)
thread_safe (0.3.4)
sinatra (1.4.5)
rack (~> 1.4)
rack-protection (~> 1.4)
tilt (~> 1.3, >= 1.3.4)
slop (3.6.0)
sqlite3 (1.3.10)
thread_safe (0.3.5)
tilt (1.4.1)
timecop (0.7.3)
tzinfo (1.2.2)
thread_safe (~> 0.1)
@@ -47,7 +77,12 @@ PLATFORMS
DEPENDENCIES
bundler (>= 1.0.0)
byebug
crono!
haml
rack-test
rake (~> 10.0)
rspec (~> 3.0)
sinatra
sqlite3
timecop (~> 0.7)

107
README.md
View File

@@ -9,9 +9,11 @@ Crono — Job scheduler for 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. You have total control of jobs performing process. You have the code in Ruby, 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)
## Requirements
@@ -24,14 +26,20 @@ Other versions are untested but might work fine.
Add the following line to your application's Gemfile:
gem 'crono'
```ruby
gem 'crono'
```
Run the bundle command to install it.
Run the `bundle` command to install it.
After you install Crono, you can run the generator:
rails generate crono:install
It will create a configuration file `config/cronotab.rb`
It will create a configuration file `config/cronotab.rb` and migration
Run the migration:
rake db:migrate
Now you are ready to move forward to create a job and schedule it.
@@ -41,41 +49,53 @@ 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.
Here's an example of a test job:
app/jobs/test_job.rb
Here's an example of a job:
class TestJob < ActiveJob::Base
def perform
# put you scheduled code here
# Comments.deleted.clean_up...
end
end
```ruby
# app/jobs/test_job.rb
class TestJob < ActiveJob::Base
def perform
# put you scheduled code here
# Comments.deleted.clean_up...
end
end
```
The ActiveJob jobs is convenient because you can use one job in both periodic and enqueued ways. But Active Job is not required. Any class can be used as a crono job if it implements a method `perform` without arguments:
class TestJob # This is not an Active Job job, but pretty legal Crono job.
def perform
# put you scheduled code here
# Comments.deleted.clean_up...
end
end
```ruby
class TestJob # This is not an Active Job job, but pretty legal Crono job.
def perform
# put you scheduled code here
# Comments.deleted.clean_up...
end
end
```
_Please note that crono uses threads, so your code should be thread-safe_
#### 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:
Crono.perform(TestJob).every 2.days, at: "15:30"
```ruby
# config/cronotab.rb
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:
Crono.perform(TestJob).every 1.day, at: "00:00"
Crono.perform(TestJob).every 1.day, at: "12:00"
```ruby
Crono.perform(TestJob).every 1.week, on: :monday
Crono.perform(TestJob).every 1.week, on: :thursday
```
The `at` can be a Hash:
Crono.perform(TestJob).every 1.day, at: {hour: 12, min: 15}
```ruby
Crono.perform(TestJob).every 1.day, at: {hour: 12, min: 15}
```
#### Run daemon
@@ -90,10 +110,41 @@ Usage: crono [options]
-L, --logfile PATH Path to writable logfile (Default: log/crono.log)
-P, --pidfile PATH Path to pidfile (Default: tmp/pids/crono.pid)
-d, --[no-]daemonize Daemonize process (Default: false)
-e, --environment ENV Application environment (Default: development)
```
## Web UI
Crono comes with a Sinatra application that can display the current state of Crono jobs.
Add `sinatra` and `haml` to your Gemfile
```ruby
gam 'haml'
gem 'sinatra', require: nil
```
Add the following to your `config/routes.rb`:
```ruby
Rails.application.routes.draw do
mount Crono::Web, at: '/crono'
...
```
Access management and other questions described in the [wiki](https://github.com/plashchynski/crono/wiki/Web-UI).
## Capistrano
Use the `capistrano-crono` gem ([github](https://github.com/plashchynski/capistrano-crono/)).
## Support
Feel free to create [issues](https://github.com/plashchynski/crono/issues)
## License
Copyright 2015 Dzmitry Plashchynski <plashchynski@gmail.com>
Licensed under the Apache License, Version 2.0
Please see [LICENSE](https://github.com/plashchynski/crono/blob/master/LICENSE) for licensing details.

View File

@@ -1,12 +1,11 @@
#!/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
cli = Crono::CLI.instance
cli.run
Crono::CLI.instance.run
rescue => e
raise e if $DEBUG
STDERR.puts e.message

View File

@@ -2,25 +2,31 @@
require File.expand_path('../lib/crono/version', __FILE__)
Gem::Specification.new do |s|
s.name = "crono"
s.name = 'crono'
s.version = Crono::VERSION
s.authors = ["Dzmitry Plashchynski"]
s.email = ["plashchynski@gmail.com"]
s.homepage = "https://github.com/plashchynski/crono"
s.description = s.summary = "Job scheduler for Rails"
s.license = "Apache-2.0"
s.authors = ['Dzmitry Plashchynski']
s.email = ['plashchynski@gmail.com']
s.homepage = 'https://github.com/plashchynski/crono'
s.description = s.summary = 'Job scheduler for Rails'
s.license = 'Apache-2.0'
s.required_rubygems_version = ">= 1.3.6"
s.rubyforge_project = "crono"
s.required_rubygems_version = '>= 1.3.6'
s.rubyforge_project = 'crono'
s.add_runtime_dependency "activejob", "~> 4.0"
s.add_runtime_dependency "activesupport", "~> 4.0"
s.add_development_dependency "rake", "~> 10.0"
s.add_development_dependency "bundler", ">= 1.0.0"
s.add_development_dependency "rspec", "~> 3.0"
s.add_development_dependency "timecop", "~> 0.7"
s.add_runtime_dependency 'activejob', '~> 4.0'
s.add_runtime_dependency 'activesupport', '~> 4.0'
s.add_runtime_dependency 'activerecord', '~> 4.0'
s.add_development_dependency 'rake', '~> 10.0'
s.add_development_dependency 'bundler', '>= 1.0.0'
s.add_development_dependency 'rspec', '~> 3.0'
s.add_development_dependency 'timecop', '~> 0.7'
s.add_development_dependency 'sqlite3'
s.add_development_dependency 'byebug'
s.add_development_dependency 'sinatra'
s.add_development_dependency 'haml'
s.add_development_dependency 'rack-test'
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'
end

BIN
examples/crono_web_ui.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

14
examples/cronotab.rb Normal file
View File

@@ -0,0 +1,14 @@
# cronotab.rb - Crono configuration file
#
# Here you can specify periodic jobs and schedule.
# You can use ActiveJob's jobs from `app/jobs/`
# You can use any class. The only requirement is that
# class should have a method `perform` without arguments.
#
class TestJob
def perform
puts 'Test!'
end
end
Crono.perform(TestJob).every 2.days, at: '15:30'

6
examples/monitrc.conf Normal file
View File

@@ -0,0 +1,6 @@
check process crono_myapp
with pidfile /path/to/crono.pid
start program = "bundle exec crono -e production" with timeout 30 seconds
stop program = "kill -s TERM `cat /path/to/crono.pid`" with timeout 30 seconds
if totalmem is greater than 500 MB for 2 cycles then restart
group myapp_crono

View File

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

View File

@@ -2,31 +2,42 @@ require 'crono'
require 'optparse'
module Crono
mattr_accessor :schedule
# Crono::CLI - The main class for the crono daemon exacutable `bin/crono`
class CLI
include Singleton
include Logging
attr_accessor :config
attr_accessor :schedule
attr_accessor :logger
def initialize
self.config = Config.new
self.schedule = Schedule.new
Crono.schedule = schedule
Crono.scheduler = Scheduler.new
end
def run
parse_options(ARGV)
init_logger
daemonize if config.daemonize
setup_log
write_pid
load_rails
print_banner
check_jobs
start_working_loop
end
private
private
def setup_log
if config.daemonize
self.logfile = config.logfile
daemonize
else
self.logfile = STDOUT
end
end
def daemonize
::Process.daemon(true, true)
@@ -34,7 +45,8 @@ module Crono
File.open(config.logfile, 'ab') { |f| io.reopen(f) }
io.sync = true
end
$stdin.reopen("/dev/null")
$stdin.reopen('/dev/null')
end
def write_pid
@@ -42,33 +54,35 @@ module Crono
File.write(pidfile, ::Process.pid)
end
def init_logger
logfile = config.daemonize ? config.logfile : STDOUT
self.logger = Logger.new(logfile)
end
def print_banner
logger.info "Loading Crono #{Crono::VERSION}"
logger.info "Running in #{RUBY_DESCRIPTION}"
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}"
end
end
def load_rails
ENV['RACK_ENV'] = ENV['RAILS_ENV'] = config.environment
require 'rails'
require File.expand_path("config/environment.rb")
require File.expand_path('config/environment.rb')
::Rails.application.eager_load!
require File.expand_path(config.cronotab)
end
def run_job(klass)
logger.info "Perform #{klass}"
Thread.new { klass.new.perform }
def check_jobs
return if Crono.scheduler.jobs.present?
logger.error "You have no jobs in you cronotab file #{config.cronotab}"
end
def start_working_loop
loop do
klass, time = schedule.next
sleep(time - Time.now)
run_job(klass)
while true
next_time, jobs = Crono.scheduler.next_jobs
sleep(next_time - Time.now)
jobs.each(&:perform)
end
end
@@ -91,7 +105,10 @@ module Crono
opts.on("-d", "--[no-]daemonize", "Daemonize process (Default: #{config.daemonize})") do |daemonize|
config.daemonize = daemonize
end
opts.on '-e', '--environment ENV', "Application environment (Default: #{config.environment})" do |env|
config.environment = env
end
end.parse!(argv)
end
end

View File

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

102
lib/crono/job.rb Normal file
View File

@@ -0,0 +1,102 @@
require 'stringio'
require 'logger'
module Crono
# Crono::Job represents a Crono job
class Job
include Logging
attr_accessor :performer, :period, :last_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)
@semaphore = Mutex.new
end
def next
period.next(since: last_performed_at)
end
def description
"Perform #{performer} #{period.description}"
end
def job_id
description
end
def perform
log "Perform #{performer}"
self.last_performed_at = Time.now
Thread.new { perform_job }
end
def save
@semaphore.synchronize do
update_model
clear_job_log
end
end
def load
self.last_performed_at = model.last_performed_at
end
private
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
logger.log severity, message
job_logger.log severity, message
end
end
def model
@model ||= Crono::CronoJob.find_or_create_by(job_id: job_id)
end
end
end

14
lib/crono/logging.rb Normal file
View File

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

View File

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

View File

@@ -1,16 +1,18 @@
module Crono
# Crono::PerformerProxy is a proxy used in cronotab.rb semantic
class PerformerProxy
def initialize(performer, schedule)
def initialize(performer, scheduler)
@performer = performer
@schedule = schedule
@scheduler = scheduler
end
def every(period, *args)
@schedule.add(@performer, Period.new(period, *args))
job = Job.new(@performer, Period.new(period, *args))
@scheduler.add_job(job)
end
end
def self.perform(performer)
PerformerProxy.new(performer, Crono.schedule)
PerformerProxy.new(performer, Crono.scheduler)
end
end

View File

@@ -1,15 +1,53 @@
module Crono
# Period describe frequency of performing a task
class Period
def initialize(period, at: nil)
DAYS = [:monday, :tuesday, :wednesday, :thursday, :friday, :saturday,
:sunday]
def initialize(period, at: nil, on: nil)
@period = period
@at_hour, @at_min = parse_at(at) if at
@on = parse_on(on) if on
end
def next
@period.from_now.change({hour: @at_hour, min: @at_min}.compact)
def next(since: nil)
return initial_next unless since
@next = @period.since(since)
@next = @next.beginning_of_week.advance(days: @on) if @on
@next.change(time_atts)
end
def description
desc = "every #{@period.inspect}"
desc += format(' at %.2i:%.2i', @at_hour, @at_min) if @at_hour && @at_min
desc += " on #{DAYS[@on].capitalize}" if @on
desc
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)
fail "period should be at least 1 day to use 'at'" if @period < 1.day
case at
when String
time = Time.parse(at)
@@ -17,8 +55,12 @@ module Crono
when Hash
return at[:hour], at[:min]
else
raise "Unknown 'at' format"
fail "Unknown 'at' format"
end
end
def time_atts
{ hour: @at_hour, min: @at_min }.compact
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,20 +0,0 @@
module Crono
class Schedule
def initialize
@schedule = []
end
def add(peformer, period)
@schedule << [peformer, period]
end
def next
[queue.first[0], queue.first[1].next]
end
private
def queue
@schedule.sort { |a,b| a[1].next <=> b[1].next }
end
end
end

21
lib/crono/scheduler.rb Normal file
View File

@@ -0,0 +1,21 @@
module Crono
# Scheduler is a container for job list and queue
class Scheduler
attr_accessor :jobs
def initialize
self.jobs = []
end
def add_job(job)
job.load
jobs << job
end
def next_jobs
jobs.group_by(&:next).sort_by {|time,_| time }.first
end
end
mattr_accessor :scheduler
end

View File

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

22
lib/crono/web.rb Normal file
View File

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

View File

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

View File

@@ -1,9 +1,15 @@
# cronotab.rb — Crono configuration file
#
# Here you can specify periodic jobs and their schedule.
# You can specify a periodic job as a ActiveJob class in `app/jobs/`
# Actually you can use any class. The only requirement is that
# the class should implement a method `perform` without arguments.
# Here you can specify periodic jobs and schedule.
# You can use ActiveJob's jobs from `app/jobs/`
# You can use any class. The only requirement is that
# class should have a method `perform` without arguments.
#
# Crono.perform(TestJob).every 2.days, at: "15:30"
# class TestJob
# def perform
# puts 'Test!'
# end
# end
#
# Crono.perform(TestJob).every 2.days, at: '15:30'
#

View File

@@ -0,0 +1,16 @@
class CreateCronoJobs < ActiveRecord::Migration
def self.up
create_table :crono_jobs do |t|
t.string :job_id, null: false
t.text :log
t.datetime :last_performed_at
t.boolean :healthy
t.timestamps null: false
end
add_index :crono_jobs, [:job_id], unique: true
end
def self.down
drop_table :crono_jobs
end
end

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'
class TestJob
def perform;end
end
describe Crono::CLI do
let(:cli) { Crono::CLI.instance }
describe "#run" do
it "should try to initialize rails with #load_rails and start working loop" do
describe '#run' do
it 'should initialize rails with #load_rails and start working loop' do
expect(cli).to receive(:load_rails)
expect(cli).to receive(:start_working_loop)
expect(cli).to receive(:parse_options)
@@ -18,36 +14,30 @@ describe Crono::CLI do
end
end
describe "#run_job" do
it "should run job in separate thread" do
thread = cli.send(:run_job, TestJob).join
expect(thread).to be_stop
end
end
describe "#start_working_loop" do
it "should start working loop"
end
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"
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
it "should set logfile" do
cli.send(:parse_options, ["--logfile", "log/crono.log"])
expect(cli.config.logfile).to be_eql "log/crono.log"
it 'should set logfile' do
cli.send(:parse_options, ['--logfile', 'log/crono.log'])
expect(cli.config.logfile).to be_eql 'log/crono.log'
end
it "should set pidfile" do
cli.send(:parse_options, ["--pidfile", "tmp/pids/crono.0.log"])
expect(cli.config.pidfile).to be_eql "tmp/pids/crono.0.log"
it 'should set pidfile' do
cli.send(:parse_options, ['--pidfile', 'tmp/pids/crono.0.log'])
expect(cli.config.pidfile).to be_eql 'tmp/pids/crono.0.log'
end
it "should set daemonize" do
cli.send(:parse_options, ["--daemonize"])
it 'should set daemonize' do
cli.send(:parse_options, ['--daemonize'])
expect(cli.config.daemonize).to be true
end
it 'should set environment' do
cli.send(:parse_options, ['--environment', 'production'])
expect(cli.config.environment).to be_eql('production')
end
end
end

View File

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

120
spec/job_spec.rb Normal file
View File

@@ -0,0 +1,120 @@
require 'spec_helper'
describe Crono::Job do
let(:period) { Crono::Period.new(2.day, at: '15:00') }
let(:job) { Crono::Job.new(TestJob, period) }
let(:failing_job) { Crono::Job.new(TestFailingJob, period) }
it 'should contain performer and period' do
expect(job.performer).to be TestJob
expect(job.period).to be period
end
describe '#next' 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)
thread = job.perform.join
expect(thread).to be_stop
end
it 'should save performin errors to log' do
thread = failing_job.perform.join
expect(thread).to be_stop
saved_log = Crono::CronoJob.find_by(job_id: failing_job.job_id).log
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
it 'should return job identificator' do
expect(job.description).to be_eql('Perform TestJob every 2 days at 15:00')
end
end
describe '#save' do
it 'should save new job to DB' do
expect(Crono::CronoJob.where(job_id: job.job_id)).to_not exist
job.save
expect(Crono::CronoJob.where(job_id: job.job_id)).to exist
end
it 'should update saved job' do
job.last_performed_at = Time.now
job.healthy = true
job.save
@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.healthy).to be true
end
it 'should save and truncate job log' do
message = 'test message'
job.send(:log, message)
job.save
expect(job.send(:model).reload.log).to include message
expect(job.job_log.string).to be_empty
end
end
describe '#load' do
before do
@saved_last_performed_at = job.last_performed_at = Time.now
job.save
end
it 'should load last_performed_at from DB' do
@job = Crono::Job.new(TestJob, period)
@job.load
expect(@job.last_performed_at.utc.to_s).to be_eql @saved_last_performed_at.utc.to_s
end
end
describe '#log' do
it 'should write log messages to both common and job log' do
message = 'Test message'
expect(job.logger).to receive(:log).with(Logger::INFO, message)
expect(job.job_logger).to receive(:log).with(Logger::INFO, message)
job.send(:log, message)
end
it 'should write job log to Job#job_log' do
message = 'Test message'
job.send(:log, message)
expect(job.job_log.string).to include(message)
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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
require "spec_helper"
require 'spec_helper'
describe Crono::Period do
around(:each) do |example|
@@ -7,29 +7,94 @@ describe Crono::Period do
end
end
describe "#next" do
context "in daily basis" do
it "should return the time 2 days from now" do
describe '#description' do
it 'should return period description' do
@period = Crono::Period.new(1.week, on: :monday, at: '15:20')
expect(@period.description).to be_eql('every 7 days at 15:20 on Monday')
end
end
describe '#next' do
context 'in weakly basis' 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)
expect(@period.next).to be_eql(2.day.from_now)
end
it "should set time to 'at' time as a string" do
@period = Crono::Period.new(2.day, at: "15:20")
expect(@period.next).to be_eql(2.day.from_now.change(hour: 15, min: 20))
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))
end
it "should set time to 'at' time as a hash" do
at = {hour: 18, min: 45}
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))
end
it "should raise error when 'at' is wrong" do
expect {
@period = Crono::Period.new(2.day, at: 1)
Crono::Period.new(2.day, at: 1)
}.to raise_error("Unknown 'at' format")
end
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)
expect(@period.next(since: 1.day.ago)).to be_eql(1.day.from_now)
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)
expect(@period.next.utc.to_s).to be_eql(Time.now.change(at).utc.to_s)
end
end
end
end

View File

@@ -1,20 +0,0 @@
require "spec_helper"
class TestJob
def perform;end
end
describe Crono::Schedule do
describe "#next" do
it "should return next job in schedule" do
@schedule = Crono::Schedule.new
[
Crono::Period.new(3.day, at: "18:55"),
Crono::Period.new(1.day, at: "15:30"),
Crono::Period.new(7.day, at: "06:05")
].each { |period| @schedule.add(TestJob, period) }
expect(@schedule.next).to be_eql([TestJob, 1.day.from_now.change(hour: 15, min: 30)])
end
end
end

38
spec/scheduler_spec.rb Normal file
View File

@@ -0,0 +1,38 @@
require 'spec_helper'
describe Crono::Scheduler do
let(:scheduler) { Crono::Scheduler.new }
describe '#add_job' do
it 'should call Job#load on Job' do
@job = Crono::Job.new(TestJob, Crono::Period.new(10.day, at: '04:05'))
expect(@job).to receive(:load)
scheduler.add_job(@job)
end
end
describe '#next_jobs' do
it 'should return next job in schedule' do
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

View File

@@ -2,7 +2,25 @@ require 'bundler/setup'
Bundler.setup
require 'timecop'
require 'byebug'
require 'crono'
require 'generators/crono/install/templates/migrations/create_crono_jobs.rb'
RSpec.configure do |config|
ActiveRecord::Base.establish_connection(
adapter: 'sqlite3',
database: 'file::memory:?cache=shared'
)
ActiveRecord::Base.logger = Logger.new(STDOUT)
CreateCronoJobs.up
class TestJob
def perform
end
end
class TestFailingJob
def perform
fail 'Some error'
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

0
tmp/.gitkeep Normal file
View File

20
web/assets/custom.css Normal file
View File

@@ -0,0 +1,20 @@
.container {
background-color: #CFD8DC;
}
body {
background-color: #455A64;
}
.page-header {
border-bottom: 1px solid #B6B6B6;
}
#job_list td,#job_list th {
border-top: 1px solid #B6B6B6;
color: #212121;
}
.breadcrumb {
background-color: #FFFFFF;
}

22
web/views/dashboard.haml Normal file
View File

@@ -0,0 +1,22 @@
%ol.breadcrumb
%li.active Home
%h3 Running Jobs
%table.table#job_list
%tr
%th Job
%th Last performed at
%th
%th
- @jobs.each do |job|
%tr
%td= job.job_id
%td= job.last_performed_at || '-'
%td
- if job.healthy == false
%a{ href: url("/job/#{job.id}") }
%span.label.label-danger Error
%td
%a{ href: url("/job/#{job.id}") }
Log
%span.glyphicon.glyphicon-menu-right

14
web/views/job.haml Normal file
View File

@@ -0,0 +1,14 @@
%ol.breadcrumb
%li
%a{ href: url('/') } Home
%li.active= @job.job_id
%h2
"#{@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

25
web/views/layout.haml Normal file
View File

@@ -0,0 +1,25 @@
!!! 5
%html{ lang: 'en' }
%head
%meta{ charset: 'utf-8' }
%meta{ 'http-equiv' => 'X-UA-Compatible', content: 'IE=edge' }
%meta{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
%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-theme.min.css', rel: 'stylesheet' }
%link{ href: "#{env['SCRIPT_NAME']}/custom.css", rel: 'stylesheet' }
%body
%br
%br
.container
.page-header
%h1
Crono #{Crono::VERSION}
%small Dashboard
= yield
%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' }