mirror of
https://github.com/plashchynski/crono.git
synced 2026-01-12 05:20:26 +01:00
Compare commits
45 Commits
materializ
...
v1.0.0.pre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@
|
||||
/pkg/
|
||||
/spec/reports/
|
||||
/tmp/
|
||||
log/*.log
|
||||
|
||||
@@ -4,8 +4,8 @@ os:
|
||||
- osx
|
||||
rvm:
|
||||
- 2.0.0
|
||||
- 2.1
|
||||
- 2.2
|
||||
- 2.1.7
|
||||
- 2.2.3
|
||||
notifications:
|
||||
webhooks:
|
||||
urls:
|
||||
|
||||
17
Changes.md
17
Changes.md
@@ -1,3 +1,20 @@
|
||||
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
|
||||
-----------
|
||||
|
||||
|
||||
73
Gemfile.lock
73
Gemfile.lock
@@ -1,70 +1,63 @@
|
||||
PATH
|
||||
remote: .
|
||||
specs:
|
||||
crono (0.8.7.pre)
|
||||
activejob (~> 4.0)
|
||||
crono (1.0.0.pre2)
|
||||
activerecord (~> 4.0)
|
||||
activesupport (~> 4.0)
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
activejob (4.2.1)
|
||||
activesupport (= 4.2.1)
|
||||
globalid (>= 0.3.0)
|
||||
activemodel (4.2.1)
|
||||
activesupport (= 4.2.1)
|
||||
activemodel (4.2.5)
|
||||
activesupport (= 4.2.5)
|
||||
builder (~> 3.1)
|
||||
activerecord (4.2.1)
|
||||
activemodel (= 4.2.1)
|
||||
activesupport (= 4.2.1)
|
||||
activerecord (4.2.5)
|
||||
activemodel (= 4.2.5)
|
||||
activesupport (= 4.2.5)
|
||||
arel (~> 6.0)
|
||||
activesupport (4.2.1)
|
||||
activesupport (4.2.5)
|
||||
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)
|
||||
arel (6.0.3)
|
||||
builder (3.2.2)
|
||||
byebug (4.0.5)
|
||||
columnize (= 0.9.0)
|
||||
columnize (0.9.0)
|
||||
byebug (8.2.1)
|
||||
daemons (1.2.3)
|
||||
diff-lcs (1.2.5)
|
||||
globalid (0.3.5)
|
||||
activesupport (>= 4.1.0)
|
||||
haml (4.0.6)
|
||||
haml (4.0.7)
|
||||
tilt
|
||||
i18n (0.7.0)
|
||||
json (1.8.2)
|
||||
minitest (5.5.1)
|
||||
rack (1.6.0)
|
||||
json (1.8.3)
|
||||
minitest (5.8.3)
|
||||
rack (1.6.4)
|
||||
rack-protection (1.5.3)
|
||||
rack
|
||||
rack-test (0.6.3)
|
||||
rack (>= 1.0)
|
||||
rake (10.4.2)
|
||||
rspec (3.2.0)
|
||||
rspec-core (~> 3.2.0)
|
||||
rspec-expectations (~> 3.2.0)
|
||||
rspec-mocks (~> 3.2.0)
|
||||
rspec-core (3.2.2)
|
||||
rspec-support (~> 3.2.0)
|
||||
rspec-expectations (3.2.0)
|
||||
rake (10.5.0)
|
||||
rspec (3.4.0)
|
||||
rspec-core (~> 3.4.0)
|
||||
rspec-expectations (~> 3.4.0)
|
||||
rspec-mocks (~> 3.4.0)
|
||||
rspec-core (3.4.1)
|
||||
rspec-support (~> 3.4.0)
|
||||
rspec-expectations (3.4.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.2.0)
|
||||
rspec-mocks (3.2.1)
|
||||
rspec-support (~> 3.4.0)
|
||||
rspec-mocks (3.4.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.2.0)
|
||||
rspec-support (3.2.2)
|
||||
sinatra (1.4.5)
|
||||
rspec-support (~> 3.4.0)
|
||||
rspec-support (3.4.1)
|
||||
sinatra (1.4.6)
|
||||
rack (~> 1.4)
|
||||
rack-protection (~> 1.4)
|
||||
tilt (~> 1.3, >= 1.3.4)
|
||||
sqlite3 (1.3.10)
|
||||
tilt (>= 1.3, < 3)
|
||||
sqlite3 (1.3.11)
|
||||
thread_safe (0.3.5)
|
||||
tilt (1.4.1)
|
||||
timecop (0.7.3)
|
||||
tilt (2.0.2)
|
||||
timecop (0.8.0)
|
||||
tzinfo (1.2.2)
|
||||
thread_safe (~> 0.1)
|
||||
|
||||
@@ -75,6 +68,7 @@ DEPENDENCIES
|
||||
bundler (>= 1.0.0)
|
||||
byebug
|
||||
crono!
|
||||
daemons
|
||||
haml
|
||||
rack-test
|
||||
rake (~> 10.0)
|
||||
@@ -82,3 +76,6 @@ DEPENDENCIES
|
||||
sinatra
|
||||
sqlite3
|
||||
timecop (~> 0.7)
|
||||
|
||||
BUNDLED WITH
|
||||
1.11.2
|
||||
|
||||
20
README.md
20
README.md
@@ -61,11 +61,11 @@ class TestJob < ActiveJob::Base
|
||||
end
|
||||
```
|
||||
|
||||
The ActiveJob jobs is convenient because you can use one job in both periodic and enqueued ways. But Active Job is not required. Any class can be used as a crono job if it implements a method `perform` without arguments:
|
||||
The ActiveJob jobs 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`:
|
||||
|
||||
```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
|
||||
@@ -124,6 +124,13 @@ The `at` can be a Hash:
|
||||
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}
|
||||
```
|
||||
|
||||
#### Run daemon
|
||||
|
||||
To run Crono daemon, in your Rails project root directory:
|
||||
@@ -132,12 +139,13 @@ To run Crono daemon, in your Rails project root directory:
|
||||
|
||||
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)
|
||||
-e, --environment ENV Application environment (Default: development)
|
||||
--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)
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ Gem::Specification.new do |spec|
|
||||
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
||||
spec.require_paths = ['lib']
|
||||
|
||||
spec.add_runtime_dependency 'activejob', '~> 4.0'
|
||||
spec.add_runtime_dependency 'activesupport', '~> 4.0'
|
||||
spec.add_runtime_dependency 'activerecord', '~> 4.0'
|
||||
spec.add_development_dependency 'rake', '~> 10.0'
|
||||
@@ -31,4 +30,5 @@ Gem::Specification.new do |spec|
|
||||
spec.add_development_dependency 'sinatra'
|
||||
spec.add_development_dependency 'haml'
|
||||
spec.add_development_dependency 'rack-test'
|
||||
spec.add_development_dependency 'daemons'
|
||||
end
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 90 KiB |
@@ -6,10 +6,13 @@ require 'active_support/all'
|
||||
require 'crono/version'
|
||||
require 'crono/logging'
|
||||
require 'crono/period'
|
||||
require 'crono/time_of_day'
|
||||
require 'crono/interval'
|
||||
require 'crono/job'
|
||||
require 'crono/scheduler'
|
||||
require 'crono/config'
|
||||
require 'crono/performer_proxy'
|
||||
require 'crono/cronotab'
|
||||
require 'crono/orm/active_record/crono_job'
|
||||
require 'crono/railtie' if defined?(Rails)
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ module Crono
|
||||
include Singleton
|
||||
include Logging
|
||||
|
||||
COMMANDS = %w(start stop restart run zap reload status)
|
||||
|
||||
attr_accessor :config
|
||||
|
||||
def initialize
|
||||
@@ -16,15 +18,21 @@ module Crono
|
||||
|
||||
def run
|
||||
parse_options(ARGV)
|
||||
parse_command(ARGV)
|
||||
|
||||
setup_log
|
||||
setup_log
|
||||
|
||||
write_pid
|
||||
write_pid unless config.daemonize
|
||||
load_rails
|
||||
Cronotab.process(File.expand_path(config.cronotab))
|
||||
print_banner
|
||||
|
||||
check_jobs
|
||||
start_working_loop
|
||||
if config.daemonize
|
||||
start_working_loop_in_daemon
|
||||
else
|
||||
start_working_loop
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
@@ -32,13 +40,15 @@ module Crono
|
||||
def setup_log
|
||||
if config.daemonize
|
||||
self.logfile = config.logfile
|
||||
daemonize
|
||||
elsif config.deprecated_daemonize
|
||||
self.logfile = config.logfile
|
||||
deprecated_daemonize
|
||||
else
|
||||
self.logfile = STDOUT
|
||||
end
|
||||
end
|
||||
|
||||
def daemonize
|
||||
def deprecated_daemonize
|
||||
::Process.daemon(true, true)
|
||||
|
||||
[$stdout, $stderr].each do |io|
|
||||
@@ -71,7 +81,6 @@ 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
|
||||
@@ -79,6 +88,30 @@ module Crono
|
||||
logger.error "You have no jobs in you cronotab file #{config.cronotab}"
|
||||
end
|
||||
|
||||
def start_working_loop_in_daemon
|
||||
unless ENV['RAILS_ENV'] == 'test'
|
||||
begin
|
||||
require 'daemons'
|
||||
rescue LoadError
|
||||
raise "You need to add gem 'daemons' to your Gemfile if you wish to use it."
|
||||
end
|
||||
end
|
||||
Daemons.run_proc(config.process_name, dir: config.piddir, dir_mode: :normal, monitor: config.monitor, ARGV: @argv) do |*_argv|
|
||||
Dir.chdir(root)
|
||||
Crono.logger = Logger.new(config.logfile)
|
||||
|
||||
start_working_loop
|
||||
end
|
||||
end
|
||||
|
||||
def root
|
||||
@root ||= rails_root_defined? ? ::Rails.root : DIR_PWD
|
||||
end
|
||||
|
||||
def rails_root_defined?
|
||||
defined?(::Rails.root)
|
||||
end
|
||||
|
||||
def start_working_loop
|
||||
loop do
|
||||
next_time, jobs = Crono.scheduler.next_jobs
|
||||
@@ -88,8 +121,8 @@ module Crono
|
||||
end
|
||||
|
||||
def parse_options(argv)
|
||||
OptionParser.new do |opts|
|
||||
opts.banner = "Usage: crono [options]"
|
||||
@argv = OptionParser.new do |opts|
|
||||
opts.banner = "Usage: crono [options] start|stop|restart|run"
|
||||
|
||||
opts.on("-C", "--cronotab PATH", "Path to cronotab file (Default: #{config.cronotab})") do |cronotab|
|
||||
config.cronotab = cronotab
|
||||
@@ -99,12 +132,24 @@ module Crono
|
||||
config.logfile = logfile
|
||||
end
|
||||
|
||||
opts.on("-P", "--pidfile PATH", "Path to pidfile (Default: #{config.pidfile})") do |pidfile|
|
||||
opts.on("-P", "--pidfile PATH", "Deprecated! use --piddir with --process_name; Path to pidfile (Default: #{config.pidfile})") do |pidfile|
|
||||
config.pidfile = pidfile
|
||||
end
|
||||
|
||||
opts.on("-d", "--[no-]daemonize", "Daemonize process (Default: #{config.daemonize})") do |daemonize|
|
||||
config.daemonize = daemonize
|
||||
opts.on("--piddir PATH", "Path to piddir (Default: #{config.piddir})") do |piddir|
|
||||
config.piddir = piddir
|
||||
end
|
||||
|
||||
opts.on("-N", "--process_name NAME", "Name of the process (Default: #{config.process_name})") do |process_name|
|
||||
config.process_name = process_name
|
||||
end
|
||||
|
||||
opts.on("-d", "--[no-]daemonize", "Deprecated! Instead use crono [start|stop|restart] without this option; Daemonize process (Default: #{config.daemonize})") do |daemonize|
|
||||
config.deprecated_daemonize = daemonize
|
||||
end
|
||||
|
||||
opts.on("-m", "--monitor", "Start monitor process for a deamon (Default #{config.monitor})") do
|
||||
config.monitor = true
|
||||
end
|
||||
|
||||
opts.on '-e', '--environment ENV', "Application environment (Default: #{config.environment})" do |env|
|
||||
@@ -112,5 +157,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,31 @@ module Crono
|
||||
CRONOTAB = 'config/cronotab.rb'
|
||||
LOGFILE = 'log/crono.log'
|
||||
PIDFILE = 'tmp/pids/crono.pid'
|
||||
PIDDIR = 'tmp/pids'
|
||||
PROCESS_NAME = 'crono'
|
||||
|
||||
attr_accessor :cronotab, :logfile, :pidfile, :daemonize, :environment
|
||||
attr_accessor :cronotab, :logfile, :pidfile, :piddir, :process_name,
|
||||
:monitor, :daemonize, :deprecated_daemonize, :environment
|
||||
|
||||
def initialize
|
||||
self.cronotab = CRONOTAB
|
||||
self.logfile = LOGFILE
|
||||
self.piddir = PIDDIR
|
||||
self.process_name = PROCESS_NAME
|
||||
self.daemonize = false
|
||||
self.deprecated_daemonize = false
|
||||
self.monitor = false
|
||||
self.environment = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
|
||||
end
|
||||
|
||||
def pidfile=(pidfile)
|
||||
@pidfile = pidfile
|
||||
self.process_name = Pathname.new(pidfile).basename(".*").to_s
|
||||
self.piddir = Pathname.new(pidfile).dirname.to_s
|
||||
end
|
||||
|
||||
def pidfile
|
||||
@pidfile || (daemonize ? PIDFILE : nil)
|
||||
@pidfile || (deprecated_daemonize ? PIDFILE : nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
10
lib/crono/cronotab.rb
Normal file
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
|
||||
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,22 @@ 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,
|
||||
:next_performed_at, :job_log, :job_logger, :healthy, :execution_interval
|
||||
|
||||
def initialize(performer, period)
|
||||
def initialize(performer, period, job_args)
|
||||
self.execution_interval = 0.minutes
|
||||
self.performer, self.period = performer, period
|
||||
self.job_args = JSON.generate(job_args)
|
||||
self.job_log = StringIO.new
|
||||
self.job_logger = Logger.new(job_log)
|
||||
self.next_performed_at = period.next
|
||||
@semaphore = Mutex.new
|
||||
end
|
||||
|
||||
def next
|
||||
period.next(since: last_performed_at)
|
||||
return next_performed_at if next_performed_at.future?
|
||||
Time.now
|
||||
end
|
||||
|
||||
def description
|
||||
@@ -29,8 +33,11 @@ module Crono
|
||||
end
|
||||
|
||||
def perform
|
||||
return Thread.new {} if perform_before_interval?
|
||||
|
||||
log "Perform #{performer}"
|
||||
self.last_performed_at = Time.now
|
||||
self.next_performed_at = period.next(since: last_performed_at)
|
||||
|
||||
Thread.new { perform_job }
|
||||
end
|
||||
@@ -39,11 +46,13 @@ module Crono
|
||||
@semaphore.synchronize do
|
||||
update_model
|
||||
clear_job_log
|
||||
ActiveRecord::Base.clear_active_connections!
|
||||
end
|
||||
end
|
||||
|
||||
def load
|
||||
self.last_performed_at = model.last_performed_at
|
||||
self.next_performed_at = period.next(since: last_performed_at)
|
||||
end
|
||||
|
||||
private
|
||||
@@ -56,28 +65,29 @@ module Crono
|
||||
saved_log = model.reload.log || ''
|
||||
log_to_save = saved_log + job_log.string
|
||||
model.update(last_performed_at: last_performed_at, log: log_to_save,
|
||||
healthy: healthy)
|
||||
healthy: healthy, args: job_args)
|
||||
end
|
||||
|
||||
def perform_job
|
||||
performer.new.perform
|
||||
finished_time_sec = format('%.2f', Time.now - last_performed_at)
|
||||
performer.new.perform *JSON.parse(job_args)
|
||||
rescue StandardError => e
|
||||
handle_job_fail(e, finished_time_sec)
|
||||
handle_job_fail(e)
|
||||
else
|
||||
handle_job_success(finished_time_sec)
|
||||
handle_job_success
|
||||
ensure
|
||||
save
|
||||
end
|
||||
|
||||
def handle_job_fail(exception, finished_time_sec)
|
||||
def handle_job_fail(exception)
|
||||
finished_time_sec = format('%.2f', Time.now - last_performed_at)
|
||||
self.healthy = false
|
||||
log_error "Finished #{performer} in #{finished_time_sec} seconds"\
|
||||
"with error: #{exception.message}"
|
||||
" with error: #{exception.message}"
|
||||
log_error exception.backtrace.join("\n")
|
||||
end
|
||||
|
||||
def handle_job_success(finished_time_sec)
|
||||
def handle_job_success
|
||||
finished_time_sec = format('%.2f', Time.now - last_performed_at)
|
||||
self.healthy = true
|
||||
log "Finished #{performer} in #{finished_time_sec} seconds"
|
||||
end
|
||||
@@ -96,5 +106,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,25 @@
|
||||
module Crono
|
||||
# Crono::PerformerProxy is a proxy used in cronotab.rb semantic
|
||||
class PerformerProxy
|
||||
def initialize(performer, scheduler)
|
||||
def initialize(performer, scheduler, job_args)
|
||||
@performer = performer
|
||||
@scheduler = scheduler
|
||||
@job_args = job_args
|
||||
end
|
||||
|
||||
def every(period, *args)
|
||||
job = Job.new(@performer, Period.new(period, *args))
|
||||
@scheduler.add_job(job)
|
||||
@job = Job.new(@performer, Period.new(period, *args), @job_args)
|
||||
@scheduler.add_job(@job)
|
||||
self
|
||||
end
|
||||
|
||||
def once_per(execution_interval)
|
||||
@job.execution_interval = execution_interval if @job
|
||||
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,15 +4,25 @@ module Crono
|
||||
DAYS = [:monday, :tuesday, :wednesday, :thursday, :friday, :saturday,
|
||||
:sunday]
|
||||
|
||||
def initialize(period, at: nil, on: nil)
|
||||
def initialize(period, at: nil, on: nil, within: nil)
|
||||
@period = period
|
||||
@at_hour, @at_min = parse_at(at) if at
|
||||
@interval = Interval.parse(within) if within
|
||||
@on = parse_on(on) if on
|
||||
end
|
||||
|
||||
def next(since: nil)
|
||||
return initial_next unless since
|
||||
@next = @period.since(since)
|
||||
if @interval
|
||||
if since
|
||||
@next = @interval.next_within(since, @period)
|
||||
else
|
||||
return initial_next if @interval.within?(initial_next)
|
||||
@next = @interval.next_within(initial_next, @period)
|
||||
end
|
||||
else
|
||||
return initial_next unless since
|
||||
@next = @period.since(since)
|
||||
end
|
||||
@next = @next.beginning_of_week.advance(days: @on) if @on
|
||||
@next = @next.change(time_atts)
|
||||
return @next if @next.future?
|
||||
@@ -21,6 +31,7 @@ module Crono
|
||||
|
||||
def description
|
||||
desc = "every #{@period.inspect}"
|
||||
desc += " between #{@interval.from} and #{@interval.to} UTC" if @interval
|
||||
desc += format(' at %.2i:%.2i', @at_hour, @at_min) if @at_hour && @at_min
|
||||
desc += " on #{DAYS[@on].capitalize}" if @on
|
||||
desc
|
||||
@@ -49,7 +60,10 @@ module Crono
|
||||
end
|
||||
|
||||
def parse_at(at)
|
||||
fail "period should be at least 1 day to use 'at'" if @period < 1.day
|
||||
if @period < 1.day && (at.is_a? String || at[:hour])
|
||||
fail "period should be at least 1 day to use 'at' with specified hour"
|
||||
end
|
||||
|
||||
case at
|
||||
when String
|
||||
time = Time.parse(at)
|
||||
|
||||
36
lib/crono/time_of_day.rb
Normal file
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.parse(value).utc
|
||||
when Hash then Time.now.change(value).utc
|
||||
when Time then value.utc
|
||||
else
|
||||
fail "Unknown TimeOfDay format: #{value.inspect}"
|
||||
end
|
||||
new time.hour, time.min
|
||||
end
|
||||
|
||||
def initialize(hour, min)
|
||||
@hour, @min = hour, min
|
||||
end
|
||||
|
||||
def to_i
|
||||
@hour * 60 + @min
|
||||
end
|
||||
|
||||
def to_s
|
||||
'%02d:%02d' % [@hour, @min]
|
||||
end
|
||||
|
||||
def <=>(other)
|
||||
to_i <=> other.to_i
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,3 +1,3 @@
|
||||
module Crono
|
||||
VERSION = '0.8.7.pre'
|
||||
VERSION = '1.0.0.pre2'
|
||||
end
|
||||
|
||||
@@ -5,6 +5,7 @@ class CreateCronoJobs < ActiveRecord::Migration
|
||||
t.text :log
|
||||
t.datetime :last_performed_at
|
||||
t.boolean :healthy
|
||||
t.text :args
|
||||
t.timestamps null: false
|
||||
end
|
||||
add_index :crono_jobs, [:job_id], unique: true
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,9 +9,25 @@ describe Crono::CLI do
|
||||
expect(cli).to receive(:load_rails)
|
||||
expect(cli).to receive(:start_working_loop)
|
||||
expect(cli).to receive(:parse_options)
|
||||
expect(cli).to receive(:parse_command)
|
||||
expect(cli).to receive(:write_pid)
|
||||
expect(Crono::Cronotab).to receive(:process)
|
||||
cli.run
|
||||
end
|
||||
context 'should run as daemon' do
|
||||
|
||||
before {cli.config.daemonize = true}
|
||||
|
||||
it 'should initialize rails with #load_rails and start working loop' do
|
||||
expect(cli).to receive(:load_rails)
|
||||
expect(cli).to receive(:start_working_loop_in_daemon)
|
||||
expect(cli).to receive(:parse_options)
|
||||
expect(cli).to receive(:parse_command)
|
||||
expect(cli).not_to receive(:write_pid)
|
||||
expect(Crono::Cronotab).to receive(:process)
|
||||
cli.run
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#parse_options' do
|
||||
@@ -30,9 +46,24 @@ describe Crono::CLI do
|
||||
expect(cli.config.pidfile).to be_eql 'tmp/pids/crono.0.log'
|
||||
end
|
||||
|
||||
it 'should set daemonize' do
|
||||
it 'should set piddir' do
|
||||
cli.send(:parse_options, ['--piddir', 'tmp/pids'])
|
||||
expect(cli.config.piddir).to be_eql 'tmp/pids'
|
||||
end
|
||||
|
||||
it 'should set process_name' do
|
||||
cli.send(:parse_options, ['--process_name', 'crono0'])
|
||||
expect(cli.config.process_name).to be_eql 'crono0'
|
||||
end
|
||||
|
||||
it 'should set monitor' do
|
||||
cli.send(:parse_options, ['--monitor'])
|
||||
expect(cli.config.monitor).to be true
|
||||
end
|
||||
|
||||
it 'should set deprecated_daemonize' do
|
||||
cli.send(:parse_options, ['--daemonize'])
|
||||
expect(cli.config.daemonize).to be true
|
||||
expect(cli.config.deprecated_daemonize).to be true
|
||||
end
|
||||
|
||||
it 'should set environment' do
|
||||
@@ -40,4 +71,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,11 @@ describe Crono::Config do
|
||||
@config = Crono::Config.new
|
||||
expect(@config.cronotab).to be Crono::Config::CRONOTAB
|
||||
expect(@config.logfile).to be Crono::Config::LOGFILE
|
||||
expect(@config.pidfile).to be nil
|
||||
expect(@config.piddir).to be Crono::Config::PIDDIR
|
||||
expect(@config.process_name).to be Crono::Config::PROCESS_NAME
|
||||
expect(@config.daemonize).to be false
|
||||
expect(@config.deprecated_daemonize).to be false
|
||||
expect(@config.monitor).to be false
|
||||
expect(@config.environment).to be_eql ENV['RAILS_ENV']
|
||||
end
|
||||
|
||||
@@ -23,8 +26,8 @@ describe Crono::Config do
|
||||
specify { expect(pidfile).to be_nil }
|
||||
end
|
||||
|
||||
context "daemonize is true" do
|
||||
before { config.daemonize = true }
|
||||
context "deprecated_daemonize is true" do
|
||||
before { config.deprecated_daemonize = true }
|
||||
|
||||
specify { expect(pidfile).to eq Crono::Config::PIDFILE }
|
||||
end
|
||||
@@ -36,7 +39,16 @@ describe Crono::Config do
|
||||
before { config.pidfile = path }
|
||||
|
||||
specify { expect(pidfile).to eq path }
|
||||
|
||||
it "trys to set piddir" do
|
||||
expect(config.piddir).to eq "foo/bar"
|
||||
end
|
||||
|
||||
it "trys to set process_name" do
|
||||
expect(config.process_name).to eq "pid"
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
20
spec/cronotab_spec.rb
Normal file
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
|
||||
@@ -2,14 +2,20 @@ require 'spec_helper'
|
||||
|
||||
describe Crono::Job do
|
||||
let(:period) { Crono::Period.new(2.day, at: '15:00') }
|
||||
let(:job) { Crono::Job.new(TestJob, period) }
|
||||
let(:failing_job) { Crono::Job.new(TestFailingJob, period) }
|
||||
let(:job_args) {[{some: 'data'}]}
|
||||
let(:job) { Crono::Job.new(TestJob, period, []) }
|
||||
let(:job_with_args) { Crono::Job.new(TestJob, period, job_args) }
|
||||
let(:failing_job) { Crono::Job.new(TestFailingJob, period, []) }
|
||||
|
||||
it 'should contain performer and period' do
|
||||
expect(job.performer).to be TestJob
|
||||
expect(job.period).to be period
|
||||
end
|
||||
|
||||
it 'should contain data as JSON String' do
|
||||
expect(job_with_args.job_args).to eq '[{"some":"data"}]'
|
||||
end
|
||||
|
||||
describe '#next' do
|
||||
it 'should return next performing time according to period' do
|
||||
expect(job.next).to be_eql period.next
|
||||
@@ -36,6 +42,46 @@ describe Crono::Job do
|
||||
failing_job.perform.join
|
||||
expect(failing_job.healthy).to be false
|
||||
end
|
||||
|
||||
it 'should execute one' do
|
||||
job.execution_interval = 5.minutes
|
||||
|
||||
expect(job).to receive(:perform_job).once
|
||||
job.perform.join
|
||||
thread = job.perform.join
|
||||
expect(thread).to be_stop
|
||||
end
|
||||
|
||||
it 'should execute twice' do
|
||||
job.execution_interval = 0.minutes
|
||||
|
||||
test_preform_job_twice
|
||||
end
|
||||
|
||||
it 'should execute twice without initialize execution_interval' do
|
||||
test_preform_job_twice
|
||||
end
|
||||
|
||||
it 'should call perform of performer' do
|
||||
expect(TestJob).to receive(:new).with(no_args)
|
||||
thread = job.perform.join
|
||||
expect(thread).to be_stop
|
||||
end
|
||||
|
||||
it 'should call perform of performer with data' do
|
||||
test_job = double()
|
||||
expect(TestJob).to receive(:new).and_return(test_job)
|
||||
expect(test_job).to receive(:perform).with({'some' => 'data'})
|
||||
thread = job_with_args.perform.join
|
||||
expect(thread).to be_stop
|
||||
end
|
||||
|
||||
def test_preform_job_twice
|
||||
expect(job).to receive(:perform_job).twice
|
||||
job.perform.join
|
||||
thread = job.perform.join
|
||||
expect(thread).to be_stop
|
||||
end
|
||||
end
|
||||
|
||||
describe '#description' do
|
||||
@@ -54,10 +100,12 @@ describe Crono::Job do
|
||||
it 'should update saved job' do
|
||||
job.last_performed_at = Time.now
|
||||
job.healthy = true
|
||||
job.job_args = JSON.generate([{some: 'data'}])
|
||||
job.save
|
||||
@crono_job = Crono::CronoJob.find_by(job_id: job.job_id)
|
||||
expect(@crono_job.last_performed_at.utc.to_s).to be_eql job.last_performed_at.utc.to_s
|
||||
expect(@crono_job.healthy).to be true
|
||||
expect(@crono_job.args).to eq '[{"some":"data"}]'
|
||||
end
|
||||
|
||||
it 'should save and truncate job log' do
|
||||
@@ -76,7 +124,7 @@ describe Crono::Job do
|
||||
end
|
||||
|
||||
it 'should load last_performed_at from DB' do
|
||||
@job = Crono::Job.new(TestJob, period)
|
||||
@job = Crono::Job.new(TestJob, period, job_args)
|
||||
@job.load
|
||||
expect(@job.last_performed_at.utc.to_s).to be_eql @saved_last_performed_at.utc.to_s
|
||||
end
|
||||
|
||||
@@ -5,4 +5,23 @@ describe Crono::PerformerProxy do
|
||||
expect(Crono.scheduler).to receive(:add_job).with(kind_of(Crono::Job))
|
||||
Crono.perform(TestJob).every(2.days, at: '15:30')
|
||||
end
|
||||
|
||||
it 'should set execution interval' do
|
||||
allow(Crono).to receive(:scheduler).and_return(Crono::Scheduler.new)
|
||||
expect_any_instance_of(Crono::Job).to receive(:execution_interval=).with(0.minutes).once
|
||||
expect_any_instance_of(Crono::Job).to receive(:execution_interval=).with(10.minutes).once
|
||||
Crono.perform(TestJob).every(2.days, at: '15:30').once_per 10.minutes
|
||||
end
|
||||
|
||||
it 'do nothing when job not initalized' do
|
||||
expect_any_instance_of(Crono::Job).not_to receive(:execution_interval=)
|
||||
expect_any_instance_of(described_class).to receive(:once_per)
|
||||
Crono.perform(TestJob).once_per 10.minutes
|
||||
end
|
||||
|
||||
it 'should add job with args to schedule' do
|
||||
expect(Crono::Job).to receive(:new).with(TestJob, kind_of(Crono::Period), [:some, {some: 'data'}])
|
||||
allow(Crono.scheduler).to receive(:add_job)
|
||||
Crono.perform(TestJob, :some, {some: 'data'}).every(2.days, at: '15:30')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
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')
|
||||
@@ -55,12 +49,12 @@ describe Crono::Period do
|
||||
context 'in daily basis' do
|
||||
it "should return Time.now if the next time in past" do
|
||||
@period = Crono::Period.new(1.day, at: '06:00')
|
||||
expect(@period.next(since: 2.days.ago)).to be_eql(Time.now)
|
||||
expect(@period.next(since: 2.days.ago).to_s).to be_eql(Time.now.to_s)
|
||||
end
|
||||
|
||||
it 'should return the time 2 days from now' do
|
||||
it 'should return time 2 days from now' do
|
||||
@period = Crono::Period.new(2.day)
|
||||
expect(@period.next).to be_eql(2.days.from_now)
|
||||
expect(@period.next.to_s).to be_eql(2.days.from_now.to_s)
|
||||
end
|
||||
|
||||
it "should set time to 'at' time as a string" do
|
||||
@@ -86,12 +80,12 @@ describe Crono::Period do
|
||||
it 'should raise error when period is less than 1 day' do
|
||||
expect {
|
||||
Crono::Period.new(5.hours, at: '15:30')
|
||||
}.to raise_error("period should be at least 1 day to use 'at'")
|
||||
}.to raise_error("period should be at least 1 day to use 'at' with specified hour")
|
||||
end
|
||||
|
||||
it 'should return time in relation to last time' do
|
||||
@period = Crono::Period.new(2.days)
|
||||
expect(@period.next(since: 1.day.ago)).to be_eql(1.day.from_now)
|
||||
expect(@period.next(since: 1.day.ago).to_s).to be_eql(1.day.from_now.to_s)
|
||||
end
|
||||
|
||||
it 'should return today time if it is first run and not too late' do
|
||||
@@ -101,5 +95,32 @@ describe Crono::Period do
|
||||
expect(@period.next.utc.to_s).to be_eql(Time.now.change(at).utc.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
context 'in hourly basis' do
|
||||
it 'should return next hour minutes if current hour minutes passed' do
|
||||
Timecop.freeze(Time.now.beginning_of_hour.advance(minutes: 20)) do
|
||||
@period = Crono::Period.new(1.hour, at: { min: 15 })
|
||||
expect(@period.next.utc.to_s).to be_eql 1.hour.from_now.beginning_of_hour.advance(minutes: 15).utc.to_s
|
||||
end
|
||||
end
|
||||
|
||||
it 'should return current hour minutes if current hour minutes not passed yet' do
|
||||
Timecop.freeze(Time.now.beginning_of_hour.advance(minutes: 10)) do
|
||||
@period = Crono::Period.new(1.hour, at: { min: 15 })
|
||||
expect(@period.next.utc.to_s).to be_eql Time.now.beginning_of_hour.advance(minutes: 15).utc.to_s
|
||||
end
|
||||
end
|
||||
|
||||
it 'should return next hour minutes within the given interval' do
|
||||
Timecop.freeze(Time.now.change(hour: 16, min: 10)) do
|
||||
@period = Crono::Period.new(1.hour, at: { min: 15 }, within: '08:00-16:00')
|
||||
expect(@period.next.utc.to_s).to be_eql Time.now.tomorrow.change(hour: 8, min: 15).utc.to_s
|
||||
end
|
||||
Timecop.freeze(Time.now.change(hour: 16, min: 10)) do
|
||||
@period = Crono::Period.new(1.hour, at: { min: 15 }, within: '23:00-07:00')
|
||||
expect(@period.next.utc.to_s).to be_eql Time.now.change(hour: 23, min: 15).utc.to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,7 +5,7 @@ describe Crono::Scheduler do
|
||||
|
||||
describe '#add_job' do
|
||||
it 'should call Job#load on Job' do
|
||||
@job = Crono::Job.new(TestJob, Crono::Period.new(10.day, at: '04:05'))
|
||||
@job = Crono::Job.new(TestJob, Crono::Period.new(10.day, at: '04:05'), [])
|
||||
expect(@job).to receive(:load)
|
||||
scheduler.add_job(@job)
|
||||
end
|
||||
@@ -17,22 +17,40 @@ describe Crono::Scheduler do
|
||||
Crono::Period.new(3.days, at: 10.minutes.from_now.strftime('%H:%M')),
|
||||
Crono::Period.new(1.day, at: 20.minutes.from_now.strftime('%H:%M')),
|
||||
Crono::Period.new(7.days, at: 40.minutes.from_now.strftime('%H:%M'))
|
||||
].map { |period| Crono::Job.new(TestJob, period) }
|
||||
].map { |period| Crono::Job.new(TestJob, period, []) }
|
||||
|
||||
time, jobs = scheduler.next_jobs
|
||||
expect(jobs).to be_eql [jobs[0]]
|
||||
end
|
||||
|
||||
it 'should return an array of jobs scheduled at same time' do
|
||||
it 'should return an array of jobs scheduled at same time with `at`' do
|
||||
time = 5.minutes.from_now
|
||||
scheduler.jobs = jobs = [
|
||||
Crono::Period.new(1.day, at: time.strftime('%H:%M')),
|
||||
Crono::Period.new(1.day, at: time.strftime('%H:%M')),
|
||||
Crono::Period.new(1.day, at: 10.minutes.from_now.strftime('%H:%M'))
|
||||
].map { |period| Crono::Job.new(TestJob, period) }
|
||||
].map { |period| Crono::Job.new(TestJob, period, []) }
|
||||
|
||||
time, jobs = scheduler.next_jobs
|
||||
expect(jobs).to be_eql [jobs[0], jobs[1]]
|
||||
end
|
||||
|
||||
it 'should handle a few jobs scheduled at same time without `at`' do
|
||||
scheduler.jobs = jobs = [
|
||||
Crono::Period.new(10.seconds),
|
||||
Crono::Period.new(10.seconds),
|
||||
Crono::Period.new(1.day, at: 10.minutes.from_now.strftime('%H:%M'))
|
||||
].map { |period| Crono::Job.new(TestJob, period, []) }
|
||||
|
||||
_, next_jobs = scheduler.next_jobs
|
||||
expect(next_jobs).to be_eql [jobs[0]]
|
||||
|
||||
Timecop.travel(4.seconds.from_now)
|
||||
expect(Thread).to receive(:new)
|
||||
jobs[0].perform
|
||||
|
||||
_, next_jobs = scheduler.next_jobs
|
||||
expect(next_jobs).to be_eql [jobs[1]]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
body {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1 0 auto;
|
||||
-webkit-box-flex: 1;
|
||||
-webkit-flex: 1 0 auto;
|
||||
-ms-flex: 1 0 auto;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
|
||||
%main.container.blue-grey.lighten-4.grey-text.text-darken-4
|
||||
- if @job.healthy == false
|
||||
.alert.alert-danger{ role: 'alert' }
|
||||
An error occurs during the last execution of this job.
|
||||
Check the log below for details.
|
||||
.row.red-text
|
||||
.s12.center-align
|
||||
%i.mdi-alert-warning
|
||||
An error occurs during the last execution of this job.
|
||||
Check the log below for details.
|
||||
|
||||
%pre= @job.log
|
||||
|
||||
Reference in New Issue
Block a user