mirror of
https://github.com/plashchynski/crono.git
synced 2026-01-12 05:20:26 +01:00
Compare commits
229 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c7ff2f7b1 | ||
|
|
2e663c2104 | ||
|
|
ab4e1e808d | ||
|
|
9a8e8049f9 | ||
|
|
6bf1ec9cfd | ||
|
|
d3dc79e4f1 | ||
|
|
e0f41858e0 | ||
|
|
e277fd9fb7 | ||
|
|
d4cc325685 | ||
|
|
0bfdc6996b | ||
|
|
7dffc5c77b | ||
|
|
bfbb8733cd | ||
|
|
4294bad43a | ||
|
|
0702e20add | ||
|
|
db8c29afbe | ||
|
|
43e411cf0f | ||
|
|
e43ad4d409 | ||
|
|
c5c58b559f | ||
|
|
81d1e3496f | ||
|
|
ddbfa4c42e | ||
|
|
e03d9b4ba6 | ||
|
|
811ab31978 | ||
|
|
cb5064852a | ||
|
|
a421a6207b | ||
|
|
4abc999583 | ||
|
|
0e1dec2956 | ||
|
|
3a17938571 | ||
|
|
5391930954 | ||
|
|
5c16c8c6dd | ||
|
|
b1cc71e9a4 | ||
|
|
4d6e7b58b4 | ||
|
|
a3587816e1 | ||
|
|
805fbc48e6 | ||
|
|
80d8713cb2 | ||
|
|
f5b5169b1f | ||
|
|
0a1ec366e0 | ||
|
|
fc18c8c787 | ||
|
|
49b1d2b85d | ||
|
|
efc4254146 | ||
|
|
abbae2bb33 | ||
|
|
e4066cfe10 | ||
|
|
9f1bd7bf72 | ||
|
|
5831e040fc | ||
|
|
258b258003 | ||
|
|
333f18d89b | ||
|
|
be1ecf2995 | ||
|
|
3d7267d97e | ||
|
|
9e8a6f0e05 | ||
|
|
2232671d86 | ||
|
|
8ea181397e | ||
|
|
e500738fd4 | ||
|
|
4279cbe27a | ||
|
|
630137d575 | ||
|
|
ae78de420f | ||
|
|
e6f5e4299b | ||
|
|
a6f8354e89 | ||
|
|
610046d0fe | ||
|
|
0391dac2e9 | ||
|
|
a74da12862 | ||
|
|
7573109519 | ||
|
|
a8e80b6160 | ||
|
|
0af1645456 | ||
|
|
ee79596509 | ||
|
|
6fee31fc81 | ||
|
|
551572fe56 | ||
|
|
d53520bbf0 | ||
|
|
87b5726919 | ||
|
|
9b9193e1c8 | ||
|
|
6ca28ae01d | ||
|
|
11350e83d9 | ||
|
|
3344c431a9 | ||
|
|
7c8246087c | ||
|
|
4136a5af1a | ||
|
|
5b72e08222 | ||
|
|
5c0c2ee195 | ||
|
|
d72a2d2b07 | ||
|
|
258052b852 | ||
|
|
a7488df85b | ||
|
|
c777933044 | ||
|
|
0c2f52d9bc | ||
|
|
33e9794621 | ||
|
|
c336b6d00b | ||
|
|
1cf37ee30f | ||
|
|
137dfe6d19 | ||
|
|
14c3e3162e | ||
|
|
d1e15b8537 | ||
|
|
f5d65b6cc7 | ||
|
|
64dde82342 | ||
|
|
e331490c48 | ||
|
|
8a61e65963 | ||
|
|
3f9ea8350d | ||
|
|
1d9d30ef89 | ||
|
|
72810a95ae | ||
|
|
4401c6bba4 | ||
|
|
e7df4bb067 | ||
|
|
935bdebd84 | ||
|
|
b22845cba6 | ||
|
|
77481f1143 | ||
|
|
42dc1bbc4f | ||
|
|
63c52896f7 | ||
|
|
977f49a3a4 | ||
|
|
7155ce797c | ||
|
|
d18866564a | ||
|
|
b39be015b1 | ||
|
|
51c914ea8a | ||
|
|
ac71db0d68 | ||
|
|
4e3ca885a8 | ||
|
|
8c998794b4 | ||
|
|
de070d5bfa | ||
|
|
e1c143db60 | ||
|
|
ad6d6cf9d6 | ||
|
|
01cc0864f2 | ||
|
|
87deadf00e | ||
|
|
3e5ff2871d | ||
|
|
b2f107f83f | ||
|
|
33867f1a25 | ||
|
|
f240036fcd | ||
|
|
fa69ed0fd9 | ||
|
|
95d8e13563 | ||
|
|
63283509cb | ||
|
|
16ca450033 | ||
|
|
acbfea2308 | ||
|
|
b1695964a1 | ||
|
|
fba29d80e2 | ||
|
|
8d6e9e3854 | ||
|
|
c0feafa099 | ||
|
|
3d71df3d2f | ||
|
|
4c223e1bf6 | ||
|
|
65cc443f1d | ||
|
|
5d9b420582 | ||
|
|
4a0c2d78e2 | ||
|
|
0affff21d1 | ||
|
|
042228900f | ||
|
|
89f3b9a8a1 | ||
|
|
dd4f92b569 | ||
|
|
4b7b03f8a1 | ||
|
|
a93b937d14 | ||
|
|
6881109934 | ||
|
|
ffe49c0557 | ||
|
|
ecc83c5142 | ||
|
|
1d25475686 | ||
|
|
e416113ac2 | ||
|
|
3a480a7d9a | ||
|
|
32bdba3244 | ||
|
|
f76dff32e4 | ||
|
|
eaa3a872bf | ||
|
|
6b627275d8 | ||
|
|
00d5c777dd | ||
|
|
c28a0bbc8a | ||
|
|
45c22ee6ba | ||
|
|
2ac14113b6 | ||
|
|
f909873165 | ||
|
|
94fed61c8a | ||
|
|
ddf7127b27 | ||
|
|
cd7e842fd2 | ||
|
|
ad8794c497 | ||
|
|
f711b6b450 | ||
|
|
c2445d831b | ||
|
|
1a5fd351b4 | ||
|
|
aee028919c | ||
|
|
6d41a19212 | ||
|
|
a28ec7b276 | ||
|
|
e8c7400caa | ||
|
|
260cf14e95 | ||
|
|
1900a06582 | ||
|
|
8174f86407 | ||
|
|
5e944ec375 | ||
|
|
f2f98bbb76 | ||
|
|
c8f9ff4e34 | ||
|
|
84ac08e5d4 | ||
|
|
e8812b1329 | ||
|
|
f6b393ad6b | ||
|
|
55e3956618 | ||
|
|
9c984cda49 | ||
|
|
dbb4f374d7 | ||
|
|
f245f33533 | ||
|
|
742cdc00c7 | ||
|
|
81ee422e30 | ||
|
|
50aec2ea87 | ||
|
|
aaa8bc40e5 | ||
|
|
a11a8985c3 | ||
|
|
b4ad8fb953 | ||
|
|
b010b7349f | ||
|
|
cd5813049c | ||
|
|
c74291a001 | ||
|
|
4aeb29891e | ||
|
|
4415211100 | ||
|
|
4b28f3dd80 | ||
|
|
48db3ef245 | ||
|
|
01a761d919 | ||
|
|
75f1b43c84 | ||
|
|
13ab4838e7 | ||
|
|
d075a55f03 | ||
|
|
2d72020ac4 | ||
|
|
cde8a2d214 | ||
|
|
2ec9cfa829 | ||
|
|
63c86c8cd9 | ||
|
|
e10daec9c6 | ||
|
|
f72c288ce8 | ||
|
|
78ce578484 | ||
|
|
0c77c490bd | ||
|
|
d889b9380d | ||
|
|
f75bdf352b | ||
|
|
fa97f573e0 | ||
|
|
dc70212f9d | ||
|
|
7328bea24c | ||
|
|
f57d440424 | ||
|
|
95a237aeb5 | ||
|
|
6508197f26 | ||
|
|
a0c612fb27 | ||
|
|
a3c4ec87f5 | ||
|
|
9b85c8b8c3 | ||
|
|
1af691ef24 | ||
|
|
8a89a9a8eb | ||
|
|
7e2e65e21b | ||
|
|
4205b12fe1 | ||
|
|
bb9f62a69b | ||
|
|
a9798acb35 | ||
|
|
1aa27baca8 | ||
|
|
af1e1959ac | ||
|
|
7af68aa591 | ||
|
|
6d2e3fbb75 | ||
|
|
46fe2548d9 | ||
|
|
6859d1f09a | ||
|
|
6674bca0be | ||
|
|
5a5c29c52a | ||
|
|
a24389f6cc | ||
|
|
54a1b53af9 | ||
|
|
eedff96d39 |
72
.github/workflows/codeql-analysis.yml
vendored
Normal file
72
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
# 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
|
||||
36
.github/workflows/rspec.yml
vendored
Normal file
36
.github/workflows/rspec.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
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', '3.2'] # 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/
|
||||
18
.gitignore
vendored
18
.gitignore
vendored
@@ -1,4 +1,14 @@
|
||||
pkg/*
|
||||
*.gem
|
||||
.bundle
|
||||
tmp/*.sqlite3
|
||||
/.bundle/
|
||||
/.yardoc
|
||||
/_yardoc/
|
||||
/coverage/
|
||||
/doc/
|
||||
/pkg/
|
||||
/spec/reports/
|
||||
/tmp/
|
||||
log/*.log
|
||||
.byebug_history
|
||||
spec/internal
|
||||
|
||||
# macOS stuff
|
||||
.DS_Store
|
||||
|
||||
15
.travis.yml
15
.travis.yml
@@ -1,15 +0,0 @@
|
||||
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
|
||||
96
Changes.md
96
Changes.md
@@ -1,24 +1,104 @@
|
||||
0.5.0
|
||||
2.1.0
|
||||
-----------
|
||||
- Add Rails 7.2 support (thanks to @janko)
|
||||
|
||||
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
|
||||
-----------
|
||||
|
||||
- Initial release!
|
||||
- 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.5.1
|
||||
|
||||
0.8.0
|
||||
-----------
|
||||
|
||||
- Added -e/--environment ENV option to set the daemon rails environment.
|
||||
- Added `on` (day of week) option to cronotab.rb semantic
|
||||
- Added job health check and job health indicator to the Web UI
|
||||
|
||||
0.5.2
|
||||
|
||||
0.7.0
|
||||
-----------
|
||||
|
||||
- Fix: Scheduled time now related to the last performing time.
|
||||
- Added simple Web UI
|
||||
|
||||
|
||||
0.6.1
|
||||
-----------
|
||||
|
||||
- Persist job state to your database.
|
||||
|
||||
0.7.0
|
||||
|
||||
0.5.2
|
||||
-----------
|
||||
|
||||
- Added simple Web UI
|
||||
- Fix: Scheduled time now related to the last performing time.
|
||||
|
||||
|
||||
0.5.1
|
||||
-----------
|
||||
|
||||
- Added -e/--environment ENV option to set the daemon rails environment.
|
||||
|
||||
|
||||
0.5.0
|
||||
-----------
|
||||
|
||||
- Initial release!
|
||||
|
||||
24
Gemfile
24
Gemfile
@@ -1,2 +1,24 @@
|
||||
source "https://rubygems.org"
|
||||
source 'https://rubygems.org'
|
||||
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
|
||||
|
||||
# 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]
|
||||
|
||||
302
Gemfile.lock
302
Gemfile.lock
@@ -1,85 +1,259 @@
|
||||
PATH
|
||||
remote: .
|
||||
specs:
|
||||
crono (0.7.0)
|
||||
activejob (~> 4.0)
|
||||
activerecord (~> 4.0)
|
||||
activesupport (~> 4.0)
|
||||
crono (2.1.0)
|
||||
rails (>= 5.2.8)
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
activejob (4.2.0)
|
||||
activesupport (= 4.2.0)
|
||||
globalid (>= 0.3.0)
|
||||
activemodel (4.2.0)
|
||||
activesupport (= 4.2.0)
|
||||
actioncable (7.2.0)
|
||||
actionpack (= 7.2.0)
|
||||
activesupport (= 7.2.0)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (7.2.0)
|
||||
actionpack (= 7.2.0)
|
||||
activejob (= 7.2.0)
|
||||
activerecord (= 7.2.0)
|
||||
activestorage (= 7.2.0)
|
||||
activesupport (= 7.2.0)
|
||||
mail (>= 2.8.0)
|
||||
actionmailer (7.2.0)
|
||||
actionpack (= 7.2.0)
|
||||
actionview (= 7.2.0)
|
||||
activejob (= 7.2.0)
|
||||
activesupport (= 7.2.0)
|
||||
mail (>= 2.8.0)
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (7.2.0)
|
||||
actionview (= 7.2.0)
|
||||
activesupport (= 7.2.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
racc
|
||||
rack (>= 2.2.4, < 3.2)
|
||||
rack-session (>= 1.0.1)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
useragent (~> 0.16)
|
||||
actiontext (7.2.0)
|
||||
actionpack (= 7.2.0)
|
||||
activerecord (= 7.2.0)
|
||||
activestorage (= 7.2.0)
|
||||
activesupport (= 7.2.0)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (7.2.0)
|
||||
activesupport (= 7.2.0)
|
||||
builder (~> 3.1)
|
||||
activerecord (4.2.0)
|
||||
activemodel (= 4.2.0)
|
||||
activesupport (= 4.2.0)
|
||||
arel (~> 6.0)
|
||||
activesupport (4.2.0)
|
||||
i18n (~> 0.7)
|
||||
json (~> 1.7, >= 1.7.7)
|
||||
minitest (~> 5.1)
|
||||
thread_safe (~> 0.3, >= 0.3.4)
|
||||
tzinfo (~> 1.1)
|
||||
arel (6.0.0)
|
||||
builder (3.2.2)
|
||||
byebug (3.5.1)
|
||||
columnize (~> 0.8)
|
||||
debugger-linecache (~> 1.2)
|
||||
slop (~> 3.6)
|
||||
columnize (0.9.0)
|
||||
debugger-linecache (1.2.0)
|
||||
diff-lcs (1.2.5)
|
||||
globalid (0.3.3)
|
||||
activesupport (>= 4.1.0)
|
||||
haml (4.0.6)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
activejob (7.2.0)
|
||||
activesupport (= 7.2.0)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.2.0)
|
||||
activesupport (= 7.2.0)
|
||||
activerecord (7.2.0)
|
||||
activemodel (= 7.2.0)
|
||||
activesupport (= 7.2.0)
|
||||
timeout (>= 0.4.0)
|
||||
activestorage (7.2.0)
|
||||
actionpack (= 7.2.0)
|
||||
activejob (= 7.2.0)
|
||||
activerecord (= 7.2.0)
|
||||
activesupport (= 7.2.0)
|
||||
marcel (~> 1.0)
|
||||
activesupport (7.2.0)
|
||||
base64
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||
connection_pool (>= 2.2.5)
|
||||
drb
|
||||
i18n (>= 1.6, < 2)
|
||||
logger (>= 1.4.2)
|
||||
minitest (>= 5.1)
|
||||
securerandom (>= 0.3)
|
||||
tzinfo (~> 2.0, >= 2.0.5)
|
||||
base64 (0.2.0)
|
||||
bigdecimal (3.1.8)
|
||||
builder (3.3.0)
|
||||
byebug (11.1.3)
|
||||
combustion (1.3.6)
|
||||
activesupport (>= 3.0.0)
|
||||
railties (>= 3.0.0)
|
||||
thor (>= 0.14.6)
|
||||
concurrent-ruby (1.3.4)
|
||||
connection_pool (2.4.1)
|
||||
crass (1.0.6)
|
||||
daemons (1.4.1)
|
||||
date (3.3.4)
|
||||
diff-lcs (1.5.1)
|
||||
drb (2.2.1)
|
||||
erubi (1.13.0)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
haml (5.2.2)
|
||||
temple (>= 0.8.0)
|
||||
tilt
|
||||
i18n (0.7.0)
|
||||
json (1.8.2)
|
||||
minitest (5.5.1)
|
||||
rack (1.6.0)
|
||||
rack-protection (1.5.3)
|
||||
i18n (1.14.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
io-console (0.7.2)
|
||||
irb (1.14.0)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
logger (1.6.0)
|
||||
loofah (2.22.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
mail (2.8.1)
|
||||
mini_mime (>= 0.1.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
marcel (1.0.4)
|
||||
mini_mime (1.1.5)
|
||||
minitest (5.25.1)
|
||||
mustermann (1.1.1)
|
||||
ruby2_keywords (~> 0.0.1)
|
||||
net-imap (0.4.14)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
net-protocol (0.2.2)
|
||||
timeout
|
||||
net-smtp (0.5.0)
|
||||
net-protocol
|
||||
nio4r (2.7.3)
|
||||
nokogiri (1.16.7-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.7-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.7-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
propshaft (0.9.1)
|
||||
actionpack (>= 7.0.0)
|
||||
activesupport (>= 7.0.0)
|
||||
rack
|
||||
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.1)
|
||||
rspec-support (~> 3.2.0)
|
||||
rspec-expectations (3.2.0)
|
||||
railties (>= 7.0.0)
|
||||
psych (5.1.2)
|
||||
stringio
|
||||
racc (1.8.1)
|
||||
rack (2.2.9)
|
||||
rack-protection (2.2.0)
|
||||
rack
|
||||
rack-session (1.0.2)
|
||||
rack (< 3)
|
||||
rack-test (2.1.0)
|
||||
rack (>= 1.3)
|
||||
rackup (1.0.0)
|
||||
rack (< 3)
|
||||
webrick
|
||||
rails (7.2.0)
|
||||
actioncable (= 7.2.0)
|
||||
actionmailbox (= 7.2.0)
|
||||
actionmailer (= 7.2.0)
|
||||
actionpack (= 7.2.0)
|
||||
actiontext (= 7.2.0)
|
||||
actionview (= 7.2.0)
|
||||
activejob (= 7.2.0)
|
||||
activemodel (= 7.2.0)
|
||||
activerecord (= 7.2.0)
|
||||
activestorage (= 7.2.0)
|
||||
activesupport (= 7.2.0)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 7.2.0)
|
||||
rails-dom-testing (2.2.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.6.0)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (~> 1.14)
|
||||
railties (7.2.0)
|
||||
actionpack (= 7.2.0)
|
||||
activesupport (= 7.2.0)
|
||||
irb (~> 1.13)
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rake (13.2.1)
|
||||
rdoc (6.7.0)
|
||||
psych (>= 4.0.0)
|
||||
reline (0.5.9)
|
||||
io-console (~> 0.5)
|
||||
rspec (3.13.0)
|
||||
rspec-core (~> 3.13.0)
|
||||
rspec-expectations (~> 3.13.0)
|
||||
rspec-mocks (~> 3.13.0)
|
||||
rspec-core (3.13.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-expectations (3.13.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.2.0)
|
||||
rspec-mocks (3.2.1)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-mocks (3.13.1)
|
||||
diff-lcs (>= 1.2.0, < 2.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)
|
||||
slop (3.6.0)
|
||||
sqlite3 (1.3.10)
|
||||
thread_safe (0.3.4)
|
||||
tilt (1.4.1)
|
||||
timecop (0.7.3)
|
||||
tzinfo (1.2.2)
|
||||
thread_safe (~> 0.1)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-rails (6.1.4)
|
||||
actionpack (>= 6.1)
|
||||
activesupport (>= 6.1)
|
||||
railties (>= 6.1)
|
||||
rspec-core (~> 3.13)
|
||||
rspec-expectations (~> 3.13)
|
||||
rspec-mocks (~> 3.13)
|
||||
rspec-support (~> 3.13)
|
||||
rspec-support (3.13.1)
|
||||
ruby2_keywords (0.0.5)
|
||||
securerandom (0.3.1)
|
||||
sinatra (2.2.0)
|
||||
mustermann (~> 1.0)
|
||||
rack (~> 2.2)
|
||||
rack-protection (= 2.2.0)
|
||||
tilt (~> 2.0)
|
||||
sqlite3 (2.0.4-arm64-darwin)
|
||||
sqlite3 (2.0.4-x86_64-darwin)
|
||||
sqlite3 (2.0.4-x86_64-linux-gnu)
|
||||
stringio (3.1.1)
|
||||
temple (0.8.2)
|
||||
thor (1.3.1)
|
||||
tilt (2.0.10)
|
||||
timecop (0.9.5)
|
||||
timeout (0.4.1)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
useragent (0.16.10)
|
||||
webrick (1.8.1)
|
||||
websocket-driver (0.7.6)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
zeitwerk (2.6.17)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
arm64-darwin-21
|
||||
arm64-darwin-23
|
||||
arm64-darwin-24
|
||||
x86_64-darwin-21
|
||||
x86_64-darwin-22
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
bundler (>= 1.0.0)
|
||||
bundler (>= 2)
|
||||
byebug
|
||||
combustion (~> 1.3)
|
||||
crono!
|
||||
daemons
|
||||
haml
|
||||
rake (~> 10.0)
|
||||
rspec (~> 3.0)
|
||||
propshaft
|
||||
rack-test
|
||||
rake (>= 10.0)
|
||||
rspec (>= 3.0)
|
||||
rspec-rails (>= 4.0)
|
||||
sinatra
|
||||
sqlite3
|
||||
timecop (~> 0.7)
|
||||
timecop (>= 0.7)
|
||||
|
||||
BUNDLED WITH
|
||||
2.4.4
|
||||
|
||||
5
LICENSE
5
LICENSE
@@ -1,4 +1,4 @@
|
||||
Apache License
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
@@ -186,7 +186,7 @@ Apache License
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2015 Dzmitry Plashchynski <plashchynski@gmail.com>
|
||||
Copyright 2022 Dzmitry Plashchynski
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -199,4 +199,3 @@ Apache License
|
||||
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
2
NOTICE
@@ -1,2 +1,2 @@
|
||||
Copyright 2015 Dzmitry Plashchynski <plashchynski@gmail.com>
|
||||
Copyright 2022 Dzmitry Plashchynski <plashchynski@gmail.com>
|
||||
Licensed under the Apache License, Version 2.0
|
||||
|
||||
218
README.md
218
README.md
@@ -1,24 +1,14 @@
|
||||
Crono — Job scheduler for Rails
|
||||
Job scheduler for Rails
|
||||
------------------------
|
||||
[](http://badge.fury.io/rb/crono)
|
||||
[](https://travis-ci.org/plashchynski/crono)
|
||||
[](https://codeclimate.com/github/plashchynski/crono)
|
||||
[](https://hakiri.io/github/plashchynski/crono/master)
|
||||
[](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
|
||||
|
||||
## The Idea
|
||||
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 Cron in Ruby for Rails. Well, there's [Whenever](https://github.com/javan/whenever) but it works on top of Unix Cron, so you have no total control of it from Ruby. Crono is pure Ruby. It doesn't use Unix Cron and other platform-dependent things. So you can use it on all platforms supported by Ruby. It persists job state to your database using Active Record. You have full control of jobs performing process. You have Ruby code, so you can understand and modify it to fit your needs.
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
@@ -41,50 +31,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
|
||||
|
||||
#### Create Job
|
||||
### The basic usage
|
||||
|
||||
Crono can use Active Job jobs from `app/jobs/`. The only requirements is that the `perform` method should take no arguments.
|
||||
|
||||
Here's an example of a test job:
|
||||
|
||||
```ruby
|
||||
# app/jobs/test_job.rb
|
||||
class TestJob < ActiveJob::Base
|
||||
def perform
|
||||
# put you scheduled code here
|
||||
# Comments.deleted.clean_up...
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
The ActiveJob jobs is convenient because you can use one job in both periodic and enqueued ways. But Active Job is not required. Any class can be used as a crono job if it implements a method `perform` without arguments:
|
||||
|
||||
```ruby
|
||||
class TestJob # This is not an Active Job job, but pretty legal Crono job.
|
||||
def perform
|
||||
# put you scheduled code here
|
||||
# Comments.deleted.clean_up...
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
#### Job Schedule
|
||||
|
||||
The schedule described in the configuration file `config/cronotab.rb`, that created using `crono:install` or manually. The semantic is pretty straightforward:
|
||||
You can specify a simple job by editing ```config/cronotab.rb```:
|
||||
|
||||
```ruby
|
||||
# config/cronotab.rb
|
||||
Crono.perform(TestJob).every 2.days, at: "15:30"
|
||||
class TestJob
|
||||
def perform
|
||||
puts 'Test!'
|
||||
end
|
||||
end
|
||||
|
||||
Crono.perform(TestJob).every 5.seconds
|
||||
```
|
||||
|
||||
You can schedule one job a few times, if you want a job to be performed a few times a day:
|
||||
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
|
||||
Crono.perform(TestJob).every 1.day, at: "00:00"
|
||||
Crono.perform(TestJob).every 1.day, at: "12:00"
|
||||
# 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:
|
||||
@@ -93,54 +80,153 @@ 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:
|
||||
|
||||
bundle exec crono RAILS_ENV=development
|
||||
|
||||
crono usage:
|
||||
```ruby
|
||||
Crono.perform(TestJob, 'some', 'args').every 1.day, at: {hour: 12, min: 15}
|
||||
```
|
||||
Usage: crono [options]
|
||||
|
||||
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:
|
||||
|
||||
```ruby
|
||||
# app/jobs/test_job.rb
|
||||
class TestJob < ActiveJob::Base
|
||||
def perform(options)
|
||||
# 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`:
|
||||
|
||||
```ruby
|
||||
class TestJob # This is not an Active Job job, but pretty legal Crono job.
|
||||
def perform(*args)
|
||||
# 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
|
||||
|
||||
class Test
|
||||
def perform
|
||||
Rake::Task['crono:hello'].execute
|
||||
end
|
||||
end
|
||||
|
||||
Crono.perform(Test).every 5.seconds
|
||||
```
|
||||
With the rake task of:
|
||||
```Ruby
|
||||
# lib/tasks/test.rake
|
||||
namespace :crono do
|
||||
desc 'Update all tables'
|
||||
task :hello => :environment do
|
||||
puts "hello"
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
_Please note that crono uses threads, so your code should be thread-safe_
|
||||
|
||||
### Run crono
|
||||
|
||||
Run crono in your Rails project root directory:
|
||||
|
||||
bundle exec crono -e development
|
||||
|
||||
Usage:
|
||||
```
|
||||
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)
|
||||
-m, --monitor Start monitor process for a deamon (Default false)
|
||||
-e, --environment ENV Application environment (Default: development)
|
||||
```
|
||||
|
||||
## Web UI
|
||||
### Run as a daemon
|
||||
|
||||
Crono comes with a Sinatra application that can display the current state of Crono jobs.
|
||||
Add `sinatra` and `haml` to your Gemfile
|
||||
To run Crono as a daemon, please add to your Gemfile:
|
||||
|
||||
```ruby
|
||||
gam 'haml'
|
||||
gem 'sinatra', require: nil
|
||||
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.
|
||||
|
||||
Add the following to your `config/routes.rb`:
|
||||
|
||||
```ruby
|
||||
require 'crono/web'
|
||||
|
||||
Rails.application.routes.draw do
|
||||
mount Crono::Web, at: '/crono'
|
||||
mount Crono::Engine, 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
|
||||
|
||||
Use the `capistrano-crono` gem ([github](https://github.com/plashchynski/capistrano-crono/)).
|
||||
|
||||
## Development
|
||||
|
||||
### Running tests
|
||||
|
||||
To run the tests, you need to have a database. You can use the default SQLite database:
|
||||
|
||||
bundle exec rspec
|
||||
|
||||
### Publishing
|
||||
|
||||
To publish a new version, you need to update the version number in `lib/crono/version.rb` and then run:
|
||||
|
||||
bundle exec rake release
|
||||
|
||||
|
||||
## Support
|
||||
|
||||
Feel free to create [issues](https://github.com/plashchynski/crono/issues)
|
||||
Feel free to create an [issues](https://github.com/plashchynski/crono/issues)
|
||||
|
||||
|
||||
## License
|
||||
|
||||
Please see [LICENSE](https://github.com/plashchynski/crono/blob/master/LICENSE) for licensing details.
|
||||
Please see [LICENSE](LICENSE) for licensing details.
|
||||
|
||||
8
Rakefile
8
Rakefile
@@ -1,7 +1,5 @@
|
||||
require 'bundler'
|
||||
Bundler::GemHelper.install_tasks
|
||||
require 'bundler/setup'
|
||||
|
||||
require 'rspec/core/rake_task'
|
||||
RSpec::Core::RakeTask.new('spec')
|
||||
load 'rails/tasks/statistics.rake'
|
||||
|
||||
task default: :spec
|
||||
require 'bundler/gem_tasks'
|
||||
|
||||
11
SECURITY.md
Normal file
11
SECURITY.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# 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)
|
||||
6
app/assets/javascripts/crono/materialize.min.js
vendored
Normal file
6
app/assets/javascripts/crono/materialize.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
26
app/assets/stylesheets/crono/application.css
Normal file
26
app/assets/stylesheets/crono/application.css
Normal file
@@ -0,0 +1,26 @@
|
||||
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;
|
||||
}
|
||||
31
app/assets/stylesheets/crono/materialize.min.css
vendored
Normal file
31
app/assets/stylesheets/crono/materialize.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
5
app/controllers/crono/application_controller.rb
Normal file
5
app/controllers/crono/application_controller.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
module Crono
|
||||
class ApplicationController < ActionController::Base
|
||||
protect_from_forgery with: :exception
|
||||
end
|
||||
end
|
||||
11
app/controllers/crono/jobs_controller.rb
Normal file
11
app/controllers/crono/jobs_controller.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
module Crono
|
||||
class JobsController < ApplicationController
|
||||
def index
|
||||
@jobs = Crono::CronoJob.all
|
||||
end
|
||||
|
||||
def show
|
||||
@job = Crono::CronoJob.find(params[:id])
|
||||
end
|
||||
end
|
||||
end
|
||||
5
app/models/crono/application_record.rb
Normal file
5
app/models/crono/application_record.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
module Crono
|
||||
class ApplicationRecord < ActiveRecord::Base
|
||||
self.abstract_class = true
|
||||
end
|
||||
end
|
||||
11
app/models/crono/crono_job.rb
Normal file
11
app/models/crono/crono_job.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
module Crono
|
||||
# Crono::CronoJob is a ActiveRecord model to store job state
|
||||
class CronoJob < ActiveRecord::Base
|
||||
self.table_name = 'crono_jobs'
|
||||
validates :job_id, presence: true, uniqueness: true
|
||||
|
||||
def self.outdated
|
||||
self
|
||||
end
|
||||
end
|
||||
end
|
||||
50
app/views/crono/jobs/index.html.erb
Normal file
50
app/views/crono/jobs/index.html.erb
Normal file
@@ -0,0 +1,50 @@
|
||||
<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>
|
||||
16
app/views/crono/jobs/show.html.erb
Normal file
16
app/views/crono/jobs/show.html.erb
Normal file
@@ -0,0 +1,16 @@
|
||||
<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>
|
||||
31
app/views/layouts/crono/application.html.erb
Normal file
31
app/views/layouts/crono/application.html.erb
Normal file
@@ -0,0 +1,31 @@
|
||||
<!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>
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
|
||||
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
||||
|
||||
require "crono/cli"
|
||||
require 'crono/cli'
|
||||
|
||||
begin
|
||||
Crono::CLI.instance.run
|
||||
|
||||
24
bin/rails
Executable file
24
bin/rails
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/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'
|
||||
9
config.ru
Normal file
9
config.ru
Normal file
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rubygems"
|
||||
require "bundler"
|
||||
|
||||
Bundler.require :default, :development
|
||||
|
||||
Combustion.initialize! :all
|
||||
run Combustion::Application
|
||||
4
config/routes.rb
Normal file
4
config/routes.rb
Normal file
@@ -0,0 +1,4 @@
|
||||
Crono::Engine.routes.draw do
|
||||
resources :jobs
|
||||
root 'jobs#index'
|
||||
end
|
||||
@@ -1,31 +1,33 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
require File.expand_path('../lib/crono/version', __FILE__)
|
||||
$:.push File.expand_path("../lib", __FILE__)
|
||||
require_relative 'lib/crono/version'
|
||||
|
||||
Gem::Specification.new do |s|
|
||||
s.name = "crono"
|
||||
s.version = Crono::VERSION
|
||||
s.authors = ["Dzmitry Plashchynski"]
|
||||
s.email = ["plashchynski@gmail.com"]
|
||||
s.homepage = "https://github.com/plashchynski/crono"
|
||||
s.description = s.summary = "Job scheduler for Rails"
|
||||
s.license = "Apache-2.0"
|
||||
s.name = 'crono'
|
||||
s.version = Crono::VERSION
|
||||
s.authors = ['Dzmitry Plashchynski']
|
||||
s.email = ['plashchynski@gmail.com']
|
||||
|
||||
s.required_rubygems_version = ">= 1.3.6"
|
||||
s.rubyforge_project = "crono"
|
||||
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'
|
||||
|
||||
s.add_runtime_dependency "activejob", "~> 4.0"
|
||||
s.add_runtime_dependency "activesupport", "~> 4.0"
|
||||
s.add_runtime_dependency "activerecord", "~> 4.0"
|
||||
s.add_development_dependency "rake", "~> 10.0"
|
||||
s.add_development_dependency "bundler", ">= 1.0.0"
|
||||
s.add_development_dependency "rspec", "~> 3.0"
|
||||
s.add_development_dependency "timecop", "~> 0.7"
|
||||
s.add_development_dependency "sqlite3"
|
||||
s.add_development_dependency "byebug"
|
||||
s.add_development_dependency "sinatra"
|
||||
s.add_development_dependency "haml"
|
||||
s.files = Dir['{app,config,db,lib}/**/*', 'LICENSE', 'Rakefile', 'README.rdoc']
|
||||
s.test_files = Dir['spec/**/*']
|
||||
s.executables = ['crono']
|
||||
s.require_paths = ["lib"]
|
||||
|
||||
s.files = `git ls-files`.split("\n")
|
||||
s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
|
||||
s.require_path = 'lib'
|
||||
s.add_dependency 'rails', '>= 5.2.8'
|
||||
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'
|
||||
s.add_development_dependency 'propshaft'
|
||||
end
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 90 KiB |
@@ -1,4 +1,4 @@
|
||||
# cronotab.rb — Crono configuration file
|
||||
# cronotab.rb - Crono configuration file
|
||||
#
|
||||
# Here you can specify periodic jobs and schedule.
|
||||
# You can use ActiveJob's jobs from `app/jobs/`
|
||||
@@ -7,9 +7,8 @@
|
||||
#
|
||||
class TestJob
|
||||
def perform
|
||||
puts "Test!"
|
||||
puts 'Test!'
|
||||
end
|
||||
end
|
||||
|
||||
Crono.perform(TestJob).every 2.days, at: "15:30"
|
||||
|
||||
Crono.perform(TestJob).every 2.days, at: '15:30'
|
||||
|
||||
24
lib/crono.rb
24
lib/crono.rb
@@ -1,12 +1,18 @@
|
||||
# Crono main module
|
||||
module Crono
|
||||
end
|
||||
|
||||
require "active_support/all"
|
||||
require "crono/version.rb"
|
||||
require "crono/logging.rb"
|
||||
require "crono/period.rb"
|
||||
require "crono/job.rb"
|
||||
require "crono/scheduler.rb"
|
||||
require "crono/config.rb"
|
||||
require "crono/performer_proxy.rb"
|
||||
require "crono/orm/active_record/crono_job.rb"
|
||||
require 'rails'
|
||||
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/railtie' if defined?(Rails)
|
||||
|
||||
118
lib/crono/cli.rb
118
lib/crono/cli.rb
@@ -1,13 +1,16 @@
|
||||
Thread.abort_on_exception = true
|
||||
|
||||
require 'crono'
|
||||
require 'optparse'
|
||||
|
||||
module Crono
|
||||
mattr_accessor :scheduler
|
||||
|
||||
# Crono::CLI - The main class for the crono daemon exacutable `bin/crono`
|
||||
class CLI
|
||||
include Singleton
|
||||
include Logging
|
||||
|
||||
COMMANDS = %w(start stop restart run zap reload status)
|
||||
|
||||
attr_accessor :config
|
||||
|
||||
def initialize
|
||||
@@ -17,35 +20,43 @@ module Crono
|
||||
|
||||
def run
|
||||
parse_options(ARGV)
|
||||
parse_command(ARGV)
|
||||
|
||||
if config.daemonize
|
||||
set_log_to(config.logfile)
|
||||
daemonize
|
||||
else
|
||||
set_log_to(STDOUT)
|
||||
end
|
||||
setup_log
|
||||
|
||||
write_pid
|
||||
write_pid if config.daemonize
|
||||
load_rails
|
||||
Cronotab.process(File.expand_path(config.cronotab))
|
||||
print_banner
|
||||
|
||||
check_jobs
|
||||
start_working_loop
|
||||
end
|
||||
|
||||
private
|
||||
def daemonize
|
||||
::Process.daemon(true, true)
|
||||
|
||||
[$stdout, $stderr].each do |io|
|
||||
File.open(config.logfile, 'ab') { |f| io.reopen(f) }
|
||||
io.sync = true
|
||||
unless have_jobs?
|
||||
logger.error "You have no jobs in you cronotab file #{config.cronotab}"
|
||||
return
|
||||
end
|
||||
|
||||
$stdin.reopen("/dev/null")
|
||||
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
|
||||
else
|
||||
self.logfile = STDOUT
|
||||
end
|
||||
end
|
||||
|
||||
def write_pid
|
||||
return unless config.pidfile
|
||||
pidfile = File.expand_path(config.pidfile)
|
||||
File.write(pidfile, ::Process.pid)
|
||||
end
|
||||
@@ -54,36 +65,56 @@ module Crono
|
||||
logger.info "Loading Crono #{Crono::VERSION}"
|
||||
logger.info "Running in #{RUBY_DESCRIPTION}"
|
||||
|
||||
logger.info "Jobs:"
|
||||
logger.info 'Jobs:'
|
||||
Crono.scheduler.jobs.each do |job|
|
||||
logger.info %{"#{job.performer}" with rule "#{job.period.description}" next time will perform at #{job.next}}
|
||||
logger.info "'#{job.performer}' with rule '#{job.period.description}'"\
|
||||
" next time will perform at #{job.next}"
|
||||
end
|
||||
end
|
||||
|
||||
def load_rails
|
||||
ENV['RACK_ENV'] = ENV['RAILS_ENV'] = config.environment
|
||||
require 'rails'
|
||||
require File.expand_path("config/environment.rb")
|
||||
require File.expand_path('config/environment.rb')
|
||||
::Rails.application.eager_load!
|
||||
require File.expand_path(config.cronotab)
|
||||
end
|
||||
|
||||
def check_jobs
|
||||
if Crono.scheduler.jobs.empty?
|
||||
logger.error "You have no jobs defined 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
|
||||
while job = Crono.scheduler.next do
|
||||
sleep(job.next - Time.now)
|
||||
job.perform
|
||||
loop do
|
||||
next_time, jobs = Crono.scheduler.next_jobs
|
||||
now = Time.zone.now
|
||||
sleep(next_time - now) if next_time > now
|
||||
jobs.each(&:perform)
|
||||
end
|
||||
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
|
||||
@@ -93,12 +124,20 @@ 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("-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|
|
||||
@@ -106,5 +145,12 @@ module Crono
|
||||
end
|
||||
end.parse!(argv)
|
||||
end
|
||||
|
||||
def parse_command(argv)
|
||||
if COMMANDS.include? argv[0]
|
||||
config.daemonize = true
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,21 +1,33 @@
|
||||
module Crono
|
||||
# Crono::Config stores Crono configuration
|
||||
class Config
|
||||
CRONOTAB = "config/cronotab.rb"
|
||||
LOGFILE = "log/crono.log"
|
||||
PIDFILE = "tmp/pids/crono.pid"
|
||||
CRONOTAB = 'config/cronotab.rb'
|
||||
LOGFILE = 'log/crono.log'
|
||||
PIDFILE = 'tmp/pids/crono.pid'
|
||||
PIDDIR = 'tmp/pids'
|
||||
PROCESS_NAME = 'crono'
|
||||
|
||||
attr_accessor :cronotab
|
||||
attr_accessor :logfile
|
||||
attr_accessor :pidfile
|
||||
attr_accessor :daemonize
|
||||
attr_accessor :environment
|
||||
attr_accessor :cronotab, :logfile, :pidfile, :piddir, :process_name,
|
||||
:monitor, :daemonize, :environment
|
||||
|
||||
def initialize
|
||||
self.cronotab = CRONOTAB
|
||||
self.logfile = LOGFILE
|
||||
self.pidfile = PIDFILE
|
||||
self.piddir = PIDDIR
|
||||
self.process_name = PROCESS_NAME
|
||||
self.daemonize = false
|
||||
self.environment = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "development"
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
10
lib/crono/cronotab.rb
Normal file
10
lib/crono/cronotab.rb
Normal 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
|
||||
19
lib/crono/engine.rb
Normal file
19
lib/crono/engine.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
module Crono
|
||||
class Engine < ::Rails::Engine
|
||||
isolate_namespace Crono
|
||||
|
||||
initializer 'crono.assets.precompile' do |app|
|
||||
if app.config.respond_to?(:assets)
|
||||
app.config.assets.precompile += %w( crono/application.css crono/materialize.min.css )
|
||||
else
|
||||
fail "Crono requires either Propshaft or Sprockets to be installed."
|
||||
end
|
||||
end
|
||||
|
||||
config.generators do |g|
|
||||
g.test_framework :rspec
|
||||
g.assets false
|
||||
g.helper false
|
||||
end
|
||||
end
|
||||
end
|
||||
43
lib/crono/interval.rb
Normal file
43
lib/crono/interval.rb
Normal 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
|
||||
126
lib/crono/job.rb
126
lib/crono/job.rb
@@ -2,25 +2,27 @@ require 'stringio'
|
||||
require 'logger'
|
||||
|
||||
module Crono
|
||||
# Crono::Job represents a Crono job
|
||||
class Job
|
||||
include Logging
|
||||
|
||||
attr_accessor :performer
|
||||
attr_accessor :period
|
||||
attr_accessor :last_performed_at
|
||||
attr_accessor :job_log
|
||||
attr_accessor :job_logger
|
||||
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 = nil, job_options = nil)
|
||||
self.execution_interval = 0.minutes
|
||||
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
|
||||
next_time = period.next(since: last_performed_at)
|
||||
next_time.past? ? period.next : next_time
|
||||
return next_performed_at if next_performed_at.future?
|
||||
Time.zone.now
|
||||
end
|
||||
|
||||
def description
|
||||
@@ -32,46 +34,110 @@ module Crono
|
||||
end
|
||||
|
||||
def perform
|
||||
log "Perform #{performer}"
|
||||
self.last_performed_at = Time.now
|
||||
return Thread.new {} if perform_before_interval?
|
||||
|
||||
Thread.new do
|
||||
begin
|
||||
performer.new.perform
|
||||
rescue Exception => e
|
||||
log "Finished #{performer} in %.2f seconds with error: #{e.message}" % (Time.now - last_performed_at)
|
||||
log e.backtrace.join("\n")
|
||||
else
|
||||
log "Finished #{performer} in %.2f seconds" % (Time.now - last_performed_at)
|
||||
ensure
|
||||
save
|
||||
end
|
||||
end
|
||||
log "Perform #{performer}"
|
||||
self.last_performed_at = Time.zone.now
|
||||
self.next_performed_at = period.next(since: last_performed_at)
|
||||
|
||||
Thread.new { perform_job }
|
||||
end
|
||||
|
||||
def save
|
||||
@semaphore.synchronize do
|
||||
log = model.reload.log || ""
|
||||
log << job_log.string
|
||||
job_log.truncate(job_log.rewind)
|
||||
model.update(last_performed_at: last_performed_at, log: log)
|
||||
update_model
|
||||
clear_job_log
|
||||
ActiveRecord::Base.connection_handler.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
|
||||
def log(message)
|
||||
private
|
||||
|
||||
def clear_job_log
|
||||
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
|
||||
rescue StandardError => e
|
||||
handle_job_fail(e)
|
||||
else
|
||||
handle_job_success
|
||||
ensure
|
||||
save
|
||||
end
|
||||
|
||||
def handle_job_fail(exception)
|
||||
finished_time_sec = format('%.2f', Time.zone.now - last_performed_at)
|
||||
self.healthy = false
|
||||
log_error "Finished #{performer} in #{finished_time_sec} seconds"\
|
||||
" with error: #{exception.message}"
|
||||
log_error exception.backtrace.join("\n")
|
||||
end
|
||||
|
||||
def handle_job_success
|
||||
finished_time_sec = format('%.2f', Time.zone.now - last_performed_at)
|
||||
self.healthy = true
|
||||
log "Finished #{performer} in #{finished_time_sec} seconds"
|
||||
end
|
||||
|
||||
def log_error(message)
|
||||
log(message, Logger::ERROR)
|
||||
end
|
||||
|
||||
def log(message, severity = Logger::INFO)
|
||||
@semaphore.synchronize do
|
||||
logger.info message
|
||||
job_logger.info message
|
||||
logger.log(severity, message) if logger
|
||||
job_logger.log severity, message
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
module Crono
|
||||
mattr_accessor :logger
|
||||
|
||||
# Crono::Logging is a standart Ruby logger wrapper
|
||||
module Logging
|
||||
def set_log_to(logfile)
|
||||
def logfile=(logfile)
|
||||
Crono.logger = Logger.new(logfile)
|
||||
end
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
require 'active_record'
|
||||
|
||||
module Crono
|
||||
class CronoJob < ActiveRecord::Base
|
||||
self.table_name = "crono_jobs"
|
||||
validates :job_id, presence: true, uniqueness: true
|
||||
end
|
||||
end
|
||||
@@ -1,17 +1,31 @@
|
||||
module Crono
|
||||
# Crono::PerformerProxy is a proxy used in cronotab.rb semantic
|
||||
class PerformerProxy
|
||||
def initialize(performer, scheduler)
|
||||
def initialize(performer, scheduler, job_args = nil, job_options = nil)
|
||||
@performer = performer
|
||||
@scheduler = scheduler
|
||||
@job_args = job_args
|
||||
@job_options = job_options
|
||||
end
|
||||
|
||||
def every(period, *args)
|
||||
job = Job.new(@performer, Period.new(period, *args))
|
||||
@scheduler.add_job(job)
|
||||
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
|
||||
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
|
||||
|
||||
@@ -1,41 +1,85 @@
|
||||
module Crono
|
||||
# Period describe frequency of jobs
|
||||
class Period
|
||||
def initialize(period, at: nil)
|
||||
DAYS = [:monday, :tuesday, :wednesday, :thursday, :friday, :saturday,
|
||||
:sunday]
|
||||
|
||||
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)
|
||||
if since.nil?
|
||||
@next = Time.now.change(time_atts)
|
||||
return @next if @next.future?
|
||||
since = Time.now
|
||||
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
|
||||
|
||||
@period.since(since).change(time_atts)
|
||||
@next = @next.beginning_of_week.advance(days: @on) if @on
|
||||
@next = @next.change(time_atts)
|
||||
return @next if @next.future?
|
||||
Time.zone.now
|
||||
end
|
||||
|
||||
def description
|
||||
desc = "every #{@period.inspect}"
|
||||
desc += " at %.2i:%.2i" % [@at_hour, @at_min] if @at_hour && @at_min
|
||||
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
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def initial_next
|
||||
next_time = initial_day.change(time_atts)
|
||||
return next_time if next_time.future?
|
||||
@period.from_now.change(time_atts)
|
||||
end
|
||||
|
||||
def initial_day
|
||||
return Time.zone.now unless @on
|
||||
day = Time.zone.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
|
||||
|
||||
def parse_on(on)
|
||||
day_number = DAYS.index(on)
|
||||
fail "Wrong 'on' day" unless day_number
|
||||
fail "period should be at least 1 week to use 'on'" if @period < 1.week
|
||||
day_number
|
||||
end
|
||||
|
||||
def parse_at(at)
|
||||
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)
|
||||
time = Time.zone.parse(at)
|
||||
return time.hour, time.min
|
||||
when Hash
|
||||
return at[:hour], at[:min]
|
||||
else
|
||||
raise "Unknown 'at' format"
|
||||
fail "Unknown 'at' format"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def time_atts
|
||||
{hour: @at_hour, min: @at_min}.compact
|
||||
atts = { hour: @at_hour, min: @at_min }
|
||||
atts.respond_to?(:compact) ? atts.compact : atts.select { |_, value| !value.nil? }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
9
lib/crono/railtie.rb
Normal file
9
lib/crono/railtie.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
module Crono
|
||||
class Railtie < ::Rails::Railtie
|
||||
rake_tasks do
|
||||
Dir[File.join(File.dirname(__FILE__), '../tasks/*.rake')].each do |file|
|
||||
load file
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,5 @@
|
||||
module Crono
|
||||
# Scheduler is a container for job list and queue
|
||||
class Scheduler
|
||||
attr_accessor :jobs
|
||||
|
||||
@@ -11,13 +12,10 @@ module Crono
|
||||
jobs << job
|
||||
end
|
||||
|
||||
def next
|
||||
queue.first
|
||||
end
|
||||
|
||||
private
|
||||
def queue
|
||||
jobs.sort_by(&:next)
|
||||
def next_jobs
|
||||
jobs.group_by(&:next).sort_by {|time,_| time }.first
|
||||
end
|
||||
end
|
||||
|
||||
mattr_accessor :scheduler
|
||||
end
|
||||
|
||||
36
lib/crono/time_of_day.rb
Normal file
36
lib/crono/time_of_day.rb
Normal 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.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
|
||||
@@ -1,3 +1,3 @@
|
||||
module Crono
|
||||
VERSION = '0.7.0'
|
||||
VERSION = '2.1.0'
|
||||
end
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
require 'haml'
|
||||
require 'sinatra/base'
|
||||
|
||||
module Crono
|
||||
class Web < Sinatra::Base
|
||||
set :root, File.expand_path(File.dirname(__FILE__) + "/../../web")
|
||||
set :public_folder, Proc.new { "#{root}/assets" }
|
||||
set :views, Proc.new { "#{root}/views" }
|
||||
|
||||
get '/' do
|
||||
@jobs = Crono::CronoJob.all
|
||||
haml :dashboard, format: :html5
|
||||
end
|
||||
|
||||
get '/jobs/:id' do
|
||||
@job = Crono::CronoJob.find(params[:id])
|
||||
haml :job
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,6 +4,7 @@ require 'rails/generators/active_record'
|
||||
|
||||
module Crono
|
||||
module Generators
|
||||
# rails generate crono:install
|
||||
class InstallGenerator < ::Rails::Generators::Base
|
||||
include Rails::Generators::Migration
|
||||
|
||||
@@ -11,15 +12,27 @@ module Crono
|
||||
ActiveRecord::Generators::Base.next_migration_number(path)
|
||||
end
|
||||
|
||||
desc "Installs crono and generates the necessary configuration files"
|
||||
source_root File.expand_path("../templates", __FILE__)
|
||||
desc 'Installs crono and generates the necessary configuration files'
|
||||
source_root File.expand_path('../templates', __FILE__)
|
||||
|
||||
def copy_config
|
||||
template 'cronotab.rb.erb', 'config/cronotab.rb'
|
||||
end
|
||||
|
||||
def create_migrations
|
||||
migration_template 'migrations/create_crono_jobs.rb', 'db/migrate/create_crono_jobs.rb'
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
#
|
||||
# class TestJob
|
||||
# def perform
|
||||
# puts "Test!"
|
||||
# puts 'Test!'
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Crono.perform(TestJob).every 2.days, at: "15:30"
|
||||
# Crono.perform(TestJob).every 2.days, at: '15:30'
|
||||
#
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
class CreateCronoJobs < ActiveRecord::Migration
|
||||
def self.up
|
||||
class CreateCronoJobs < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
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
|
||||
end
|
||||
add_index :crono_jobs, [:job_id], unique: true
|
||||
end
|
||||
|
||||
def self.down
|
||||
drop_table :crono_jobs
|
||||
end
|
||||
end
|
||||
|
||||
16
lib/tasks/crono_tasks.rake
Normal file
16
lib/tasks/crono_tasks.rake
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace :crono do
|
||||
desc 'Clean unused job stats from DB'
|
||||
task clean: :environment do
|
||||
Crono.scheduler = Crono::Scheduler.new
|
||||
Crono::Cronotab.process
|
||||
current_job_ids = Crono.scheduler.jobs.map(&:job_id)
|
||||
Crono::CronoJob.where.not(job_id: current_job_ids).destroy_all
|
||||
end
|
||||
|
||||
desc 'Check cronotab.rb syntax'
|
||||
task check: :environment do
|
||||
Crono.scheduler = Crono::Scheduler.new
|
||||
Crono::Cronotab.process
|
||||
puts 'Syntax ok'
|
||||
end
|
||||
end
|
||||
12
spec/assets/bad_cronotab.rb
Normal file
12
spec/assets/bad_cronotab.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
# This is an example of a bad cronotab for tests
|
||||
|
||||
class TestJob
|
||||
def perform
|
||||
puts 'Test!'
|
||||
end
|
||||
end
|
||||
|
||||
# This is an error, because you can use `on` options with
|
||||
# a period less than 7 days.
|
||||
|
||||
Crono.perform(TestJob).every 5.days, on: :sunday
|
||||
9
spec/assets/good_cronotab.rb
Normal file
9
spec/assets/good_cronotab.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
# This is an example of a good cronotab for tests
|
||||
|
||||
class TestJob
|
||||
def perform
|
||||
puts 'Test!'
|
||||
end
|
||||
end
|
||||
|
||||
Crono.perform(TestJob).every 5.seconds
|
||||
119
spec/cli_spec.rb
119
spec/cli_spec.rb
@@ -1,51 +1,110 @@
|
||||
require "spec_helper"
|
||||
require 'spec_helper'
|
||||
require 'crono/cli'
|
||||
|
||||
class TestJob
|
||||
def perform;end
|
||||
end
|
||||
|
||||
describe Crono::CLI do
|
||||
let(:cli) { Crono::CLI.instance }
|
||||
|
||||
describe "#run" do
|
||||
it "should try to initialize rails with #load_rails and start working loop" do
|
||||
describe '#run' do
|
||||
it 'should initialize rails with #load_rails and start working loop' do
|
||||
expect(cli).to receive(:load_rails)
|
||||
expect(cli).to receive(:have_jobs?).and_return(true)
|
||||
expect(cli).to receive(:start_working_loop)
|
||||
expect(cli).to receive(:parse_options)
|
||||
expect(cli).to receive(:write_pid)
|
||||
expect(cli).to receive(:parse_command)
|
||||
expect(cli).not_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 "#start_working_loop" do
|
||||
it "should start working loop"
|
||||
describe '#parse_options' do
|
||||
it 'should set cronotab' do
|
||||
cli.send(:parse_options, ['--cronotab', '/tmp/cronotab.rb'])
|
||||
expect(cli.config.cronotab).to be_eql '/tmp/cronotab.rb'
|
||||
end
|
||||
|
||||
it 'should set logfile' do
|
||||
cli.send(:parse_options, ['--logfile', 'log/crono.log'])
|
||||
expect(cli.config.logfile).to be_eql 'log/crono.log'
|
||||
end
|
||||
|
||||
it 'should set pidfile' do
|
||||
cli.send(:parse_options, ['--pidfile', 'tmp/pids/crono.0.log'])
|
||||
expect(cli.config.pidfile).to be_eql 'tmp/pids/crono.0.log'
|
||||
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
|
||||
end
|
||||
|
||||
it 'should set environment' do
|
||||
cli.send(:parse_options, ['--environment', 'production'])
|
||||
expect(cli.config.environment).to be_eql('production')
|
||||
end
|
||||
end
|
||||
|
||||
describe "#parse_options" do
|
||||
it "should set cronotab" do
|
||||
cli.send(:parse_options, ["--cronotab", "/tmp/cronotab.rb"])
|
||||
expect(cli.config.cronotab).to be_eql "/tmp/cronotab.rb"
|
||||
end
|
||||
describe '#parse_command' do
|
||||
|
||||
it "should set logfile" do
|
||||
cli.send(:parse_options, ["--logfile", "log/crono.log"])
|
||||
expect(cli.config.logfile).to be_eql "log/crono.log"
|
||||
end
|
||||
|
||||
it "should set pidfile" do
|
||||
cli.send(:parse_options, ["--pidfile", "tmp/pids/crono.0.log"])
|
||||
expect(cli.config.pidfile).to be_eql "tmp/pids/crono.0.log"
|
||||
end
|
||||
|
||||
it "should set daemonize" do
|
||||
cli.send(:parse_options, ["--daemonize"])
|
||||
it 'should set daemonize on start' do
|
||||
cli.send(:parse_command, ['start'])
|
||||
expect(cli.config.daemonize).to be true
|
||||
end
|
||||
|
||||
it "should set environment" do
|
||||
cli.send(:parse_options, ["--environment", "production"])
|
||||
expect(cli.config.environment).to be_eql("production")
|
||||
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
|
||||
|
||||
@@ -1,15 +1,47 @@
|
||||
require "spec_helper"
|
||||
require 'spec_helper'
|
||||
|
||||
describe Crono::Config do
|
||||
describe "#initialize" do
|
||||
it "should initialize with default configuration options" do
|
||||
ENV["RAILS_ENV"] = "test"
|
||||
let(:config) { Crono::Config.new }
|
||||
describe '#initialize' do
|
||||
it 'should initialize with default configuration options' do
|
||||
ENV['RAILS_ENV'] = 'test'
|
||||
@config = Crono::Config.new
|
||||
expect(@config.cronotab).to be Crono::Config::CRONOTAB
|
||||
expect(@config.logfile).to be Crono::Config::LOGFILE
|
||||
expect(@config.pidfile).to be Crono::Config::PIDFILE
|
||||
expect(@config.piddir).to be Crono::Config::PIDDIR
|
||||
expect(@config.process_name).to be Crono::Config::PROCESS_NAME
|
||||
expect(@config.daemonize).to be false
|
||||
expect(@config.environment).to be_eql ENV["RAILS_ENV"]
|
||||
expect(@config.monitor).to be false
|
||||
expect(@config.environment).to be_eql ENV['RAILS_ENV']
|
||||
end
|
||||
|
||||
describe "#pidfile" do
|
||||
subject(:pidfile) { config.pidfile }
|
||||
|
||||
context "not explicity configured" do
|
||||
context "daemonize is false" do
|
||||
before { config.daemonize = false }
|
||||
|
||||
specify { expect(pidfile).to be_nil }
|
||||
end
|
||||
end
|
||||
|
||||
context "explicity configured" do
|
||||
let(:path) { "foo/bar/pid.pid" }
|
||||
|
||||
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
|
||||
|
||||
7
spec/crono_spec.rb
Normal file
7
spec/crono_spec.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe Crono do
|
||||
it 'has a version number' do
|
||||
expect(Crono::VERSION).not_to be nil
|
||||
end
|
||||
end
|
||||
20
spec/cronotab_spec.rb
Normal file
20
spec/cronotab_spec.rb
Normal 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
|
||||
3
spec/internal/app/controllers/application_controller.rb
Normal file
3
spec/internal/app/controllers/application_controller.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
class ApplicationController < ActionController::Base
|
||||
protect_from_forgery with: :exception
|
||||
end
|
||||
5
spec/internal/app/controllers/pages_controller.rb
Normal file
5
spec/internal/app/controllers/pages_controller.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class PagesController < ApplicationController
|
||||
def index
|
||||
#
|
||||
end
|
||||
end
|
||||
1
spec/internal/app/views/pages/index.html.erb
Normal file
1
spec/internal/app/views/pages/index.html.erb
Normal file
@@ -0,0 +1 @@
|
||||
<p>Hello World</p>
|
||||
22
spec/internal/config/application.rb
Normal file
22
spec/internal/config/application.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
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
|
||||
5
spec/internal/config/boot.rb
Normal file
5
spec/internal/config/boot.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
# 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__)
|
||||
3
spec/internal/config/database.yml
Normal file
3
spec/internal/config/database.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
test:
|
||||
adapter: sqlite3
|
||||
database: db/crono_test.sqlite
|
||||
2
spec/internal/config/environment.rb
Normal file
2
spec/internal/config/environment.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
# Load the Rails application.
|
||||
require_relative 'application'
|
||||
3
spec/internal/config/routes.rb
Normal file
3
spec/internal/config/routes.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
Rails.application.routes.draw do
|
||||
root 'pages#index'
|
||||
end
|
||||
3
spec/internal/config/storage.yml
Normal file
3
spec/internal/config/storage.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
test:
|
||||
service: Disk
|
||||
root: /Users/chris/Sites/_playground/crono/tmp/storage
|
||||
10
spec/internal/db/schema.rb
Normal file
10
spec/internal/db/schema.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
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
|
||||
1
spec/internal/log/.gitignore
vendored
Normal file
1
spec/internal/log/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.log
|
||||
0
spec/internal/public/favicon.ico
Normal file
0
spec/internal/public/favicon.ico
Normal file
163
spec/job_spec.rb
163
spec/job_spec.rb
@@ -1,95 +1,194 @@
|
||||
require "spec_helper"
|
||||
require 'spec_helper'
|
||||
|
||||
class TestJob
|
||||
def perform;end
|
||||
def perform(*args)
|
||||
puts 'Test!'
|
||||
end
|
||||
end
|
||||
|
||||
class TestFailingJob
|
||||
def perform(*args)
|
||||
raise 'Some error'
|
||||
end
|
||||
end
|
||||
|
||||
class TestNoArgsJob
|
||||
def perform
|
||||
raise "Some error"
|
||||
puts 'Test!'
|
||||
end
|
||||
end
|
||||
|
||||
describe Crono::Job do
|
||||
let(:period) { Crono::Period.new(2.day) }
|
||||
let(:job) { Crono::Job.new(TestJob, period) }
|
||||
let(:failing_job) { Crono::Job.new(TestFailingJob, period) }
|
||||
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) }
|
||||
|
||||
it "should contain performer and period" do
|
||||
it 'should contain performer and period' do
|
||||
expect(job.performer).to be TestJob
|
||||
expect(job.period).to be period
|
||||
end
|
||||
|
||||
describe "#perform" do
|
||||
it "should run performer in separate thread" do
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
after { job.send(:model).destroy }
|
||||
|
||||
it 'should run performer in separate thread' do
|
||||
expect(job).to receive(:save)
|
||||
thread = job.perform.join
|
||||
expect(thread).to be_stop
|
||||
job.send(:model).destroy
|
||||
end
|
||||
|
||||
it "should save performin errors to log" do
|
||||
it 'should save performin errors to log' do
|
||||
thread = failing_job.perform.join
|
||||
expect(thread).to be_stop
|
||||
saved_log = Crono::CronoJob.find_by(job_id: failing_job.job_id).log
|
||||
expect(saved_log).to include "Some error"
|
||||
expect(saved_log).to include 'Some error'
|
||||
end
|
||||
|
||||
it 'should set Job#healthy to false if perform with error' do
|
||||
failing_job.perform.join
|
||||
expect(failing_job.healthy).to be false
|
||||
end
|
||||
|
||||
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
|
||||
it "should return job identificator" do
|
||||
expect(job.description).to be_eql("Perform TestJob every 2 days")
|
||||
describe '#description' do
|
||||
it 'should return job identificator' do
|
||||
expect(job.description).to be_eql('Perform TestJob every 2 days at 15:00')
|
||||
end
|
||||
end
|
||||
|
||||
describe "#save" do
|
||||
it "should save new job to DB" do
|
||||
describe '#save' do
|
||||
it 'should save new job to DB' do
|
||||
expect(Crono::CronoJob.where(job_id: job.job_id)).to_not exist
|
||||
job.save
|
||||
expect(Crono::CronoJob.where(job_id: job.job_id)).to exist
|
||||
end
|
||||
|
||||
it "should update saved job" do
|
||||
job.last_performed_at = Time.now
|
||||
it 'should update saved job' do
|
||||
job.last_performed_at = Time.zone.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
|
||||
message = "test message"
|
||||
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
|
||||
describe '#load' do
|
||||
before do
|
||||
@saved_last_performed_at = job.last_performed_at = Time.now
|
||||
@saved_last_performed_at = job.last_performed_at = Time.zone.now
|
||||
job.save
|
||||
end
|
||||
|
||||
it "should load last_performed_at from DB" do
|
||||
@job = Crono::Job.new(TestJob, period)
|
||||
it 'should load last_performed_at from DB' do
|
||||
@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
|
||||
end
|
||||
|
||||
describe "#log" do
|
||||
it "should write log messages to both common and job log" do
|
||||
message = "Test message"
|
||||
expect(job.logger).to receive(:info).with(message)
|
||||
expect(job.job_logger).to receive(:info).with(message)
|
||||
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)
|
||||
end
|
||||
|
||||
it "should write job log to Job#job_log" do
|
||||
message = "Test message"
|
||||
it 'should write job log to Job#job_log' do
|
||||
message = 'Test message'
|
||||
job.send(:log, message)
|
||||
expect(job.job_log.string).to include(message)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#log_error' do
|
||||
it 'should call log with ERROR severity' do
|
||||
message = 'Test message'
|
||||
expect(job).to receive(:log).with(message, Logger::ERROR)
|
||||
job.send(:log_error, message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
31
spec/models/crono/crono_job_spec.rb
Normal file
31
spec/models/crono/crono_job_spec.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
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
|
||||
@@ -1,28 +0,0 @@
|
||||
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
|
||||
@@ -1,12 +1,45 @@
|
||||
require "spec_helper"
|
||||
require 'spec_helper'
|
||||
|
||||
class TestJob
|
||||
def perform;end
|
||||
def perform
|
||||
puts 'Test!'
|
||||
end
|
||||
end
|
||||
|
||||
describe Crono::PerformerProxy do
|
||||
it "should add job to schedule" do
|
||||
it 'should add job to schedule' do
|
||||
expect(Crono.scheduler).to receive(:add_job).with(kind_of(Crono::Job))
|
||||
Crono.perform(TestJob).every(2.days, at: "15:30")
|
||||
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
|
||||
|
||||
@@ -1,55 +1,140 @@
|
||||
require "spec_helper"
|
||||
require 'spec_helper'
|
||||
|
||||
describe Crono::Period do
|
||||
around(:each) do |example|
|
||||
Timecop.freeze do
|
||||
example.run
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#description" do
|
||||
it "should return period description" do
|
||||
@period = Crono::Period.new(2.day, at: "15:20")
|
||||
expect(@period.description).to be_eql("every 2 days at 15:20")
|
||||
end
|
||||
end
|
||||
describe '#next' do
|
||||
context 'in weakly basis' do
|
||||
it "should raise error if 'on' is wrong" do
|
||||
expect { @period = Crono::Period.new(7.days, on: :bad_day) }
|
||||
.to raise_error("Wrong 'on' day")
|
||||
end
|
||||
|
||||
describe "#next" do
|
||||
context "in daily basis" do
|
||||
it "should return the time 2 days from now" do
|
||||
it 'should raise error when period is less than 1 week' do
|
||||
expect { @period = Crono::Period.new(6.days, on: :monday) }
|
||||
.to raise_error("period should be at least 1 week to use 'on'")
|
||||
end
|
||||
|
||||
it "should return a 'on' day" do
|
||||
@period = Crono::Period.new(1.week, on: :thursday, at: '15:30')
|
||||
current_week = Time.zone.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)
|
||||
.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))
|
||||
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
|
||||
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
|
||||
@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)
|
||||
end
|
||||
|
||||
it 'should return time 2 days from now' do
|
||||
@period = Crono::Period.new(2.day)
|
||||
expect(@period.next).to be_eql(2.day.from_now)
|
||||
expect(@period.next.to_s).to be_eql(2.days.from_now.to_s)
|
||||
end
|
||||
|
||||
it "should set time to 'at' time as a string" do
|
||||
time = 10.minutes.ago
|
||||
@period = Crono::Period.new(2.day, at: [time.hour, time.min].join(':'))
|
||||
expect(@period.next).to be_eql(2.day.from_now.change(hour: time.hour, min: time.min))
|
||||
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)
|
||||
end
|
||||
|
||||
it "should set time to 'at' time as a hash" do
|
||||
time = 10.minutes.ago
|
||||
at = {hour: time.hour, min: time.min}
|
||||
@period = Crono::Period.new(2.day, at: at)
|
||||
expect(@period.next).to be_eql(2.day.from_now.change(at))
|
||||
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)
|
||||
end
|
||||
|
||||
it "should raise error when 'at' is wrong" do
|
||||
expect {
|
||||
@period = Crono::Period.new(2.day, at: 1)
|
||||
Crono::Period.new(2.days, at: 1)
|
||||
}.to raise_error("Unknown 'at' format")
|
||||
end
|
||||
|
||||
it "should return time in relation to last time" do
|
||||
@period = Crono::Period.new(2.day)
|
||||
expect(@period.next(since: 1.day.ago)).to be_eql(1.day.from_now)
|
||||
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")
|
||||
end
|
||||
|
||||
it "should return today time if it is first run and not too late" do
|
||||
it 'should return time in relation to last time' do
|
||||
@period = Crono::Period.new(2.days)
|
||||
expect(@period.next(since: 1.day.ago).to_s).to be_eql(1.day.from_now.to_s)
|
||||
end
|
||||
|
||||
it 'should return today time if it is first run and not too late' do
|
||||
time = 10.minutes.from_now
|
||||
at = {hour: time.hour, min: time.min}
|
||||
@period = Crono::Period.new(2.day, at: at)
|
||||
expect(@period.next).to be_eql(Time.now.change(at))
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
89
spec/rails_helper.rb
Normal file
89
spec/rails_helper.rb
Normal file
@@ -0,0 +1,89 @@
|
||||
# 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
|
||||
@@ -1,31 +1,62 @@
|
||||
require "spec_helper"
|
||||
require 'spec_helper'
|
||||
|
||||
class TestJob
|
||||
def perform;end
|
||||
def perform
|
||||
puts 'Test!'
|
||||
end
|
||||
end
|
||||
|
||||
describe Crono::Scheduler do
|
||||
before(:each) do
|
||||
@scheduler = Crono::Scheduler.new
|
||||
@jobs = [
|
||||
Crono::Period.new(3.day, at: 10.minutes.from_now.strftime("%H:%M")),
|
||||
Crono::Period.new(1.day, at: 20.minutes.from_now.strftime("%H:%M")),
|
||||
Crono::Period.new(7.day, at: 40.minutes.from_now.strftime("%H:%M"))
|
||||
].map { |period| Crono::Job.new(TestJob, period) }
|
||||
@scheduler.jobs = @jobs
|
||||
end
|
||||
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"))
|
||||
describe '#add_job' do
|
||||
it 'should call Job#load on Job' do
|
||||
@job = Crono::Job.new(TestJob, Crono::Period.new(10.day, at: '04:05'), [])
|
||||
expect(@job).to receive(:load)
|
||||
@scheduler.add_job(@job)
|
||||
scheduler.add_job(@job)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#next" do
|
||||
it "should return next job in schedule" do
|
||||
expect(@scheduler.next).to be @jobs[0]
|
||||
describe '#next_jobs' do
|
||||
it 'should return next job in schedule' do
|
||||
scheduler.jobs = jobs = [
|
||||
Crono::Period.new(3.days, at: 10.minutes.from_now.strftime('%H:%M')),
|
||||
Crono::Period.new(1.day, at: 20.minutes.from_now.strftime('%H:%M')),
|
||||
Crono::Period.new(7.days, at: 40.minutes.from_now.strftime('%H:%M'))
|
||||
].map { |period| Crono::Job.new(TestJob, period, []) }
|
||||
|
||||
time, jobs = scheduler.next_jobs
|
||||
expect(jobs).to be_eql [jobs[0]]
|
||||
end
|
||||
|
||||
it 'should return an array of jobs scheduled at same time 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, []) }
|
||||
|
||||
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
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
require 'bundler/setup'
|
||||
Bundler.setup
|
||||
require 'bundler'
|
||||
|
||||
require 'timecop'
|
||||
require 'byebug'
|
||||
require 'crono'
|
||||
require 'generators/crono/install/templates/migrations/create_crono_jobs.rb'
|
||||
Bundler.require :default, :development
|
||||
|
||||
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: "file::memory:?cache=shared")
|
||||
ActiveRecord::Base.logger = Logger.new(STDOUT)
|
||||
CreateCronoJobs.up
|
||||
# If you're using all parts of Rails:
|
||||
Combustion.initialize! :all
|
||||
# Or, load just what you need:
|
||||
# Combustion.initialize! :active_record, :action_controller
|
||||
|
||||
require 'rspec/rails'
|
||||
# If you're using Capybara:
|
||||
# require 'capybara/rails'
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.use_transactional_fixtures = true
|
||||
config.mock_with :rspec do |mocks|
|
||||
mocks.allow_message_expectations_on_nil = true
|
||||
end
|
||||
end
|
||||
|
||||
23
spec/tasks/crono_tasks_spec.rb
Normal file
23
spec/tasks/crono_tasks_spec.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
require 'spec_helper'
|
||||
require 'rake'
|
||||
|
||||
load 'tasks/crono_tasks.rake'
|
||||
Rake::Task.define_task(:environment)
|
||||
|
||||
describe 'rake' do
|
||||
describe 'crono:clean' do
|
||||
it 'should clean unused tasks from DB' do
|
||||
Crono::CronoJob.create!(job_id: 'used_job')
|
||||
ENV['CRONOTAB'] = File.expand_path('../../assets/good_cronotab.rb', __FILE__)
|
||||
Rake::Task['crono:clean'].invoke
|
||||
expect(Crono::CronoJob.where(job_id: 'used_job')).not_to exist
|
||||
end
|
||||
end
|
||||
|
||||
describe 'crono:check' do
|
||||
it 'should check cronotab syntax' do
|
||||
ENV['CRONOTAB'] = File.expand_path('../../assets/bad_cronotab.rb', __FILE__)
|
||||
expect { Rake::Task['crono:check'].invoke }.to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
61
spec/web_spec.rb
Normal file
61
spec/web_spec.rb
Normal file
@@ -0,0 +1,61 @@
|
||||
require 'spec_helper'
|
||||
require 'rack/test'
|
||||
include Rack::Test::Methods
|
||||
|
||||
describe Crono::Engine do
|
||||
let(:app) { Crono::Engine }
|
||||
|
||||
before do
|
||||
Crono::CronoJob.destroy_all
|
||||
@test_job_id = 'Perform TestJob every 5 seconds'
|
||||
@test_job_log = 'All runs ok'
|
||||
@test_job = Crono::CronoJob.create!(
|
||||
job_id: @test_job_id,
|
||||
log: @test_job_log
|
||||
)
|
||||
end
|
||||
|
||||
after { @test_job.destroy }
|
||||
|
||||
describe '/' do
|
||||
it 'should show all jobs' do
|
||||
get '/'
|
||||
expect(last_response).to be_ok
|
||||
expect(last_response.body).to include @test_job_id
|
||||
end
|
||||
|
||||
it 'should show a error mark when a job is unhealthy' do
|
||||
@test_job.update(healthy: false, 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, last_performed_at: 10.minutes.ago)
|
||||
get '/'
|
||||
expect(last_response.body).to include 'Success'
|
||||
end
|
||||
|
||||
it 'should show a pending mark when a job is pending' do
|
||||
@test_job.update(healthy: nil)
|
||||
get '/'
|
||||
expect(last_response.body).to include 'Pending'
|
||||
end
|
||||
end
|
||||
|
||||
describe '/job/:id' do
|
||||
it 'should show job log' do
|
||||
get "/jobs/#{@test_job.id}"
|
||||
expect(last_response).to be_ok
|
||||
expect(last_response.body).to include @test_job_id
|
||||
expect(last_response.body).to include @test_job_log
|
||||
end
|
||||
|
||||
it 'should show a message about the unhealthy job' do
|
||||
message = 'An error occurs during the last execution of this job'
|
||||
@test_job.update(healthy: false)
|
||||
get "/jobs/#{@test_job.id}"
|
||||
expect(last_response.body).to include message
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,20 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
%ol.breadcrumb
|
||||
%li.active Home
|
||||
|
||||
%h3 Running Jobs
|
||||
%table.table#job_list
|
||||
%tr
|
||||
%th Job
|
||||
%th Last performed at
|
||||
%th
|
||||
- for job in @jobs
|
||||
%tr
|
||||
%td= job.job_id
|
||||
%td= job.last_performed_at || "—"
|
||||
%td
|
||||
%a{href: url("/jobs/#{job.id}")}
|
||||
Log
|
||||
%span.glyphicon.glyphicon-menu-right
|
||||
@@ -1,8 +0,0 @@
|
||||
%ol.breadcrumb
|
||||
%li
|
||||
%a{href: url("/")} Home
|
||||
%li.active= @job.job_id
|
||||
|
||||
%h2
|
||||
"#{@job.job_id}" Log:
|
||||
%pre= @job.log
|
||||
@@ -1,25 +0,0 @@
|
||||
!!! 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"}
|
||||
Reference in New Issue
Block a user