Skip to content
This repository was archived by the owner on Jun 2, 2021. It is now read-only.

Commit 2b5a926

Browse files
Derik Evangelistablgm
andcommitted
v3: Create service instance handles last operation errors
[#171930482](https://www.pivotaltracker.com/story/show/171930482) Co-authored-by: George Blue <gblue@pivotal.io> Co-authored-by: Derik Evangelista <devangelista@pivotal.io>
1 parent 9450929 commit 2b5a926

8 files changed

Lines changed: 219 additions & 143 deletions

File tree

app/jobs/v3/services/fetch_last_operation_job.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ def perform
4444
last_operation_result = client.fetch_service_instance_last_operation(service_instance)
4545
update_with_attributes(last_operation_result[:last_operation], service_instance, intended_operation)
4646

47+
if last_operation_result[:last_operation][:state] == 'failed'
48+
operation_failed!(last_operation_result[:last_operation][:description])
49+
end
50+
4751
@retry_after = last_operation_result[:retry_after]
4852
rescue HttpRequestError, HttpResponseError, Sequel::Error => e
4953
logger = Steno.logger('cc-background')
@@ -66,6 +70,10 @@ def aborted!
6670
raise CloudController::Errors::ApiError.new_from_details('UnableToPerform', 'Create', 'delete in progress')
6771
end
6872

73+
def operation_failed!(msg)
74+
raise CloudController::Errors::ApiError.new_from_details('ServiceInstanceProvisionFailed', msg)
75+
end
76+
6977
def pollable_job
7078
PollableJobModel.where(guid: @pollable_job_guid)
7179
end
@@ -100,6 +108,7 @@ def end_timestamp_reached
100108
description: 'Service Broker failed to provision within the required time.',
101109
}
102110
)
111+
raise CloudController::Errors::ApiError.new_from_details('JobTimeout')
103112
end
104113

105114
def apply_proposed_changes(service_instance)

lib/services/service_brokers/v2/client.rb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -256,12 +256,10 @@ def fetch_service_instance_last_operation(instance)
256256
parsed_response = @response_parser.parse_fetch_state(path, response)
257257
last_operation_hash = parsed_response.delete('last_operation') || {}
258258

259-
state = extract_state(instance, last_operation_hash)
260-
261259
result = {
262260
last_operation:
263261
{
264-
state: state
262+
state: extract_state(instance, last_operation_hash)
265263
}
266264
}
267265

lib/services/service_brokers/v2/response_parser.rb

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -182,19 +182,25 @@ def parse_fetch_state(path, response)
182182
validator =
183183
case unvalidated_response.code
184184
when 200
185-
JsonObjectValidator.new(@logger,
186-
StateValidator.new(['succeeded', 'failed', 'in progress'],
187-
SuccessValidator.new))
185+
JsonObjectValidator.new(
186+
@logger,
187+
StateValidator.new(
188+
['succeeded', 'failed', 'in progress'],
189+
SuccessValidator.new
190+
)
191+
)
188192
when 201, 202
189193
JsonObjectValidator.new(@logger,
190-
FailingValidator.new(Errors::ServiceBrokerBadResponse))
194+
FailingValidator.new(Errors::ServiceBrokerBadResponse)
195+
)
196+
when 400
197+
BadRequestValidator.new
191198
when 410
192199
SuccessValidator.new { |res| {} }
193200
else
194-
FailingValidator.new(Errors::ServiceBrokerBadResponse)
201+
CommonErrorValidator.new(FailingValidator.new(Errors::ServiceBrokerBadResponse))
195202
end
196203

197-
validator = CommonErrorValidator.new(validator)
198204
validator.validate(unvalidated_response.to_hash)
199205
end
200206

@@ -698,6 +704,24 @@ def remove_trailing_validation_schema_id(err_msg)
698704
err_msg.sub(/ in schema.*$/, '')
699705
end
700706
end
707+
708+
class BadRequestValidator
709+
def validate(method:, uri:, code:, response:)
710+
description = 'Bad request'
711+
begin
712+
parsed_response = MultiJson.load(response.body)
713+
description = parsed_response['description'] if parsed_response.is_a?(Hash) && parsed_response.key?('description')
714+
rescue MultiJson::ParseError
715+
end
716+
717+
{
718+
'last_operation' => {
719+
'state' => 'failed',
720+
'description' => description
721+
}
722+
}
723+
end
724+
end
701725
end
702726
end
703727
end

spec/request/service_instances_spec.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,6 +1119,37 @@ def check_filtered_instances(*instances)
11191119
expect(instance.last_operation.state).to eq('succeeded')
11201120
end
11211121
end
1122+
1123+
context 'when last operation eventually returns `create failed`' do
1124+
before do
1125+
stub_request(:get, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}/last_operation").
1126+
with(
1127+
query: {
1128+
operation: 'task12',
1129+
service_id: service_plan.service.unique_id,
1130+
plan_id: service_plan.unique_id,
1131+
}).
1132+
to_return(status: last_operation_status_code, body: last_operation_response.to_json, headers: {}).times(1).then.
1133+
to_return(status: 200, body: { state: 'failed' }.to_json, headers: {})
1134+
1135+
execute_all_jobs(expected_successes: 2, expected_failures: 0)
1136+
expect(job.state).to eq(VCAP::CloudController::PollableJobModel::POLLING_STATE)
1137+
1138+
Timecop.freeze(Time.now + 1.hour) do
1139+
execute_all_jobs(expected_successes: 0, expected_failures: 1)
1140+
end
1141+
end
1142+
1143+
it 'completes the job' do
1144+
updated_job = VCAP::CloudController::PollableJobModel.find(guid: job.guid)
1145+
expect(updated_job.state).to eq(VCAP::CloudController::PollableJobModel::FAILED_STATE)
1146+
end
1147+
1148+
it 'sets the service instance last operation to create failed' do
1149+
expect(instance.last_operation.type).to eq('create')
1150+
expect(instance.last_operation.state).to eq('failed')
1151+
end
1152+
end
11221153
end
11231154
end
11241155
end

spec/unit/jobs/v3/services/fetch_last_operation_job_spec.rb

Lines changed: 87 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ def run_job(job, successes: 1, failures: 0)
206206
end
207207
end
208208

209-
context 'when the broker responds with state is `in progress`' do
209+
context 'when the broker responds with state `in progress`' do
210210
let(:state) { 'in progress' }
211211

212212
it 'fetches and updates the service instance state' do
@@ -248,6 +248,92 @@ def run_job(job, successes: 1, failures: 0)
248248
end
249249
end
250250

251+
context 'when the broker responds with state `failed`' do
252+
let(:state) { 'failed' }
253+
let(:description) { 'soz' }
254+
255+
it 'fetches and updates the service instance state' do
256+
run_job(job, successes: 0, failures: 1)
257+
258+
db_service_instance = ManagedServiceInstance.first(guid: service_instance.guid)
259+
expect(db_service_instance.last_operation.state).to eq('failed')
260+
end
261+
262+
it 'fails with an appropriate message' do
263+
run_job(job, successes: 0, failures: 1)
264+
265+
pollable_job.reload
266+
267+
expect(pollable_job.state).to eq(PollableJobModel::FAILED_STATE)
268+
expect(pollable_job.cf_api_error).
269+
to include('The service broker reported an error during provisioning: soz')
270+
end
271+
272+
it 'should not enqueue another fetch job' do
273+
run_job(job, successes: 0, failures: 1)
274+
expect(Delayed::Job.all).to have(1).jobs
275+
expect(Delayed::Job.where(failed_at: nil).all).to have(0).jobs
276+
end
277+
278+
it 'does not apply the instance attributes that were proposed in the operation' do
279+
run_job(job, successes: 0, failures: 1)
280+
281+
db_service_instance = ManagedServiceInstance.first(guid: service_instance.guid)
282+
expect(db_service_instance.service_plan).to_not eq(proposed_service_plan)
283+
expect(db_service_instance.name).to eq(service_instance.name)
284+
end
285+
286+
it 'should not create an audit event' do
287+
run_job(job, successes: 0, failures: 1)
288+
289+
expect(Event.find(type: 'audit.service_instance.create')).to be_nil
290+
end
291+
end
292+
293+
context 'when the job has fetched for more than the max poll duration' do
294+
let(:state) { 'in progress' }
295+
296+
before do
297+
run_job(job)
298+
Timecop.travel(Time.now + max_duration.minutes + 1.minute) do
299+
execute_all_jobs(expected_successes: 0, expected_failures: 1)
300+
end
301+
end
302+
303+
it 'should not enqueue another fetch job' do
304+
Timecop.freeze(Time.now + max_duration.minutes + 1.minute) do
305+
execute_all_jobs(expected_successes: 0, expected_failures: 0)
306+
end
307+
end
308+
309+
it 'should mark the service instance operation as failed' do
310+
service_instance.reload
311+
312+
expect(service_instance.last_operation.state).to eq('failed')
313+
expect(service_instance.last_operation.description).to eq('Service Broker failed to provision within the required time.')
314+
end
315+
316+
it 'fails the job with an appropriate message' do
317+
pollable_job.reload
318+
319+
expect(pollable_job.state).to eq(PollableJobModel::FAILED_STATE)
320+
expect(pollable_job.cf_api_error).
321+
to include('The job execution has timed out.')
322+
end
323+
end
324+
325+
context 'when enqueuing the job would exceed the max poll duration by the time it runs' do
326+
let(:state) { 'in progress' }
327+
328+
it 'should not enqueue another fetch job' do
329+
Timecop.freeze(job.end_timestamp - (job.poll_interval * 0.5))
330+
run_job(job, failures: 1, successes: 0)
331+
332+
Timecop.freeze(Time.now + job.poll_interval * 2)
333+
execute_all_jobs(expected_successes: 0, expected_failures: 0)
334+
end
335+
end
336+
251337
context 'failures' do
252338
context 'when fetching the last operation from the broker fails' do
253339
context 'due to an HttpRequestError' do
@@ -407,43 +493,6 @@ def run_job(job, successes: 1, failures: 0)
407493
end
408494
end
409495

410-
context 'when the last operation type is `create`' do
411-
before do
412-
service_instance.save_with_new_operation({}, { type: 'create' })
413-
end
414-
415-
context 'when during create, a delete operation was started' do
416-
before do
417-
allow(client).to receive(:fetch_service_instance_last_operation) do
418-
service_instance.save_with_new_operation(
419-
{},
420-
{
421-
type: 'delete',
422-
state: 'in progress'
423-
}
424-
)
425-
426-
last_operation_response
427-
end
428-
429-
run_job(job)
430-
end
431-
432-
let(:db_service_instance) { ManagedServiceInstance.first(guid: service_instance.guid) }
433-
434-
it 'does not finish creating' do
435-
expect(db_service_instance.last_operation.type).to eq('delete')
436-
expect(db_service_instance.last_operation.state).to eq('in progress')
437-
expect(Event.find(type: 'audit.service_instance.create')).not_to be
438-
end
439-
440-
it 'does not delete' do
441-
expect(db_service_instance).not_to be_nil
442-
expect(Event.find(type: 'audit.service_instance.delete')).not_to be
443-
end
444-
end
445-
end
446-
447496
context 'when the last operation type is `update`' do
448497
before do
449498
service_instance.last_operation.type = 'update'
@@ -485,101 +534,6 @@ def run_job(job, successes: 1, failures: 0)
485534
end
486535
end
487536

488-
context 'when the state is `failed`' do
489-
pending('do not run')
490-
let(:state) { 'failed' }
491-
492-
it 'does not apply the instance attributes that were proposed in the operation' do
493-
run_job(job)
494-
495-
db_service_instance = ManagedServiceInstance.first(guid: service_instance.guid)
496-
expect(db_service_instance.service_plan).to_not eq(proposed_service_plan)
497-
expect(db_service_instance.name).to eq(service_instance.name)
498-
end
499-
500-
it 'fetches and updates the service instance state' do
501-
run_job(job)
502-
503-
db_service_instance = ManagedServiceInstance.first(guid: service_instance.guid)
504-
expect(db_service_instance.last_operation.state).to eq('failed')
505-
end
506-
507-
it 'should not enqueue another fetch job' do
508-
run_job(job)
509-
510-
expect(Delayed::Job.count).to eq 0
511-
end
512-
513-
it 'should not create an audit event' do
514-
run_job(job)
515-
516-
expect(Event.find(type: 'audit.service_instance.create')).to be_nil
517-
end
518-
end
519-
520-
context 'when the job has fetched for more than the max poll duration' do
521-
let(:state) { 'in progress' }
522-
523-
before do
524-
run_job(job)
525-
Timecop.travel(Time.now + max_duration.minutes + 1.minute) do
526-
execute_all_jobs(expected_successes: 1, expected_failures: 0)
527-
end
528-
end
529-
530-
it 'should not enqueue another fetch job' do
531-
Timecop.freeze(Time.now + max_duration.minutes + 1.minute) do
532-
execute_all_jobs(expected_successes: 0, expected_failures: 0)
533-
end
534-
end
535-
536-
it 'should mark the service instance operation as failed' do
537-
service_instance.reload
538-
539-
expect(service_instance.last_operation.state).to eq('failed')
540-
expect(service_instance.last_operation.description).to eq('Service Broker failed to provision within the required time.')
541-
end
542-
end
543-
544-
context 'when enqueuing the job would exceed the max poll duration by the time it runs' do
545-
let(:state) { 'in progress' }
546-
547-
it 'should not enqueue another fetch job' do
548-
Timecop.freeze(job.end_timestamp - (job.poll_interval * 0.5))
549-
run_job(job)
550-
551-
Timecop.freeze(Time.now + job.poll_interval * 2)
552-
execute_all_jobs(expected_successes: 0, expected_failures: 0)
553-
end
554-
end
555-
556-
context 'when the job was migrated before the addition of end_timestamp' do
557-
let(:state) { 'in progress' }
558-
559-
it 'should compute the end_timestamp based on the current time' do
560-
Timecop.freeze(Time.now)
561-
562-
run_job(job)
563-
564-
# should run enqueued job
565-
Timecop.travel(Time.now + max_duration.minutes - 1.minute) do
566-
execute_all_jobs(expected_successes: 1, expected_failures: 0)
567-
end
568-
569-
# should not run enqueued job
570-
Timecop.travel(Time.now + max_duration.minutes) do
571-
execute_all_jobs(expected_successes: 0, expected_failures: 0)
572-
end
573-
end
574-
575-
it 'should enqueue another fetch job' do
576-
run_job(job)
577-
578-
expect(Delayed::Job.count).to eq 1
579-
expect(Delayed::Job.first).to be_a_fully_wrapped_job_of(FetchLastOperationJob)
580-
end
581-
end
582-
583537
context 'when the poll_interval is changed after the job was created' do
584538
let(:default_polling_interval) { VCAP::CloudController::Config.config.get(:broker_client_default_async_poll_interval_seconds) }
585539
let(:new_polling_interval) { default_polling_interval * 2 }

0 commit comments

Comments
 (0)