mirror of
https://github.com/plashchynski/crono.git
synced 2026-01-11 21:10:24 +01:00
Compare commits
173 Commits
v0.8.7.pre
...
main
| 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 | ||
|
|
9c984cda49 | ||
|
|
dbb4f374d7 | ||
|
|
f245f33533 | ||
|
|
742cdc00c7 | ||
|
|
81ee422e30 |
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/
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -6,3 +6,9 @@
|
||||
/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
|
||||
69
Changes.md
69
Changes.md
@@ -1,3 +1,72 @@
|
||||
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
|
||||
-----------
|
||||
|
||||
- We moved Web UI to materializecss.com CSS framework
|
||||
- We moved from CDN to local assets for Web UI
|
||||
- We show current state of a job in Web UI (thanks to @michaelachrisco) https://github.com/plashchynski/crono/issues/16
|
||||
- We won't write a pidfile unless daemonized (thanks to @thomasfedb) https://github.com/plashchynski/crono/pull/13
|
||||
- Fixed `rake crono:clean` task error
|
||||
- Fixed issue when jobs scheduled at same time exclude each other https://github.com/plashchynski/crono/issues/19
|
||||
- Fixed issue with a daemon crash due to `time interval must be positive` error
|
||||
|
||||
|
||||
0.8.0
|
||||
-----------
|
||||
|
||||
|
||||
22
Gemfile
22
Gemfile
@@ -1,4 +1,24 @@
|
||||
source 'https://rubygems.org'
|
||||
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
|
||||
|
||||
# Specify your gem's dependencies in crono.gemspec
|
||||
# Specify your gem's dependencies in crono.gemspec.
|
||||
gemspec
|
||||
|
||||
group :development, :test do
|
||||
# gem 'activerecord'
|
||||
# gem 'actionpack' # action_controller, action_view
|
||||
# gem 'sprockets'
|
||||
|
||||
gem 'byebug'
|
||||
gem 'combustion', '~> 1.3'
|
||||
gem 'daemons'
|
||||
gem 'rack-test'
|
||||
gem 'rake', '>= 10.0'
|
||||
gem 'rspec', '>= 3.0'
|
||||
gem 'rspec-rails', '>= 4.0'
|
||||
gem 'sqlite3'
|
||||
gem 'timecop', '>= 0.7'
|
||||
end
|
||||
|
||||
# To use a debugger
|
||||
# gem 'byebug', group: [:development, :test]
|
||||
|
||||
299
Gemfile.lock
299
Gemfile.lock
@@ -1,84 +1,259 @@
|
||||
PATH
|
||||
remote: .
|
||||
specs:
|
||||
crono (0.8.7.pre)
|
||||
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.1)
|
||||
activesupport (= 4.2.1)
|
||||
globalid (>= 0.3.0)
|
||||
activemodel (4.2.1)
|
||||
activesupport (= 4.2.1)
|
||||
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.1)
|
||||
activemodel (= 4.2.1)
|
||||
activesupport (= 4.2.1)
|
||||
arel (~> 6.0)
|
||||
activesupport (4.2.1)
|
||||
i18n (~> 0.7)
|
||||
json (~> 1.7, >= 1.7.7)
|
||||
minitest (~> 5.1)
|
||||
thread_safe (~> 0.3, >= 0.3.4)
|
||||
tzinfo (~> 1.1)
|
||||
arel (6.0.0)
|
||||
builder (3.2.2)
|
||||
byebug (4.0.5)
|
||||
columnize (= 0.9.0)
|
||||
columnize (0.9.0)
|
||||
diff-lcs (1.2.5)
|
||||
globalid (0.3.5)
|
||||
activesupport (>= 4.1.0)
|
||||
haml (4.0.6)
|
||||
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
|
||||
rack-test (0.6.3)
|
||||
rack (>= 1.0)
|
||||
rake (10.4.2)
|
||||
rspec (3.2.0)
|
||||
rspec-core (~> 3.2.0)
|
||||
rspec-expectations (~> 3.2.0)
|
||||
rspec-mocks (~> 3.2.0)
|
||||
rspec-core (3.2.2)
|
||||
rspec-support (~> 3.2.0)
|
||||
rspec-expectations (3.2.0)
|
||||
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)
|
||||
sqlite3 (1.3.10)
|
||||
thread_safe (0.3.5)
|
||||
tilt (1.4.1)
|
||||
timecop (0.7.3)
|
||||
tzinfo (1.2.2)
|
||||
thread_safe (~> 0.1)
|
||||
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
|
||||
propshaft
|
||||
rack-test
|
||||
rake (~> 10.0)
|
||||
rspec (~> 3.0)
|
||||
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
|
||||
|
||||
183
README.md
183
README.md
@@ -1,25 +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
|
||||
|
||||
Currently there is no such thing as Ruby Cron for Rails. Well, there's [Whenever](https://github.com/javan/whenever) but it works on top of Unix Cron, so you haven't control of it from Ruby. Crono is pure Ruby. It doesn't use Unix Cron and other platform-dependent things. So you can use it on all platforms supported by Ruby. It persists job states to your database using Active Record. You have full control of jobs performing process. It's Ruby, so you can understand and modify it to fit your needs.
|
||||
Currently, there is no such thing as Ruby Cron for Rails. Well, there's [Whenever](https://github.com/javan/whenever) but it works on top of Unix Cron, so you can't manage it from Ruby. Crono is pure Ruby. It doesn't use Unix Cron and other platform-dependent things. So you can use it on all platforms supported by Ruby. It persists job states to your database using Active Record. You have full control of jobs performing process. It's Ruby, so you can understand and modify it to fit your needs.
|
||||
|
||||

|
||||
|
||||
|
||||
## 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
|
||||
@@ -42,47 +31,105 @@ 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.
|
||||
You can specify a simple job by editing ```config/cronotab.rb```:
|
||||
|
||||
Here's an example of a job:
|
||||
```ruby
|
||||
# config/cronotab.rb
|
||||
class TestJob
|
||||
def perform
|
||||
puts 'Test!'
|
||||
end
|
||||
end
|
||||
|
||||
Crono.perform(TestJob).every 5.seconds
|
||||
```
|
||||
|
||||
Then, run a crono process:
|
||||
|
||||
```bundle exec crono -e development```
|
||||
|
||||
### Job Schedule
|
||||
|
||||
Schedule list is defined in the file `config/cronotab.rb`, that created using `rake crono:install`. The semantic is pretty straightforward:
|
||||
|
||||
```ruby
|
||||
# config/cronotab.rb
|
||||
Crono.perform(TestJob).every 2.days, at: {hour: 15, min: 30}
|
||||
Crono.perform(TestJob).every 1.week, on: :monday, at: "15:30"
|
||||
```
|
||||
|
||||
You can schedule one job a few times if you want the job to be performed a few times a day or a week:
|
||||
|
||||
```ruby
|
||||
Crono.perform(TestJob).every 1.week, on: :monday
|
||||
Crono.perform(TestJob).every 1.week, on: :thursday
|
||||
```
|
||||
|
||||
The `at` can be a Hash:
|
||||
|
||||
```ruby
|
||||
Crono.perform(TestJob).every 1.day, at: {hour: 12, min: 15}
|
||||
```
|
||||
|
||||
You can schedule a job with arguments, which can contain objects that can be serialized using JSON.generate
|
||||
|
||||
```ruby
|
||||
Crono.perform(TestJob, 'some', 'args').every 1.day, at: {hour: 12, min: 15}
|
||||
```
|
||||
|
||||
You can set some options that not passed to the job but affect how the job will be treated by Crono. For example, you can set to truncate job logs (which stored in the database) to a certain number of records:
|
||||
|
||||
```ruby
|
||||
Crono.perform(TestJob).with_options(truncate_log: 100).every 1.week, on: :monday
|
||||
```
|
||||
|
||||
### Job classes
|
||||
|
||||
Crono can use Active Job jobs from `app/jobs/`. Here's an example of a job:
|
||||
|
||||
```ruby
|
||||
# app/jobs/test_job.rb
|
||||
class TestJob < ActiveJob::Base
|
||||
def perform
|
||||
def perform(options)
|
||||
# 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:
|
||||
The ActiveJob jobs are convenient because you can use one job in both periodic and enqueued ways. But Active Job is not required. Any class can be used as a crono job if it implements a method `perform`:
|
||||
|
||||
```ruby
|
||||
class TestJob # This is not an Active Job job, but pretty legal Crono job.
|
||||
def perform
|
||||
def perform(*args)
|
||||
# put you scheduled code here
|
||||
# Comments.deleted.clean_up...
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Run rake tasks
|
||||
|
||||
Here's an example of a Rake Task within a job:
|
||||
|
||||
```ruby
|
||||
# config/cronotab.rb
|
||||
require 'rake'
|
||||
# Be sure to change AppName to your application name!
|
||||
AppName::Application.load_tasks
|
||||
|
||||
Rails.app_class.load_tasks
|
||||
|
||||
class Test
|
||||
def perform
|
||||
Rake::Task['crono:hello'].invoke
|
||||
Rake::Task['crono:hello'].execute
|
||||
end
|
||||
end
|
||||
|
||||
@@ -101,77 +148,85 @@ end
|
||||
|
||||
_Please note that crono uses threads, so your code should be thread-safe_
|
||||
|
||||
#### Job Schedule
|
||||
### Run crono
|
||||
|
||||
Schedule list is defined in the file `config/cronotab.rb`, that created using `crono:install`. The semantic is pretty straightforward:
|
||||
Run crono in your Rails project root directory:
|
||||
|
||||
```ruby
|
||||
# config/cronotab.rb
|
||||
Crono.perform(TestJob).every 2.days, at: {hour: 15, min: 30}
|
||||
Crono.perform(TestJob).every 1.week, on: :monday, at: "15:30"
|
||||
bundle exec crono -e development
|
||||
|
||||
Usage:
|
||||
```
|
||||
|
||||
You can schedule one job a few times, if you want the job to be performed a few times a day or a week:
|
||||
|
||||
```ruby
|
||||
Crono.perform(TestJob).every 1.week, on: :monday
|
||||
Crono.perform(TestJob).every 1.week, on: :thursday
|
||||
```
|
||||
|
||||
The `at` can be a Hash:
|
||||
|
||||
```ruby
|
||||
Crono.perform(TestJob).every 1.day, at: {hour: 12, min: 15}
|
||||
```
|
||||
|
||||
#### Run daemon
|
||||
|
||||
To run Crono daemon, in your Rails project root directory:
|
||||
|
||||
bundle exec crono RAILS_ENV=development
|
||||
|
||||
crono usage:
|
||||
```
|
||||
Usage: crono [options]
|
||||
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)
|
||||
```
|
||||
|
||||
### Run as a daemon
|
||||
|
||||
To run Crono as a daemon, please add to your Gemfile:
|
||||
|
||||
```ruby
|
||||
gem 'daemons'
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
bundle install; bundle exec crono start RAILS_ENV=development
|
||||
|
||||
There are "start", "stop", and "restart" commands.
|
||||
|
||||
## Web UI
|
||||
|
||||
Crono comes with a Sinatra application that can display the current state of Crono jobs.
|
||||
Add `sinatra` and `haml` to your Gemfile
|
||||
|
||||
```ruby
|
||||
gem 'haml'
|
||||
gem 'sinatra', require: nil
|
||||
```
|
||||
Crono can display the current state of Crono jobs.
|
||||
|
||||
Add the following to your `config/routes.rb`:
|
||||
|
||||
```ruby
|
||||
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.
|
||||
|
||||
9
Rakefile
9
Rakefile
@@ -1,6 +1,5 @@
|
||||
require 'bundler/setup'
|
||||
|
||||
load 'rails/tasks/statistics.rake'
|
||||
|
||||
require 'bundler/gem_tasks'
|
||||
|
||||
require 'rspec/core/rake_task'
|
||||
RSpec::Core::RakeTask.new('spec')
|
||||
|
||||
task default: :spec
|
||||
|
||||
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
|
||||
@@ -1,5 +1,3 @@
|
||||
require 'active_record'
|
||||
|
||||
module Crono
|
||||
# Crono::CronoJob is a ActiveRecord model to store job state
|
||||
class CronoJob < ActiveRecord::Base
|
||||
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>
|
||||
14
bin/console
14
bin/console
@@ -1,14 +0,0 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
require "bundler/setup"
|
||||
require "crono"
|
||||
|
||||
# You can add fixtures and/or initialization code here to make experimenting
|
||||
# with your gem easier. You can also use a different console, if you like.
|
||||
|
||||
# (If you use this, don't forget to add pry to your Gemfile!)
|
||||
# require "pry"
|
||||
# Pry.start
|
||||
|
||||
require "irb"
|
||||
IRB.start
|
||||
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'
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
|
||||
bundle install
|
||||
|
||||
# Do any other automated setup that you need to do here
|
||||
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,34 +1,33 @@
|
||||
# coding: utf-8
|
||||
lib = File.expand_path('../lib', __FILE__)
|
||||
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
||||
require 'crono/version'
|
||||
# -*- encoding: utf-8 -*-
|
||||
$:.push File.expand_path("../lib", __FILE__)
|
||||
require_relative 'lib/crono/version'
|
||||
|
||||
Gem::Specification.new do |spec|
|
||||
spec.name = 'crono'
|
||||
spec.version = Crono::VERSION
|
||||
spec.authors = ['Dzmitry Plashchynski']
|
||||
spec.email = ['plashchynski@gmail.com']
|
||||
Gem::Specification.new do |s|
|
||||
s.name = 'crono'
|
||||
s.version = Crono::VERSION
|
||||
s.authors = ['Dzmitry Plashchynski']
|
||||
s.email = ['plashchynski@gmail.com']
|
||||
|
||||
spec.summary = 'Job scheduler for Rails'
|
||||
spec.description = 'A time-based background job scheduler daemon (just like Cron) for Rails'
|
||||
spec.homepage = 'https://github.com/plashchynski/crono'
|
||||
spec.license = 'Apache-2.0'
|
||||
s.summary = 'Job scheduler for Rails'
|
||||
s.description = 'A time-based background job scheduler daemon (just like Cron) for Rails'
|
||||
s.homepage = 'https://github.com/plashchynski/crono'
|
||||
s.license = 'Apache-2.0'
|
||||
|
||||
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
||||
spec.bindir = 'exe' # http://bundler.io/blog/2015/03/20/moving-bins-to-exe.html
|
||||
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
||||
spec.require_paths = ['lib']
|
||||
s.files = Dir['{app,config,db,lib}/**/*', 'LICENSE', 'Rakefile', 'README.rdoc']
|
||||
s.test_files = Dir['spec/**/*']
|
||||
s.executables = ['crono']
|
||||
s.require_paths = ["lib"]
|
||||
|
||||
spec.add_runtime_dependency 'activejob', '~> 4.0'
|
||||
spec.add_runtime_dependency 'activesupport', '~> 4.0'
|
||||
spec.add_runtime_dependency 'activerecord', '~> 4.0'
|
||||
spec.add_development_dependency 'rake', '~> 10.0'
|
||||
spec.add_development_dependency 'bundler', '>= 1.0.0'
|
||||
spec.add_development_dependency 'rspec', '~> 3.0'
|
||||
spec.add_development_dependency 'timecop', '~> 0.7'
|
||||
spec.add_development_dependency 'sqlite3'
|
||||
spec.add_development_dependency 'byebug'
|
||||
spec.add_development_dependency 'sinatra'
|
||||
spec.add_development_dependency 'haml'
|
||||
spec.add_development_dependency 'rack-test'
|
||||
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 |
@@ -2,15 +2,17 @@
|
||||
module Crono
|
||||
end
|
||||
|
||||
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/orm/active_record/crono_job'
|
||||
require 'crono/cronotab'
|
||||
require 'crono/railtie' if defined?(Rails)
|
||||
|
||||
Crono.autoload :Web, 'crono/web'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
Thread.abort_on_exception = true
|
||||
|
||||
require 'crono'
|
||||
require 'optparse'
|
||||
|
||||
@@ -7,6 +9,8 @@ module Crono
|
||||
include Singleton
|
||||
include Logging
|
||||
|
||||
COMMANDS = %w(start stop restart run zap reload status)
|
||||
|
||||
attr_accessor :config
|
||||
|
||||
def initialize
|
||||
@@ -16,39 +20,41 @@ module Crono
|
||||
|
||||
def run
|
||||
parse_options(ARGV)
|
||||
parse_command(ARGV)
|
||||
|
||||
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
|
||||
unless have_jobs?
|
||||
logger.error "You have no jobs in you cronotab file #{config.cronotab}"
|
||||
return
|
||||
end
|
||||
|
||||
if config.daemonize
|
||||
start_working_loop_in_daemon
|
||||
else
|
||||
start_working_loop
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def have_jobs?
|
||||
Crono.scheduler.jobs.present?
|
||||
end
|
||||
|
||||
def setup_log
|
||||
if config.daemonize
|
||||
self.logfile = config.logfile
|
||||
daemonize
|
||||
else
|
||||
self.logfile = STDOUT
|
||||
end
|
||||
end
|
||||
|
||||
def daemonize
|
||||
::Process.daemon(true, true)
|
||||
|
||||
[$stdout, $stderr].each do |io|
|
||||
File.open(config.logfile, 'ab') { |f| io.reopen(f) }
|
||||
io.sync = true
|
||||
end
|
||||
|
||||
$stdin.reopen('/dev/null')
|
||||
end
|
||||
|
||||
def write_pid
|
||||
return unless config.pidfile
|
||||
pidfile = File.expand_path(config.pidfile)
|
||||
@@ -71,25 +77,44 @@ module Crono
|
||||
require 'rails'
|
||||
require File.expand_path('config/environment.rb')
|
||||
::Rails.application.eager_load!
|
||||
require File.expand_path(config.cronotab)
|
||||
end
|
||||
|
||||
def check_jobs
|
||||
return if Crono.scheduler.jobs.present?
|
||||
logger.error "You have no jobs in you cronotab file #{config.cronotab}"
|
||||
def start_working_loop_in_daemon
|
||||
unless ENV['RAILS_ENV'] == 'test'
|
||||
begin
|
||||
require 'daemons'
|
||||
rescue LoadError
|
||||
raise "You need to add gem 'daemons' to your Gemfile if you wish to use it."
|
||||
end
|
||||
end
|
||||
Daemons.run_proc(config.process_name, dir: config.piddir, dir_mode: :normal, monitor: config.monitor, ARGV: @argv) do |*_argv|
|
||||
Dir.chdir(root)
|
||||
Crono.logger = Logger.new(config.logfile)
|
||||
|
||||
start_working_loop
|
||||
end
|
||||
end
|
||||
|
||||
def root
|
||||
@root ||= rails_root_defined? ? ::Rails.root : DIR_PWD
|
||||
end
|
||||
|
||||
def rails_root_defined?
|
||||
defined?(::Rails.root)
|
||||
end
|
||||
|
||||
def start_working_loop
|
||||
loop do
|
||||
next_time, jobs = Crono.scheduler.next_jobs
|
||||
sleep(next_time - Time.now) if next_time > Time.now
|
||||
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
|
||||
@@ -99,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|
|
||||
@@ -112,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
|
||||
|
||||
@@ -4,18 +4,30 @@ module Crono
|
||||
CRONOTAB = 'config/cronotab.rb'
|
||||
LOGFILE = 'log/crono.log'
|
||||
PIDFILE = 'tmp/pids/crono.pid'
|
||||
PIDDIR = 'tmp/pids'
|
||||
PROCESS_NAME = 'crono'
|
||||
|
||||
attr_accessor :cronotab, :logfile, :pidfile, :daemonize, :environment
|
||||
attr_accessor :cronotab, :logfile, :pidfile, :piddir, :process_name,
|
||||
:monitor, :daemonize, :environment
|
||||
|
||||
def initialize
|
||||
self.cronotab = CRONOTAB
|
||||
self.logfile = LOGFILE
|
||||
self.piddir = PIDDIR
|
||||
self.process_name = PROCESS_NAME
|
||||
self.daemonize = false
|
||||
self.monitor = false
|
||||
self.environment = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
|
||||
end
|
||||
|
||||
def pidfile=(pidfile)
|
||||
@pidfile = pidfile
|
||||
self.process_name = Pathname.new(pidfile).basename(".*").to_s
|
||||
self.piddir = Pathname.new(pidfile).dirname.to_s
|
||||
end
|
||||
|
||||
def pidfile
|
||||
@pidfile || (daemonize ? PIDFILE : nil)
|
||||
@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
|
||||
@@ -6,18 +6,23 @@ module Crono
|
||||
class Job
|
||||
include Logging
|
||||
|
||||
attr_accessor :performer, :period, :last_performed_at, :job_log,
|
||||
:job_logger, :healthy
|
||||
attr_accessor :performer, :period, :job_args, :last_performed_at, :job_options,
|
||||
:next_performed_at, :job_log, :job_logger, :healthy, :execution_interval
|
||||
|
||||
def initialize(performer, period)
|
||||
def initialize(performer, period, job_args = 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
|
||||
period.next(since: last_performed_at)
|
||||
return next_performed_at if next_performed_at.future?
|
||||
Time.zone.now
|
||||
end
|
||||
|
||||
def description
|
||||
@@ -29,8 +34,11 @@ module Crono
|
||||
end
|
||||
|
||||
def perform
|
||||
return Thread.new {} if perform_before_interval?
|
||||
|
||||
log "Perform #{performer}"
|
||||
self.last_performed_at = Time.now
|
||||
self.last_performed_at = Time.zone.now
|
||||
self.next_performed_at = period.next(since: last_performed_at)
|
||||
|
||||
Thread.new { perform_job }
|
||||
end
|
||||
@@ -39,11 +47,13 @@ module Crono
|
||||
@semaphore.synchronize do
|
||||
update_model
|
||||
clear_job_log
|
||||
ActiveRecord::Base.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
|
||||
@@ -52,32 +62,43 @@ module Crono
|
||||
job_log.truncate(job_log.rewind)
|
||||
end
|
||||
|
||||
def truncate_log(log)
|
||||
return log.lines.last(job_options[:truncate_log]).join if job_options[:truncate_log]
|
||||
return log
|
||||
end
|
||||
|
||||
def update_model
|
||||
saved_log = model.reload.log || ''
|
||||
log_to_save = saved_log + job_log.string
|
||||
log_to_save = truncate_log(log_to_save)
|
||||
model.update(last_performed_at: last_performed_at, log: log_to_save,
|
||||
healthy: healthy)
|
||||
end
|
||||
|
||||
def perform_job
|
||||
performer.new.perform
|
||||
finished_time_sec = format('%.2f', Time.now - last_performed_at)
|
||||
if job_args
|
||||
performer.new.perform(JSON.parse(job_args))
|
||||
else
|
||||
performer.new.perform
|
||||
end
|
||||
rescue StandardError => e
|
||||
handle_job_fail(e, finished_time_sec)
|
||||
handle_job_fail(e)
|
||||
else
|
||||
handle_job_success(finished_time_sec)
|
||||
handle_job_success
|
||||
ensure
|
||||
save
|
||||
end
|
||||
|
||||
def handle_job_fail(exception, finished_time_sec)
|
||||
def handle_job_fail(exception)
|
||||
finished_time_sec = format('%.2f', Time.zone.now - last_performed_at)
|
||||
self.healthy = false
|
||||
log_error "Finished #{performer} in #{finished_time_sec} seconds"\
|
||||
"with error: #{exception.message}"
|
||||
" with error: #{exception.message}"
|
||||
log_error exception.backtrace.join("\n")
|
||||
end
|
||||
|
||||
def handle_job_success(finished_time_sec)
|
||||
def handle_job_success
|
||||
finished_time_sec = format('%.2f', Time.zone.now - last_performed_at)
|
||||
self.healthy = true
|
||||
log "Finished #{performer} in #{finished_time_sec} seconds"
|
||||
end
|
||||
@@ -88,7 +109,7 @@ module Crono
|
||||
|
||||
def log(message, severity = Logger::INFO)
|
||||
@semaphore.synchronize do
|
||||
logger.log severity, message
|
||||
logger.log(severity, message) if logger
|
||||
job_logger.log severity, message
|
||||
end
|
||||
end
|
||||
@@ -96,5 +117,27 @@ module Crono
|
||||
def model
|
||||
@model ||= Crono::CronoJob.find_or_create_by(job_id: job_id)
|
||||
end
|
||||
|
||||
def perform_before_interval?
|
||||
return false if execution_interval == 0.minutes
|
||||
|
||||
return true if self.last_performed_at.present? && self.last_performed_at > execution_interval.ago
|
||||
return true if model.updated_at.present? && model.created_at != model.updated_at && model.updated_at > execution_interval.ago
|
||||
|
||||
Crono::CronoJob.transaction do
|
||||
job_record = Crono::CronoJob.where(job_id: job_id).lock(true).first
|
||||
|
||||
return true if job_record.updated_at.present? &&
|
||||
job_record.updated_at != job_record.created_at &&
|
||||
job_record.updated_at > execution_interval.ago
|
||||
|
||||
job_record.touch
|
||||
|
||||
return true unless job_record.save
|
||||
end
|
||||
|
||||
# Means that this node is permit to perform the job.
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,18 +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
|
||||
|
||||
@@ -4,23 +4,35 @@ module Crono
|
||||
DAYS = [:monday, :tuesday, :wednesday, :thursday, :friday, :saturday,
|
||||
:sunday]
|
||||
|
||||
def initialize(period, at: nil, on: nil)
|
||||
def initialize(period, at: nil, on: nil, within: nil)
|
||||
@period = period
|
||||
@at_hour, @at_min = parse_at(at) if at
|
||||
@interval = Interval.parse(within) if within
|
||||
@on = parse_on(on) if on
|
||||
end
|
||||
|
||||
def next(since: nil)
|
||||
return initial_next unless since
|
||||
@next = @period.since(since)
|
||||
if @interval
|
||||
if since
|
||||
@next = @interval.next_within(since, @period)
|
||||
else
|
||||
return initial_next if @interval.within?(initial_next)
|
||||
@next = @interval.next_within(initial_next, @period)
|
||||
end
|
||||
else
|
||||
return initial_next unless since
|
||||
@next = @period.since(since)
|
||||
end
|
||||
|
||||
@next = @next.beginning_of_week.advance(days: @on) if @on
|
||||
@next = @next.change(time_atts)
|
||||
return @next if @next.future?
|
||||
Time.now
|
||||
Time.zone.now
|
||||
end
|
||||
|
||||
def description
|
||||
desc = "every #{@period.inspect}"
|
||||
desc += " between #{@interval.from} and #{@interval.to} UTC" if @interval
|
||||
desc += format(' at %.2i:%.2i', @at_hour, @at_min) if @at_hour && @at_min
|
||||
desc += " on #{DAYS[@on].capitalize}" if @on
|
||||
desc
|
||||
@@ -35,8 +47,9 @@ module Crono
|
||||
end
|
||||
|
||||
def initial_day
|
||||
return Time.now unless @on
|
||||
day = Time.now.beginning_of_week.advance(days: @on)
|
||||
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
|
||||
@@ -49,10 +62,13 @@ module Crono
|
||||
end
|
||||
|
||||
def parse_at(at)
|
||||
fail "period should be at least 1 day to use 'at'" if @period < 1.day
|
||||
if @period < 1.day && (at.is_a? String || at[:hour])
|
||||
fail "period should be at least 1 day to use 'at' with specified hour"
|
||||
end
|
||||
|
||||
case at
|
||||
when String
|
||||
time = Time.parse(at)
|
||||
time = Time.zone.parse(at)
|
||||
return time.hour, time.min
|
||||
when Hash
|
||||
return at[:hour], at[:min]
|
||||
@@ -62,7 +78,8 @@ module Crono
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
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.8.7.pre'
|
||||
VERSION = '2.1.0'
|
||||
end
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
require 'haml'
|
||||
require 'sinatra/base'
|
||||
require 'crono'
|
||||
|
||||
module Crono
|
||||
# Web is a Web UI Sinatra app
|
||||
class Web < Sinatra::Base
|
||||
set :root, File.expand_path(File.dirname(__FILE__) + '/../../web')
|
||||
set :public_folder, proc { "#{root}/assets" }
|
||||
set :views, proc { "#{root}/views" }
|
||||
|
||||
get '/' do
|
||||
@jobs = Crono::CronoJob.all
|
||||
haml :dashboard, format: :html5
|
||||
end
|
||||
|
||||
get '/job/:id' do
|
||||
@job = Crono::CronoJob.find(params[:id])
|
||||
haml :job
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -21,7 +21,18 @@ module Crono
|
||||
|
||||
def create_migrations
|
||||
migration_template 'migrations/create_crono_jobs.rb',
|
||||
'db/migrate/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
|
||||
|
||||
@@ -1,16 +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
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
module Crono
|
||||
def self.load_cronotab
|
||||
cronotab_path = ENV['CRONOTAB'] || (defined?(Rails) &&
|
||||
File.join(Rails.root, cronotab_path))
|
||||
fail 'No cronotab defined' unless cronotab_path
|
||||
puts "Load cronotab #{cronotab_path}"
|
||||
require cronotab_path
|
||||
end
|
||||
end
|
||||
|
||||
namespace :crono do
|
||||
desc 'Clean unused job stats from DB'
|
||||
task clean: :environment do
|
||||
Crono.scheduler = Crono::Scheduler.new
|
||||
Crono.load_cronotab
|
||||
Crono::Cronotab.process
|
||||
current_job_ids = Crono.scheduler.jobs.map(&:job_id)
|
||||
Crono::CronoJob.where.not(job_id: current_job_ids).destroy_all
|
||||
end
|
||||
@@ -20,7 +10,7 @@ namespace :crono do
|
||||
desc 'Check cronotab.rb syntax'
|
||||
task check: :environment do
|
||||
Crono.scheduler = Crono::Scheduler.new
|
||||
Crono.load_cronotab
|
||||
Crono::Cronotab.process
|
||||
puts 'Syntax ok'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,11 +7,30 @@ describe Crono::CLI do
|
||||
describe '#run' do
|
||||
it 'should initialize rails with #load_rails and start working loop' do
|
||||
expect(cli).to receive(:load_rails)
|
||||
expect(cli).to receive(:have_jobs?).and_return(true)
|
||||
expect(cli).to receive(:start_working_loop)
|
||||
expect(cli).to receive(:parse_options)
|
||||
expect(cli).to receive(: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 '#parse_options' do
|
||||
@@ -30,9 +49,19 @@ describe Crono::CLI do
|
||||
expect(cli.config.pidfile).to be_eql 'tmp/pids/crono.0.log'
|
||||
end
|
||||
|
||||
it 'should set daemonize' do
|
||||
cli.send(:parse_options, ['--daemonize'])
|
||||
expect(cli.config.daemonize).to be true
|
||||
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
|
||||
@@ -40,4 +69,42 @@ describe Crono::CLI do
|
||||
expect(cli.config.environment).to be_eql('production')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#parse_command' do
|
||||
|
||||
it 'should set daemonize on start' do
|
||||
cli.send(:parse_command, ['start'])
|
||||
expect(cli.config.daemonize).to be true
|
||||
end
|
||||
|
||||
it 'should set daemonize on stop' do
|
||||
cli.send(:parse_command, ['stop'])
|
||||
expect(cli.config.daemonize).to be true
|
||||
end
|
||||
|
||||
it 'should set daemonize on restart' do
|
||||
cli.send(:parse_command, ['restart'])
|
||||
expect(cli.config.daemonize).to be true
|
||||
end
|
||||
|
||||
it 'should set daemonize on run' do
|
||||
cli.send(:parse_command, ['run'])
|
||||
expect(cli.config.daemonize).to be true
|
||||
end
|
||||
|
||||
it 'should set daemonize on zap' do
|
||||
cli.send(:parse_command, ['zap'])
|
||||
expect(cli.config.daemonize).to be true
|
||||
end
|
||||
|
||||
it 'should set daemonize on reload' do
|
||||
cli.send(:parse_command, ['reload'])
|
||||
expect(cli.config.daemonize).to be true
|
||||
end
|
||||
|
||||
it 'should set daemonize on status' do
|
||||
cli.send(:parse_command, ['status'])
|
||||
expect(cli.config.daemonize).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,8 +8,10 @@ describe Crono::Config do
|
||||
@config = Crono::Config.new
|
||||
expect(@config.cronotab).to be Crono::Config::CRONOTAB
|
||||
expect(@config.logfile).to be Crono::Config::LOGFILE
|
||||
expect(@config.pidfile).to be nil
|
||||
expect(@config.piddir).to be Crono::Config::PIDDIR
|
||||
expect(@config.process_name).to be Crono::Config::PROCESS_NAME
|
||||
expect(@config.daemonize).to be false
|
||||
expect(@config.monitor).to be false
|
||||
expect(@config.environment).to be_eql ENV['RAILS_ENV']
|
||||
end
|
||||
|
||||
@@ -22,12 +24,6 @@ describe Crono::Config do
|
||||
|
||||
specify { expect(pidfile).to be_nil }
|
||||
end
|
||||
|
||||
context "daemonize is true" do
|
||||
before { config.daemonize = true }
|
||||
|
||||
specify { expect(pidfile).to eq Crono::Config::PIDFILE }
|
||||
end
|
||||
end
|
||||
|
||||
context "explicity configured" do
|
||||
@@ -36,7 +32,16 @@ describe Crono::Config do
|
||||
before { config.pidfile = path }
|
||||
|
||||
specify { expect(pidfile).to eq path }
|
||||
|
||||
it "trys to set piddir" do
|
||||
expect(config.piddir).to eq "foo/bar"
|
||||
end
|
||||
|
||||
it "trys to set process_name" do
|
||||
expect(config.process_name).to eq "pid"
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
20
spec/cronotab_spec.rb
Normal file
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
@@ -1,15 +1,40 @@
|
||||
require 'spec_helper'
|
||||
|
||||
class TestJob
|
||||
def perform(*args)
|
||||
puts 'Test!'
|
||||
end
|
||||
end
|
||||
|
||||
class TestFailingJob
|
||||
def perform(*args)
|
||||
raise 'Some error'
|
||||
end
|
||||
end
|
||||
|
||||
class TestNoArgsJob
|
||||
def perform
|
||||
puts 'Test!'
|
||||
end
|
||||
end
|
||||
|
||||
describe Crono::Job do
|
||||
let(:period) { Crono::Period.new(2.day, at: '15:00') }
|
||||
let(:job) { Crono::Job.new(TestJob, period) }
|
||||
let(:failing_job) { Crono::Job.new(TestFailingJob, period) }
|
||||
let(:job_args) {[{some: 'data'}]}
|
||||
let(:job) { Crono::Job.new(TestJob, period, []) }
|
||||
let(:job_with_args) { Crono::Job.new(TestJob, period, job_args) }
|
||||
let(:failing_job) { Crono::Job.new(TestFailingJob, period, []) }
|
||||
let(:not_args_job) { Crono::Job.new(TestJob, period) }
|
||||
|
||||
it 'should contain performer and period' do
|
||||
expect(job.performer).to be TestJob
|
||||
expect(job.period).to be period
|
||||
end
|
||||
|
||||
it 'should contain data as JSON String' do
|
||||
expect(job_with_args.job_args).to eq '[{"some":"data"}]'
|
||||
end
|
||||
|
||||
describe '#next' do
|
||||
it 'should return next performing time according to period' do
|
||||
expect(job.next).to be_eql period.next
|
||||
@@ -36,6 +61,50 @@ describe Crono::Job do
|
||||
failing_job.perform.join
|
||||
expect(failing_job.healthy).to be false
|
||||
end
|
||||
|
||||
it 'should execute one' do
|
||||
job.execution_interval = 5.minutes
|
||||
|
||||
expect(job).to receive(:perform_job).once
|
||||
job.perform.join
|
||||
thread = job.perform.join
|
||||
expect(thread).to be_stop
|
||||
end
|
||||
|
||||
it 'should execute twice' do
|
||||
job.execution_interval = 0.minutes
|
||||
|
||||
test_preform_job_twice
|
||||
end
|
||||
|
||||
it 'should execute twice without args' do
|
||||
test_preform_job_twice not_args_job
|
||||
end
|
||||
|
||||
it 'should execute twice without initialize execution_interval' do
|
||||
test_preform_job_twice
|
||||
end
|
||||
|
||||
it 'should call perform of performer' do
|
||||
expect(TestJob).to receive(:new).with(no_args)
|
||||
thread = job.perform.join
|
||||
expect(thread).to be_stop
|
||||
end
|
||||
|
||||
it 'should call perform of performer with data' do
|
||||
test_job = double()
|
||||
expect(TestJob).to receive(:new).and_return(test_job)
|
||||
expect(test_job).to receive(:perform).with([{ 'some' => 'data' }])
|
||||
thread = job_with_args.perform.join
|
||||
expect(thread).to be_stop
|
||||
end
|
||||
|
||||
def test_preform_job_twice(jon_instance = job)
|
||||
expect(jon_instance).to receive(:perform_job).twice
|
||||
jon_instance.perform.join
|
||||
thread = jon_instance.perform.join
|
||||
expect(thread).to be_stop
|
||||
end
|
||||
end
|
||||
|
||||
describe '#description' do
|
||||
@@ -52,31 +121,48 @@ describe Crono::Job do
|
||||
end
|
||||
|
||||
it 'should update saved job' do
|
||||
job.last_performed_at = Time.now
|
||||
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
|
||||
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
|
||||
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)
|
||||
@job = Crono::Job.new(TestJob, period, job_args)
|
||||
@job.load
|
||||
expect(@job.last_performed_at.utc.to_s).to be_eql @saved_last_performed_at.utc.to_s
|
||||
end
|
||||
@@ -85,6 +171,7 @@ describe Crono::Job do
|
||||
describe '#log' do
|
||||
it 'should write log messages to both common and job log' do
|
||||
message = 'Test message'
|
||||
job.logfile = "/dev/null"
|
||||
expect(job.logger).to receive(:log).with(Logger::INFO, message)
|
||||
expect(job.job_logger).to receive(:log).with(Logger::INFO, message)
|
||||
job.send(:log, message)
|
||||
|
||||
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,8 +1,45 @@
|
||||
require 'spec_helper'
|
||||
|
||||
class TestJob
|
||||
def perform
|
||||
puts 'Test!'
|
||||
end
|
||||
end
|
||||
|
||||
describe Crono::PerformerProxy do
|
||||
it 'should add job to schedule' do
|
||||
expect(Crono.scheduler).to receive(:add_job).with(kind_of(Crono::Job))
|
||||
Crono.perform(TestJob).every(2.days, at: '15:30')
|
||||
end
|
||||
|
||||
it 'should set execution interval' do
|
||||
allow(Crono).to receive(:scheduler).and_return(Crono::Scheduler.new)
|
||||
expect_any_instance_of(Crono::Job).to receive(:execution_interval=).with(0.minutes).once
|
||||
expect_any_instance_of(Crono::Job).to receive(:execution_interval=).with(10.minutes).once
|
||||
Crono.perform(TestJob).every(2.days, at: '15:30').once_per 10.minutes
|
||||
end
|
||||
|
||||
it 'do nothing when job not initalized' do
|
||||
expect_any_instance_of(Crono::Job).not_to receive(:execution_interval=)
|
||||
expect_any_instance_of(described_class).to receive(:once_per)
|
||||
Crono.perform(TestJob).once_per 10.minutes
|
||||
end
|
||||
|
||||
it 'should add job with args to schedule' do
|
||||
expect(Crono::Job).to receive(:new).with(TestJob, kind_of(Crono::Period), :some, { some: 'data' })
|
||||
allow(Crono.scheduler).to receive(:add_job)
|
||||
Crono.perform(TestJob, :some, { some: 'data' }).every(2.days, at: '15:30')
|
||||
end
|
||||
|
||||
it 'should add job without args to schedule' do
|
||||
expect(Crono::Job).to receive(:new).with(TestJob, kind_of(Crono::Period), nil, nil)
|
||||
allow(Crono.scheduler).to receive(:add_job)
|
||||
Crono.perform(TestJob).every(2.days, at: '15:30')
|
||||
end
|
||||
|
||||
it 'should add job with options to schedule' do
|
||||
expect(Crono::Job).to receive(:new).with(TestJob, kind_of(Crono::Period), nil, { some_option: true })
|
||||
allow(Crono.scheduler).to receive(:add_job)
|
||||
Crono.perform(TestJob).with_options(some_option: true).every(2.days, at: '15:30')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe Crono::Period do
|
||||
around(:each) do |example|
|
||||
Timecop.freeze do
|
||||
example.run
|
||||
end
|
||||
end
|
||||
|
||||
describe '#description' do
|
||||
it 'should return period description' do
|
||||
@period = Crono::Period.new(1.week, on: :monday, at: '15:20')
|
||||
expect(@period.description).to be_eql('every 7 days at 15:20 on Monday')
|
||||
expected_description = if ActiveSupport::VERSION::MAJOR >= 5
|
||||
'every 1 week at 15:20 on Monday'
|
||||
else
|
||||
'every 7 days at 15:20 on Monday'
|
||||
end
|
||||
expect(@period.description).to be_eql(expected_description)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -28,53 +27,63 @@ describe Crono::Period do
|
||||
|
||||
it "should return a 'on' day" do
|
||||
@period = Crono::Period.new(1.week, on: :thursday, at: '15:30')
|
||||
current_week = Time.now.beginning_of_week
|
||||
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.now.next_week.advance(days: 3)
|
||||
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.now.beginning_of_week.advance(days: 4)) do
|
||||
expect(@period.next).to be_eql(Time.now.next_week.advance(days: 3))
|
||||
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.now.beginning_of_week
|
||||
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.now if the next time in past" 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 be_eql(Time.now)
|
||||
expect(@period.next(since: 2.days.ago).to_s).to be_eql(Time.zone.now.to_s)
|
||||
end
|
||||
|
||||
it 'should return the time 2 days from now' do
|
||||
it 'should return time 2 days from now' do
|
||||
@period = Crono::Period.new(2.day)
|
||||
expect(@period.next).to be_eql(2.days.from_now)
|
||||
expect(@period.next.to_s).to be_eql(2.days.from_now.to_s)
|
||||
end
|
||||
|
||||
it "should set time to 'at' time as a string" do
|
||||
time = 10.minutes.ago
|
||||
at = [time.hour, time.min].join(':')
|
||||
@period = Crono::Period.new(2.days, at: at)
|
||||
expect(@period.next).to be_eql(2.days.from_now.change(hour: time.hour, min: time.min))
|
||||
expect(@period.next.to_s).to be_eql(2.days.from_now.change(hour: time.hour, min: time.min).to_s)
|
||||
end
|
||||
|
||||
it "should set time to 'at' time as a hash" do
|
||||
time = 10.minutes.ago
|
||||
at = { hour: time.hour, min: time.min }
|
||||
@period = Crono::Period.new(2.days, at: at)
|
||||
expect(@period.next).to be_eql(2.days.from_now.change(at))
|
||||
expect(@period.next.to_s).to be_eql(2.days.from_now.change(at).to_s)
|
||||
end
|
||||
|
||||
it "should raise error when 'at' is wrong" do
|
||||
@@ -86,19 +95,46 @@ describe Crono::Period do
|
||||
it 'should raise error when period is less than 1 day' do
|
||||
expect {
|
||||
Crono::Period.new(5.hours, at: '15:30')
|
||||
}.to raise_error("period should be at least 1 day to use 'at'")
|
||||
}.to raise_error("period should be at least 1 day to use 'at' with specified hour")
|
||||
end
|
||||
|
||||
it 'should return time in relation to last time' do
|
||||
@period = Crono::Period.new(2.days)
|
||||
expect(@period.next(since: 1.day.ago)).to be_eql(1.day.from_now)
|
||||
expect(@period.next(since: 1.day.ago).to_s).to be_eql(1.day.from_now.to_s)
|
||||
end
|
||||
|
||||
it 'should return today time if it is first run and not too late' do
|
||||
time = 10.minutes.from_now
|
||||
at = { hour: time.hour, min: time.min }
|
||||
@period = Crono::Period.new(2.days, at: at)
|
||||
expect(@period.next.utc.to_s).to be_eql(Time.now.change(at).utc.to_s)
|
||||
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,11 +1,17 @@
|
||||
require 'spec_helper'
|
||||
|
||||
class TestJob
|
||||
def perform
|
||||
puts 'Test!'
|
||||
end
|
||||
end
|
||||
|
||||
describe Crono::Scheduler do
|
||||
let(:scheduler) { Crono::Scheduler.new }
|
||||
|
||||
describe '#add_job' do
|
||||
it 'should call Job#load on Job' do
|
||||
@job = Crono::Job.new(TestJob, Crono::Period.new(10.day, at: '04:05'))
|
||||
@job = Crono::Job.new(TestJob, Crono::Period.new(10.day, at: '04:05'), [])
|
||||
expect(@job).to receive(:load)
|
||||
scheduler.add_job(@job)
|
||||
end
|
||||
@@ -17,22 +23,40 @@ describe Crono::Scheduler do
|
||||
Crono::Period.new(3.days, at: 10.minutes.from_now.strftime('%H:%M')),
|
||||
Crono::Period.new(1.day, at: 20.minutes.from_now.strftime('%H:%M')),
|
||||
Crono::Period.new(7.days, at: 40.minutes.from_now.strftime('%H:%M'))
|
||||
].map { |period| Crono::Job.new(TestJob, period) }
|
||||
].map { |period| Crono::Job.new(TestJob, period, []) }
|
||||
|
||||
time, jobs = scheduler.next_jobs
|
||||
expect(jobs).to be_eql [jobs[0]]
|
||||
end
|
||||
|
||||
it 'should return an array of jobs scheduled at same time' do
|
||||
it 'should return an array of jobs scheduled at same time with `at`' do
|
||||
time = 5.minutes.from_now
|
||||
scheduler.jobs = jobs = [
|
||||
Crono::Period.new(1.day, at: time.strftime('%H:%M')),
|
||||
Crono::Period.new(1.day, at: time.strftime('%H:%M')),
|
||||
Crono::Period.new(1.day, at: 10.minutes.from_now.strftime('%H:%M'))
|
||||
].map { |period| Crono::Job.new(TestJob, period) }
|
||||
].map { |period| Crono::Job.new(TestJob, period, []) }
|
||||
|
||||
time, jobs = scheduler.next_jobs
|
||||
expect(jobs).to be_eql [jobs[0], jobs[1]]
|
||||
end
|
||||
|
||||
it 'should handle a few jobs scheduled at same time without `at`' do
|
||||
scheduler.jobs = jobs = [
|
||||
Crono::Period.new(10.seconds),
|
||||
Crono::Period.new(10.seconds),
|
||||
Crono::Period.new(1.day, at: 10.minutes.from_now.strftime('%H:%M'))
|
||||
].map { |period| Crono::Job.new(TestJob, period, []) }
|
||||
|
||||
_, next_jobs = scheduler.next_jobs
|
||||
expect(next_jobs).to be_eql [jobs[0]]
|
||||
|
||||
Timecop.travel(4.seconds.from_now)
|
||||
expect(Thread).to receive(:new)
|
||||
jobs[0].perform
|
||||
|
||||
_, next_jobs = scheduler.next_jobs
|
||||
expect(next_jobs).to be_eql [jobs[1]]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,28 +1,19 @@
|
||||
require 'bundler/setup'
|
||||
Bundler.setup
|
||||
require 'bundler'
|
||||
|
||||
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
||||
Bundler.require :default, :development
|
||||
|
||||
require 'timecop'
|
||||
require 'byebug'
|
||||
require 'crono'
|
||||
require 'generators/crono/install/templates/migrations/create_crono_jobs.rb'
|
||||
# If you're using all parts of Rails:
|
||||
Combustion.initialize! :all
|
||||
# Or, load just what you need:
|
||||
# Combustion.initialize! :active_record, :action_controller
|
||||
|
||||
ActiveRecord::Base.establish_connection(
|
||||
adapter: 'sqlite3',
|
||||
database: 'file::memory:?cache=shared'
|
||||
)
|
||||
require 'rspec/rails'
|
||||
# If you're using Capybara:
|
||||
# require 'capybara/rails'
|
||||
|
||||
ActiveRecord::Base.logger = Logger.new(STDOUT)
|
||||
CreateCronoJobs.up
|
||||
|
||||
class TestJob
|
||||
def perform
|
||||
end
|
||||
end
|
||||
|
||||
class TestFailingJob
|
||||
def perform
|
||||
fail 'Some error'
|
||||
RSpec.configure do |config|
|
||||
config.use_transactional_fixtures = true
|
||||
config.mock_with :rspec do |mocks|
|
||||
mocks.allow_message_expectations_on_nil = true
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,8 +2,8 @@ require 'spec_helper'
|
||||
require 'rack/test'
|
||||
include Rack::Test::Methods
|
||||
|
||||
describe Crono::Web do
|
||||
let(:app) { Crono::Web }
|
||||
describe Crono::Engine do
|
||||
let(:app) { Crono::Engine }
|
||||
|
||||
before do
|
||||
Crono::CronoJob.destroy_all
|
||||
@@ -25,13 +25,13 @@ describe Crono::Web do
|
||||
end
|
||||
|
||||
it 'should show a error mark when a job is unhealthy' do
|
||||
@test_job.update(healthy: false)
|
||||
@test_job.update(healthy: false, last_performed_at: 10.minutes.ago)
|
||||
get '/'
|
||||
expect(last_response.body).to include 'Error'
|
||||
end
|
||||
|
||||
it 'should show a success mark when a job is healthy' do
|
||||
@test_job.update(healthy: true)
|
||||
@test_job.update(healthy: true, last_performed_at: 10.minutes.ago)
|
||||
get '/'
|
||||
expect(last_response.body).to include 'Success'
|
||||
end
|
||||
@@ -45,7 +45,7 @@ describe Crono::Web do
|
||||
|
||||
describe '/job/:id' do
|
||||
it 'should show job log' do
|
||||
get "/job/#{@test_job.id}"
|
||||
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
|
||||
@@ -54,7 +54,7 @@ describe Crono::Web do
|
||||
it 'should show a message about the unhealthy job' do
|
||||
message = 'An error occurs during the last execution of this job'
|
||||
@test_job.update(healthy: false)
|
||||
get "/job/#{@test_job.id}"
|
||||
get "/jobs/#{@test_job.id}"
|
||||
expect(last_response.body).to include message
|
||||
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,28 +0,0 @@
|
||||
%ol.breadcrumb
|
||||
%li.active Home
|
||||
|
||||
%h3 Running Jobs
|
||||
%table.table#job_list
|
||||
%tr
|
||||
%th Job
|
||||
%th Last performed at
|
||||
%th Status
|
||||
%th
|
||||
- @jobs.each do |job|
|
||||
%tr
|
||||
%td= job.job_id
|
||||
%td= job.last_performed_at || '-'
|
||||
%td
|
||||
- if job.healthy == false
|
||||
%a{ href: url("/job/#{job.id}") }
|
||||
%span.label.label-danger Error
|
||||
- if job.healthy == true
|
||||
%a{ href: url("/job/#{job.id}") }
|
||||
%span.label.label-success Success
|
||||
- else
|
||||
%a{ href: url("/job/#{job.id}") }
|
||||
%span.label.label-default Pending
|
||||
%td
|
||||
%a{ href: url("/job/#{job.id}") }
|
||||
Log
|
||||
%span.glyphicon.glyphicon-menu-right
|
||||
@@ -1,14 +0,0 @@
|
||||
%ol.breadcrumb
|
||||
%li
|
||||
%a{ href: url('/') } Home
|
||||
%li.active= @job.job_id
|
||||
|
||||
%h2
|
||||
"#{@job.job_id}" Log:
|
||||
|
||||
- if @job.healthy == false
|
||||
.alert.alert-danger{ role: 'alert' }
|
||||
An error occurs during the last execution of this job.
|
||||
Check the log below for details.
|
||||
|
||||
%pre= @job.log
|
||||
@@ -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