Skip to content

Commit

Permalink
Fixes #3329 - Scheduler jobs don't ensure current ticket information …
Browse files Browse the repository at this point in the history
…if they're running a long time
  • Loading branch information
mantas authored and thorsteneckel committed Aug 31, 2021
1 parent 080ccc4 commit 423fafb
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 43 deletions.
120 changes: 77 additions & 43 deletions app/models/job.rb
Expand Up @@ -53,50 +53,15 @@ def self.run
def run(force = false, start_at = Time.zone.now)
logger.debug { "Execute job #{inspect}" }

tickets = nil
Transaction.execute(reset_user_id: true) do
if !executable?(start_at) && force == false
if next_run_at && next_run_at <= Time.zone.now
save!
end
return
end
ticket_ids = start_job(start_at, force)

# find tickets to change
matching = matching_count
if self.matching != matching
self.matching = matching
save!
end

if !in_timeplan?(start_at) && force == false
if next_run_at && next_run_at <= Time.zone.now
save!
end
return
end
return if ticket_ids.nil?

ticket_count, tickets = Ticket.selectors(condition, limit: 2_000, execution_time: true)

logger.debug { "Job #{name} with #{ticket_count} tickets" }

self.processed = ticket_count || 0
self.running = true
self.last_run_at = Time.zone.now
save!
end

tickets&.each do |ticket|
Transaction.execute(disable_notification: disable_notification, reset_user_id: true) do
ticket.perform_changes(self, 'job')
end
ticket_ids&.each_slice(10) do |slice|
run_slice(slice)
end

Transaction.execute(reset_user_id: true) do
self.running = false
self.last_run_at = Time.zone.now
save!
end
finish_job
end

def executable?(start_at = Time.zone.now)
Expand Down Expand Up @@ -144,10 +109,79 @@ def update_next_run_at
self.next_run_at = next_run_at_calculate
end

def match_minutes(minutes)
return 0 if minutes < 10
def finish_job
Transaction.execute(reset_user_id: true) do
mark_as_finished
end
end

def mark_as_finished
self.running = false
self.last_run_at = Time.zone.now
save!
end

def start_job(start_at, force)
Transaction.execute(reset_user_id: true) do
if start_job_executable?(start_at, force) && start_job_ensure_matching_count && start_job_in_timeplan?(start_at, force)
ticket_count, tickets = Ticket.selectors(condition, limit: 2_000, execution_time: true)

logger.debug { "Job #{name} with #{ticket_count} tickets" }

mark_as_started(ticket_count)

tickets&.pluck(:id) || []
end
end
end

def start_job_executable?(start_at, force)
return true if executable?(start_at) || force

if next_run_at && next_run_at <= Time.zone.now
save!
end

false
end

def start_job_ensure_matching_count
matching = matching_count

if self.matching != matching
self.matching = matching
save!
end

true
end

def start_job_in_timeplan?(start_at, force)
return true if in_timeplan?(start_at) || force

if next_run_at && next_run_at <= Time.zone.now
save!
end

"#{minutes.to_s.gsub(%r{(\d)\d}, '\\1')}0".to_i
false
end

def mark_as_started(ticket_count)
self.processed = ticket_count || 0
self.running = true
self.last_run_at = Time.zone.now
save!
end

def run_slice(slice)
Transaction.execute(disable_notification: disable_notification, reset_user_id: true) do
_, tickets = Ticket.selectors(condition, limit: 2_000, execution_time: true)

tickets
&.where(id: slice)
&.each do |ticket|
ticket.perform_changes(self, 'job')
end
end
end
end
45 changes: 45 additions & 0 deletions spec/models/job_spec.rb
Expand Up @@ -487,4 +487,49 @@
end
end
end

# when running a very large job, tickets may change during the job
# if tickets are fetched once, their action may be performed later on
# when it no longer matches the conditions
# https://github.com/zammad/zammad/issues/3329
context 'job re-checks conditions' do
let(:job) { create(:job, condition: condition, perform: perform) }
let(:ticket) { create(:ticket, title: initial_title) }
let(:initial_title) { 'initial 3329' }
let(:changed_title) { 'performed 3329' }

let(:condition) do
{ 'ticket.title' => { 'value' => initial_title, 'operator' => 'is' } }
end

let(:perform) do
{ 'ticket.title' => { 'value'=> changed_title } }
end

it 'condition matches ticket' do
ticket
expect(job.send(:start_job, Time.zone.now, true)).to eq [ticket.id]
end

it 'action is performed' do
ticket

ticket_ids = job.send(:start_job, Time.zone.now, true)
job.send(:run_slice, ticket_ids)

expect(ticket.reload.title).to eq changed_title
end

it 'checks conditions' do
ticket

ticket_ids = job.send(:start_job, Time.zone.now, true)

ticket.update! title: 'another title'

job.send(:run_slice, ticket_ids)

expect(ticket.reload.title).not_to eq changed_title
end
end
end

0 comments on commit 423fafb

Please sign in to comment.