Skip to content

Commit b4a64e3

Browse files
authored
Add idempotent droplet creation (#4650)
Add idempotent droplet creation: - if there exists already a droplet, take that one and ensure buildpack_lifecycle_data is attached (but don’t overwrite) * add log if droplet already exists
1 parent 41171b1 commit b4a64e3

File tree

3 files changed

+69
-9
lines changed

3 files changed

+69
-9
lines changed

app/actions/droplet_create.rb

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,24 @@ def create_docker_droplet(build)
4949
droplet
5050
end
5151

52-
def create_buildpack_droplet(build)
53-
droplet = droplet_from_build(build)
54-
52+
def find_or_create_buildpack_droplet(build)
53+
droplet = nil
5554
DropletModel.db.transaction do
55+
existing = DropletModel.where(build_guid: build.guid).first
56+
if existing
57+
Steno.logger('droplet_existing').info("existing droplet found: #{existing.guid}")
58+
if build.cnb_lifecycle?
59+
existing.cnb_lifecycle_data ||= build.cnb_lifecycle_data
60+
else
61+
existing.buildpack_lifecycle_data ||= build.buildpack_lifecycle_data
62+
end
63+
existing.save_changes
64+
return existing
65+
end
66+
67+
droplet = droplet_from_build(build)
5668
droplet.save
69+
5770
if build.cnb_lifecycle?
5871
droplet.cnb_lifecycle_data = build.cnb_lifecycle_data
5972
else

app/controllers/runtime/stagings_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ def droplet_from_build(build, guid)
133133
end
134134

135135
def create_droplet_from_build(build)
136-
DropletCreate.new.create_buildpack_droplet(build)
136+
DropletCreate.new.find_or_create_buildpack_droplet(build)
137137
end
138138

139139
def upload_path

spec/unit/actions/droplet_create_spec.rb

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -158,13 +158,13 @@ module VCAP::CloudController
158158
end
159159
end
160160

161-
describe '#create_buildpack_droplet' do
161+
describe '#find_or_create_buildpack_droplet' do
162162
context 'buildpack lifecycle' do
163163
let!(:buildpack_lifecycle_data) { BuildpackLifecycleDataModel.make(build:) }
164164

165165
it 'sets it on the droplet' do
166166
expect do
167-
droplet_create.create_buildpack_droplet(build)
167+
droplet_create.find_or_create_buildpack_droplet(build)
168168
end.to change { [DropletModel.count, Event.count] }.by([1, 1])
169169

170170
droplet = DropletModel.last
@@ -195,6 +195,53 @@ module VCAP::CloudController
195195
})
196196
end
197197

198+
it 'creates the droplet once and attaches lifecycle data' do
199+
expect do
200+
droplet_create.find_or_create_buildpack_droplet(build)
201+
end.to change { [DropletModel.count, Event.count] }.by([1, 1])
202+
203+
droplet = DropletModel.last
204+
expect(droplet.state).to eq(DropletModel::STAGING_STATE)
205+
expect(droplet.app).to eq(app)
206+
expect(droplet.package).to eq(package)
207+
expect(droplet.build).to eq(build)
208+
209+
buildpack_lifecycle_data.reload
210+
expect(buildpack_lifecycle_data.droplet).to eq(droplet)
211+
end
212+
213+
it 'is idempotent on repeated calls (no duplicate droplet, same GUID, no extra event)' do
214+
first = droplet_create.find_or_create_buildpack_droplet(build)
215+
216+
expect do
217+
second = droplet_create.find_or_create_buildpack_droplet(build)
218+
expect(second.guid).to eq(first.guid)
219+
end.not_to(change { [DropletModel.count, Event.count] })
220+
221+
# Ensure still only one droplet for this build
222+
expect(DropletModel.where(build_guid: build.guid).count).to eq(1)
223+
224+
buildpack_lifecycle_data.reload
225+
expect(buildpack_lifecycle_data.droplet).to eq(first)
226+
end
227+
228+
it 'returns the pre-existing droplet when one already exists for the build' do
229+
existing = DropletModel.make(
230+
app: app,
231+
package: package,
232+
build: build,
233+
state: DropletModel::STAGING_STATE
234+
)
235+
buildpack_lifecycle_data.update(droplet: existing)
236+
237+
expect do
238+
returned = droplet_create.find_or_create_buildpack_droplet(build)
239+
expect(returned.guid).to eq(existing.guid)
240+
end.not_to(change { [DropletModel.count, Event.count] })
241+
242+
expect(DropletModel.where(build_guid: build.guid).first.guid).to eq(existing.guid)
243+
end
244+
198245
context 'when the build does not contain created_by fields' do
199246
let(:build) do
200247
BuildModel.make(
@@ -205,7 +252,7 @@ module VCAP::CloudController
205252

206253
it 'sets the actor to UNKNOWN' do
207254
expect do
208-
droplet_create.create_buildpack_droplet(build)
255+
droplet_create.find_or_create_buildpack_droplet(build)
209256
end.to change { [DropletModel.count, Event.count] }.by([1, 1])
210257

211258
droplet = DropletModel.last
@@ -230,7 +277,7 @@ module VCAP::CloudController
230277

231278
it 'sets it on the droplet' do
232279
expect do
233-
droplet_create.create_buildpack_droplet(build)
280+
droplet_create.find_or_create_buildpack_droplet(build)
234281
end.to change { [DropletModel.count, Event.count] }.by([1, 1])
235282

236283
droplet = DropletModel.last
@@ -271,7 +318,7 @@ module VCAP::CloudController
271318

272319
it 'sets the actor to UNKNOWN' do
273320
expect do
274-
droplet_create.create_buildpack_droplet(build)
321+
droplet_create.find_or_create_buildpack_droplet(build)
275322
end.to change { [DropletModel.count, Event.count] }.by([1, 1])
276323

277324
droplet = DropletModel.last

0 commit comments

Comments
 (0)