Added next_run_at attribute to jobs.

This commit is contained in:
Martin Edenhofer 2016-03-18 15:29:03 +01:00
parent 563f4d9e1d
commit 96fbc9034e
6 changed files with 449 additions and 30 deletions

View file

@ -3,26 +3,28 @@ class App.Job extends App.Model
@extend Spine.Model.Ajax
@url: @apiPath + '/jobs'
@configure_attributes = [
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false },
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false },
{ name: 'timeplan', display: 'When should the job run?', tag: 'timer', null: true },
{ name: 'condition', display: 'Conditions for effected objects', tag: 'ticket_selector', null: true },
{ name: 'perform', display: 'Execute changes on objects', tag: 'ticket_perform_action', null: true },
{ name: 'disable_notiifcation', display: 'Disable Notifications', tag: 'boolean', default: true },
{ name: 'note', display: 'Note', tag: 'textarea', note: 'Notes are visible to agents only, never to customers.', limit: 250, null: true },
{ name: 'active', display: 'Active', tag: 'active', default: true },
{ name: 'matching', display: 'Matching', readonly: 1 },
{ name: 'processed', display: 'Processed', readonly: 1 },
{ name: 'last_run_at', display: 'Last run', tag: 'datetime', readonly: 1 },
{ name: 'running', display: 'Running', tag: 'boolean', readonly: 1 },
{ name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 },
{ name: 'created_at', display: 'Created', tag: 'datetime', readonly: 1 },
{ name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 },
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },
{ name: 'note', display: 'Note', tag: 'textarea', note: 'Notes are visible to agents only, never to customers.', limit: 250, null: true },
{ name: 'active', display: 'Active', tag: 'active', default: true },
{ name: 'matching', display: 'Will process', readonly: 1 },
{ name: 'processed', display: 'Has processed', readonly: 1 },
{ name: 'last_run_at', display: 'Last run', tag: 'datetime', readonly: 1 },
{ name: 'next_run_at', display: 'Scheduled for', tag: 'datetime', readonly: 1 },
{ name: 'running', display: 'Running', tag: 'boolean', readonly: 1 },
{ name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 },
{ name: 'created_at', display: 'Created', tag: 'datetime', readonly: 1 },
{ name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 },
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },
]
@configure_delete = true
@configure_overview = [
'name',
'last_run_at',
'matching',
'processed',
'next_run_at',
'matching',
]

View file

@ -507,7 +507,7 @@ retrns
end
end
# execute ticket events
# execute ticket notification events
Observer::Ticket::Notification.transaction
# run postmaster post filter

View file

@ -6,8 +6,8 @@ class Job < ApplicationModel
store :perform
validates :name, presence: true
before_create :updated_matching
before_update :updated_matching
before_create :updated_matching, :update_next_run_at
before_update :updated_matching, :update_next_run_at
notify_clients_support
@ -37,21 +37,31 @@ class Job < ApplicationModel
if tickets
tickets.each do |ticket|
logger.debug "Perform job #{job.perform.inspect} in Ticket.find(#{ticket.id})"
changed = false
job.perform.each do |key, value|
(object_name, attribute) = key.split('.', 2)
raise "Unable to update object #{object_name}.#{attribute}, only can update tickets!" if object_name != 'ticket'
next if ticket[attribute].to_s == value['value'].to_s
changed = true
# use transaction
ActiveRecord::Base.transaction do
UserInfo.current_user_id = 1
ticket[attribute] = value['value']
logger.debug "set #{object_name}.#{attribute} = #{value['value'].inspect}"
logger.debug "Perform job #{job.perform.inspect} in Ticket.find(#{ticket.id})"
changed = false
job.perform.each do |key, value|
(object_name, attribute) = key.split('.', 2)
raise "Unable to update object #{object_name}.#{attribute}, only can update tickets!" if object_name != 'ticket'
next if ticket[attribute].to_s == value['value'].to_s
changed = true
ticket[attribute] = value['value']
logger.debug "set #{object_name}.#{attribute} = #{value['value'].inspect}"
end
next if !changed
ticket.save
# execute ticket notification events
if !job.disable_notification
Observer::Ticket::Notification.transaction
end
end
next if !changed
ticket.updated_by_id = 1
ticket.save
end
end
@ -63,6 +73,7 @@ class Job < ApplicationModel
end
def executable?
return false if !active
# only execute jobs, older then 1 min, to give admin posibility to change
return false if updated_at > Time.zone.now - 1.minute
@ -74,8 +85,7 @@ class Job < ApplicationModel
true
end
def in_timeplan?
time = Time.zone.now
def in_timeplan?(time = Time.zone.now)
day_map = {
0 => 'Sun',
1 => 'Mon',
@ -106,12 +116,117 @@ class Job < ApplicationModel
ticket_count || 0
end
def next_run_at_calculate(time = Time.zone.now)
if last_run_at
diff = time - last_run_at
if diff > 0
time = time + 10.minutes
end
end
day_map = {
0 => 'Sun',
1 => 'Mon',
2 => 'Tue',
3 => 'Wed',
4 => 'Thu',
5 => 'Fri',
6 => 'Sat',
}
return nil if !active
return nil if !timeplan['days']
return nil if !timeplan['hours']
return nil if !timeplan['minutes']
# loop week days
(0..7).each do |day_counter|
time_to_check = nil
day_to_check = if day_counter == 0
time
else
time + 1.day
end
if !timeplan['days'][day_map[day_to_check.wday]]
# start on next day at 00:00:00
time = day_to_check - day_to_check.sec.seconds
time = time - day_to_check.min.minutes
time = time - day_to_check.hour.hours
next
end
min = day_to_check.min
if min < 9
min = 0
elsif min < 20
min = 10
elsif min < 30
min = 20
elsif min < 40
min = 30
elsif min < 50
min = 40
elsif min < 60
min = 50
end
# move to [0-5]0:00 time stamps
day_to_check = day_to_check - day_to_check.min.minutes + min.minutes
day_to_check = day_to_check - day_to_check.sec.seconds
# loop minutes till next full hour
if day_to_check.min != 0
(0..5).each do |minute_counter|
if minute_counter != 0
break if day_to_check.min == 0
day_to_check = day_to_check + 10.minutes
end
next if !timeplan['hours'][day_to_check.hour] && !timeplan['hours'][day_to_check.hour.to_s]
next if !timeplan['minutes'][match_minutes(day_to_check.min)] && !timeplan['minutes'][match_minutes(day_to_check.min).to_s]
return day_to_check
end
end
# loop hours
hour_to_check = nil
(0..23).each do |hour_counter|
hour_to_check = day_to_check + hour_counter.hours
# start on next day
if hour_to_check.day != day_to_check.day
time = day_to_check - day_to_check.hour.hours
break
end
# ignore not configured hours
next if !timeplan['hours'][hour_to_check.hour] && !timeplan['hours'][hour_to_check.hour.to_s]
return nil if !hour_to_check
# loop minutes
minute_to_check = nil
(0..5).each do |minute_counter|
minute_to_check = hour_to_check + minute_counter.minutes * 10
next if !timeplan['minutes'][match_minutes(minute_to_check.min)] && !timeplan['minutes'][match_minutes(minute_to_check.min).to_s]
time_to_check = minute_to_check
break
end
next if !minute_to_check
return time_to_check
end
end
nil
end
private
def updated_matching
self.matching = matching_count
end
def update_next_run_at
self.next_run_at = next_run_at_calculate
end
def match_minutes(minutes)
return 0 if minutes < 10
"#{minutes.to_s.gsub(/(\d)\d/, '\\1')}0".to_i

View file

@ -238,6 +238,7 @@ class CreateTicket < ActiveRecord::Migration
t.column :perform, :string, limit: 2500, null: false
t.column :disable_notification, :boolean, null: false, default: true
t.column :last_run_at, :timestamp, null: true
t.column :next_run_at, :timestamp, null: true
t.column :running, :boolean, null: false, default: false
t.column :processed, :integer, null: false, default: 0
t.column :matching, :integer, null: false

View file

@ -22,6 +22,7 @@ class RenewTriggers < ActiveRecord::Migration
t.column :perform, :string, limit: 2500, null: false
t.column :disable_notification, :boolean, null: false, default: true
t.column :last_run_at, :timestamp, null: true
t.column :next_run_at, :timestamp, null: true
t.column :running, :boolean, null: false, default: false
t.column :processed, :integer, null: false, default: 0
t.column :matching, :integer, null: false, default: 0

View file

@ -130,6 +130,7 @@ class JobTest < ActiveSupport::TestCase
updated_by_id: 1,
updated_at: Time.zone.now,
)
assert_not(job1.next_run_at)
assert_not(job1.executable?)
job1.last_run_at = Time.zone.now - 15.minutes
@ -140,6 +141,14 @@ class JobTest < ActiveSupport::TestCase
job1.save
assert(job1.executable?)
job1.active = false
job1.save
assert_not(job1.executable?)
job1.active = true
job1.save
assert_not(job1.executable?)
assert_not(job1.in_timeplan?)
time = Time.zone.now
day_map = {
@ -168,7 +177,7 @@ class JobTest < ActiveSupport::TestCase
min = 30
elsif min < 50
min = 40
elsif min < 59
elsif min < 60
min = 50
end
job1.timeplan['minutes'][min.to_s] = true
@ -187,6 +196,7 @@ class JobTest < ActiveSupport::TestCase
job1.save
Job.run
assert(job1.next_run_at)
assert(job1.executable?)
assert(job1.in_timeplan?)
@ -422,4 +432,294 @@ class JobTest < ActiveSupport::TestCase
end
test 'case 3' do
# create jobs
job1 = Job.create_or_update(
name: 'Test Job1',
timeplan: {
days: {
Mon: true,
Tue: false,
Wed: false,
Thu: false,
Fri: true,
Sat: false,
Sun: false,
},
hours: {
0 => false,
1 => true,
2 => false,
3 => false,
4 => false,
5 => false,
6 => false,
7 => false,
8 => false,
9 => false,
10 => true,
11 => false,
12 => false,
13 => false,
14 => false,
15 => false,
16 => false,
17 => false,
18 => false,
19 => false,
20 => false,
21 => false,
22 => false,
23 => false,
},
minutes: {
0 => true,
10 => false,
20 => false,
30 => false,
40 => true,
50 => false,
},
},
condition: {
'ticket.state_id' => { 'operator' => 'is', 'value' => [Ticket::State.lookup(name: 'new').id.to_s, Ticket::State.lookup(name: 'open').id.to_s] },
'ticket.created_at' => { 'operator' => 'before (relative)', 'value' => '2', 'range' => 'day' },
},
perform: {
'ticket.state_id' => { 'value' => Ticket::State.lookup(name: 'closed').id.to_s }
},
disable_notification: true,
last_run_at: nil,
active: true,
created_by_id: 1,
created_at: Time.zone.now,
updated_by_id: 1,
updated_at: Time.zone.now,
)
time_now = Time.zone.parse('2016-03-18 09:17:13 UTC')
next_run_at = job1.next_run_at_calculate(time_now)
assert_equal('2016-03-18 10:00:00 UTC', next_run_at.to_s)
time_now = Time.zone.parse('2016-03-18 10:37:13 UTC')
next_run_at = job1.next_run_at_calculate(time_now)
assert_equal('2016-03-18 10:40:00 UTC', next_run_at.to_s)
time_now = Time.zone.parse('2016-03-17 09:17:13 UTC')
next_run_at = job1.next_run_at_calculate(time_now)
assert_equal('2016-03-18 01:00:00 UTC', next_run_at.to_s)
time_now = Time.zone.parse('2016-03-17 11:17:13 UTC')
next_run_at = job1.next_run_at_calculate(time_now)
assert_equal('2016-03-18 01:00:00 UTC', next_run_at.to_s)
time_now = Time.zone.parse('2016-03-19 11:17:13 UTC')
next_run_at = job1.next_run_at_calculate(time_now)
assert_equal('2016-03-21 01:00:00 UTC', next_run_at.to_s)
time_now = Time.zone.parse('2016-03-22 00:59:59 UTC')
next_run_at = job1.next_run_at_calculate(time_now)
assert_equal('2016-03-25 01:00:00 UTC', next_run_at.to_s)
time_now = Time.zone.parse('2016-03-25 00:59:59 UTC')
next_run_at = job1.next_run_at_calculate(time_now)
assert_equal('2016-03-25 01:00:00 UTC', next_run_at.to_s)
time_now = Time.zone.parse('2016-03-24 00:59:59 UTC')
next_run_at = job1.next_run_at_calculate(time_now)
assert_equal('2016-03-25 01:00:00 UTC', next_run_at.to_s)
time_now = Time.zone.parse('2016-03-24 23:59:59 UTC')
next_run_at = job1.next_run_at_calculate(time_now)
assert_equal('2016-03-25 01:00:00 UTC', next_run_at.to_s)
time_now = Time.zone.parse('2016-03-25 01:00:01 UTC')
next_run_at = job1.next_run_at_calculate(time_now)
assert_equal('2016-03-25 01:00:00 UTC', next_run_at.to_s)
time_now = Time.zone.parse('2016-03-25 01:09:01 UTC')
next_run_at = job1.next_run_at_calculate(time_now)
assert_equal('2016-03-25 01:40:00 UTC', next_run_at.to_s)
time_now = Time.zone.parse('2016-03-25 01:09:59 UTC')
next_run_at = job1.next_run_at_calculate(time_now)
assert_equal('2016-03-25 01:40:00 UTC', next_run_at.to_s)
job1.last_run_at = Time.zone.parse('2016-03-18 10:00:01 UTC')
job1.save
time_now = Time.zone.parse('2016-03-18 10:00:02 UTC')
next_run_at = job1.next_run_at_calculate(time_now)
assert_equal('2016-03-18 10:40:00 UTC', next_run_at.to_s)
job1.last_run_at = Time.zone.parse('2016-03-18 10:40:01 UTC')
job1.save
time_now = Time.zone.parse('2016-03-18 10:40:02 UTC')
next_run_at = job1.next_run_at_calculate(time_now)
assert_equal('2016-03-21 01:00:00 UTC', next_run_at.to_s)
end
test 'case 4' do
# create jobs
job1 = Job.create_or_update(
name: 'Test Job1',
timeplan: {
days: {
Mon: true,
Tue: false,
Wed: false,
Thu: false,
Fri: true,
Sat: false,
Sun: false,
},
hours: {
0 => true,
1 => false,
2 => false,
3 => false,
4 => false,
5 => false,
6 => false,
7 => false,
8 => false,
9 => false,
10 => true,
11 => false,
12 => false,
13 => false,
14 => false,
15 => false,
16 => false,
17 => false,
18 => false,
19 => false,
20 => false,
21 => false,
22 => false,
23 => false,
},
minutes: {
0 => true,
10 => false,
20 => false,
30 => false,
40 => true,
50 => false,
},
},
condition: {
'ticket.state_id' => { 'operator' => 'is', 'value' => [Ticket::State.lookup(name: 'new').id.to_s, Ticket::State.lookup(name: 'open').id.to_s] },
'ticket.created_at' => { 'operator' => 'before (relative)', 'value' => '2', 'range' => 'day' },
},
perform: {
'ticket.state_id' => { 'value' => Ticket::State.lookup(name: 'closed').id.to_s }
},
disable_notification: true,
last_run_at: nil,
active: true,
created_by_id: 1,
created_at: Time.zone.now,
updated_by_id: 1,
updated_at: Time.zone.now,
)
time_now = Time.zone.parse('2016-03-17 23:51:23 UTC')
next_run_at = job1.next_run_at_calculate(time_now)
assert_equal('2016-03-18 00:00:00 UTC', next_run_at.to_s)
job1.last_run_at = Time.zone.parse('2016-03-17 23:45:01 UTC')
job1.save
time_now = Time.zone.parse('2016-03-17 23:51:23 UTC')
next_run_at = job1.next_run_at_calculate(time_now)
assert_equal('2016-03-18 00:00:00 UTC', next_run_at.to_s)
job1.last_run_at = Time.zone.parse('2016-03-17 23:59:01 UTC')
job1.save
time_now = Time.zone.parse('2016-03-17 23:59:23 UTC')
next_run_at = job1.next_run_at_calculate(time_now)
assert_equal('2016-03-18 00:40:00 UTC', next_run_at.to_s)
time_now = Time.zone.parse('2016-03-17 23:59:23 UTC')
assert_not(job1.in_timeplan?(time_now))
time_now = Time.zone.parse('2016-03-18 00:01:23 UTC')
assert(job1.in_timeplan?(time_now))
end
test 'case 5' do
# create jobs
job1 = Job.create_or_update(
name: 'Test Job1',
timeplan: {
days: {
Mon: true,
Tue: false,
Wed: false,
Thu: false,
Fri: false,
Sat: false,
Sun: false,
},
hours: {
'0' => true,
'1' => false,
'2' => false,
'3' => false,
'4' => false,
'5' => false,
'6' => false,
'7' => false,
'8' => false,
'9' => false,
'10' => false,
'11' => false,
'12' => false,
'13' => false,
'14' => false,
'15' => false,
'16' => false,
'17' => false,
'18' => false,
'19' => false,
'20' => false,
'21' => false,
'22' => false,
'23' => false,
},
minutes: {
'0' => true,
'10' => false,
'20' => false,
'30' => false,
'40' => false,
'50' => false,
},
},
condition: {
'ticket.state_id' => { 'operator' => 'is', 'value' => [Ticket::State.lookup(name: 'new').id.to_s, Ticket::State.lookup(name: 'open').id.to_s] },
'ticket.created_at' => { 'operator' => 'before (relative)', 'value' => '2', 'range' => 'day' },
},
perform: {
'ticket.state_id' => { 'value' => Ticket::State.lookup(name: 'closed').id.to_s }
},
disable_notification: true,
last_run_at: nil,
active: true,
created_by_id: 1,
created_at: Time.zone.now,
updated_by_id: 1,
updated_at: Time.zone.now,
)
time_now = Time.zone.parse('2016-03-17 23:51:23 UTC')
next_run_at = job1.next_run_at_calculate(time_now)
assert_equal('2016-03-21 00:00:00 UTC', next_run_at.to_s)
end
end