Skip to content

Commit 012c54f

Browse files
committed
fix: gracefully handle race conditions when publishing a new revision of a pact
1 parent 7934b12 commit 012c54f

File tree

3 files changed

+38
-11
lines changed

3 files changed

+38
-11
lines changed

lib/pact_broker/pacts/repository.rb

+15-5
Original file line numberDiff line numberDiff line change
@@ -35,20 +35,30 @@ def update id, params
3535
existing_model = PactPublication.find(id: id)
3636
pact_version = find_or_create_pact_version(existing_model.consumer_version.pacticipant_id, existing_model.provider_id, params[:json_content])
3737
if existing_model.pact_version_id != pact_version.id
38-
pact_publication = PactPublication.new(
38+
key = {
3939
consumer_version_id: existing_model.consumer_version_id,
40-
consumer_id: existing_model.consumer_id,
4140
provider_id: existing_model.provider_id,
42-
revision_number: (existing_model.revision_number + 1),
43-
pact_version: pact_version,
44-
).save
41+
revision_number: next_revision_number(existing_model),
42+
}
43+
new_params = key.merge(
44+
consumer_id: existing_model.consumer_id,
45+
pact_version_id: pact_version.id,
46+
created_at: Sequel.datetime_class.now
47+
)
48+
PactPublication.upsert(new_params, key.keys)
49+
pact_publication = PactPublication.where(key).single_record
4550
update_latest_pact_publication_ids(pact_publication)
4651
pact_publication.to_domain
4752
else
4853
existing_model.to_domain
4954
end
5055
end
5156

57+
# This logic is a separate method so we can stub it to create a "conflict" scenario
58+
def next_revision_number(existing_model)
59+
existing_model.revision_number + 1
60+
end
61+
5262
def update_latest_pact_publication_ids(pact_publication)
5363
params = {
5464
consumer_version_id: pact_publication.consumer_version_id,

lib/pact_broker/repositories/helpers.rb

+3-2
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,12 @@ def select_for_subquery column
3636
end
3737
end
3838

39-
def upsert row, key_names
39+
def upsert row, key_names, columns_to_update = nil
4040
if postgres?
4141
insert_conflict(update: row, target: key_names).insert(row)
4242
elsif mysql?
43-
on_duplicate_key_update.insert(row)
43+
update_cols = columns_to_update || (row.keys - key_names)
44+
on_duplicate_key_update(*update_cols).insert(row)
4445
else
4546
# Sqlite
4647
key = row.reject{ |k, v| !key_names.include?(k) }

spec/lib/pact_broker/pacts/repository_spec.rb

+20-4
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ module Pacts
139139
let(:existing_pact) do
140140
TestDataBuilder.new.create_pact_with_hierarchy("A Consumer", "1.2.3", "A Provider", original_json_content).and_return(:pact)
141141
end
142+
let(:repository) { Repository.new }
142143

143144
before do
144145
::DB::PACT_BROKER_DB[:pact_publications]
@@ -155,10 +156,8 @@ module Pacts
155156
let(:original_json_content) { {some: 'json'}.to_json }
156157
let(:json_content) { {some_other: 'json'}.to_json }
157158

158-
159159
context "when the attributes have changed" do
160-
161-
subject { Repository.new.update existing_pact.id, json_content: json_content }
160+
subject { repository.update existing_pact.id, json_content: json_content }
162161

163162
it "creates a new PactVersion" do
164163
expect { subject }.to change{ PactBroker::Pacts::PactPublication.count }.by(1)
@@ -186,11 +185,27 @@ module Pacts
186185
it "increments the revision_number by 1" do
187186
expect(subject.revision_number).to eq 2
188187
end
188+
189+
context "when there is a race condition" do
190+
before do
191+
allow(repository).to receive(:next_revision_number) { | existing_pact | existing_pact.revision_number }
192+
end
193+
194+
it "updates the existing row - yes this is destructive, by MySQL not supporting inner queries stops us doing a SELECT revision_number + 1" do
195+
# And if we're conflicting the time between the two publications is so small that nobody
196+
# can have depended on the content of the first pact
197+
expect { subject }.to_not change{ PactBroker::Pacts::PactPublication.count }
198+
end
199+
200+
it "sets the content to the new content" do
201+
expect(subject.json_content).to eq json_content
202+
end
203+
end
189204
end
190205

191206
context "when the content has not changed" do
192207

193-
subject { Repository.new.update existing_pact.id, json_content: original_json_content }
208+
subject { repository.update existing_pact.id, json_content: original_json_content }
194209

195210
it "does not create a new PactVersion" do
196211
expect { subject }.to_not change{ PactBroker::Pacts::PactPublication.count }
@@ -217,6 +232,7 @@ module Pacts
217232
.create_consumer_version("1.2.3")
218233
.create_provider(provider_name)
219234
.create_pact
235+
.create_webhook
220236
.revise_pact
221237
.create_consumer_version("2.3.4")
222238
.create_pact

0 commit comments

Comments
 (0)