Skip to content

Commit ecfb385

Browse files
committed
fix: ensure publishing a verification does not cause a unique constraint violation
1 parent 5ac0097 commit ecfb385

File tree

9 files changed

+108
-50
lines changed

9 files changed

+108
-50
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Sequel.migration do
2+
up do
3+
create_table(:verification_sequence_number) do
4+
Integer :value, null: false
5+
end
6+
7+
start = (from(:verifications).max(:number) || 0) + 100
8+
from(:verification_sequence_number).insert(value: start)
9+
end
10+
11+
down do
12+
drop_table(:verification_sequence_number)
13+
end
14+
end

lib/pact_broker/api/resources/verifications.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def pact
5555
end
5656

5757
def next_verification_number
58-
@next_verification_number ||= verification_service.next_number_for(pact)
58+
@next_verification_number ||= verification_service.next_number
5959
end
6060

6161
def decorator_for model

lib/pact_broker/verifications/repository.rb

+9-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
require 'pact_broker/domain/verification'
33
require 'pact_broker/verifications/latest_verifications_by_consumer_version'
44
require 'pact_broker/verifications/all_verifications'
5+
require 'pact_broker/verifications/sequence'
56

67
module PactBroker
78
module Verifications
@@ -10,6 +11,14 @@ class Repository
1011
include PactBroker::Repositories::Helpers
1112
include PactBroker::Repositories
1213

14+
# Ideally this would just be a sequence, but Sqlite and MySQL don't support sequences
15+
# in the way we need to use them ie. determining what the next number will be before we
16+
# create the record, because Webmachine wants to set the URL of the resource that is about
17+
# to be created *before* we actually create it.
18+
def next_number
19+
Sequence.next_val
20+
end
21+
1322
def create verification, provider_version_number, pact
1423
provider = pacticipant_repository.find_by_name(pact.provider_name)
1524
version = version_repository.find_by_pacticipant_id_and_number_or_create(provider.id, provider_version_number)
@@ -18,10 +27,6 @@ def create verification, provider_version_number, pact
1827
verification.save
1928
end
2029

21-
def verification_count_for_pact pact
22-
PactBroker::Domain::Verification.where(pact_version_id: pact_version_id_for(pact)).count
23-
end
24-
2530
def find consumer_name, provider_name, pact_version_sha, verification_number
2631
PactBroker::Domain::Verification
2732
.select_all_qualified
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
2+
require 'sequel'
3+
4+
module PactBroker
5+
module Verifications
6+
class Sequence < Sequel::Model(:verification_sequence_number)
7+
8+
dataset_module do
9+
# The easiest way to implement a cross database compatible sequence.
10+
# Sad, I know.
11+
def next_val
12+
db.transaction do
13+
for_update.first
14+
select_all.update(value: Sequel[:value]+1)
15+
row = first
16+
if row
17+
row.value
18+
else
19+
# The first row should have been created in the migration, so this code
20+
# should only ever be executed in a test context.
21+
# There would be a risk of a race condition creating two rows if this
22+
# code executed in prod, as I don't think you can lock an empty table
23+
# to prevent another record being inserted.
24+
max_verification_number = PactBroker::Domain::Verification.max(:number)
25+
value = max_verification_number ? max_verification_number + 100 : 1
26+
insert(value: value)
27+
value
28+
end
29+
end
30+
end
31+
end
32+
end
33+
end
34+
end

lib/pact_broker/verifications/service.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ module Service
1212
extend PactBroker::Repositories
1313
extend PactBroker::Services
1414

15-
def next_number_for pact
16-
verification_repository.verification_count_for_pact(pact) + 1
15+
def next_number
16+
verification_repository.next_number
1717
end
1818

1919
def create next_verification_number, params, pact

spec/lib/pact_broker/api/resources/verifications_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ module Resources
4646

4747
before do
4848
allow(Pacts::Service).to receive(:find_pact).and_return(pact)
49-
allow(PactBroker::Verifications::Service).to receive(:next_number_for).and_return(next_verification_number)
49+
allow(PactBroker::Verifications::Service).to receive(:next_number).and_return(next_verification_number)
5050
allow(PactBroker::Api::Decorators::VerificationDecorator).to receive(:new).and_return(decorator)
5151
end
5252

spec/lib/pact_broker/verifications/repository_spec.rb

-29
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,6 @@
33
module PactBroker
44
module Verifications
55
describe Repository do
6-
7-
describe "#verification_count_for_pact" do
8-
let!(:pact_1) do
9-
TestDataBuilder.new
10-
.create_consumer("Consumer")
11-
.create_provider("Provider")
12-
.create_consumer_version("1.2.3")
13-
.create_pact
14-
.create_verification(number: 1)
15-
.create_verification(number: 2)
16-
.and_return(:pact)
17-
end
18-
let!(:pact_2) do
19-
TestDataBuilder.new
20-
.create_consumer("Foo")
21-
.create_provider("Bar")
22-
.create_consumer_version("4.5.6")
23-
.create_pact
24-
.create_verification(number: 1)
25-
.and_return(:pact)
26-
end
27-
28-
subject { Repository.new.verification_count_for_pact(pact_1) }
29-
30-
it "returns the number of verifications for the given pact" do
31-
expect(subject).to eq 2
32-
end
33-
end
34-
356
describe "#find" do
367
let!(:pact) do
378
builder = TestDataBuilder.new
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
require 'pact_broker/verifications/sequence'
2+
3+
module PactBroker
4+
module Verifications
5+
describe Sequence do
6+
describe "#next_val", migration: true do
7+
8+
before do
9+
PactBroker::Database.migrate
10+
end
11+
12+
context "when there is a row in the verification_sequence_number table" do
13+
before do
14+
Sequence.select_all.delete
15+
Sequence.insert(value: 1)
16+
end
17+
18+
it "increments the value and returns it" do
19+
expect(Sequence.next_val).to eq 2
20+
end
21+
end
22+
23+
context "when there is no row in the verification_sequence_number table and no existing verifications" do
24+
before do
25+
Sequence.select_all.delete
26+
end
27+
28+
it "inserts and returns the value 1" do
29+
expect(Sequence.next_val).to eq 1
30+
end
31+
end
32+
33+
context "when there is no row in the verification_sequence_number table and there are existing verifications" do
34+
before do
35+
Sequence.select_all.delete
36+
TestDataBuilder.new.create_pact_with_hierarchy("A", "1", "B")
37+
.create_verification(provider_version: "2")
38+
end
39+
40+
it "inserts a number that is guaranteed to be higher than any of the existing verification numbers from when we tried to do this without a sequence" do
41+
expect(Sequence.next_val).to eq 101
42+
end
43+
end
44+
end
45+
end
46+
end
47+
end

spec/lib/pact_broker/verifications/service_spec.rb

-13
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,6 @@ module Verifications
88

99
subject { PactBroker::Verifications::Service }
1010

11-
describe "#next_number_for" do
12-
13-
let(:pact) { double(:pact) }
14-
15-
before do
16-
allow_any_instance_of(PactBroker::Verifications::Repository).to receive(:verification_count_for_pact).and_return(2)
17-
end
18-
19-
it "returns the number for the next build to be recorded for a pact" do
20-
expect(subject.next_number_for(pact)).to eq 3
21-
end
22-
end
23-
2411
describe "#create" do
2512
let(:params) { {'success' => true, 'providerApplicationVersion' => '4.5.6'} }
2613
let(:pact) { TestDataBuilder.new.create_pact_with_hierarchy.and_return(:pact) }

0 commit comments

Comments
 (0)