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

Commit b5da1a2

Browse files
v3(services): update managed service instance - async broker
update managed service instance when broker is async
1 parent 49f603c commit b5da1a2

6 files changed

Lines changed: 610 additions & 279 deletions

File tree

app/jobs/v3/update_service_instance_job.rb

Lines changed: 82 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1-
# require 'messages/service_instance_update_managed_message'
1+
require 'jobs/reoccurring_job'
22

33
module VCAP::CloudController
44
module V3
5-
class UpdateServiceInstanceJob < VCAP::CloudController::Jobs::CCJob
5+
class UpdateServiceInstanceJob < VCAP::CloudController::Jobs::ReoccurringJob
66
attr_reader :warnings
77

88
def initialize(service_instance_guid, message:, user_audit_info:)
99
super()
1010
@service_instance_guid = service_instance_guid
1111
@message = message
1212
@user_audit_info = user_audit_info
13+
@first_time = true
14+
@broker_response = {}
1315
@warnings = []
1416
end
1517

@@ -23,36 +25,25 @@ def perform
2325
aborted! if operation_in_progress != 'update'
2426

2527
begin
26-
compatibility_checks
27-
28-
client = VCAP::Services::ServiceClientProvider.provide({ instance: service_instance })
29-
3028
maintenance_info = updated_maintenance_info(message, service_instance, service_plan)
29+
client = VCAP::Services::ServiceClientProvider.provide({ instance: service_instance })
3130

32-
broker_response, err = client.update(
33-
service_instance,
34-
service_plan,
35-
accepts_incomplete: false,
36-
arbitrary_parameters: message.parameters || {},
37-
previous_values: previous_values,
38-
maintenance_info: maintenance_info,
39-
name: message.requested?(:name) ? message.name : service_instance.name,
40-
)
41-
raise err if err
42-
43-
updates = message.updates.tap do |u|
44-
u[:service_plan_guid] = service_plan.guid
45-
u[:dashboard_url] = broker_response[:dashboard_url] if broker_response.key?(:dashboard_url)
46-
u[:maintenance_info] = maintenance_info if maintenance_info_updated?(message, service_instance, service_plan)
31+
if first_time
32+
compute_maximum_duration
33+
compatibility_checks
34+
send_update_request(client, maintenance_info)
35+
@first_time = false
4736
end
4837

49-
ServiceInstance.db.transaction do
50-
service_instance.save_with_new_operation(updates, broker_response[:last_operation])
51-
MetadataUpdate.update(service_instance, message)
52-
record_event(service_instance, message.audit_hash)
38+
if service_instance.operation_in_progress?
39+
fetch_last_operation(client)
5340
end
5441

55-
logger.info("Service instance update complete #{service_instance_guid}")
42+
if service_instance.last_operation.state == 'succeeded'
43+
update_service_instance(broker_response, maintenance_info)
44+
logger.info("Service instance update complete #{service_instance_guid}")
45+
finish
46+
end
5647
rescue => err
5748
logger.info("Service instance update failed: #{err.message}")
5849
service_instance.save_with_new_operation({}, {
@@ -62,6 +53,20 @@ def perform
6253
})
6354
raise err
6455
end
56+
57+
if service_instance.last_operation.state == 'failed'
58+
logger.info("Service instance update failed: #{service_instance.last_operation.description}")
59+
operation_failed!(service_instance.last_operation.description)
60+
end
61+
end
62+
63+
def handle_timeout
64+
service_instance.save_and_update_operation(
65+
last_operation: {
66+
state: 'failed',
67+
description: 'Service Broker failed to update within the required time.',
68+
}
69+
)
6570
end
6671

6772
def job_name_in_configuration
@@ -86,7 +91,22 @@ def display_name
8691

8792
private
8893

89-
attr_reader :service_instance_guid, :message, :user_audit_info
94+
attr_reader :service_instance_guid, :message, :user_audit_info, :first_time, :broker_response
95+
96+
def send_update_request(client, maintenance_info)
97+
@broker_response, err = client.update(
98+
service_instance,
99+
service_plan,
100+
accepts_incomplete: true,
101+
arbitrary_parameters: message.parameters || {},
102+
previous_values: previous_values,
103+
maintenance_info: maintenance_info,
104+
name: message.requested?(:name) ? message.name : service_instance.name,
105+
)
106+
raise err if err
107+
108+
service_instance.save_with_new_operation({}, broker_response[:last_operation])
109+
end
90110

91111
def service_instance
92112
ManagedServiceInstance.first(guid: service_instance_guid)
@@ -145,6 +165,37 @@ def compatibility_checks
145165
end
146166
end
147167

168+
def compute_maximum_duration
169+
max_poll_duration_on_plan = service_instance.service_plan.try(:maximum_polling_duration)
170+
self.maximum_duration_seconds = max_poll_duration_on_plan if max_poll_duration_on_plan
171+
end
172+
173+
def update_service_instance(broker_response, maintenance_info)
174+
updates = message.updates.tap do |u|
175+
u[:service_plan_guid] = service_plan.guid
176+
u[:dashboard_url] = broker_response[:dashboard_url] if broker_response.key?(:dashboard_url)
177+
u[:maintenance_info] = maintenance_info if maintenance_info_updated?(message, service_instance, service_plan)
178+
end
179+
180+
ServiceInstance.db.transaction do
181+
service_instance.update_service_instance(updates)
182+
MetadataUpdate.update(service_instance, message)
183+
record_event(service_instance, message.audit_hash)
184+
end
185+
end
186+
187+
def fetch_last_operation(client)
188+
last_operation_result = client.fetch_service_instance_last_operation(service_instance)
189+
self.polling_interval_seconds = last_operation_result[:retry_after] if last_operation_result[:retry_after]
190+
191+
service_instance.save_and_update_operation(
192+
last_operation: last_operation_result[:last_operation].slice(:state, :description)
193+
)
194+
rescue HttpRequestError, HttpResponseError, Sequel::Error => e
195+
logger = Steno.logger('cc-background')
196+
logger.error("There was an error while fetching the service instance operation state: #{e}")
197+
end
198+
148199
def volume_services_disabled?
149200
!VCAP::CloudController::Config.config.get(:volume_services_enabled)
150201
end
@@ -164,6 +215,10 @@ def gone!
164215
def aborted!
165216
raise CloudController::Errors::ApiError.new_from_details('UnableToPerform', 'Update', 'delete in progress')
166217
end
218+
219+
def operation_failed!(msg)
220+
raise CloudController::Errors::ApiError.new_from_details('UnableToPerform', 'Update', msg)
221+
end
167222
end
168223
end
169224
end

docs/v3/source/includes/resources/service_instances/_update.md.erb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ The response will be asynchronous if:
132132

133133
* `parameters` are specified
134134
* `service_plan` is specified
135+
* `maintenance_info` is specified
135136
* `name` is specified and the service offering has the `allow_context_updates` feature enabled
136137

137138
Otherwise the response will be synchronous.

spec/request/service_instances_spec.rb

Lines changed: 173 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1502,12 +1502,13 @@ def check_filtered_instances(*instances)
15021502
)
15031503
end
15041504
let(:new_service_plan) { VCAP::CloudController::ServicePlan.make(service: service_offering) }
1505+
let(:original_maintenance_info) { { version: '1.1.0' } }
15051506
let!(:service_instance) do
15061507
si = VCAP::CloudController::ManagedServiceInstance.make(
15071508
tags: %w(foo bar),
15081509
space: space,
15091510
service_plan: original_service_plan,
1510-
maintenance_info: { version: '1.1.0' }
1511+
maintenance_info: original_maintenance_info
15111512
)
15121513
si.annotation_ids = [
15131514
VCAP::CloudController::ServiceInstanceAnnotationModel.make(key_prefix: 'pre.fix', key_name: 'to_delete', value: 'value').id,
@@ -1597,11 +1598,10 @@ def check_filtered_instances(*instances)
15971598
api_call.call(space_dev_headers)
15981599

15991600
instance = VCAP::CloudController::ServiceInstance.last
1601+
16001602
stub_request(:patch, "#{instance.service_broker.broker_url}/v2/service_instances/#{instance.guid}").
1603+
with(query: { 'accepts_incomplete' => true }).
16011604
to_return(status: broker_status_code, body: broker_response.to_json, headers: {})
1602-
1603-
# TODO: add this when doing async responses:
1604-
# with(query: { 'accepts_incomplete' => true }).
16051605
end
16061606

16071607
it 'sends a UPDATE request with the right arguments to the service broker' do
@@ -1610,8 +1610,7 @@ def check_filtered_instances(*instances)
16101610
expect(
16111611
a_request(:patch, "#{service_instance.service_broker.broker_url}/v2/service_instances/#{service_instance.guid}").
16121612
with(
1613-
# TODO: add in when async part done:
1614-
# query: { accepts_incomplete: true },
1613+
query: { accepts_incomplete: true },
16151614
body: {
16161615
service_id: new_service_plan.service.unique_id,
16171616
plan_id: new_service_plan.unique_id,
@@ -1675,6 +1674,174 @@ def check_filtered_instances(*instances)
16751674
end
16761675
end
16771676

1677+
context 'when the update is asynchronous' do
1678+
let(:broker_status_code) { 202 }
1679+
let(:broker_response) { { operation: 'task12' } }
1680+
let(:last_operation_status_code) { 200 }
1681+
let(:last_operation_response) { { state: 'in progress' } }
1682+
1683+
before do
1684+
stub_request(:get, "#{service_instance.service_broker.broker_url}/v2/service_instances/#{service_instance.guid}/last_operation").
1685+
with(
1686+
query: {
1687+
operation: 'task12',
1688+
service_id: service_instance.service_plan.service.unique_id,
1689+
plan_id: service_instance.service_plan.unique_id,
1690+
}).
1691+
to_return(status: last_operation_status_code, body: last_operation_response.to_json, headers: {})
1692+
end
1693+
1694+
it 'marks the job state as polling' do
1695+
execute_all_jobs(expected_successes: 1, expected_failures: 0)
1696+
expect(job.state).to eq(VCAP::CloudController::PollableJobModel::POLLING_STATE)
1697+
end
1698+
1699+
it 'calls last operation immediately' do
1700+
execute_all_jobs(expected_successes: 1, expected_failures: 0)
1701+
expect(
1702+
a_request(:get, "#{service_instance.service_broker.broker_url}/v2/service_instances/#{service_instance.guid}/last_operation").
1703+
with(
1704+
query: {
1705+
operation: 'task12',
1706+
service_id: service_instance.service_plan.service.unique_id,
1707+
plan_id: service_instance.service_plan.unique_id,
1708+
})
1709+
).to have_been_made.once
1710+
end
1711+
1712+
it 'enqueues the next fetch last operation job' do
1713+
execute_all_jobs(expected_successes: 1, expected_failures: 0)
1714+
expect(Delayed::Job.count).to eq(1)
1715+
end
1716+
1717+
context 'when last operation eventually returns `update succeeded`' do
1718+
let(:last_operation_status_code) { 200 }
1719+
let(:last_operation_response) { { state: 'in progress' } }
1720+
1721+
before do
1722+
stub_request(:get, "#{service_instance.service_broker.broker_url}/v2/service_instances/#{service_instance.guid}/last_operation").
1723+
with(
1724+
query: {
1725+
operation: 'task12',
1726+
service_id: service_instance.service_plan.service.unique_id,
1727+
plan_id: service_instance.service_plan.unique_id,
1728+
}).
1729+
to_return(status: last_operation_status_code, body: last_operation_response.to_json, headers: {}).times(1).then.
1730+
to_return(status: 200, body: { state: 'succeeded' }.to_json, headers: {})
1731+
1732+
execute_all_jobs(expected_successes: 1, expected_failures: 0)
1733+
expect(job.state).to eq(VCAP::CloudController::PollableJobModel::POLLING_STATE)
1734+
1735+
Timecop.freeze(Time.now + 1.hour) do
1736+
execute_all_jobs(expected_successes: 1, expected_failures: 0)
1737+
end
1738+
end
1739+
1740+
it 'completes the job' do
1741+
updated_job = VCAP::CloudController::PollableJobModel.find(guid: job.guid)
1742+
expect(updated_job.state).to eq(VCAP::CloudController::PollableJobModel::COMPLETE_STATE)
1743+
end
1744+
1745+
it 'sets the service instance last operation to create succeeded' do
1746+
expect(service_instance.last_operation.type).to eq('update')
1747+
expect(service_instance.last_operation.state).to eq('succeeded')
1748+
end
1749+
end
1750+
1751+
context 'when last operation eventually returns `update failed`' do
1752+
before do
1753+
stub_request(:get, "#{service_instance.service_broker.broker_url}/v2/service_instances/#{service_instance.guid}/last_operation").
1754+
with(
1755+
query: {
1756+
operation: 'task12',
1757+
service_id: service_instance.service_plan.service.unique_id,
1758+
plan_id: service_instance.service_plan.unique_id,
1759+
}).
1760+
to_return(status: last_operation_status_code, body: last_operation_response.to_json, headers: {}).times(1).then.
1761+
to_return(status: 200, body: { state: 'failed' }.to_json, headers: {})
1762+
1763+
execute_all_jobs(expected_successes: 1, expected_failures: 0)
1764+
expect(job.state).to eq(VCAP::CloudController::PollableJobModel::POLLING_STATE)
1765+
1766+
Timecop.freeze(Time.now + 1.hour) do
1767+
execute_all_jobs(expected_successes: 0, expected_failures: 1, jobs_to_execute: 1)
1768+
end
1769+
end
1770+
1771+
it 'completes the job' do
1772+
updated_job = VCAP::CloudController::PollableJobModel.find(guid: job.guid)
1773+
expect(updated_job.state).to eq(VCAP::CloudController::PollableJobModel::FAILED_STATE)
1774+
end
1775+
1776+
it 'sets the service instance last operation to update failed' do
1777+
expect(service_instance.last_operation.type).to eq('update')
1778+
expect(service_instance.last_operation.state).to eq('failed')
1779+
end
1780+
end
1781+
1782+
context 'when last operation eventually returns error 400' do
1783+
before do
1784+
stub_request(:get, "#{service_instance.service_broker.broker_url}/v2/service_instances/#{service_instance.guid}/last_operation").
1785+
with(
1786+
query: {
1787+
operation: 'task12',
1788+
service_id: service_instance.service_plan.service.unique_id,
1789+
plan_id: service_instance.service_plan.unique_id,
1790+
}).
1791+
to_return(status: last_operation_status_code, body: last_operation_response.to_json, headers: {}).times(1).then.
1792+
to_return(status: 400, body: {}.to_json, headers: {})
1793+
1794+
execute_all_jobs(expected_successes: 1, expected_failures: 0)
1795+
expect(job.state).to eq(VCAP::CloudController::PollableJobModel::POLLING_STATE)
1796+
1797+
Timecop.freeze(Time.now + 1.hour) do
1798+
execute_all_jobs(expected_successes: 0, expected_failures: 1, jobs_to_execute: 1)
1799+
end
1800+
end
1801+
1802+
it 'completes the job' do
1803+
updated_job = VCAP::CloudController::PollableJobModel.find(guid: job.guid)
1804+
expect(updated_job.state).to eq(VCAP::CloudController::PollableJobModel::FAILED_STATE)
1805+
end
1806+
1807+
it 'sets the service instance last operation to update failed' do
1808+
expect(service_instance.last_operation.type).to eq('update')
1809+
expect(service_instance.last_operation.state).to eq('failed')
1810+
end
1811+
1812+
it 'does not update the instance' do
1813+
# TODO maybe look in the client to add this test and make sure what it returns? so we can test at a unit level in the job as well
1814+
service_instance.reload
1815+
expect(service_instance.reload.tags).to eq(%w(foo bar))
1816+
expect(service_instance.service_plan).to eq(original_service_plan)
1817+
expect_metadata(
1818+
service_instance,
1819+
annotations: [
1820+
{ prefix: 'pre.fix', key: 'to_delete', value: 'value' },
1821+
{ prefix: 'pre.fix', key: 'fox', value: 'bushy' },
1822+
],
1823+
labels: [
1824+
{ prefix: 'pre.fix', key: 'to_delete', value: 'value' },
1825+
{ prefix: 'pre.fix', key: 'tail', value: 'fluffy' }
1826+
]
1827+
)
1828+
end
1829+
1830+
context 'when changing maintenance_info' do
1831+
let(:request_body) do
1832+
{
1833+
maintenance_info: { version: '1.1.1' },
1834+
}
1835+
end
1836+
1837+
it 'does not update the instance' do
1838+
service_instance.reload
1839+
expect(service_instance.maintenance_info.symbolize_keys).to eq(original_maintenance_info)
1840+
end
1841+
end
1842+
end
1843+
end
1844+
16781845
context 'changing maintenance_info alongside other parameters' do
16791846
let(:new_maintenance_info) { { version: '1.1.1' } }
16801847
let(:request_body) do

0 commit comments

Comments
 (0)