Skip to content

Commit 85d60a5

Browse files
committed
Warn when a stop event is skipped for a task that was running
Cloud Controller skips the TASK_STOPPED usage event when a task finishes with no start event on record. That is the right choice: a stop event with no start event is a record consumers cannot use. But the skip has always been silent, so nobody learns that a task's start record went missing. Now, when the skip fires for a task that was RUNNING, log a warning. A task can only reach RUNNING through the state change that also writes its TASK_STARTED event, in the same transaction. So a running task with no start record means something deleted the record later: the destructive v2 purge, or the events cleanup job of an older version, which deleted the start events of long-running tasks. Someone should find out which one happened, and the warning names the repair: run 'rake db:was_running_backfill'. The warning fires only when the task was RUNNING, because only then is it certain the task ran. A task canceled while still PENDING also passes through CANCELING, so a CANCELING task with no start record may simply never have run. Warning there would be noise. The tasks sync spec created a task directly in RUNNING state with no TASK_STARTED event and then completed it, which now triggers the warning and fails the spec's strict logger double. It now creates the start event, so the test data matches what production always has.
1 parent 6a68b30 commit 85d60a5

3 files changed

Lines changed: 138 additions & 6 deletions

File tree

app/models/runtime/task_model.rb

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,13 +159,39 @@ def create_stop_event_if_needed
159159
Repositories::AppUsageEventRepository::TASK_WAS_RUNNING_EVENT_STATE
160160
]
161161
started = app_usage_repo.find_by_task_and_state(task: self, state: start_evidence_states)
162-
return if started.nil?
162+
if started.nil?
163+
warn_if_running_task_has_no_start_record
164+
return
165+
end
163166

164167
create_stop_event
165168
end
166169

167170
def create_stop_event
168171
Repositories::AppUsageEventRepository.new.create_from_task(self, 'TASK_STOPPED')
169172
end
173+
174+
# The skip above is silent. That is fine for a task that never ran. But a
175+
# task can only reach RUNNING through the state change that also writes
176+
# its TASK_STARTED event, in the same transaction. So a running task with
177+
# no start record means something deleted the record later: the
178+
# destructive v2 purge, or the events cleanup job of an older version
179+
# that did not yet keep the start events of running tasks. Someone should
180+
# find out which, so log a warning. We warn only for RUNNING because only
181+
# RUNNING is certain: a task canceled while still PENDING also passes
182+
# through CANCELING, so a CANCELING task may never have run.
183+
def warn_if_running_task_has_no_start_record
184+
state_before_stop = column_changed?(:state) ? initial_value(:state) : state
185+
return unless state_before_stop == RUNNING_STATE
186+
187+
logger.warn("Not writing a TASK_STOPPED usage event for task #{guid}: the task was running but has no " \
188+
'TASK_STARTED or TASK_WAS_RUNNING usage event on record. This should not happen. Find out ' \
189+
"what deleted or purged the usage events, and run 'rake db:was_running_backfill' so the other " \
190+
'tasks that are still running get a baseline. (It cannot help this task; it is no longer running.)')
191+
end
192+
193+
def logger
194+
@logger ||= Steno.logger('cc.models.task')
195+
end
170196
end
171197
end

spec/unit/lib/cloud_controller/diego/tasks_sync_spec.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,10 @@ def exceptions
351351
context 'and the newly started task completes before the iteration completes', isolation: :truncation do
352352
# Can't use transactions for isolation because we're using multiple threads
353353
let!(:cc_task) { create(:task_model, guid: 'some-task-guid', state: TaskModel::RUNNING_STATE) }
354+
# A real running task always has a start event; without it, the
355+
# update to SUCCEEDED skips the stop event and logs a warning that
356+
# the strict logger double rejects.
357+
let!(:cc_task_start_event) { create(:app_usage_event, task_guid: cc_task.guid, state: 'TASK_STARTED') }
354358
let(:bbs_tasks) { [] }
355359

356360
before do

spec/unit/models/runtime/task_model_spec.rb

Lines changed: 107 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,29 @@ module VCAP::CloudController
4444

4545
context 'when there is neither a TASK_STARTED event nor a TASK_WAS_RUNNING baseline' do
4646
let!(:start_event) { nil }
47+
let(:fake_logger) { instance_double(Steno::Logger, info: nil, warn: nil, debug: nil, error: nil) }
48+
49+
before do
50+
allow(Steno).to receive(:logger).and_return(fake_logger)
51+
end
4752

4853
it 'does not create a TASK_STOPPED event, since no consumer ever saw the task start' do
4954
task.update(state: TaskModel::SUCCEEDED_STATE)
5055

5156
event = AppUsageEvent.find(task_guid: task.guid, state: 'TASK_STOPPED')
5257
expect(event).to be_nil
5358
end
59+
60+
it 'logs a warning, because a task that reached RUNNING must have had a start record once' do
61+
task.update(state: TaskModel::SUCCEEDED_STATE)
62+
63+
expect(fake_logger).to have_received(:warn).with(
64+
"Not writing a TASK_STOPPED usage event for task #{task.guid}: the task was running but has no " \
65+
'TASK_STARTED or TASK_WAS_RUNNING usage event on record. This should not happen. Find out ' \
66+
"what deleted or purged the usage events, and run 'rake db:was_running_backfill' so the other " \
67+
'tasks that are still running get a baseline. (It cannot help this task; it is no longer running.)'
68+
)
69+
end
5470
end
5571
end
5672

@@ -81,25 +97,52 @@ module VCAP::CloudController
8197

8298
context 'when there is neither a TASK_STARTED event nor a TASK_WAS_RUNNING baseline' do
8399
let!(:start_event) { nil }
100+
let(:fake_logger) { instance_double(Steno::Logger, info: nil, warn: nil, debug: nil, error: nil) }
101+
102+
before do
103+
allow(Steno).to receive(:logger).and_return(fake_logger)
104+
end
84105

85106
it 'does not create a TASK_STOPPED event, since no consumer ever saw the task start' do
86107
task.update(state: TaskModel::FAILED_STATE)
87108

88109
event = AppUsageEvent.find(task_guid: task.guid, state: 'TASK_STOPPED')
89110
expect(event).to be_nil
90111
end
112+
113+
it 'logs a warning, because a task that reached RUNNING must have had a start record once' do
114+
task.update(state: TaskModel::FAILED_STATE)
115+
116+
expect(fake_logger).to have_received(:warn).with(
117+
"Not writing a TASK_STOPPED usage event for task #{task.guid}: the task was running but has no " \
118+
'TASK_STARTED or TASK_WAS_RUNNING usage event on record. This should not happen. Find out ' \
119+
"what deleted or purged the usage events, and run 'rake db:was_running_backfill' so the other " \
120+
'tasks that are still running get a baseline. (It cannot help this task; it is no longer running.)'
121+
)
122+
end
91123
end
92124
end
93125

94126
context 'when the task is moving from the PENDING state' do
95127
let(:task) { create(:task_model, app: parent_app, state: TaskModel::PENDING_STATE) }
128+
let(:fake_logger) { instance_double(Steno::Logger, info: nil, warn: nil, debug: nil, error: nil) }
129+
130+
before do
131+
allow(Steno).to receive(:logger).and_return(fake_logger)
132+
end
96133

97134
it 'does not create a TASK_STOPPED event' do
98135
task.update(state: TaskModel::FAILED_STATE)
99136

100137
event = AppUsageEvent.find(task_guid: task.guid, state: 'TASK_STOPPED')
101138
expect(event).to be_nil
102139
end
140+
141+
it 'does not log a warning, because a task that never ran is not supposed to have a start record' do
142+
task.update(state: TaskModel::FAILED_STATE)
143+
144+
expect(fake_logger).not_to have_received(:warn)
145+
end
103146
end
104147

105148
context 'when the task is moving from the CANCELING state' do
@@ -131,13 +174,25 @@ module VCAP::CloudController
131174

132175
context 'when the task does not have a TASK_STARTED event' do
133176
let!(:start_event) { nil }
177+
let(:fake_logger) { instance_double(Steno::Logger, info: nil, warn: nil, debug: nil, error: nil) }
178+
179+
before do
180+
allow(Steno).to receive(:logger).and_return(fake_logger)
181+
end
134182

135183
it 'does not create a TASK_STOPPED event' do
136184
task.update(state: TaskModel::FAILED_STATE)
137185

138186
event = AppUsageEvent.find(task_guid: task.guid, state: 'TASK_STOPPED')
139187
expect(event).to be_nil
140188
end
189+
190+
it 'does not log a warning, because a task canceled while PENDING also passes through CANCELING, ' \
191+
'so we cannot be sure this task ever ran' do
192+
task.update(state: TaskModel::FAILED_STATE)
193+
194+
expect(fake_logger).not_to have_received(:warn)
195+
end
141196
end
142197
end
143198

@@ -203,24 +258,71 @@ module VCAP::CloudController
203258

204259
context 'when the TASK_STARTED event has been pruned and a TASK_WAS_RUNNING baseline exists' do
205260
let!(:start_event) { create(:app_usage_event, task_guid: task.guid, state: 'TASK_WAS_RUNNING') }
261+
let(:fake_logger) { instance_double(Steno::Logger, info: nil, warn: nil, debug: nil, error: nil) }
262+
263+
before do
264+
allow(Steno).to receive(:logger).and_return(fake_logger)
265+
end
206266

207267
it 'still creates a TASK_STOPPED event' do
208268
task.destroy
209269

210270
event = AppUsageEvent.find(task_guid: task.guid, state: 'TASK_STOPPED')
211271
expect(event).not_to be_nil
212272
end
273+
274+
it 'does not log a warning, because the stop event was written' do
275+
task.destroy
276+
277+
expect(fake_logger).not_to have_received(:warn)
278+
end
213279
end
214280

215281
context 'when there is neither a TASK_STARTED event nor a TASK_WAS_RUNNING baseline' do
216-
let(:task) { create(:task_model, app: parent_app, state: TaskModel::PENDING_STATE) }
217282
let!(:start_event) { nil }
283+
let(:fake_logger) { instance_double(Steno::Logger, info: nil, warn: nil, debug: nil, error: nil) }
218284

219-
it 'does not create a TASK_STOPPED event, since no consumer ever saw the task start' do
220-
task.destroy
285+
before do
286+
allow(Steno).to receive(:logger).and_return(fake_logger)
287+
end
221288

222-
event = AppUsageEvent.find(task_guid: task.guid, state: 'TASK_STOPPED')
223-
expect(event).to be_nil
289+
context 'when the task is PENDING' do
290+
let(:task) { create(:task_model, app: parent_app, state: TaskModel::PENDING_STATE) }
291+
292+
it 'does not create a TASK_STOPPED event, since no consumer ever saw the task start' do
293+
task.destroy
294+
295+
event = AppUsageEvent.find(task_guid: task.guid, state: 'TASK_STOPPED')
296+
expect(event).to be_nil
297+
end
298+
299+
it 'does not log a warning, because a task that never ran is not supposed to have a start record' do
300+
task.destroy
301+
302+
expect(fake_logger).not_to have_received(:warn)
303+
end
304+
end
305+
306+
context 'when the task is RUNNING' do
307+
let(:task) { create(:task_model, app: parent_app, state: TaskModel::RUNNING_STATE) }
308+
309+
it 'does not create a TASK_STOPPED event, since no consumer ever saw the task start' do
310+
task.destroy
311+
312+
event = AppUsageEvent.find(task_guid: task.guid, state: 'TASK_STOPPED')
313+
expect(event).to be_nil
314+
end
315+
316+
it 'logs a warning, because a task that reached RUNNING must have had a start record once' do
317+
task.destroy
318+
319+
expect(fake_logger).to have_received(:warn).with(
320+
"Not writing a TASK_STOPPED usage event for task #{task.guid}: the task was running but has no " \
321+
'TASK_STARTED or TASK_WAS_RUNNING usage event on record. This should not happen. Find out ' \
322+
"what deleted or purged the usage events, and run 'rake db:was_running_backfill' so the other " \
323+
'tasks that are still running get a baseline. (It cannot help this task; it is no longer running.)'
324+
)
325+
end
224326
end
225327
end
226328

0 commit comments

Comments
 (0)