Compare commits

..

1 Commits

Author SHA1 Message Date
Dzmitry Plashchynski
d31aaaffbe v8.8.0.pre 2015-04-13 15:54:00 +03:00
73 changed files with 464 additions and 1533 deletions

View File

@@ -1,72 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '36 15 * * 4'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'ruby' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

View File

@@ -1,36 +0,0 @@
name: "RSpec"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '00 12 * * *' # daily at 12:00
jobs:
specs:
name: specs
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
ruby: ['2.7', '3.0', '3.1'] # Due to https://github.com/actions/runner/issues/849, we have to use quotes
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup Ruby and install gems
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
cache-version: 1 # change this value when you have to empty the cache manually
- name: Run specs
run: |
bundle exec rspec spec/

3
.gitignore vendored
View File

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

1
.rspec
View File

@@ -1,3 +1,2 @@
--require spec_helper
--format documentation
--color

15
.travis.yml Normal file
View File

@@ -0,0 +1,15 @@
language: ruby
os:
- linux
- osx
rvm:
- 2.0.0
- 2.1
- 2.2
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

View File

@@ -1,68 +1,3 @@
2.0.1
-----------
- Fix a job argument error
2.0.0
-----------
- Converted this gem to a proper Rails engine
- Gets rid of `sinatra` and `haml` dependencies for the Web UI
- Web UI is now responsive and thus usable on smartphones, etc.
- Fixed crash with Ruby 3.0
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
-----------

22
Gemfile
View File

@@ -1,24 +1,4 @@
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
# Specify your gem's dependencies in crono.gemspec.
# Specify your gem's dependencies in crono.gemspec
gemspec
group :development, :test do
# gem 'activerecord'
# gem 'actionpack' # action_controller, action_view
# gem 'sprockets'
gem 'byebug'
gem 'combustion', '~> 1.3'
gem 'daemons'
gem 'rack-test'
gem 'rake', '>= 10.0'
gem 'rspec', '>= 3.0'
gem 'rspec-rails', '>= 4.0'
gem 'sqlite3'
gem 'timecop', '>= 0.7'
end
# To use a debugger
# gem 'byebug', group: [:development, :test]

View File

@@ -1,229 +1,84 @@
PATH
remote: .
specs:
crono (2.0.0)
rails (>= 5.2.8)
sprockets-rails
crono (0.8.8.pre)
activejob (~> 4.0)
activerecord (~> 4.0)
activesupport (~> 4.0)
GEM
remote: https://rubygems.org/
specs:
actioncable (7.0.3.1)
actionpack (= 7.0.3.1)
activesupport (= 7.0.3.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (7.0.3.1)
actionpack (= 7.0.3.1)
activejob (= 7.0.3.1)
activerecord (= 7.0.3.1)
activestorage (= 7.0.3.1)
activesupport (= 7.0.3.1)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.0.3.1)
actionpack (= 7.0.3.1)
actionview (= 7.0.3.1)
activejob (= 7.0.3.1)
activesupport (= 7.0.3.1)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.0)
actionpack (7.0.3.1)
actionview (= 7.0.3.1)
activesupport (= 7.0.3.1)
rack (~> 2.0, >= 2.2.0)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (7.0.3.1)
actionpack (= 7.0.3.1)
activerecord (= 7.0.3.1)
activestorage (= 7.0.3.1)
activesupport (= 7.0.3.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.0.3.1)
activesupport (= 7.0.3.1)
activejob (4.2.1)
activesupport (= 4.2.1)
globalid (>= 0.3.0)
activemodel (4.2.1)
activesupport (= 4.2.1)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (7.0.3.1)
activesupport (= 7.0.3.1)
globalid (>= 0.3.6)
activemodel (7.0.3.1)
activesupport (= 7.0.3.1)
activerecord (7.0.3.1)
activemodel (= 7.0.3.1)
activesupport (= 7.0.3.1)
activestorage (7.0.3.1)
actionpack (= 7.0.3.1)
activejob (= 7.0.3.1)
activerecord (= 7.0.3.1)
activesupport (= 7.0.3.1)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (7.0.3.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
builder (3.2.4)
byebug (11.1.3)
combustion (1.3.6)
activesupport (>= 3.0.0)
railties (>= 3.0.0)
thor (>= 0.14.6)
concurrent-ruby (1.1.10)
crass (1.0.6)
daemons (1.4.1)
diff-lcs (1.5.0)
digest (3.1.0)
erubi (1.10.0)
globalid (1.0.0)
activesupport (>= 5.0)
haml (5.2.2)
temple (>= 0.8.0)
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 (4.0.5)
columnize (= 0.9.0)
columnize (0.9.0)
diff-lcs (1.2.5)
globalid (0.3.5)
activesupport (>= 4.1.0)
haml (4.0.6)
tilt
i18n (1.12.0)
concurrent-ruby (~> 1.0)
loofah (2.18.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
mini_mime (>= 0.1.1)
marcel (1.0.2)
method_source (1.0.0)
mini_mime (1.1.2)
minitest (5.16.2)
mustermann (1.1.1)
ruby2_keywords (~> 0.0.1)
net-imap (0.2.3)
digest
net-protocol
strscan
net-pop (0.1.1)
digest
net-protocol
timeout
net-protocol (0.1.3)
timeout
net-smtp (0.3.1)
digest
net-protocol
timeout
nio4r (2.5.8)
nokogiri (1.13.7-arm64-darwin)
racc (~> 1.4)
nokogiri (1.13.7-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.13.7-x86_64-linux)
racc (~> 1.4)
racc (1.6.0)
rack (2.2.4)
rack-protection (2.2.0)
i18n (0.7.0)
json (1.8.2)
minitest (5.5.1)
rack (1.6.0)
rack-protection (1.5.3)
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (7.0.3.1)
actioncable (= 7.0.3.1)
actionmailbox (= 7.0.3.1)
actionmailer (= 7.0.3.1)
actionpack (= 7.0.3.1)
actiontext (= 7.0.3.1)
actionview (= 7.0.3.1)
activejob (= 7.0.3.1)
activemodel (= 7.0.3.1)
activerecord (= 7.0.3.1)
activestorage (= 7.0.3.1)
activesupport (= 7.0.3.1)
bundler (>= 1.15.0)
railties (= 7.0.3.1)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.4.3)
loofah (~> 2.3)
railties (7.0.3.1)
actionpack (= 7.0.3.1)
activesupport (= 7.0.3.1)
method_source
rake (>= 12.2)
thor (~> 1.0)
zeitwerk (~> 2.5)
rake (13.0.6)
rspec (3.11.0)
rspec-core (~> 3.11.0)
rspec-expectations (~> 3.11.0)
rspec-mocks (~> 3.11.0)
rspec-core (3.11.0)
rspec-support (~> 3.11.0)
rspec-expectations (3.11.0)
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)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.11.0)
rspec-mocks (3.11.1)
rspec-support (~> 3.2.0)
rspec-mocks (3.2.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.11.0)
rspec-rails (5.1.2)
actionpack (>= 5.2)
activesupport (>= 5.2)
railties (>= 5.2)
rspec-core (~> 3.10)
rspec-expectations (~> 3.10)
rspec-mocks (~> 3.10)
rspec-support (~> 3.10)
rspec-support (3.11.0)
ruby2_keywords (0.0.5)
sinatra (2.2.0)
mustermann (~> 1.0)
rack (~> 2.2)
rack-protection (= 2.2.0)
tilt (~> 2.0)
sprockets (4.1.1)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.4.2)
actionpack (>= 5.2)
activesupport (>= 5.2)
sprockets (>= 3.0.0)
sqlite3 (1.4.2)
strscan (3.0.3)
temple (0.8.2)
thor (1.2.1)
tilt (2.0.10)
timecop (0.9.5)
timeout (0.3.0)
tzinfo (2.0.4)
concurrent-ruby (~> 1.0)
websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
zeitwerk (2.6.0)
rspec-support (~> 3.2.0)
rspec-support (3.2.2)
sinatra (1.4.5)
rack (~> 1.4)
rack-protection (~> 1.4)
tilt (~> 1.3, >= 1.3.4)
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)
PLATFORMS
arm64-darwin-21
x86_64-darwin-21
x86_64-linux
ruby
DEPENDENCIES
bundler (>= 2)
bundler (>= 1.0.0)
byebug
combustion (~> 1.3)
crono!
daemons
haml
rack-test
rake (>= 10.0)
rspec (>= 3.0)
rspec-rails (>= 4.0)
rake (~> 10.0)
rspec (~> 3.0)
sinatra
sqlite3
timecop (>= 0.7)
BUNDLED WITH
2.3.14
timecop (~> 0.7)

View File

@@ -1,4 +1,4 @@
Apache License
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2022 Dzmitry Plashchynski
Copyright 2015 Dzmitry Plashchynski <plashchynski@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -199,3 +199,4 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

2
NOTICE
View File

@@ -1,2 +1,2 @@
Copyright 2022 Dzmitry Plashchynski <plashchynski@gmail.com>
Copyright 2015 Dzmitry Plashchynski <plashchynski@gmail.com>
Licensed under the Apache License, Version 2.0

169
README.md
View File

@@ -1,14 +1,25 @@
Job scheduler for Rails
Crono — Job scheduler for Rails
------------------------
[![Gem Version](https://badge.fury.io/rb/crono.svg)](http://badge.fury.io/rb/crono)
[![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 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.
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/main/examples/crono_web_ui.png)
![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+
Other versions are untested but might work fine.
## Installation
@@ -31,105 +42,47 @@ Run the migration:
Now you are ready to move forward to create a job and schedule it.
### Compatibility
* **Crono v1.1.2** and older are compatible with Ruby 2.7.x and older
* **Crono v2.0.0** and newer are compatible with Ruby 2.7.x and _newer_
## Usage
### The basic usage
#### Create Job
You can specigy a simple job by editing ```config/cronotab.rb```:
Crono can use Active Job jobs from `app/jobs/`. The only requirements is that the `perform` method should take no arguments.
```ruby
# config/cronotab.rb
class TestJob
def perform
puts 'Test!'
end
end
Crono.perform(TestJob).every 5.seconds
```
Then, run a crono process:
```bundle exec crono -e development```
### Job Schedule
Schedule list is defined in the file `config/cronotab.rb`, that created using `rake crono:install`. The semantic is pretty straightforward:
```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 the job to be performed a few times a day or a week:
```ruby
Crono.perform(TestJob).every 1.week, on: :monday
Crono.perform(TestJob).every 1.week, on: :thursday
```
The `at` can be a Hash:
```ruby
Crono.perform(TestJob).every 1.day, at: {hour: 12, min: 15}
```
You can schedule a job with arguments, which can contain objects that can be serialized using JSON.generate
```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
```
### Job classes
Crono can use Active Job jobs from `app/jobs/`. Here's an example of a job:
Here's an example of a job:
```ruby
# app/jobs/test_job.rb
class TestJob < ActiveJob::Base
def perform(options)
def perform
# put you scheduled code here
# Comments.deleted.clean_up...
end
end
```
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`:
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:
```ruby
class TestJob # This is not an Active Job job, but pretty legal Crono job.
def perform(*args)
def perform
# put you scheduled code here
# Comments.deleted.clean_up...
end
end
```
### Run rake tasks
Here's an example of a Rake Task within a job:
```ruby
# config/cronotab.rb
require 'rake'
Rails.app_class.load_tasks
# Be sure to change AppName to your application name!
AppName::Application.load_tasks
class Test
def perform
Rake::Task['crono:hello'].execute
Rake::Task['crono:hello'].invoke
end
end
@@ -148,60 +101,66 @@ end
_Please note that crono uses threads, so your code should be thread-safe_
### Run crono
#### Job Schedule
Run crono in your Rails project root directory:
Schedule list is defined in the file `config/cronotab.rb`, that created using `crono:install`. The semantic is pretty straightforward:
bundle exec crono -e development
Usage:
```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"
```
Usage: crono [options] [start|stop|restart|run]
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
Crono.perform(TestJob).every 1.week, on: :thursday
```
The `at` can be a Hash:
```ruby
Crono.perform(TestJob).every 1.day, at: {hour: 12, min: 15}
```
#### Run daemon
To run Crono daemon, in your Rails project root directory:
bundle exec crono RAILS_ENV=development
crono usage:
```
Usage: crono [options]
-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 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)
-m, --monitor Start monitor process for a deamon (Default false)
-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)
```
#### 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
Crono can display the current state of Crono jobs.
Crono comes with a Sinatra application that can display the current state of Crono jobs.
Add `sinatra` and `haml` to your Gemfile
```ruby
gem 'haml'
gem 'sinatra', require: nil
```
Add the following to your `config/routes.rb`:
```ruby
Rails.application.routes.draw do
mount Crono::Engine, at: '/crono'
mount Crono::Web, at: '/crono'
...
```
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
@@ -210,9 +169,9 @@ Use the `capistrano-crono` gem ([github](https://github.com/plashchynski/capistr
## Support
Feel free to create an [issues](https://github.com/plashchynski/crono/issues)
Feel free to create [issues](https://github.com/plashchynski/crono/issues)
## License
Please see [LICENSE](LICENSE) for licensing details.
Please see [LICENSE](https://github.com/plashchynski/crono/blob/master/LICENSE) for licensing details.

View File

@@ -1,5 +1,6 @@
require 'bundler/setup'
load 'rails/tasks/statistics.rake'
require 'bundler/gem_tasks'
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new('spec')
task default: :spec

View File

@@ -1,11 +0,0 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 2.x | :white_check_mark: |
## Reporting a Vulnerability
You are welcome to report security information to [plashchynski@gmail.com](mailto:plashchynski@gmail.com?subject=[GitHub]%20Security)

File diff suppressed because one or more lines are too long

View File

@@ -1,26 +0,0 @@
nav {
position: relative;
z-index: 2;
}
nav .brand-logo {
font-size: 1.75em;
letter-spacing: -0.05em;
}
main {
position: relative;
z-index: 1;
padding: 20px;
}
main .page-footer {
margin: 0 -20px -20px -20px;
}
.page-footer > .footer-copyright {
padding: 20px;
}
.responsive-overflow {
overflow-x: auto;
}

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +0,0 @@
module Crono
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
end

View File

@@ -1,11 +0,0 @@
module Crono
class JobsController < ApplicationController
def index
@jobs = Crono::CronoJob.all
end
def show
@job = Crono::CronoJob.find(params[:id])
end
end
end

View File

@@ -1,5 +0,0 @@
module Crono
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
end

View File

@@ -1,50 +0,0 @@
<header>
<h4>Running Jobs</h4>
</header>
<div class="responsive-overflow">
<table id="job_list">
<tr>
<th>Job</th>
<th>Last performed at</th>
<th>Status</th>
<th></th>
</tr>
<% @jobs.each do |job| %>
<tr>
<td>
<%= job.job_id %>
</td>
<td>
<%= job.last_performed_at || 'Never performed yet' %>
</td>
<td>
<% if job.last_performed_at.nil? %>
<span class="grey-text darken-3" title="This job has never been performed yet.">
◷ Pending
</span>
<% elsif job.healthy %>
<a href="<%= job_path(job) %>">
<span class="green-text darken-3" title="This job was performed successfully.">
✔ Success
</span>
</a>
<% else %>
<a href="<%= job_path(job) %>">
<span class="red-text" title="There were problems with this job. Follow the link to check the log.">
⚠ Error
</span>
</a>
<% end %>
</td>
<td>
<% if job.last_performed_at %>
<a href="<%= job_path(job) %>" class="right">
Log
</a>
<% end %>
</td>
</tr>
<% end %>
</table>
</div>

View File

@@ -1,16 +0,0 @@
<header class="blue-grey lighten-4 grey-text text-darken-4">
<a href="<%= root_url %>">
Back to Home
</a>
<h4>Last log of <i><%= @job.job_id %></i>:</h4>
</header>
<% if @job.healthy == false %>
<div class="row red-text">
<div class="s12 center-align">
An error occurs during the last execution of this job.
Check the log below for details.
</div>
</div>
<% end %>
<pre class="responsive-overflow"><%= @job.log %></pre>

View File

@@ -1,31 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta content="IE=edge" http-equiv="X-UA-Compatible"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
<title>Crono Dashboard</title>
<%= stylesheet_link_tag 'crono/materialize.min.css', media: "all" %>
<%= stylesheet_link_tag 'crono/application', media: "all" %>
<%= csrf_meta_tags %>
</head>
<body class="blue-grey darken-2">
<nav class="blue-grey white-text" role="navigation">
<a class="brand-logo center" href="<%= root_path %>">
<b>Crono</b> Dashboard
</a>
</nav>
<main class="container blue-grey lighten-4">
<%= yield %>
<footer class="page-footer blue-grey darken-2">
<div class="footer-copyright blue-grey">
Crono v<%= Crono::VERSION %>
<a class="right" href="https://github.com/plashchynski/crono" target="_blank">Documentation</a>
</div>
</footer>
</main>
</body>
</html>

14
bin/console Executable file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env ruby
require "bundler/setup"
require "crono"
# You can add fixtures and/or initialization code here to make experimenting
# with your gem easier. You can also use a different console, if you like.
# (If you use this, don't forget to add pry to your Gemfile!)
# require "pry"
# Pry.start
require "irb"
IRB.start

View File

@@ -1,24 +0,0 @@
#!/usr/bin/env ruby
# This command will automatically be run when you run "rails" with Rails gems
# installed from the root of your application.
ENGINE_ROOT = File.expand_path('..', __dir__)
ENGINE_PATH = File.expand_path('../lib/crono/engine', __dir__)
# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
require 'rails'
# Pick the frameworks you want:
require 'active_model/railtie'
require 'active_job/railtie'
require 'active_record/railtie'
require 'active_storage/engine'
require 'action_controller/railtie'
require 'action_mailer/railtie'
require 'action_view/railtie'
require 'action_cable/engine'
# require "sprockets/railtie"
# require "rails/test_unit/railtie"
require 'rails/engine/commands'

7
bin/setup Normal file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
set -euo pipefail
IFS=$'\n\t'
bundle install
# Do any other automated setup that you need to do here

View File

@@ -1,9 +0,0 @@
# frozen_string_literal: true
require "rubygems"
require "bundler"
Bundler.require :default, :development
Combustion.initialize! :all
run Combustion::Application

View File

@@ -1,4 +0,0 @@
Crono::Engine.routes.draw do
resources :jobs
root 'jobs#index'
end

View File

@@ -1,33 +1,34 @@
# -*- encoding: utf-8 -*-
$:.push File.expand_path("../lib", __FILE__)
require_relative 'lib/crono/version'
# coding: utf-8
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'crono/version'
Gem::Specification.new do |s|
s.name = 'crono'
s.version = Crono::VERSION
s.authors = ['Dzmitry Plashchynski']
s.email = ['plashchynski@gmail.com']
Gem::Specification.new do |spec|
spec.name = 'crono'
spec.version = Crono::VERSION
spec.authors = ['Dzmitry Plashchynski']
spec.email = ['plashchynski@gmail.com']
s.summary = 'Job scheduler for Rails'
s.description = 'A time-based background job scheduler daemon (just like Cron) for Rails'
s.homepage = 'https://github.com/plashchynski/crono'
s.license = 'Apache-2.0'
spec.summary = 'Job scheduler for Rails'
spec.description = 'A time-based background job scheduler daemon (just like Cron) for Rails'
spec.homepage = 'https://github.com/plashchynski/crono'
spec.license = 'Apache-2.0'
s.files = Dir['{app,config,db,lib}/**/*', 'LICENSE', 'Rakefile', 'README.rdoc']
s.test_files = Dir['spec/**/*']
s.executables = ['crono']
s.require_paths = ["lib"]
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
spec.bindir = 'exe' # http://bundler.io/blog/2015/03/20/moving-bins-to-exe.html
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ['lib']
s.add_dependency 'rails', '>= 5.2.8'
s.add_dependency 'sprockets-rails'
s.add_development_dependency 'rake', '>= 13.0.1'
s.add_development_dependency 'bundler', '>= 2'
s.add_development_dependency 'rspec', '>= 3.10'
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.add_development_dependency 'daemons'
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_development_dependency 'bundler', '>= 1.0.0'
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'
end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -2,18 +2,16 @@
module Crono
end
require 'rails'
require 'sprockets/railtie'
require 'active_support/all'
require 'crono/version'
require 'crono/engine'
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)
Crono.autoload :Web, 'crono/web'

View File

@@ -1,5 +1,3 @@
Thread.abort_on_exception = true
require 'crono'
require 'optparse'
@@ -9,8 +7,6 @@ module Crono
include Singleton
include Logging
COMMANDS = %w(start stop restart run zap reload status)
attr_accessor :config
def initialize
@@ -20,41 +16,40 @@ module Crono
def run
parse_options(ARGV)
parse_command(ARGV)
setup_log
write_pid if config.daemonize
write_pid
load_rails
Cronotab.process(File.expand_path(config.cronotab))
print_banner
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
check_jobs
start_working_loop
end
private
def have_jobs?
Crono.scheduler.jobs.present?
end
def setup_log
if config.daemonize
self.logfile = config.logfile
daemonize
else
self.logfile = STDOUT
end
end
def daemonize
::Process.daemon(true, true)
[$stdout, $stderr].each do |io|
File.open(config.logfile, 'ab') { |f| io.reopen(f) }
io.sync = true
end
$stdin.reopen('/dev/null')
end
def write_pid
return unless config.pidfile
pidfile = File.expand_path(config.pidfile)
@@ -79,42 +74,22 @@ module Crono
::Rails.application.eager_load!
end
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)
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
next_time, jobs = Crono.scheduler.next_jobs
now = Time.zone.now
sleep(next_time - now) if next_time > now
sleep(next_time - Time.now) if next_time > Time.now
jobs.each(&:perform)
end
end
def parse_options(argv)
@argv = OptionParser.new do |opts|
opts.banner = "Usage: crono [options] [start|stop|restart|run]"
OptionParser.new do |opts|
opts.banner = "Usage: crono [options]"
opts.on("-C", "--cronotab PATH", "Path to cronotab file (Default: #{config.cronotab})") do |cronotab|
config.cronotab = cronotab
@@ -124,20 +99,12 @@ module Crono
config.logfile = logfile
end
opts.on("-P", "--pidfile PATH", "Deprecated! use --piddir with --process_name; Path to pidfile (Default: #{config.pidfile})") do |pidfile|
opts.on("-P", "--pidfile PATH", "Path to pidfile (Default: #{config.pidfile})") do |pidfile|
config.pidfile = pidfile
end
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("-m", "--monitor", "Start monitor process for a deamon (Default #{config.monitor})") do
config.monitor = true
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|
@@ -145,12 +112,5 @@ 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,30 +4,18 @@ 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, :piddir, :process_name,
:monitor, :daemonize, :environment
attr_accessor :cronotab, :logfile, :pidfile, :daemonize, :environment
def initialize
self.cronotab = CRONOTAB
self.logfile = LOGFILE
self.piddir = PIDDIR
self.process_name = PROCESS_NAME
self.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
@pidfile || (daemonize ? PIDFILE : nil)
end
end
end

View File

@@ -1,15 +0,0 @@
module Crono
class Engine < ::Rails::Engine
isolate_namespace Crono
initializer 'crono.assets.precompile' do |app|
app.config.assets.precompile += %w( crono/application.css crono/materialize.min.css )
end
config.generators do |g|
g.test_framework :rspec
g.assets false
g.helper false
end
end
end

View File

@@ -1,43 +0,0 @@
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,23 +6,20 @@ module Crono
class Job
include Logging
attr_accessor :performer, :period, :job_args, :last_performed_at, :job_options,
:next_performed_at, :job_log, :job_logger, :healthy, :execution_interval
attr_accessor :performer, :period, :last_performed_at,
:next_performed_at, :job_log, :job_logger, :healthy
def initialize(performer, period, job_args = nil, job_options = nil)
self.execution_interval = 0.minutes
def initialize(performer, period)
self.performer, self.period = performer, period
self.job_args = JSON.generate(job_args) if job_args.present?
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
return next_performed_at if next_performed_at.future?
Time.zone.now
Time.now
end
def description
@@ -34,10 +31,8 @@ module Crono
end
def perform
return Thread.new {} if perform_before_interval?
log "Perform #{performer}"
self.last_performed_at = Time.zone.now
self.last_performed_at = Time.now
self.next_performed_at = period.next(since: last_performed_at)
Thread.new { perform_job }
@@ -47,7 +42,6 @@ module Crono
@semaphore.synchronize do
update_model
clear_job_log
ActiveRecord::Base.clear_active_connections!
end
end
@@ -62,25 +56,15 @@ 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
if job_args
performer.new.perform(JSON.parse(job_args))
else
performer.new.perform
end
performer.new.perform
rescue StandardError => e
handle_job_fail(e)
else
@@ -90,7 +74,7 @@ module Crono
end
def handle_job_fail(exception)
finished_time_sec = format('%.2f', Time.zone.now - last_performed_at)
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}"
@@ -98,7 +82,7 @@ module Crono
end
def handle_job_success
finished_time_sec = format('%.2f', Time.zone.now - last_performed_at)
finished_time_sec = format('%.2f', Time.now - last_performed_at)
self.healthy = true
log "Finished #{performer} in #{finished_time_sec} seconds"
end
@@ -109,7 +93,7 @@ module Crono
def log(message, severity = Logger::INFO)
@semaphore.synchronize do
logger.log(severity, message) if logger
logger.log severity, message
job_logger.log severity, message
end
end
@@ -117,27 +101,5 @@ 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

@@ -1,3 +1,5 @@
require 'active_record'
module Crono
# Crono::CronoJob is a ActiveRecord model to store job state
class CronoJob < ActiveRecord::Base

View File

@@ -1,31 +1,18 @@
module Crono
# Crono::PerformerProxy is a proxy used in cronotab.rb semantic
class PerformerProxy
def initialize(performer, scheduler, job_args = nil, job_options = nil)
def initialize(performer, scheduler)
@performer = performer
@scheduler = scheduler
@job_args = job_args
@job_options = job_options
end
def every(period, **options)
@job = Job.new(@performer, Period.new(period, **options), @job_args, @job_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)
@job_options = options
self
def every(period, *args)
job = Job.new(@performer, Period.new(period, *args))
@scheduler.add_job(job)
end
end
def self.perform(performer, *job_args)
PerformerProxy.new(performer, Crono.scheduler, *job_args)
def self.perform(performer)
PerformerProxy.new(performer, Crono.scheduler)
end
end

View File

@@ -4,35 +4,23 @@ module Crono
DAYS = [:monday, :tuesday, :wednesday, :thursday, :friday, :saturday,
:sunday]
def initialize(period, at: nil, on: nil, within: nil)
def initialize(period, at: nil, on: 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)
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
return initial_next unless since
@next = @period.since(since)
@next = @next.beginning_of_week.advance(days: @on) if @on
@next = @next.change(time_atts)
return @next if @next.future?
Time.zone.now
Time.now
end
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
@@ -47,9 +35,8 @@ module Crono
end
def initial_day
return Time.zone.now unless @on
day = Time.zone.now.beginning_of_week.advance(days: @on)
day = day.change(time_atts)
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
@@ -62,13 +49,10 @@ module Crono
end
def parse_at(at)
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
fail "period should be at least 1 day to use 'at'" if @period < 1.day
case at
when String
time = Time.zone.parse(at)
time = Time.parse(at)
return time.hour, time.min
when Hash
return at[:hour], at[:min]
@@ -78,8 +62,7 @@ module Crono
end
def time_atts
atts = { hour: @at_hour, min: @at_min }
atts.respond_to?(:compact) ? atts.compact : atts.select { |_, value| !value.nil? }
{ hour: @at_hour, min: @at_min }.compact
end
end
end

View File

@@ -1,36 +0,0 @@
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.zone.parse(value).utc
when Hash then Time.zone.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 = '2.0.1'
VERSION = '0.8.8.pre'
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

@@ -21,18 +21,7 @@ module Crono
def create_migrations
migration_template 'migrations/create_crono_jobs.rb',
'db/migrate/create_crono_jobs.rb',
migration_version: migration_version
end
def rails5?
Rails.version.start_with? '5'
end
def migration_version
if rails5?
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
end
'db/migrate/create_crono_jobs.rb'
end
end
end

View File

@@ -1,12 +1,16 @@
class CreateCronoJobs < ActiveRecord::Migration[6.1]
def change
class CreateCronoJobs < ActiveRecord::Migration
def self.up
create_table :crono_jobs do |t|
t.string :job_id, null: false
t.text :log, limit: 1073741823 # LONGTEXT for MySQL
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

@@ -7,30 +7,12 @@ 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).not_to receive(:write_pid)
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).to receive(:setup_log)
expect(cli).to receive(:write_pid)
expect(Crono::Cronotab).to receive(:process)
cli.run
end
end
end
describe '#parse_options' do
@@ -49,19 +31,9 @@ describe Crono::CLI do
expect(cli.config.pidfile).to be_eql 'tmp/pids/crono.0.log'
end
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
it 'should set daemonize' do
cli.send(:parse_options, ['--daemonize'])
expect(cli.config.daemonize).to be true
end
it 'should set environment' do
@@ -69,42 +41,4 @@ 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,10 +8,8 @@ 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.piddir).to be Crono::Config::PIDDIR
expect(@config.process_name).to be Crono::Config::PROCESS_NAME
expect(@config.pidfile).to be nil
expect(@config.daemonize).to be false
expect(@config.monitor).to be false
expect(@config.environment).to be_eql ENV['RAILS_ENV']
end
@@ -24,6 +22,12 @@ describe Crono::Config do
specify { expect(pidfile).to be_nil }
end
context "daemonize is true" do
before { config.daemonize = true }
specify { expect(pidfile).to eq Crono::Config::PIDFILE }
end
end
context "explicity configured" do
@@ -32,16 +36,7 @@ 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

Binary file not shown.

View File

@@ -1,3 +0,0 @@
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end

View File

@@ -1,5 +0,0 @@
class PagesController < ApplicationController
def index
#
end
end

View File

@@ -1 +0,0 @@
<p>Hello World</p>

View File

@@ -1,22 +0,0 @@
require_relative 'boot'
require 'rails/all'
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
require 'crono'
module Dummy
class Application < Rails::Application
config.load_defaults Rails::VERSION::STRING.to_f
# Configuration for the application, engines, and railties goes here.
#
# These settings can be overridden in specific environments using the files
# in config/environments, which are processed later.
#
# config.time_zone = "Central Time (US & Canada)"
# config.eager_load_paths << Rails.root.join("extras")
end
end

View File

@@ -1,5 +0,0 @@
# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__)
require "bundler/setup" if File.exist?(ENV['BUNDLE_GEMFILE'])
$LOAD_PATH.unshift File.expand_path('../../../lib', __dir__)

View File

@@ -1,3 +0,0 @@
test:
adapter: sqlite3
database: db/crono_test.sqlite

View File

@@ -1,2 +0,0 @@
# Load the Rails application.
require_relative 'application'

View File

@@ -1,3 +0,0 @@
Rails.application.routes.draw do
root 'pages#index'
end

View File

@@ -1,3 +0,0 @@
test:
service: Disk
root: /Users/chris/Sites/_playground/crono/tmp/storage

View File

@@ -1,10 +0,0 @@
ActiveRecord::Schema.define do
create_table :crono_jobs do |t|
t.string :job_id, null: false
t.text :log, limit: 1_073_741_823 # LONGTEXT for MySQL
t.datetime :last_performed_at
t.boolean :healthy
t.timestamps null: false
end
add_index :crono_jobs, [:job_id], unique: true
end

View File

@@ -1 +0,0 @@
*.log

View File

@@ -1,40 +1,15 @@
require 'spec_helper'
class TestJob
def perform(*args)
puts 'Test!'
end
end
class TestFailingJob
def perform(*args)
raise 'Some error'
end
end
class TestNoArgsJob
def perform
puts 'Test!'
end
end
describe Crono::Job do
let(:period) { Crono::Period.new(2.day, at: '15:00') }
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, []) }
let(:not_args_job) { Crono::Job.new(TestJob, period) }
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
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
@@ -61,50 +36,6 @@ 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 args' do
test_preform_job_twice not_args_job
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(jon_instance = job)
expect(jon_instance).to receive(:perform_job).twice
jon_instance.perform.join
thread = jon_instance.perform.join
expect(thread).to be_stop
end
end
describe '#description' do
@@ -121,48 +52,31 @@ describe Crono::Job do
end
it 'should update saved job' do
job.last_performed_at = Time.zone.now
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 log' do
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
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
before do
@saved_last_performed_at = job.last_performed_at = Time.zone.now
@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_args)
@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
@@ -171,7 +85,6 @@ 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

@@ -1,31 +0,0 @@
require 'spec_helper'
require 'rack/test'
module Crono
RSpec.describe Crono::CronoJob, type: :model 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.size).to be 1
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
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,45 +1,8 @@
require 'spec_helper'
class TestJob
def perform
puts 'Test!'
end
end
describe Crono::PerformerProxy do
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
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' })
allow(Crono.scheduler).to receive(:add_job)
Crono.perform(TestJob, :some, { some: 'data' }).every(2.days, at: '15:30')
end
it 'should add job without args to schedule' do
expect(Crono::Job).to receive(:new).with(TestJob, kind_of(Crono::Period), nil, nil)
allow(Crono.scheduler).to receive(:add_job)
Crono.perform(TestJob).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), nil, { 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

@@ -4,12 +4,7 @@ describe Crono::Period do
describe '#description' do
it 'should return period description' do
@period = Crono::Period.new(1.week, on: :monday, at: '15:20')
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)
expect(@period.description).to be_eql('every 7 days at 15:20 on Monday')
end
end
@@ -27,47 +22,37 @@ describe Crono::Period do
it "should return a 'on' day" do
@period = Crono::Period.new(1.week, on: :thursday, at: '15:30')
current_week = Time.zone.now.beginning_of_week
current_week = Time.now.beginning_of_week
last_run_time = current_week.advance(days: 1) # last run on the tuesday
next_run_at = Time.zone.now.next_week.advance(days: 3)
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.zone.now.beginning_of_week.advance(days: 4)) do
expect(@period.next).to be_eql(Time.zone.now.next_week.advance(days: 3))
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.zone.now.beginning_of_week
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
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.zone.now.beginning_of_week.advance(days: 6)
.change(hour: 21, min: 0)) do
expect(@period.next).to be_eql(
Time.zone.now.beginning_of_week.advance(days: 6).change(hour: 22, min: 0)
)
end
end
end
context 'in daily basis' do
it "should return Time.zone.now if the next time in past" do
it "should return Time.now if the next time in past" do
@period = Crono::Period.new(1.day, at: '06:00')
expect(@period.next(since: 2.days.ago).to_s).to be_eql(Time.zone.now.to_s)
expect(@period.next(since: 2.days.ago).to_s).to be_eql(Time.now.to_s)
end
it 'should return time 2 days from now' do
it 'should return the time 2 days from now' do
@period = Crono::Period.new(2.day)
expect(@period.next.to_s).to be_eql(2.days.from_now.to_s)
end
@@ -76,14 +61,14 @@ describe Crono::Period do
time = 10.minutes.ago
at = [time.hour, time.min].join(':')
@period = Crono::Period.new(2.days, at: at)
expect(@period.next.to_s).to be_eql(2.days.from_now.change(hour: time.hour, min: time.min).to_s)
expect(@period.next).to be_eql(2.days.from_now.change(hour: time.hour, min: time.min))
end
it "should set time to 'at' time as a hash" do
time = 10.minutes.ago
at = { hour: time.hour, min: time.min }
@period = Crono::Period.new(2.days, at: at)
expect(@period.next.to_s).to be_eql(2.days.from_now.change(at).to_s)
expect(@period.next).to be_eql(2.days.from_now.change(at))
end
it "should raise error when 'at' is wrong" do
@@ -95,7 +80,7 @@ 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' with specified hour")
}.to raise_error("period should be at least 1 day to use 'at'")
end
it 'should return time in relation to last time' do
@@ -107,34 +92,7 @@ describe Crono::Period do
time = 10.minutes.from_now
at = { hour: time.hour, min: time.min }
@period = Crono::Period.new(2.days, at: at)
expect(@period.next.utc.to_s).to be_eql(Time.zone.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.zone.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.zone.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.zone.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.zone.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.zone.now.tomorrow.change(hour: 8, min: 15).utc.to_s
end
Timecop.freeze(Time.zone.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.zone.now.change(hour: 23, min: 15).utc.to_s
end
expect(@period.next.utc.to_s).to be_eql(Time.now.change(at).utc.to_s)
end
end
end

View File

@@ -1,89 +0,0 @@
# This file is copied to spec/ when you run 'rails generate rspec:install'
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../spec/internal/config/environment.rb', __dir__)
# Prevent database truncation if the environment is production
abort('The Rails environment is running in production mode!') if Rails.env.production?
require 'rspec/rails'
# Add additional requires below this line. Rails is not loaded until this point!
require 'bundler/setup'
Bundler.setup
$LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
require 'timecop'
require 'byebug'
require 'crono'
# setting default time zone
# In Rails project, Time.zone_default equals "UTC"
Time.zone_default = Time.find_zone('UTC')
ActiveRecord::Base.logger = Logger.new($stdout)
ActiveRecord::Base.establish_connection(
adapter: 'sqlite3',
database: ':memory'
)
ActiveRecord::Schema.define do
require_relative '../lib/generators/crono/install/templates/migrations/create_crono_jobs.rb'
@migration_version = '[6.1]'
end
CreateCronoJobs.up
# Requires supporting ruby files with custom matchers and macros, etc, in
# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
# run as spec files by default. This means that files in spec/support that end
# in _spec.rb will both be required and run as specs, causing the specs to be
# run twice. It is recommended that you do not name files matching this glob to
# end with _spec.rb. You can configure this pattern with the --pattern
# option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
#
# The following line is provided for convenience purposes. It has the downside
# of increasing the boot-up time by auto-requiring all files in the support
# directory. Alternatively, in the individual `*_spec.rb` files, manually
# require only the support files necessary.
#
# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
# Checks for pending migrations and applies them before tests are run.
# If you are not using ActiveRecord, you can remove these lines.
begin
ActiveRecord::Migration.maintain_test_schema!
rescue ActiveRecord::PendingMigrationError => e
puts e.to_s.strip
exit 1
end
RSpec.configure do |config|
# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
# config.fixture_path = "#{::Rails.root}/spec/fixtures"
# If you're not using ActiveRecord, or you'd prefer not to run each of your
# examples within a transaction, remove the following line or assign false
# instead of true.
# config.use_transactional_fixtures = true
# You can uncomment this line to turn off ActiveRecord support entirely.
# config.use_active_record = false
# RSpec Rails can automatically mix in different behaviours to your tests
# based on their file location, for example enabling you to call `get` and
# `post` in specs under `spec/controllers`.
#
# You can disable this behaviour by removing the line below, and instead
# explicitly tag your specs with their type, e.g.:
#
# RSpec.describe UsersController, type: :controller do
# # ...
# end
#
# The different available types are documented in the features, such as in
# https://relishapp.com/rspec/rspec-rails/docs
config.infer_spec_type_from_file_location!
# Filter lines from Rails gems in backtraces.
config.filter_rails_from_backtrace!
# arbitrary gems may also be filtered via:
# config.filter_gems_from_backtrace("gem name")
end

View File

@@ -1,17 +1,11 @@
require 'spec_helper'
class TestJob
def perform
puts 'Test!'
end
end
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'), [])
@job = Crono::Job.new(TestJob, Crono::Period.new(10.day, at: '04:05'))
expect(@job).to receive(:load)
scheduler.add_job(@job)
end
@@ -23,7 +17,7 @@ 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]]
@@ -35,7 +29,7 @@ describe Crono::Scheduler do
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]]
@@ -46,7 +40,7 @@ describe Crono::Scheduler do
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, []) }
].map { |period| Crono::Job.new(TestJob, period) }
_, next_jobs = scheduler.next_jobs
expect(next_jobs).to be_eql [jobs[0]]

View File

@@ -1,19 +1,28 @@
require 'bundler'
require 'bundler/setup'
Bundler.setup
Bundler.require :default, :development
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
# If you're using all parts of Rails:
Combustion.initialize! :all
# Or, load just what you need:
# Combustion.initialize! :active_record, :action_controller
require 'timecop'
require 'byebug'
require 'crono'
require 'generators/crono/install/templates/migrations/create_crono_jobs.rb'
require 'rspec/rails'
# If you're using Capybara:
# require 'capybara/rails'
ActiveRecord::Base.establish_connection(
adapter: 'sqlite3',
database: 'file::memory:?cache=shared'
)
RSpec.configure do |config|
config.use_transactional_fixtures = true
config.mock_with :rspec do |mocks|
mocks.allow_message_expectations_on_nil = true
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

@@ -2,8 +2,8 @@ require 'spec_helper'
require 'rack/test'
include Rack::Test::Methods
describe Crono::Engine do
let(:app) { Crono::Engine }
describe Crono::Web do
let(:app) { Crono::Web }
before do
Crono::CronoJob.destroy_all
@@ -25,13 +25,13 @@ describe Crono::Engine do
end
it 'should show a error mark when a job is unhealthy' do
@test_job.update(healthy: false, last_performed_at: 10.minutes.ago)
@test_job.update(healthy: false)
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, last_performed_at: 10.minutes.ago)
@test_job.update(healthy: true)
get '/'
expect(last_response.body).to include 'Success'
end
@@ -45,7 +45,7 @@ describe Crono::Engine do
describe '/job/:id' do
it 'should show job log' do
get "/jobs/#{@test_job.id}"
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
@@ -54,7 +54,7 @@ describe Crono::Engine do
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 "/jobs/#{@test_job.id}"
get "/job/#{@test_job.id}"
expect(last_response.body).to include message
end
end

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;
}

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

@@ -0,0 +1,28 @@
%ol.breadcrumb
%li.active Home
%h3 Running Jobs
%table.table#job_list
%tr
%th Job
%th Last performed at
%th Status
%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
- if job.healthy == true
%a{ href: url("/job/#{job.id}") }
%span.label.label-success Success
- else
%a{ href: url("/job/#{job.id}") }
%span.label.label-default Pending
%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' }