Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolve next scheduling ploblems #76

Merged
merged 10 commits into from
Jul 7, 2017
9 changes: 9 additions & 0 deletions app/assets/javascripts/kuroko2/suspend_schedule_handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
$(function(){
$("#new_job_suspend_schedule").
on("ajax:success", function(e, data, status, xhr){
$("#suspend-cron-error").hide();
}).
on("ajax:error", function(e, xhr, status, error) {
$("#suspend-cron-error").show().html("<p>Error: " + xhr.responseJSON.join(' ') + "</p>");
});
});
5 changes: 5 additions & 0 deletions app/assets/stylesheets/kuroko2/job_definitions.scss
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,8 @@
color: #888;
}
}

#suspend-cron-error {
display: none;
color: #843534;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def create
if suspend_schedule.valid?
render json: suspend_schedule, status: :created
else
render json: suspend_schedule, status: :bad_request
render json: suspend_schedule.errors.full_messages, status: :bad_request
end
end

Expand Down
48 changes: 42 additions & 6 deletions app/models/kuroko2/job_schedule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,16 @@ class Kuroko2::JobSchedule < Kuroko2::ApplicationRecord
(?:[0-6]|(?:(?:[0-6]\-[0-6]|\*)(?:\/[0-6])?))(?:,(?:[0-6]|(?:(?:[0-6]\-[0-6]|\*)(?:\/[0-6])?)))*
\z/x

CHRONO_SCHEDULE_METHODS = %i[minutes hours days months wdays]

validates :cron, format: { with: CRON_FORMAT }, uniqueness: { scope: :job_definition_id }
validate :validate_cron_schedule

def next(now = Time.current)
if 1.month.ago(now).future?
Kuroko2.logger.warn("Exceeds the time of criteria #{now}. (Up to 1 month since)")
return
end
return if suspended_all?

next_time = Chrono::Iterator.new(self.cron, now: now).next
suspend_times = suspend_times(now, next_time)
suspend_times = suspend_times(next_time, next_time)

if suspend_times.include?(next_time)
self.next(next_time)
Expand All @@ -51,7 +50,7 @@ def scheduled_times(time_from, time_to)
end

def suspend_times(time_from, time_to)
if job_definition && job_definition.job_suspend_schedules.present?
if has_suspend_schedules?
job_definition.job_suspend_schedules.
map { |schedule| schedule.suspend_times(time_from, time_to) }.flatten.uniq
else
Expand All @@ -61,6 +60,43 @@ def suspend_times(time_from, time_to)

private

def suspended_all?
return false unless has_suspend_schedules?

launch_schedule = Chrono::Schedule.new(cron)
schedule = CHRONO_SCHEDULE_METHODS.each_with_object({}) do |method, h|
h[method] = launch_schedule.public_send(method)
end

job_definition.job_suspend_schedules.each do |suspend_schedule_model|
suspend_schedule = Chrono::Schedule.new(suspend_schedule_model.cron)
CHRONO_SCHEDULE_METHODS.each do |method|
schedule[method] -= suspend_schedule.public_send(method)
end

# https://linux.die.net/man/5/crontab
# > Note: The day of a command's execution can be specified by two fields
# > day of month, and day of week. If both fields are restricted (ie, aren't *),
# > the command will be run when either field matches the current time.
# > For example, "30 4 1,15 * 5" would cause a command to be run at 4:30 am
# > on the 1st and 15th of each month, plus every Friday.
if suspend_schedule.wdays? && suspend_schedule.days?
case
when launch_schedule.wdays? && !launch_schedule.days?
schedule[:days] = []
when !launch_schedule.wdays? && launch_schedule.days?
schedule[:wdays] = []
end
end
end

schedule.values.all?(&:empty?)
end

def has_suspend_schedules?
job_definition && !job_definition.job_suspend_schedules.empty?
end

def validate_cron_schedule
if CRON_FORMAT === cron
self.next
Expand Down
32 changes: 30 additions & 2 deletions app/models/kuroko2/job_suspend_schedule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,37 @@ def suspend_times(time_from, time_to)

def validate_cron_schedule
if Kuroko2::JobSchedule::CRON_FORMAT === cron
Chrono::Iterator.new(self.cron).next
suspend_schedule = Chrono::Schedule.new(cron)
if job_definition.job_schedules.empty?
errors.add(:cron, "needs job schedules")
else
schedule = job_definition.job_schedules.each_with_object({}) do |launch_schedule_model, h|
launch_schedule = Chrono::Schedule.new(launch_schedule_model.cron)
Kuroko2::JobSchedule::CHRONO_SCHEDULE_METHODS.each do |method|
h[method] ||= []

suspend_schedule_list = suspend_schedule.public_send(method)
# https://linux.die.net/man/5/crontab
# > Note: The day of a command's execution can be specified by two fields
# > day of month, and day of week. If both fields are restricted (ie, aren't *),
# > the command will be run when either field matches the current time.
# > For example, "30 4 1,15 * 5" would cause a command to be run at 4:30 am
# > on the 1st and 15th of each month, plus every Friday.
if launch_schedule.wdays? && launch_schedule.days?
if (method == :wdays && suspend_schedule.wdays? && !suspend_schedule.days?) || (method == :days && !suspend_schedule.wdays? && suspend_schedule.days?)
suspend_schedule_list = []
end
end

h[method] |= launch_schedule.public_send(method) - suspend_schedule_list
end
end

if schedule.values.all?(&:empty?)
errors.add(:cron, "suspends all launched schedules")
end
end
end
nil
rescue Chrono::Fields::Base::InvalidField => e
errors.add(:cron, "has invalid field: #{e.message}")
end
Expand Down
7 changes: 5 additions & 2 deletions app/views/kuroko2/job_definitions/_list.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@
tr
td= definition.id
td.no-decorate= link_to definition.name, definition
- if !definition.suspended? && definition.job_schedules.present?
- if !definition.suspended? && !definition.job_schedules.empty?
- next_job_schedule = definition.job_schedules.map(&:next).min
td data-value=next_job_schedule.to_i = l(next_job_schedule, format: :short)
- if next_job_schedule
td data-value=next_job_schedule.to_i = l(next_job_schedule, format: :short)
- else
td data-value="9999999999" --
- else
td data-value="9999999999" --
td= labeled_status(definition.job_instances.take)
Expand Down
3 changes: 3 additions & 0 deletions app/views/kuroko2/job_suspend_schedules/index.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
= form_for([@definition, @suspend_schedule], remote: true) do |form|
.box-body
#suspend-cron-field.row.form-group
.col-md-12
div id="suspend-cron-error"
.col-md-8
= form.text_field :cron, class: 'form-control script-input', placeholder: '0 9 * * *'
= javascript_include_tag 'kuroko2/suspend_schedule_handler'
.col-md-4
= form.submit 'Add Suspend Schedule', class: 'btn btn-default btn-block'
2 changes: 1 addition & 1 deletion config/initializers/assets.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
# Precompile additional assets.
# application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
# Rails.application.config.assets.precompile += %w( search.js )
Rails.application.config.assets.precompile += %w( kuroko2/instance_linker.js timeline/* network/* )
Rails.application.config.assets.precompile += %w( kuroko2/instance_linker.js kuroko2/suspend_schedule_handler.js timeline/* network/* )
18 changes: 8 additions & 10 deletions spec/controllers/job_suspend_schedules_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,34 @@
routes { Kuroko2::Engine.routes }

before { sign_in }

let(:schedules) { create_list(:job_suspend_schedule, 1) }
let(:definition) { create(:job_definition, job_suspend_schedules: schedules) }
let(:schedule) { create(:job_schedule, cron: '0 10 * * *') }
let(:suspend_schedule) { create(:job_suspend_schedule, job_definition: schedule.job_definition, cron: '0 10 * * 1') }

describe '#index' do
it do
get :index, params: { job_definition_id: definition.id }
get :index, params: { job_definition_id: suspend_schedule.job_definition.id }

expect(response).to have_http_status(:ok)
expect(assigns(:suspend_schedule)).to be_new_record
expect(assigns(:suspend_schedules)).to eq schedules
expect(assigns(:suspend_schedules)).to eq [suspend_schedule]
end
end

describe '#create' do
it do
post :create, params: { job_definition_id: definition.id, job_suspend_schedule: { cron: '* * * * *' } }

post :create, params: { job_definition_id: suspend_schedule.job_definition.id, job_suspend_schedule: { cron: '0 10 * * 0' } }
expect(response).to have_http_status(:created)
end
end

describe '#destroy' do

it do
delete :destroy, params: { job_definition_id: definition.id, id: schedules.first.id }
definition.reload
delete :destroy, params: { job_definition_id: suspend_schedule.job_definition.id, id: suspend_schedule.id }
schedule.job_definition.reload

expect(response).to have_http_status(:ok)
expect(definition.job_schedules.size).to be 0
expect(schedule.job_definition.job_suspend_schedules.size).to be 0
end
end
end
2 changes: 2 additions & 0 deletions spec/factories/job_schedule_factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@

"#{n % 60} #{hour} #{day} * *"
end

job_definition
end
end
5 changes: 1 addition & 4 deletions spec/factories/job_suspend_schedule_factory.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
FactoryGirl.define do
factory :job_suspend_schedule, class: Kuroko2::JobSuspendSchedule do
sequence(:cron) do |n|
hour = n / 60 >= 1 ? (n / 60) : '*'
"#{n % 60} #{hour} * * *"
end
job_definition
end
end
16 changes: 15 additions & 1 deletion spec/features/job_definition_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,21 @@

fill_in 'job_suspend_schedule_cron', with: '* * * * *'
click_on 'Add Suspend Schedule'
expect(page).to have_selector('#suspend-schedules table tbody tr .log', text: '* * * * *', count: 1)
expect(page).to have_content('Cron needs job schedules')
expect(page).to have_selector('#suspend-schedules table tbody tr .log', text: '* * * * *', count: 0)

fill_in 'job_schedule_cron', with: '* * * * *'
click_on 'Add Schedule'
expect(page).to have_selector('#schedules table tbody tr .log', text: '* * * * *', count: 1)

fill_in 'job_suspend_schedule_cron', with: '* * * * *'
click_on 'Add Suspend Schedule'
expect(page).to have_content('Cron suspends all launched schedules')
expect(page).to have_selector('#suspend-schedules table tbody tr .log', text: '* * * * *', count: 0)

fill_in 'job_suspend_schedule_cron', with: '* 10 * * *'
click_on 'Add Suspend Schedule'
expect(page).to have_selector('#suspend-schedules table tbody tr .log', text: '* 10 * * *', count: 1)

within '#suspend-schedules' do
click_on 'Delete'
Expand Down
53 changes: 51 additions & 2 deletions spec/models/job_schedule_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

context 'With suspend_schelule' do
before do
create(:job_suspend_schedule, job_definition: definition, cron: '0-29 10 * * *')
create(:job_suspend_schedule, job_definition: schedule.job_definition, cron: '0-29 10 * * *')
end

it 'skips suspend time range' do
Expand All @@ -38,14 +38,63 @@

context 'When suspend schelules covers all schedule' do
before do
create(:job_suspend_schedule, job_definition: definition, cron: '0-50 10 * * *')
suspend_schedule = create(:job_suspend_schedule, job_definition: schedule.job_definition, cron: '50 10 * * *')
suspend_schedule.update_column(:cron, '0-50 10 * * *')
end

it 'returns nil' do
expect(schedule.next(time)).to be_nil
end
end

context 'When schedules suspended long time ' do
let(:time) { Time.new(2016, 1, 2, 10, 0) }
let(:cron) { '0 10 1-7 * *' }

before do
create(:job_suspend_schedule, job_definition: schedule.job_definition, cron: '* * * * 0-5')
end

it 'returns next schedule' do
expect(schedule.next(time)).to eq(Time.new(2016, 2, 6, 10, 0))
end
end

context 'When suspended schedule has wdays and days' do
let(:time) { Time.new(2016, 1, 2, 10, 0) }
before do
create(:job_suspend_schedule, job_definition: schedule.job_definition, cron: '* * 1 * 0') # suspend every sunday or first day of month
end

context 'If the schedule has days only' do
let(:cron) { '0 10 1 * *' }
it 'returns nil' do
expect(schedule.next(time)).to be_nil
end
end

context 'If the schedule has wdays only' do
let(:cron) { '0 10 * * 0' }
it 'returns nil' do
expect(schedule.next(time)).to be_nil
end
end

context 'If the schedule has wdays and days' do
let(:cron) { '0 10 2 * 0' }
it 'returns next schedule' do
expect(schedule.next(time)).to eq(Time.new(2016, 2, 2, 10, 0))
end
end

context 'If the schedule has wdays and days' do
let(:cron) { '0 10 1 * 1' }
it 'returns next schedule' do
expect(schedule.next(time)).to eq(Time.new(2016, 1, 4, 10, 0))
end
end
end

context 'With invalid date' do
let(:cron) { '* * 31 2 *' }
let(:time) { Time.new(2016, 2, 28, 10, 0) }
Expand Down
44 changes: 39 additions & 5 deletions spec/models/job_suspend_schedule_spec.rb
Original file line number Diff line number Diff line change
@@ -1,20 +1,54 @@
require 'rails_helper'

describe Kuroko2::JobSuspendSchedule do
let(:launch_schedule) { create(:job_schedule, cron: '* * * * *') }

describe '#valid?' do
it 'accepts only CRON notation' do
expect(Kuroko2::JobSuspendSchedule.new(cron: '* * * * *')).to be_valid
expect(Kuroko2::JobSuspendSchedule.new(cron: '1,2-3,*,*/4,5-6/7 1,2-3,*,*/4,5-6/7 1,2-3,*,*/4,5-6/7 1,2-3,*,*/4,5-6/7 1,2-3,*,*/4,1-3/5')).to be_valid
expect(Kuroko2::JobSuspendSchedule.new(cron: '* * * *')).not_to be_valid
expect(Kuroko2::JobSuspendSchedule.new(cron: '0-30 10 * * *', job_definition: launch_schedule.job_definition)).to be_valid
expect(Kuroko2::JobSuspendSchedule.new(cron: '1,2-3,*/4,5-6/7 1,2-3,*,*/4,5-6/7 1,2-3,*,*/4,5-6/7 1,2-3,*,*/4,5-6/7 1,2-3,*,*/4,1-3/5', job_definition: launch_schedule.job_definition)).to be_valid
expect(Kuroko2::JobSuspendSchedule.new(cron: '* * * *', job_definition: launch_schedule.job_definition)).not_to be_valid
end

it 'accepts only valid CRON notation' do
expect(Kuroko2::JobSuspendSchedule.new(cron: '5-0 * * * *')).not_to be_valid
expect(Kuroko2::JobSuspendSchedule.new(cron: '5-0 * * * *', job_definition: launch_schedule.job_definition)).not_to be_valid
end

context 'when suspend all schedules' do
let(:launch_schedule) { create(:job_schedule, cron: '5 10 * * 0') }

it 'does not accept' do
expect(Kuroko2::JobSuspendSchedule.new(cron: '* * * * 0', job_definition: launch_schedule.job_definition)).not_to be_valid
end

context 'when launch schedule has wdays and days' do
let(:launch_schedule) { create(:job_schedule, cron: '5 10 1 * 1') }

it 'does not accept' do
expect(Kuroko2::JobSuspendSchedule.new(cron: '* * 1 * 1', job_definition: launch_schedule.job_definition)).not_to be_valid
end

context 'when supend_schedule has only wdays' do
let(:launch_schedule) { create(:job_schedule, cron: '5 10 1 * 1') }

it 'accepts' do
expect(Kuroko2::JobSuspendSchedule.new(cron: '* * * * 1', job_definition: launch_schedule.job_definition)).to be_valid
end
end

context 'when supend_schedule has only days' do
let(:launch_schedule) { create(:job_schedule, cron: '5 10 1 * 1') }

it 'accepts' do
expect(Kuroko2::JobSuspendSchedule.new(cron: '* * 1 * *', job_definition: launch_schedule.job_definition)).to be_valid
end
end
end
end
end

describe '#suspend_times' do
let(:suspend_schedule) { create(:job_suspend_schedule, cron: '* 10 * * *') }
let(:suspend_schedule) { create(:job_suspend_schedule, cron: '* 10 * * *', job_definition: launch_schedule.job_definition) }
let(:time_from) { Time.new(2016, 1, 1, 10, 0, 0) }
let(:time_to) { Time.new(2016, 1, 1, 10, 5, 0) }

Expand Down