Compare commits

...

89 Commits

Author SHA1 Message Date
Dzmitry Plashchynski
a7488df85b Bump v1.1.2 2016-12-02 03:42:32 +02:00
Dzmitry Plashchynski
c777933044 Fix PG text limit. Closes #54 2016-12-02 03:41:38 +02:00
Dzmitry Plashchynski
0c2f52d9bc Bump 1.1.1 2016-11-30 01:06:29 +02:00
Dzmitry Plashchynski
33e9794621 Last log instead of Log since it can be truncated 2016-11-30 01:05:47 +02:00
Dzmitry Plashchynski
c336b6d00b Fix empty job_options error 2016-11-30 00:52:54 +02:00
Dzmitry Plashchynski
1cf37ee30f Bump 1.1.0 2016-11-30 00:43:52 +02:00
Dzmitry Plashchynski
137dfe6d19 Fix MySQL default text size limit 2016-11-30 00:16:58 +02:00
Dzmitry Plashchynski
14c3e3162e Log truncating Close #41 2016-11-30 00:11:56 +02:00
Dzmitry Plashchynski
d1e15b8537 Ignore .byebug_history 2016-11-30 00:01:54 +02:00
Dzmitry Plashchynski
f5d65b6cc7 Job options 2016-11-29 15:41:39 +02:00
Dzmitry Plashchynski
64dde82342 Update gems 2016-11-29 15:39:49 +02:00
Dzmitry Plashchynski
e331490c48 Update ruby versions for travis 2016-11-29 15:35:09 +02:00
Dzmitry Plashchynski
8a61e65963 Merge pull request #53 from acolyer/same_day
schedule on: today if at: time not passed
2016-11-16 23:25:56 +02:00
Adrian Colyer
3f9ea8350d schedule today if at: time not passed 2016-11-06 15:47:50 +00:00
Dzmitry Plashchynski
1d9d30ef89 Fix issue link 2016-09-22 18:39:56 +03:00
Dzmitry Plashchynski
72810a95ae Add information about the showexceptions issue. Closes #1055 2016-09-22 18:38:44 +03:00
Dzmitry Plashchynski
4401c6bba4 Merge branch 'master' of github.com:plashchynski/crono 2016-09-22 18:24:56 +03:00
Dzmitry Plashchynski
e7df4bb067 Fix crash when no jobs defined 2016-09-22 18:24:46 +03:00
Dzmitry Plashchynski
935bdebd84 Merge pull request #51 from pachacamac/patch-1
deprecate need to config your app name.
2016-09-22 18:08:51 +03:00
Marc
b22845cba6 deprecate need to config your app name.
Instead of `<AppName>::Application.load_tasks` the same can be achieved by `Rails.app_class.load_tasks`, therefore not requiring the user to configure the AppName. Less margin for errors :)
2016-09-17 12:38:48 +02:00
Dzmitry Plashchynski
77481f1143 Fix asterisk 2016-07-02 21:27:55 +03:00
Dzmitry Plashchynski
42dc1bbc4f Remove gitter 2016-07-02 21:23:34 +03:00
Dzmitry Plashchynski
63c52896f7 Since Rails 5 requires Ruby 2.2.2 or newer. 2016-07-01 04:46:02 +03:00
Dzmitry Plashchynski
977f49a3a4 Rails 3 is not supported anymore 2016-07-01 04:40:00 +03:00
Dzmitry Plashchynski
7155ce797c Bump 1.0.3 2016-07-01 04:36:45 +03:00
Dzmitry Plashchynski
d18866564a Update change log 2016-07-01 04:35:56 +03:00
Dzmitry Plashchynski
b39be015b1 Rails 5 supports "week" in time notations 2016-07-01 04:35:09 +03:00
Dzmitry Plashchynski
51c914ea8a Tested on Rails 5, closes #49 2016-07-01 04:04:48 +03:00
Dzmitry Plashchynski
ac71db0d68 Added Known Issues section to Readme. Closes #43 2016-06-26 15:50:09 +03:00
Dzmitry Plashchynski
4e3ca885a8 Fix table_name_suffix/prefix issue. Closes #33 2016-06-26 02:12:04 +03:00
Dzmitry Plashchynski
8c998794b4 Bump 1.0.1 2016-06-24 13:36:20 +03:00
Dzmitry Plashchynski
de070d5bfa Fix job saving 2016-06-24 13:33:57 +03:00
Dzmitry Plashchynski
e1c143db60 Fix typo 2016-06-24 02:43:15 +03:00
Dzmitry Plashchynski
ad6d6cf9d6 Exclude ruby-2.3 from osx since it isn't installed on Travis 2016-06-24 02:41:10 +03:00
Dzmitry Plashchynski
01cc0864f2 Fix README grammar 2016-06-24 02:23:12 +03:00
Dzmitry Plashchynski
87deadf00e Update gems 2016-06-24 02:17:29 +03:00
Dzmitry Plashchynski
3e5ff2871d Add ruby 2.3 to test on Travis 2016-06-24 02:16:24 +03:00
Dzmitry Plashchynski
b2f107f83f Fix Travis build 2016-06-24 02:10:13 +03:00
Dzmitry Plashchynski
33867f1a25 Merge pull request #47 from adamico/master
Fix for Rails 5
2016-04-18 03:00:35 +03:00
Andrea D'Amico
f240036fcd Fix for Rails 5 2016-04-14 16:16:05 +02:00
Dzmitry Plashchynski
fa69ed0fd9 Bump version to 1.0.0 2016-03-30 16:41:31 +03:00
Dzmitry Plashchynski
95d8e13563 Update gems 2016-03-30 16:40:29 +03:00
Dzmitry Plashchynski
63283509cb Update gems 2016-01-29 18:08:14 +02:00
Dzmitry Plashchynski
16ca450033 Fix option parser 2016-01-20 20:41:26 +02:00
Dzmitry Plashchynski
acbfea2308 Bump 1.0.0.pre2 2016-01-20 16:20:59 +02:00
Dzmitry Plashchynski
b1695964a1 Fix rails loading issue 2016-01-20 15:29:16 +02:00
Dzmitry Plashchynski
fba29d80e2 Bump 1.0.0.pre 2016-01-20 01:23:51 +02:00
Dzmitry Plashchynski
8d6e9e3854 Add log to .gitignore 2016-01-20 01:21:07 +02:00
Dzmitry Plashchynski
c0feafa099 Update gems 2016-01-20 01:20:33 +02:00
Dzmitry Plashchynski
3d71df3d2f Merge pull request #36 from preisanalytics/jhuebl_add_data_to_scheduled_job_20160113
Add the possibility to schedule jobs with arguments
2016-01-16 01:18:22 +02:00
Dzmitry Plashchynski
4c223e1bf6 Merge pull request #34 from lhz/intervals
Added :within option to Period to run only within given time interval.
2016-01-16 01:17:54 +02:00
Dzmitry Plashchynski
65cc443f1d Merge pull request #37 from preisanalytics/jhuebl_add_start_stop_and_restart_to_cli_20160114
add start stop and restart to cli
2016-01-16 01:17:16 +02:00
Jannis Hübl
5d9b420582 fix build and config_spec 2016-01-15 11:34:32 +01:00
Jannis Hübl
4a0c2d78e2 add documentation and fix --help 2016-01-15 11:18:53 +01:00
Jannis Hübl
0affff21d1 add start|stop|restart|run to crono executable
this is done using daemonize gem, but it is not breaking the old
interface. But sets a deprected comment to the old one daemonize
process.
2016-01-15 11:03:38 +01:00
Jannis Hübl
042228900f document how to schedule jobs with arguments 2016-01-14 10:23:46 +01:00
Jannis Hübl
89f3b9a8a1 change to args which will be passed to Job#perform 2016-01-14 09:33:14 +01:00
Jannis Hübl
dd4f92b569 add data to Cron.perform which will be passed to ExampleJob.new(data).perform 2016-01-13 17:44:27 +01:00
Lars Haugseth
4b7b03f8a1 Convert TimeOfDay values to UTC. 2015-10-03 16:34:27 +02:00
Lars Haugseth
a93b937d14 Added :within option to Period to run only within given time interval. 2015-10-03 15:08:43 +02:00
Dzmitry Plashchynski
6881109934 Fix .travis.yml 2015-09-25 12:09:34 +03:00
Dzmitry Plashchynski
ffe49c0557 Update rack 2015-09-21 01:52:33 +03:00
Dzmitry Plashchynski
ecc83c5142 Update Gemfile.lock 2015-09-21 01:51:25 +03:00
Dzmitry Plashchynski
1d25475686 Merge pull request #32 from Natural-Intelligence/support_multiple_nodes
Support multiple nodes
2015-09-21 01:49:19 +03:00
avi_alima
e416113ac2 Add ability to define minimal time between job executions to support multiple corno nodes, so two different nodes will not execute the same job
Add Locking for the case that two nodes start perform job together.

If execution_interval == 0.minutes, skip locking and immediately perform
2015-08-20 13:35:41 +03:00
avi_alima
3a480a7d9a Add ability to define minimal time between job executions to support multiple corno nodes, so two different nodes will not execute the same job
Add Locking for the case that two nodes start perform job together.
2015-08-20 12:54:16 +03:00
avi_alima
32bdba3244 Add ability to define minimal time between job executions to support multiple corno nodes, so two different nodes will not execute the same job 2015-08-19 18:08:48 +03:00
Dzmitry Plashchynski
f76dff32e4 Merge pull request #28 from ChandravatiSG/db_connection_pool_fix
#27 Fixed DB connection pool issue.
2015-06-17 15:53:41 +03:00
ChandravatiSG
eaa3a872bf #27 Fixed DB connection pool issue. 2015-06-17 18:11:41 +05:30
Dzmitry Plashchynski
6b627275d8 v0.9.0 2015-05-29 23:29:46 +03:00
Dzmitry Plashchynski
00d5c777dd Merge branch 'master' of github.com:plashchynski/crono 2015-05-28 17:43:12 +03:00
Dzmitry Plashchynski
c28a0bbc8a Able to specify minutes for hour-based schedule. Closing #26 2015-05-28 17:43:02 +03:00
Dzmitry Plashchynski
45c22ee6ba Merge pull request #24 from rogercampos/feature/no-activejob-dependency
activejob is not a real dependency
2015-05-12 13:33:13 +03:00
Roger Campos
2ac14113b6 activejob is not a real dependency 2015-05-11 18:03:56 +02:00
Dzmitry Plashchynski
f909873165 v0.8.9 2015-04-14 13:48:51 +03:00
Dzmitry Plashchynski
94fed61c8a Update web ui preview 2015-04-13 18:05:29 +03:00
Dzmitry Plashchynski
ddf7127b27 Update web ui preview 2015-04-13 18:01:37 +03:00
Dzmitry Plashchynski
cd7e842fd2 v0.8.9.pre 2015-04-13 17:52:31 +03:00
Dzmitry Plashchynski
ad8794c497 Prefixes for Safari 2015-04-13 17:52:02 +03:00
Dzmitry Plashchynski
f711b6b450 Fix error message 2015-04-13 17:31:53 +03:00
Dzmitry Plashchynski
c2445d831b Fix specs 2015-04-13 16:45:38 +03:00
Dzmitry Plashchynski
1a5fd351b4 Merge branch 'materialize_css' 2015-04-13 16:43:24 +03:00
Dzmitry Plashchynski
aee028919c v0.8.8.pre 2015-04-13 16:18:18 +03:00
Dzmitry Plashchynski
6d41a19212 Handle a few jobs scheduled at the same time 2015-04-13 15:53:20 +03:00
Dzmitry Plashchynski
a28ec7b276 Add specs for bug with jobs scheduled at same time without at 2015-04-13 14:19:34 +03:00
Dzmitry Plashchynski
e8c7400caa Do not freeze time for period specs 2015-04-13 14:18:55 +03:00
Dzmitry Plashchynski
260cf14e95 Fix Cli specs 2015-04-13 14:18:32 +03:00
Dzmitry Plashchynski
1900a06582 Add Crono::Cronotab to process cronotab 2015-04-13 12:43:52 +03:00
Dzmitry Plashchynski
8174f86407 Fix crono:clean task 2015-04-13 12:29:27 +03:00
31 changed files with 726 additions and 161 deletions

2
.gitignore vendored
View File

@@ -6,3 +6,5 @@
/pkg/
/spec/reports/
/tmp/
log/*.log
.byebug_history

View File

@@ -3,13 +3,12 @@ os:
- linux
- osx
rvm:
- 2.0.0
- 2.1
- 2.2
- 2.2.6
- 2.3.3
notifications:
webhooks:
urls:
- https://webhooks.gitter.im/e/907e95dada362be2a13c
on_success: change # options: [always|never|change] default: always
on_failure: always # options: [always|never|change] default: always
on_start: false # default: false
on_start: never # options: [always|never|change] default: always

View File

@@ -1,3 +1,56 @@
1.1.0
-----------
- Rails 3 and old Rubies are not supported anymore, sorry rails 3 guys...
- Requires Ruby 2.2.2 or newer
- Fixed crash when no jobs defined in your cronotab
- Some doc updates (thanks to @pachacamac)
- Job will schedule on: today if at: time not passed (thanks to @acolyer)
- Job log truncating (thanks to @reiz)
1.0.3
-----------
- "every 1 week" jobs now displaying on Rails 5 as "1 week" not as "7 days"
- Liberal gem dependencies to support both Rails 4 and Rails 5
1.0.2
-----------
- Fix table_name_suffix/prefix issue: https://github.com/plashchynski/crono/issues/33
1.0.1
-----------
- Fix job saving
1.0.0
-----------
- Rails 5 support (thanks to @adamico)
- Possibility to schedule jobs with arguments (thanks to @preisanalytics)
- Added :within option to run only within given time interval (thanks to @lhz)
- daemon gem support (thanks to @preisanalytics) https://github.com/plashchynski/crono/pull/37
- Support multiple nodes (thanks to @Natural-Intelligence)
- Fixed DB connection pool issue (thanks to @ChandravatiSG)
0.9.1
-----------
- Add ability to define minimal time between job executions to support multiple corno nodes, so two different nodes will not execute the same job
0.8.9
-----------
- We moved Web UI to materializecss.com CSS framework
- We moved from CDN to local assets for Web UI
- We show current state of a job in Web UI (thanks to @michaelachrisco) https://github.com/plashchynski/crono/issues/16
- We won't write a pidfile unless daemonized (thanks to @thomasfedb) https://github.com/plashchynski/crono/pull/13
- Fixed `rake crono:clean` task error
- Fixed issue when jobs scheduled at same time exclude each other https://github.com/plashchynski/crono/issues/19
- Fixed issue with a daemon crash due to `time interval must be positive` error
0.8.0
-----------

View File

@@ -1,70 +1,60 @@
PATH
remote: .
specs:
crono (0.8.7.pre)
activejob (~> 4.0)
activerecord (~> 4.0)
activesupport (~> 4.0)
crono (1.1.2)
activerecord (>= 4.0)
activesupport (>= 4.0)
GEM
remote: https://rubygems.org/
specs:
activejob (4.2.1)
activesupport (= 4.2.1)
globalid (>= 0.3.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)
activemodel (5.0.0.1)
activesupport (= 5.0.0.1)
activerecord (5.0.0.1)
activemodel (= 5.0.0.1)
activesupport (= 5.0.0.1)
arel (~> 7.0)
activesupport (5.0.0.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
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 (4.0.5)
columnize (= 0.9.0)
columnize (0.9.0)
arel (7.1.4)
byebug (9.0.6)
concurrent-ruby (1.0.2)
daemons (1.2.4)
diff-lcs (1.2.5)
globalid (0.3.5)
activesupport (>= 4.1.0)
haml (4.0.6)
haml (4.0.7)
tilt
i18n (0.7.0)
json (1.8.2)
minitest (5.5.1)
rack (1.6.0)
minitest (5.9.1)
rack (1.6.5)
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)
rspec-expectations (~> 3.2.0)
rspec-mocks (~> 3.2.0)
rspec-core (3.2.2)
rspec-support (~> 3.2.0)
rspec-expectations (3.2.0)
rake (11.3.0)
rspec (3.5.0)
rspec-core (~> 3.5.0)
rspec-expectations (~> 3.5.0)
rspec-mocks (~> 3.5.0)
rspec-core (3.5.4)
rspec-support (~> 3.5.0)
rspec-expectations (3.5.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.2.0)
rspec-mocks (3.2.1)
rspec-support (~> 3.5.0)
rspec-mocks (3.5.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.2.0)
rspec-support (3.2.2)
sinatra (1.4.5)
rack (~> 1.4)
rspec-support (~> 3.5.0)
rspec-support (3.5.0)
sinatra (1.4.7)
rack (~> 1.5)
rack-protection (~> 1.4)
tilt (~> 1.3, >= 1.3.4)
sqlite3 (1.3.10)
tilt (>= 1.3, < 3)
sqlite3 (1.3.12)
thread_safe (0.3.5)
tilt (1.4.1)
timecop (0.7.3)
tilt (2.0.5)
timecop (0.8.1)
tzinfo (1.2.2)
thread_safe (~> 0.1)
@@ -75,10 +65,14 @@ DEPENDENCIES
bundler (>= 1.0.0)
byebug
crono!
daemons
haml
rack-test
rake (~> 10.0)
rspec (~> 3.0)
rake (>= 10.0)
rspec (>= 3.0)
sinatra
sqlite3
timecop (~> 0.7)
timecop (>= 0.7)
BUNDLED WITH
1.13.6

View File

@@ -4,21 +4,20 @@ Crono — Job scheduler for Rails
[![Build Status](https://travis-ci.org/plashchynski/crono.svg?branch=master)](https://travis-ci.org/plashchynski/crono)
[![Code Climate](https://codeclimate.com/github/plashchynski/crono/badges/gpa.svg)](https://codeclimate.com/github/plashchynski/crono)
[![security](https://hakiri.io/github/plashchynski/crono/master.svg)](https://hakiri.io/github/plashchynski/crono/master)
[![Join the chat at https://gitter.im/plashchynski/crono](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/plashchynski/crono?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
Crono is a time-based background job scheduler daemon (just like Cron) for Ruby on Rails.
## The Purpose
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.
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 can't manage 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
Tested with latest MRI Ruby (2.2, 2.1 and 2.0) and Rails 3.2+
Tested with latest MRI Ruby 2.2+, 2.3+, Rails 4.\*, and Rails 5.\*.
Other versions are untested but might work fine.
@@ -47,7 +46,7 @@ Now you are ready to move forward to create a job and schedule it.
#### Create Job
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 requirement is that the `perform` method should take no arguments.
Here's an example of a job:
@@ -61,11 +60,11 @@ class TestJob < ActiveJob::Base
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:
The ActiveJob jobs are 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`:
```ruby
class TestJob # This is not an Active Job job, but pretty legal Crono job.
def perform
def perform(*args)
# put you scheduled code here
# Comments.deleted.clean_up...
end
@@ -77,8 +76,8 @@ Here's an example of a Rake Task within a job:
```ruby
# config/cronotab.rb
require 'rake'
# Be sure to change AppName to your application name!
AppName::Application.load_tasks
Rails.app_class.load_tasks
class Test
def perform
@@ -111,7 +110,7 @@ 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 the job to be performed a few times a day or a week:
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
Crono.perform(TestJob).every 1.week, on: :monday
@@ -124,22 +123,51 @@ The `at` can be a Hash:
Crono.perform(TestJob).every 1.day, at: {hour: 12, min: 15}
```
#### Run daemon
You can schedule a job with arguments, which can contain objects that can be
serialized using JSON.generate
To run Crono daemon, in your Rails project root directory:
```ruby
Crono.perform(TestJob, 'some', 'args').every 1.day, at: {hour: 12, min: 15}
```
You can set some options that not passed to the job but affect how the job will be treated by Crono. For example, you can set to truncate job logs (which stored in the database) to a certain number of records:
```ruby
Crono.perform(TestJob).with_options(truncate_log: 100).every 1.week, on: :monday
```
#### Run
To run Crono, in your Rails project root directory:
bundle exec crono RAILS_ENV=development
crono usage:
```
Usage: crono [options]
Usage: crono [options] [start|stop|restart|run]
-C, --cronotab PATH Path to cronotab file (Default: config/cronotab.rb)
-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)
-P, --pidfile PATH Deprecated! use --piddir with --process_name; Path to pidfile (Default: )
-D, --piddir PATH Path to piddir (Default: tmp/pids)
-N, --process_name NAME Name of the process (Default: crono)
-d, --[no-]daemonize Deprecated! Instead use crono [start|stop|restart] without this option; Daemonize process (Default: false)
-m, --monitor Start monitor process for a deamon (Default false)
-e, --environment ENV Application environment (Default: development)
```
#### Run as a daemon
To run Crono as a daemon, please add to your Gemfile:
```ruby
gem 'daemons'
```
Then:
bundle install; bundle exec crono start RAILS_ENV=development
There are "start", "stop", and "restart" commands.
## Web UI
@@ -161,6 +189,14 @@ Rails.application.routes.draw do
Access management and other questions described in the [wiki](https://github.com/plashchynski/crono/wiki/Web-UI).
#### Known issues
For Rails 5, in case of the errors:
```
`require': cannot load such file -- rack/showexceptions (LoadError)
```
See the related issue [#52](https://github.com/plashchynski/crono/issues/52)
## Capistrano
@@ -172,6 +208,11 @@ Use the `capistrano-crono` gem ([github](https://github.com/plashchynski/capistr
Feel free to create [issues](https://github.com/plashchynski/crono/issues)
## Known Issues
* Is not compatible with the `protected_attributes` gem. See: [https://github.com/plashchynski/crono/issues/43](https://github.com/plashchynski/crono/issues/43)
## License
Please see [LICENSE](https://github.com/plashchynski/crono/blob/master/LICENSE) for licensing details.

View File

@@ -19,16 +19,16 @@ Gem::Specification.new do |spec|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ['lib']
spec.add_runtime_dependency 'activejob', '~> 4.0'
spec.add_runtime_dependency 'activesupport', '~> 4.0'
spec.add_runtime_dependency 'activerecord', '~> 4.0'
spec.add_development_dependency 'rake', '~> 10.0'
spec.add_runtime_dependency 'activesupport', '>= 4.0'
spec.add_runtime_dependency 'activerecord', '>= 4.0'
spec.add_development_dependency 'rake', '>= 10.0'
spec.add_development_dependency 'bundler', '>= 1.0.0'
spec.add_development_dependency 'rspec', '~> 3.0'
spec.add_development_dependency 'timecop', '~> 0.7'
spec.add_development_dependency 'rspec', '>= 3.0'
spec.add_development_dependency 'timecop', '>= 0.7'
spec.add_development_dependency 'sqlite3'
spec.add_development_dependency 'byebug'
spec.add_development_dependency 'sinatra'
spec.add_development_dependency 'haml'
spec.add_development_dependency 'rack-test'
spec.add_development_dependency 'daemons'
end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 90 KiB

View File

@@ -6,10 +6,13 @@ require 'active_support/all'
require 'crono/version'
require 'crono/logging'
require 'crono/period'
require 'crono/time_of_day'
require 'crono/interval'
require 'crono/job'
require 'crono/scheduler'
require 'crono/config'
require 'crono/performer_proxy'
require 'crono/cronotab'
require 'crono/orm/active_record/crono_job'
require 'crono/railtie' if defined?(Rails)

View File

@@ -1,3 +1,5 @@
Thread.abort_on_exception = true
require 'crono'
require 'optparse'
@@ -7,6 +9,8 @@ module Crono
include Singleton
include Logging
COMMANDS = %w(start stop restart run zap reload status)
attr_accessor :config
def initialize
@@ -16,29 +20,45 @@ module Crono
def run
parse_options(ARGV)
parse_command(ARGV)
setup_log
setup_log
write_pid
write_pid unless config.daemonize
load_rails
Cronotab.process(File.expand_path(config.cronotab))
print_banner
check_jobs
start_working_loop
unless have_jobs?
logger.error "You have no jobs in you cronotab file #{config.cronotab}"
return
end
if config.daemonize
start_working_loop_in_daemon
else
start_working_loop
end
end
private
def have_jobs?
Crono.scheduler.jobs.present?
end
def setup_log
if config.daemonize
self.logfile = config.logfile
daemonize
elsif config.deprecated_daemonize
self.logfile = config.logfile
deprecated_daemonize
else
self.logfile = STDOUT
end
end
def daemonize
def deprecated_daemonize
::Process.daemon(true, true)
[$stdout, $stderr].each do |io|
@@ -71,12 +91,30 @@ module Crono
require 'rails'
require File.expand_path('config/environment.rb')
::Rails.application.eager_load!
require File.expand_path(config.cronotab)
end
def check_jobs
return if Crono.scheduler.jobs.present?
logger.error "You have no jobs in you cronotab file #{config.cronotab}"
def start_working_loop_in_daemon
unless ENV['RAILS_ENV'] == 'test'
begin
require 'daemons'
rescue LoadError
raise "You need to add gem 'daemons' to your Gemfile if you wish to use it."
end
end
Daemons.run_proc(config.process_name, dir: config.piddir, dir_mode: :normal, monitor: config.monitor, ARGV: @argv) do |*_argv|
Dir.chdir(root)
Crono.logger = Logger.new(config.logfile)
start_working_loop
end
end
def root
@root ||= rails_root_defined? ? ::Rails.root : DIR_PWD
end
def rails_root_defined?
defined?(::Rails.root)
end
def start_working_loop
@@ -88,8 +126,8 @@ module Crono
end
def parse_options(argv)
OptionParser.new do |opts|
opts.banner = "Usage: crono [options]"
@argv = OptionParser.new do |opts|
opts.banner = "Usage: crono [options] [start|stop|restart|run]"
opts.on("-C", "--cronotab PATH", "Path to cronotab file (Default: #{config.cronotab})") do |cronotab|
config.cronotab = cronotab
@@ -99,12 +137,24 @@ module Crono
config.logfile = logfile
end
opts.on("-P", "--pidfile PATH", "Path to pidfile (Default: #{config.pidfile})") do |pidfile|
opts.on("-P", "--pidfile PATH", "Deprecated! use --piddir with --process_name; Path to pidfile (Default: #{config.pidfile})") do |pidfile|
config.pidfile = pidfile
end
opts.on("-d", "--[no-]daemonize", "Daemonize process (Default: #{config.daemonize})") do |daemonize|
config.daemonize = daemonize
opts.on("-D", "--piddir PATH", "Path to piddir (Default: #{config.piddir})") do |piddir|
config.piddir = piddir
end
opts.on("-N", "--process_name NAME", "Name of the process (Default: #{config.process_name})") do |process_name|
config.process_name = process_name
end
opts.on("-d", "--[no-]daemonize", "Deprecated! Instead use crono [start|stop|restart] without this option; Daemonize process (Default: #{config.daemonize})") do |daemonize|
config.deprecated_daemonize = daemonize
end
opts.on("-m", "--monitor", "Start monitor process for a deamon (Default #{config.monitor})") do
config.monitor = true
end
opts.on '-e', '--environment ENV', "Application environment (Default: #{config.environment})" do |env|
@@ -112,5 +162,12 @@ module Crono
end
end.parse!(argv)
end
def parse_command(argv)
if COMMANDS.include? argv[0]
config.daemonize = true
end
end
end
end

View File

@@ -4,18 +4,31 @@ module Crono
CRONOTAB = 'config/cronotab.rb'
LOGFILE = 'log/crono.log'
PIDFILE = 'tmp/pids/crono.pid'
PIDDIR = 'tmp/pids'
PROCESS_NAME = 'crono'
attr_accessor :cronotab, :logfile, :pidfile, :daemonize, :environment
attr_accessor :cronotab, :logfile, :pidfile, :piddir, :process_name,
:monitor, :daemonize, :deprecated_daemonize, :environment
def initialize
self.cronotab = CRONOTAB
self.logfile = LOGFILE
self.piddir = PIDDIR
self.process_name = PROCESS_NAME
self.daemonize = false
self.deprecated_daemonize = false
self.monitor = false
self.environment = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
end
def pidfile=(pidfile)
@pidfile = pidfile
self.process_name = Pathname.new(pidfile).basename(".*").to_s
self.piddir = Pathname.new(pidfile).dirname.to_s
end
def pidfile
@pidfile || (daemonize ? PIDFILE : nil)
@pidfile || (deprecated_daemonize ? PIDFILE : nil)
end
end
end

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

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

43
lib/crono/interval.rb Normal file
View File

@@ -0,0 +1,43 @@
module Crono
# Interval describes a period between two specific times of day
class Interval
attr_accessor :from, :to
def self.parse(value)
from_to =
case value
when Array then value
when Hash then value.values_at(:from, :to)
when String then value.split('-')
else
fail "Unknown interval format: #{value.inspect}"
end
from, to = from_to.map { |v| TimeOfDay.parse(v) }
new from, to
end
def initialize(from, to)
@from, @to = from, to
end
def within?(value)
tod = ((value.is_a? TimeOfDay) ? value : TimeOfDay.parse(value))
if @from <= @to
tod >= @from && tod < @to
else
tod >= @from || tod < @to
end
end
def next_within(time, period)
begin
time = period.since(time)
end until within? TimeOfDay.parse(time)
time
end
def to_s
"#{@from}-#{@to}"
end
end
end

View File

@@ -6,18 +6,23 @@ module Crono
class Job
include Logging
attr_accessor :performer, :period, :last_performed_at, :job_log,
:job_logger, :healthy
attr_accessor :performer, :period, :job_args, :last_performed_at, :job_options,
:next_performed_at, :job_log, :job_logger, :healthy, :execution_interval
def initialize(performer, period)
def initialize(performer, period, job_args, job_options = nil)
self.execution_interval = 0.minutes
self.performer, self.period = performer, period
self.job_args = JSON.generate(job_args)
self.job_log = StringIO.new
self.job_logger = Logger.new(job_log)
self.job_options = job_options || {}
self.next_performed_at = period.next
@semaphore = Mutex.new
end
def next
period.next(since: last_performed_at)
return next_performed_at if next_performed_at.future?
Time.now
end
def description
@@ -29,8 +34,11 @@ module Crono
end
def perform
return Thread.new {} if perform_before_interval?
log "Perform #{performer}"
self.last_performed_at = Time.now
self.next_performed_at = period.next(since: last_performed_at)
Thread.new { perform_job }
end
@@ -39,11 +47,13 @@ module Crono
@semaphore.synchronize do
update_model
clear_job_log
ActiveRecord::Base.clear_active_connections!
end
end
def load
self.last_performed_at = model.last_performed_at
self.next_performed_at = period.next(since: last_performed_at)
end
private
@@ -52,32 +62,39 @@ module Crono
job_log.truncate(job_log.rewind)
end
def truncate_log(log)
return log.lines.last(job_options[:truncate_log]).join if job_options[:truncate_log]
return log
end
def update_model
saved_log = model.reload.log || ''
log_to_save = saved_log + job_log.string
log_to_save = truncate_log(log_to_save)
model.update(last_performed_at: last_performed_at, log: log_to_save,
healthy: healthy)
end
def perform_job
performer.new.perform
finished_time_sec = format('%.2f', Time.now - last_performed_at)
performer.new.perform *JSON.parse(job_args)
rescue StandardError => e
handle_job_fail(e, finished_time_sec)
handle_job_fail(e)
else
handle_job_success(finished_time_sec)
handle_job_success
ensure
save
end
def handle_job_fail(exception, finished_time_sec)
def handle_job_fail(exception)
finished_time_sec = format('%.2f', Time.now - last_performed_at)
self.healthy = false
log_error "Finished #{performer} in #{finished_time_sec} seconds"\
"with error: #{exception.message}"
" with error: #{exception.message}"
log_error exception.backtrace.join("\n")
end
def handle_job_success(finished_time_sec)
def handle_job_success
finished_time_sec = format('%.2f', Time.now - last_performed_at)
self.healthy = true
log "Finished #{performer} in #{finished_time_sec} seconds"
end
@@ -88,7 +105,7 @@ module Crono
def log(message, severity = Logger::INFO)
@semaphore.synchronize do
logger.log severity, message
logger.log(severity, message) if logger
job_logger.log severity, message
end
end
@@ -96,5 +113,27 @@ module Crono
def model
@model ||= Crono::CronoJob.find_or_create_by(job_id: job_id)
end
def perform_before_interval?
return false if execution_interval == 0.minutes
return true if self.last_performed_at.present? && self.last_performed_at > execution_interval.ago
return true if model.updated_at.present? && model.created_at != model.updated_at && model.updated_at > execution_interval.ago
Crono::CronoJob.transaction do
job_record = Crono::CronoJob.where(job_id: job_id).lock(true).first
return true if job_record.updated_at.present? &&
job_record.updated_at != job_record.created_at &&
job_record.updated_at > execution_interval.ago
job_record.touch
return true unless job_record.save
end
# Means that this node is permit to perform the job.
return false
end
end
end

View File

@@ -3,7 +3,6 @@ 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

View File

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

View File

@@ -4,15 +4,26 @@ module Crono
DAYS = [:monday, :tuesday, :wednesday, :thursday, :friday, :saturday,
:sunday]
def initialize(period, at: nil, on: nil)
def initialize(period, at: nil, on: nil, within: nil)
@period = period
@at_hour, @at_min = parse_at(at) if at
@interval = Interval.parse(within) if within
@on = parse_on(on) if on
end
def next(since: nil)
return initial_next unless since
@next = @period.since(since)
if @interval
if since
@next = @interval.next_within(since, @period)
else
return initial_next if @interval.within?(initial_next)
@next = @interval.next_within(initial_next, @period)
end
else
return initial_next unless since
@next = @period.since(since)
end
@next = @next.beginning_of_week.advance(days: @on) if @on
@next = @next.change(time_atts)
return @next if @next.future?
@@ -21,6 +32,7 @@ module Crono
def description
desc = "every #{@period.inspect}"
desc += " between #{@interval.from} and #{@interval.to} UTC" if @interval
desc += format(' at %.2i:%.2i', @at_hour, @at_min) if @at_hour && @at_min
desc += " on #{DAYS[@on].capitalize}" if @on
desc
@@ -37,6 +49,7 @@ module Crono
def initial_day
return Time.now unless @on
day = Time.now.beginning_of_week.advance(days: @on)
day = day.change(time_atts)
return day if day.future?
@period.from_now.beginning_of_week.advance(days: @on)
end
@@ -49,7 +62,10 @@ module Crono
end
def parse_at(at)
fail "period should be at least 1 day to use 'at'" if @period < 1.day
if @period < 1.day && (at.is_a? String || at[:hour])
fail "period should be at least 1 day to use 'at' with specified hour"
end
case at
when String
time = Time.parse(at)

36
lib/crono/time_of_day.rb Normal file
View File

@@ -0,0 +1,36 @@
module Crono
# TimeOfDay describes a certain hour and minute (on any day)
class TimeOfDay
include Comparable
attr_accessor :hour, :min
def self.parse(value)
time =
case value
when String then Time.parse(value).utc
when Hash then Time.now.change(value).utc
when Time then value.utc
else
fail "Unknown TimeOfDay format: #{value.inspect}"
end
new time.hour, time.min
end
def initialize(hour, min)
@hour, @min = hour, min
end
def to_i
@hour * 60 + @min
end
def to_s
'%02d:%02d' % [@hour, @min]
end
def <=>(other)
to_i <=> other.to_i
end
end
end

View File

@@ -1,3 +1,3 @@
module Crono
VERSION = '0.8.7.pre'
VERSION = '1.1.2'
end

View File

@@ -2,7 +2,7 @@ class CreateCronoJobs < ActiveRecord::Migration
def self.up
create_table :crono_jobs do |t|
t.string :job_id, null: false
t.text :log
t.text :log, limit: 1073741823 # LONGTEXT for MySQL
t.datetime :last_performed_at
t.boolean :healthy
t.timestamps null: false

View File

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

0
log/.keep Normal file
View File

View File

@@ -7,11 +7,29 @@ describe Crono::CLI 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(:have_jobs?).and_return(true)
expect(cli).to receive(:start_working_loop)
expect(cli).to receive(:parse_options)
expect(cli).to receive(:parse_command)
expect(cli).to receive(:write_pid)
expect(Crono::Cronotab).to receive(:process)
cli.run
end
context 'should run as daemon' do
before { cli.config.daemonize = true }
it 'should initialize rails with #load_rails and start working loop' do
expect(cli).to receive(:load_rails)
expect(cli).to receive(:have_jobs?).and_return(true)
expect(cli).to receive(:start_working_loop_in_daemon)
expect(cli).to receive(:parse_options)
expect(cli).to receive(:parse_command)
expect(cli).not_to receive(:write_pid)
expect(Crono::Cronotab).to receive(:process)
cli.run
end
end
end
describe '#parse_options' do
@@ -30,9 +48,24 @@ describe Crono::CLI do
expect(cli.config.pidfile).to be_eql 'tmp/pids/crono.0.log'
end
it 'should set daemonize' do
it 'should set piddir' do
cli.send(:parse_options, ['--piddir', 'tmp/pids'])
expect(cli.config.piddir).to be_eql 'tmp/pids'
end
it 'should set process_name' do
cli.send(:parse_options, ['--process_name', 'crono0'])
expect(cli.config.process_name).to be_eql 'crono0'
end
it 'should set monitor' do
cli.send(:parse_options, ['--monitor'])
expect(cli.config.monitor).to be true
end
it 'should set deprecated_daemonize' do
cli.send(:parse_options, ['--daemonize'])
expect(cli.config.daemonize).to be true
expect(cli.config.deprecated_daemonize).to be true
end
it 'should set environment' do
@@ -40,4 +73,42 @@ describe Crono::CLI do
expect(cli.config.environment).to be_eql('production')
end
end
describe '#parse_command' do
it 'should set daemonize on start' do
cli.send(:parse_command, ['start'])
expect(cli.config.daemonize).to be true
end
it 'should set daemonize on stop' do
cli.send(:parse_command, ['stop'])
expect(cli.config.daemonize).to be true
end
it 'should set daemonize on restart' do
cli.send(:parse_command, ['restart'])
expect(cli.config.daemonize).to be true
end
it 'should set daemonize on run' do
cli.send(:parse_command, ['run'])
expect(cli.config.daemonize).to be true
end
it 'should set daemonize on zap' do
cli.send(:parse_command, ['zap'])
expect(cli.config.daemonize).to be true
end
it 'should set daemonize on reload' do
cli.send(:parse_command, ['reload'])
expect(cli.config.daemonize).to be true
end
it 'should set daemonize on status' do
cli.send(:parse_command, ['status'])
expect(cli.config.daemonize).to be true
end
end
end

View File

@@ -8,8 +8,11 @@ describe Crono::Config do
@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 nil
expect(@config.piddir).to be Crono::Config::PIDDIR
expect(@config.process_name).to be Crono::Config::PROCESS_NAME
expect(@config.daemonize).to be false
expect(@config.deprecated_daemonize).to be false
expect(@config.monitor).to be false
expect(@config.environment).to be_eql ENV['RAILS_ENV']
end
@@ -23,8 +26,8 @@ describe Crono::Config do
specify { expect(pidfile).to be_nil }
end
context "daemonize is true" do
before { config.daemonize = true }
context "deprecated_daemonize is true" do
before { config.deprecated_daemonize = true }
specify { expect(pidfile).to eq Crono::Config::PIDFILE }
end
@@ -36,7 +39,16 @@ describe Crono::Config do
before { config.pidfile = path }
specify { expect(pidfile).to eq path }
it "trys to set piddir" do
expect(config.piddir).to eq "foo/bar"
end
it "trys to set process_name" do
expect(config.process_name).to eq "pid"
end
end
end
end
end

20
spec/cronotab_spec.rb Normal file
View File

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

View File

@@ -2,14 +2,20 @@ 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) }
let(:job_args) {[{some: 'data'}]}
let(:job) { Crono::Job.new(TestJob, period, []) }
let(:job_with_args) { Crono::Job.new(TestJob, period, job_args) }
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
it 'should contain data as JSON String' do
expect(job_with_args.job_args).to eq '[{"some":"data"}]'
end
describe '#next' do
it 'should return next performing time according to period' do
expect(job.next).to be_eql period.next
@@ -36,6 +42,46 @@ describe Crono::Job do
failing_job.perform.join
expect(failing_job.healthy).to be false
end
it 'should execute one' do
job.execution_interval = 5.minutes
expect(job).to receive(:perform_job).once
job.perform.join
thread = job.perform.join
expect(thread).to be_stop
end
it 'should execute twice' do
job.execution_interval = 0.minutes
test_preform_job_twice
end
it 'should execute twice without initialize execution_interval' do
test_preform_job_twice
end
it 'should call perform of performer' do
expect(TestJob).to receive(:new).with(no_args)
thread = job.perform.join
expect(thread).to be_stop
end
it 'should call perform of performer with data' do
test_job = double()
expect(TestJob).to receive(:new).and_return(test_job)
expect(test_job).to receive(:perform).with({'some' => 'data'})
thread = job_with_args.perform.join
expect(thread).to be_stop
end
def test_preform_job_twice
expect(job).to receive(:perform_job).twice
job.perform.join
thread = job.perform.join
expect(thread).to be_stop
end
end
describe '#description' do
@@ -54,19 +100,36 @@ describe Crono::Job do
it 'should update saved job' do
job.last_performed_at = Time.now
job.healthy = true
job.job_args = JSON.generate([{some: 'data'}])
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
it 'should save 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
it 'should not truncate log if not specified' do
log = (1..100).map {|n| "line #{n}" }.join("\n")
job = Crono::Job.new(TestJob, period, [])
job.send(:log, log)
job.save
expect(job.send(:model).reload.log.lines.size).to be >= log.lines.size
end
it 'should truncate log if specified' do
log = (1..100).map {|n| "line #{n}" }.join("\n")
job = Crono::Job.new(TestJob, period, [], truncate_log: 50)
job.send(:log, log)
job.save
expect(job.send(:model).reload.log.lines.size).to be 50
end
end
describe '#load' do
@@ -76,7 +139,7 @@ describe Crono::Job do
end
it 'should load last_performed_at from DB' do
@job = Crono::Job.new(TestJob, period)
@job = Crono::Job.new(TestJob, period, job_args)
@job.load
expect(@job.last_performed_at.utc.to_s).to be_eql @saved_last_performed_at.utc.to_s
end
@@ -85,6 +148,7 @@ describe Crono::Job do
describe '#log' do
it 'should write log messages to both common and job log' do
message = 'Test message'
job.logfile = "/dev/null"
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)

View File

@@ -5,4 +5,29 @@ describe Crono::PerformerProxy do
expect(Crono.scheduler).to receive(:add_job).with(kind_of(Crono::Job))
Crono.perform(TestJob).every(2.days, at: '15:30')
end
it 'should set execution interval' do
allow(Crono).to receive(:scheduler).and_return(Crono::Scheduler.new)
expect_any_instance_of(Crono::Job).to receive(:execution_interval=).with(0.minutes).once
expect_any_instance_of(Crono::Job).to receive(:execution_interval=).with(10.minutes).once
Crono.perform(TestJob).every(2.days, at: '15:30').once_per 10.minutes
end
it 'do nothing when job not initalized' do
expect_any_instance_of(Crono::Job).not_to receive(:execution_interval=)
expect_any_instance_of(described_class).to receive(:once_per)
Crono.perform(TestJob).once_per 10.minutes
end
it 'should add job with args to schedule' do
expect(Crono::Job).to receive(:new).with(TestJob, kind_of(Crono::Period), [:some, {some: 'data'}], nil)
allow(Crono.scheduler).to receive(:add_job)
Crono.perform(TestJob, :some, {some: 'data'}).every(2.days, at: '15:30')
end
it 'should add job with options to schedule' do
expect(Crono::Job).to receive(:new).with(TestJob, kind_of(Crono::Period), [], {some_option: true})
allow(Crono.scheduler).to receive(:add_job)
Crono.perform(TestJob).with_options(some_option: true).every(2.days, at: '15:30')
end
end

View File

@@ -1,16 +1,15 @@
require 'spec_helper'
describe Crono::Period do
around(:each) do |example|
Timecop.freeze do
example.run
end
end
describe '#description' do
it 'should return period description' do
@period = Crono::Period.new(1.week, on: :monday, at: '15:20')
expect(@period.description).to be_eql('every 7 days at 15:20 on Monday')
expected_description = if ActiveSupport::VERSION::MAJOR >= 5
'every 1 week at 15:20 on Monday'
else
'every 7 days at 15:20 on Monday'
end
expect(@period.description).to be_eql(expected_description)
end
end
@@ -50,31 +49,41 @@ describe Crono::Period do
expect(@period.next).to be_eql(tuesday)
end
end
it 'should return today on the first run if not too late' do
@period = Crono::Period.new(1.week, on: :sunday, at: '22:00')
Timecop.freeze(Time.now.beginning_of_week.advance(days: 6)
.change(hour: 21, min: 0)) do
expect(@period.next).to be_eql(
Time.now.beginning_of_week.advance(days: 6).change(hour: 22, min: 0)
)
end
end
end
context 'in daily basis' do
it "should return Time.now if the next time in past" do
@period = Crono::Period.new(1.day, at: '06:00')
expect(@period.next(since: 2.days.ago)).to be_eql(Time.now)
expect(@period.next(since: 2.days.ago).to_s).to be_eql(Time.now.to_s)
end
it 'should return the time 2 days from now' do
it 'should return time 2 days from now' do
@period = Crono::Period.new(2.day)
expect(@period.next).to be_eql(2.days.from_now)
expect(@period.next.to_s).to be_eql(2.days.from_now.to_s)
end
it "should set time to 'at' time as a string" do
time = 10.minutes.ago
at = [time.hour, time.min].join(':')
@period = Crono::Period.new(2.days, at: at)
expect(@period.next).to be_eql(2.days.from_now.change(hour: time.hour, min: time.min))
expect(@period.next.to_s).to be_eql(2.days.from_now.change(hour: time.hour, min: time.min).to_s)
end
it "should set time to 'at' time as a hash" do
time = 10.minutes.ago
at = { hour: time.hour, min: time.min }
@period = Crono::Period.new(2.days, at: at)
expect(@period.next).to be_eql(2.days.from_now.change(at))
expect(@period.next.to_s).to be_eql(2.days.from_now.change(at).to_s)
end
it "should raise error when 'at' is wrong" do
@@ -86,12 +95,12 @@ describe Crono::Period 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'")
}.to raise_error("period should be at least 1 day to use 'at' with specified hour")
end
it 'should return time in relation to last time' do
@period = Crono::Period.new(2.days)
expect(@period.next(since: 1.day.ago)).to be_eql(1.day.from_now)
expect(@period.next(since: 1.day.ago).to_s).to be_eql(1.day.from_now.to_s)
end
it 'should return today time if it is first run and not too late' do
@@ -101,5 +110,32 @@ describe Crono::Period do
expect(@period.next.utc.to_s).to be_eql(Time.now.change(at).utc.to_s)
end
end
context 'in hourly basis' do
it 'should return next hour minutes if current hour minutes passed' do
Timecop.freeze(Time.now.beginning_of_hour.advance(minutes: 20)) do
@period = Crono::Period.new(1.hour, at: { min: 15 })
expect(@period.next.utc.to_s).to be_eql 1.hour.from_now.beginning_of_hour.advance(minutes: 15).utc.to_s
end
end
it 'should return current hour minutes if current hour minutes not passed yet' do
Timecop.freeze(Time.now.beginning_of_hour.advance(minutes: 10)) do
@period = Crono::Period.new(1.hour, at: { min: 15 })
expect(@period.next.utc.to_s).to be_eql Time.now.beginning_of_hour.advance(minutes: 15).utc.to_s
end
end
it 'should return next hour minutes within the given interval' do
Timecop.freeze(Time.now.change(hour: 16, min: 10)) do
@period = Crono::Period.new(1.hour, at: { min: 15 }, within: '08:00-16:00')
expect(@period.next.utc.to_s).to be_eql Time.now.tomorrow.change(hour: 8, min: 15).utc.to_s
end
Timecop.freeze(Time.now.change(hour: 16, min: 10)) do
@period = Crono::Period.new(1.hour, at: { min: 15 }, within: '23:00-07:00')
expect(@period.next.utc.to_s).to be_eql Time.now.change(hour: 23, min: 15).utc.to_s
end
end
end
end
end

View File

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

View File

@@ -25,13 +25,13 @@ describe Crono::Web do
end
it 'should show a error mark when a job is unhealthy' do
@test_job.update(healthy: false)
@test_job.update(healthy: false, last_performed_at: 10.minutes.ago)
get '/'
expect(last_response.body).to include 'Error'
end
it 'should show a success mark when a job is healthy' do
@test_job.update(healthy: true)
@test_job.update(healthy: true, last_performed_at: 10.minutes.ago)
get '/'
expect(last_response.body).to include 'Success'
end

View File

@@ -1,9 +1,19 @@
body {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
min-height: 100vh;
flex-direction: column;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
}
main {
flex: 1 0 auto;
-webkit-box-flex: 1;
-webkit-flex: 1 0 auto;
-ms-flex: 1 0 auto;
flex: 1 0 auto;
}

View File

@@ -2,12 +2,14 @@
%a{ href: url('/') }
%i.mdi-navigation-chevron-left
Back to Home
%h4 "#{@job.job_id}" Log:
%h4 "#{@job.job_id}" Last log:
%main.container.blue-grey.lighten-4.grey-text.text-darken-4
- 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.
.row.red-text
.s12.center-align
%i.mdi-alert-warning
An error occurs during the last execution of this job.
Check the log below for details.
%pre= @job.log