pact_broker 2.109.0 → 2.110.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +1 -2
- data/docs/developer/design_pattern_for_eager_loading_collections.md +23 -0
- data/docs/developer/matrix.md +95 -0
- data/docs/developer/rack.md +11 -0
- data/lib/pact_broker/api/contracts/consumer_version_selector_contract.rb +1 -1
- data/lib/pact_broker/api/decorators/base_decorator.rb +10 -1
- data/lib/pact_broker/api/decorators/version_decorator.rb +14 -0
- data/lib/pact_broker/api/resources/branch_versions.rb +1 -1
- data/lib/pact_broker/api/resources/provider_pacts_for_verification.rb +5 -0
- data/lib/pact_broker/api/resources/versions.rb +1 -1
- data/lib/pact_broker/app.rb +24 -9
- data/lib/pact_broker/contracts/service.rb +6 -2
- data/lib/pact_broker/db/advisory_lock.rb +58 -0
- data/lib/pact_broker/domain/version.rb +3 -2
- data/lib/pact_broker/integrations/integration.rb +11 -1
- data/lib/pact_broker/integrations/repository.rb +46 -7
- data/lib/pact_broker/integrations/service.rb +2 -0
- data/lib/pact_broker/locale/en.yml +1 -1
- data/lib/pact_broker/matrix/matrix_row.rb +0 -1
- data/lib/pact_broker/matrix/resolved_selector.rb +0 -1
- data/lib/pact_broker/pacts/pact_publication_dataset_module.rb +56 -2
- data/lib/pact_broker/pacts/pact_publication_selector_dataset_module.rb +1 -0
- data/lib/pact_broker/pacts/pact_publication_wip_dataset_module.rb +59 -79
- data/lib/pact_broker/pacts/pacts_for_verification_repository.rb +5 -4
- data/lib/pact_broker/pacts/repository.rb +2 -2
- data/lib/pact_broker/pacts/selector.rb +8 -2
- data/lib/pact_broker/tasks/clean_task.rb +68 -26
- data/lib/pact_broker/test/test_data_builder.rb +8 -3
- data/lib/pact_broker/version.rb +1 -1
- data/lib/pact_broker/versions/branch_service.rb +1 -0
- data/lib/pact_broker/versions/repository.rb +2 -20
- data/lib/pact_broker/versions/service.rb +2 -6
- data/lib/pact_broker/webhooks/execution.rb +8 -8
- data/lib/sequel/extensions/pg_advisory_lock.rb +101 -0
- data/pact_broker.gemspec +1 -1
- metadata +10 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0747ca988604f56bec96bb0511242e8b3e9e2c18c823ec53742a1c58f80eee16
|
4
|
+
data.tar.gz: fbdc8b9be85cf443ccd8ddcb58704a1aece12759e267aea1b4f79ba0d05fbf9c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 416b29a9c6fe749e990ce9a051e526568d1a453c78db704d68e63002bb0ba1c17f8117b1d54ec540bea609168e26d2e7583bf087394aa249d9bacd6f0ad58ea5
|
7
|
+
data.tar.gz: b729576d06caac277699def3e48bfd1c75400467018da3b740fad3ff1ba84e628388369dddb60f964cf4b948d66d27096feca800f8e80314be762d327cdd1e54
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,28 @@
|
|
1
|
+
<a name="v2.110.0"></a>
|
2
|
+
### v2.110.0 (2024-04-02)
|
3
|
+
|
4
|
+
#### Features
|
5
|
+
|
6
|
+
* reduce contention when updating the contract_data_updated_at field for integrations (#671) ([ff72d03c](/../../commit/ff72d03c))
|
7
|
+
* support consumer version selector for all branches (#667) ([34334ca8](/../../commit/34334ca8))
|
8
|
+
|
9
|
+
* **clean**
|
10
|
+
* use postgres advisory locks to ensure only one process can run a clean at a time (#672) ([637c25fa](/../../commit/637c25fa))
|
11
|
+
|
12
|
+
#### Bug Fixes
|
13
|
+
|
14
|
+
* use for_all_tag_heads instead of latest_by_consumer_tag when fetching wip by branch ([14148a34](/../../commit/14148a34))
|
15
|
+
* optimise WIP pacts by using branch/tag heads (#668) ([871209e1](/../../commit/871209e1))
|
16
|
+
* improve performance of WIP pacts by using branch heads instead of calculating latest pact for branch ([f9705583](/../../commit/f9705583))
|
17
|
+
|
18
|
+
<a name="v2.109.1"></a>
|
19
|
+
### v2.109.1 (2024-02-21)
|
20
|
+
|
21
|
+
#### Bug Fixes
|
22
|
+
|
23
|
+
* improve performance for 'pacts for verification' queries ([299a6abe](/../../commit/299a6abe))
|
24
|
+
* correct spelling in message when pact is modified ([ae62ae7a](/../../commit/ae62ae7a))
|
25
|
+
|
1
26
|
<a name="v2.109.0"></a>
|
2
27
|
### v2.109.0 (2024-02-01)
|
3
28
|
|
data/README.md
CHANGED
@@ -2,7 +2,6 @@
|
|
2
2
|
[![Gem Version](https://badge.fury.io/rb/pact_broker.svg)](http://badge.fury.io/rb/pact_broker)
|
3
3
|
![Build status](https://github.com/pact-foundation/pact_broker/workflows/Test/badge.svg)
|
4
4
|
[![Join the chat at https://pact-foundation.slack.com/](https://img.shields.io/badge/chat-on%20slack-blue.svg?logo=slack)](https://slack.pact.io)
|
5
|
-
[![security](https://hakiri.io/github/pact-foundation/pact_broker/master.svg)](https://hakiri.io/github/pact-foundation/pact_broker/master)
|
6
5
|
|
7
6
|
The Pact Broker is an application for sharing of consumer driven contracts and verification results. It is optimised for use with "pacts" (contracts created by the [Pact][pact-docs] framework), but can be used for any type of contract that can be serialized to JSON.
|
8
7
|
|
@@ -151,7 +150,7 @@ You can use the [Pact Broker Docker image][docker] or [Terraform on AWS][terrafo
|
|
151
150
|
|
152
151
|
* Are you sure you don't just want to use the [Pact Broker Docker image][docker]? No Docker at your company yet? Ah well, keep reading.
|
153
152
|
* Create a PostgreSQL database.
|
154
|
-
* To ensure you're on a supported version of the database that you choose, check the [
|
153
|
+
* To ensure you're on a supported version of the database that you choose, check the [.github/workflows/test.yml](.github/workflows/test.yml) file to see which versions we're currently running our tests against.
|
155
154
|
* MySQL was supported for the native Ruby application until around 2021, but the official `pactfoundation/pact-broker` Docker image does not support it. New features will not be optimised for MySQL, and some new features may not even be supported on it (eg. the database clean feature).
|
156
155
|
* You'll find a sample database creation script in the [example/config.ru](https://github.com/pact-foundation/pact_broker/blob/master/example/config.ru).
|
157
156
|
* Install ruby 2.7 and the latest version of bundler (if you've come this far, I'm assuming you know how to do both of these. Did I mention there was a [Docker][docker] image?)
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Design pattern for eager loading collections
|
2
|
+
|
3
|
+
For collection resources (eg. `/versions` ), associations included in the items (eg. branch versions) must be eager loaded for performance reasons.
|
4
|
+
|
5
|
+
The responsiblities of each class used to render a collection resource are as follows:
|
6
|
+
|
7
|
+
* collection decorator (eg. `VersionsDecorator`) - delegate each item in the collection to be rendered by the decorator for the individual item, render pagination links
|
8
|
+
* item decorator (eg. `VersionDecorator`) - render the JSON for each item
|
9
|
+
* resource (eg. `PactBroker::Api::Resources::Versions`) - coordinate between, and delegate to, the service and the decorator
|
10
|
+
* service (eg. `PactBroker::Versions::Service`) - just delegate to repository, as there is no business logic required
|
11
|
+
* repository (eg. `PactBroker::Versions::Repository`) - load the domain objects from the database
|
12
|
+
|
13
|
+
If the associations for a model are not eager loaded, then each individual association will be lazy loaded when the decorator for the item calls the association method to render it. This results in at least `<number of items in the collection> * <number of associations to render>` calls to the database, and potentially more if any of the associations have their own associations that are required to render the item. This can cause significant performance issues.
|
14
|
+
|
15
|
+
To efficiently render a collection resource, associations must be eager loaded when the collection items are loaded from the database in the repository. Since the repository method for loading the collection may be used in multiple places, and the eager loaded associations required for each of those places may be different (some may not require any associations to be eager loaded), we do not want to hard code the repository to load a fixed set of associations. The list of associations to eager load is therefore passed in to the repository finder method as an argument `eager_load_associations`.
|
16
|
+
|
17
|
+
The decorator is the class that knows what associations are going to be called on the model to render the JSON, so following the design guideline of "put things together that change together", the best place for the declaration of "what associations should be eager loaded for this decorator" is in the decorator itself. The `PactBroker::Api::Decorators::BaseDecorator` has a default implementation of this method called `eager_load_associations` which attempts to automatically identify the required associations, but this can be overridden when necessary.
|
18
|
+
|
19
|
+
We can therefore add the following responsiblities to our previous list:
|
20
|
+
|
21
|
+
* item decorator - return a list of all the associations (including nested associations) that should be eager loaded in order to render its item
|
22
|
+
* repository - eager load the associations that have been passed into it
|
23
|
+
* resource - pass in the eager load associations to the repository from the decorator
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# The Matrix
|
2
|
+
|
3
|
+
Read [these docs](https://docs.pact.io/pact_broker/advanced_topics/matrix_selectors) first for a introduction to matrix selectors and options from a user's perspective.
|
4
|
+
|
5
|
+
## Terminology
|
6
|
+
|
7
|
+
### Selectors
|
8
|
+
|
9
|
+
#### Selector classes
|
10
|
+
|
11
|
+
* `Unresolved selectors` - `PactBroker::Matrix::UnresolvedSelector` the class of the specified and ignore selectors, as they are created directly from the HTTP query. At this point, they are "unresolved" in that we do not know the IDs of the pacticipants/versions that they represent, and whether or not they even exist.
|
12
|
+
|
13
|
+
* `Resolved selectors` - `PactBroker::Matrix::ResolvedSelector` the class of the object created for each selector, after its pacticipant and version IDs have been identified. If a selector represents multiple pacticipant versions (eg `{ branch: "main" }`) then one `ResolvedSelector` object will be returned for each pacticipant version found. The resolved selector keeps a reference to its original unresolved selector, as well as the pacticipant ID and the version ID. If the version or pacticipant specified does not actually exist, then a resolved selector is returned that indicates this.
|
14
|
+
|
15
|
+
#### Selector collections
|
16
|
+
|
17
|
+
* `Specified selectors` - the collection of objects that describe the application version(s) specified explicitly in the matrix query. eg. in `can-i-deploy --pacticipant Foo --version 1` the specified selector is `PactBroker::Matrix::UnresolvedSelector.new(pacticipant_name: "Foo", pacticipant_version_number: "1")`. There may be multiple specified selectors, for example, in the Matrix page for an integration Foo/Bar, the specified selectors would be `[ UnresolvedSelector.new(pacticipant_name: "Foo"), UnresolvedSelector.new(pacticipant_name: "Bar")]`. The selectors may use various combinations of pacticipant name, version number, branch, tag or environment to identify the pacticipant versions.
|
18
|
+
|
19
|
+
* `Ignore selectors` - the collection of objects that describe the applications (or application versions) to ignore, if there are any missing or failed verifications between the versions for the specified selectors and the ignore selectors. eg. If we know that provider Dog is not ready and the verifications are failing, but the integration is feature toggled off in the Foo code, we would use the command `can-i-deploy --pacticipant Foo --version 1 --to-environment production --ignore Dog` to allow `can-i-deploy` to pass. An ignore selector can have a version number, but it's more common to just provide the name.
|
20
|
+
|
21
|
+
* `Inferred selectors` - the collection of objects that describe the application versions(s) that already exist in the target environment/with the target tag/on the target branches. These are identified during the matrix query when the `can-i-deploy` query has a `--to TAG`/`--to-environment`/`--with-main-branches` option specified, and they allow us to find the full set of matrix rows that tell us whether or the application version we're about to deploy is compatible with its integrated applications. For example, given Foo v1 is a consumer of Bar, and Bar v8 is in production, and the `can-i-deploy` command is `can-i-deploy --pacticipant Foo --version 1 --to-environment production`, then an inferred unresolved selector is created for Bar in the production environment (`UnresolvedSelector.new(pacticipant_name: "Bar", environment_name: "production")`) which is then resolved to Bar v8.
|
22
|
+
|
23
|
+
#### Notes on selector classes/collections
|
24
|
+
|
25
|
+
Specified, ignore, and inferred selectors all start life as `UnresolvedSelector` objects, which then get resolved into `ResolvedSelector` objects.
|
26
|
+
|
27
|
+
### Options
|
28
|
+
|
29
|
+
TBC.
|
30
|
+
|
31
|
+
## How the matrix query works
|
32
|
+
|
33
|
+
1. Given that Foo v1 is a consumer of Bar, and Bar v8 is currently in production, take the pact-broker CLI command `can-i-deploy --pacticipant Foo --version 1 --to-environment production --ignore Dog`
|
34
|
+
|
35
|
+
1. Turn that into a query string and make a request to the `/matrix` endpoint.
|
36
|
+
|
37
|
+
1. Parse the HTTP query into the specified selectors, the ignore selectors and options.
|
38
|
+
|
39
|
+
* specified selectors - `PactBroker::Matrix::UnresolvedSelector.new(pacticipant_name: "Foo", pacticipant_version_number: "1")`
|
40
|
+
* ignore selectors - `PactBroker::Matrix::UnresolvedSelector.new(pacticipant_name: "Dog")`
|
41
|
+
* options: `{ to_environment: "production" }`
|
42
|
+
|
43
|
+
1. Validate the selectors.
|
44
|
+
|
45
|
+
* Ensure conflicting fields are not used.
|
46
|
+
* Return an error if any of the pacticipants in the specified selectors or the environment do not exist.
|
47
|
+
* Do not check for the existance of version numbers or tags, or pacticipants in the ignore selectors.
|
48
|
+
|
49
|
+
1. "Resolve" the ignore selectors (find the pacticipant and version IDs for the selectors).
|
50
|
+
|
51
|
+
1. "Resolve" the specified selectors, passing in the ignore selectors to identify whether or not the resolved selector should be "ignored".
|
52
|
+
|
53
|
+
1. Identify the integrations that the versions in the specified selectors are involved in.
|
54
|
+
|
55
|
+
* Identify the providers of the consumers: for the versions of each specified selector, find any pacts created for that version. If any exist, that means
|
56
|
+
the provider for each pact MUST be deployed to the target environment, and the deployed version must have a successful verification with the specified consumer version for the consumer to be safe to deploy. The providers must be identified for the specific consumer *version* of the selector, not just *consumer*, as different versions of a consumer may have different providers as integrations are created and removed over time. This is why we cannot just query the integrations table.
|
57
|
+
|
58
|
+
Represent the integration by creating a `PactBroker::Matrix::Integration` object for each consumer/provider pair, and mark it as `required: true`.
|
59
|
+
|
60
|
+
* Identify the consumers of the providers: for the pacticipant of each specified selector, find any integrations in the integrations table where
|
61
|
+
the specified pacticipant is the provider. Because specific provider versions don't have a dependency on any specific consumer versions being already present in an environment, we do not need to run this query at the pacticipant version level - we can just check the presence of any consumers at the integration level.
|
62
|
+
|
63
|
+
Represent the integration by creating a `PactBroker::Matrix::Integration` object for each consumer/provider pair, and mark it as `required: false`.
|
64
|
+
|
65
|
+
1. Identify the inferred unresolved selectors (the selectors for the pacticipant versions that are already in the target scope)
|
66
|
+
|
67
|
+
* Collect all the pacticipant names from the integrations identified in the previous step. For every pacticipant name that does NOT already have a specified selector, create a new unresolved "inferred" selector for that pacticipant, and set the version selection attributes to be the target scope from the original `can-i-deploy` query. eg. Given we have identifed the required `Integration` for consumer Foo and provider Bar, and we are determining if we can deploy Foo to production, create an unresolved selector for Bar in the production environment (`UnresolvedSelector.new(pacticipant_name: "Bar", environment_name: "production"`).
|
68
|
+
|
69
|
+
1. "Resolve" the inferred selectors (find the pacticipant and version IDs).
|
70
|
+
|
71
|
+
1. Add the specified and inferred resolved selectors together to make the final collection of selectors that will be used to query the matrix.
|
72
|
+
|
73
|
+
1. Peform the matrix query.
|
74
|
+
|
75
|
+
* When there are 2 or more total resolved selectors (the usual usecase):
|
76
|
+
|
77
|
+
* Create a collection of all the pacticipants in the selectors (let's call it `all_pacticipants_in_selectors`).
|
78
|
+
|
79
|
+
* Create the pact/consumer version dataset
|
80
|
+
|
81
|
+
* For each selector, find the pact publications where the consumer version is one of the versions described by the selector, and the provider is one of `all_pacticipants_in_selectors`.
|
82
|
+
|
83
|
+
* Create the verification/provider version dataset.
|
84
|
+
|
85
|
+
* For each selector, find the verifications where the provider version is one of the versions described by the selector, and the consumer is one of `all_pacticipants_in_selectors`.
|
86
|
+
|
87
|
+
* Join the pact/consumer verison dataset to the verification/provider version dataset.
|
88
|
+
|
89
|
+
* The two datasets are joined on the `pact_version_id` - this is the ID of the unique pact content that every pact publication and provider verification has a reference to. A left outer join is used, so that if there is a pact that doesn't have a verification, a row is present for the pact, with empty provider version columns. This allows us to identify that there is a missing verification.
|
90
|
+
|
91
|
+
|
92
|
+
* When there is only 1 total resolved selector: this is an unusual usecase, and cannot be done via the UI or the CLI, so I'm not going to spend time documenting it. Just know it is supported and theoretically possible.
|
93
|
+
|
94
|
+
|
95
|
+
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# Rack
|
2
|
+
|
3
|
+
https://medium.com/quick-code/rack-middleware-vs-rack-application-vs-rack-the-gem-vs-rack-the-architecture-912cd583ed24
|
4
|
+
https://github.com/rack/rack/blob/main/SPEC.rdoc
|
5
|
+
https://www.rubyguides.com/2018/09/rack-middleware/
|
6
|
+
|
7
|
+
|
8
|
+
* Responds to `call`
|
9
|
+
* Accepts a hash of parameters
|
10
|
+
* Returns an array where the first item is the http status, the second is a hash of headers, and the third is an object that responds to `each` (or `call`) that provides the body (99% of the time it's an array of length 1 with a string)
|
11
|
+
|
@@ -13,7 +13,7 @@ module PactBroker
|
|
13
13
|
json do
|
14
14
|
optional(:mainBranch).filled(included_in?: [true])
|
15
15
|
optional(:tag).filled(:str?)
|
16
|
-
optional(:branch).filled
|
16
|
+
optional(:branch).filled { str? | eql?(true) }
|
17
17
|
optional(:matchingBranch).filled(included_in?: [true])
|
18
18
|
optional(:latest).filled(included_in?: [true, false])
|
19
19
|
optional(:fallbackTag).filled(:str?)
|
@@ -34,7 +34,16 @@ module PactBroker
|
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
37
|
-
# Returns the names of the model associations to eager load for use with this decorator
|
37
|
+
# Returns the names of the model associations to eager load for use with this decorator.
|
38
|
+
# The default implementation attempts to do an "auto detect" of the associations.
|
39
|
+
# For single item decorators, it attempts to identify the attributes that are themselves models.
|
40
|
+
# For collection decorators, it delegates to the eager_load_associations
|
41
|
+
# method of the single item decorator used to decorate the collection.
|
42
|
+
#
|
43
|
+
# The "auto detect" logic can only go so far. It cannot identify when a child object needs its own
|
44
|
+
# child object(s) to render the attribute.
|
45
|
+
# This method should be overridden when the "auto detect" logic cannot identify the correct associations
|
46
|
+
# to load. eg VersionDecorator
|
38
47
|
# @return [Array<Symbol>]
|
39
48
|
def self.eager_load_associations
|
40
49
|
if is_collection_resource?
|
@@ -17,6 +17,20 @@ module PactBroker
|
|
17
17
|
|
18
18
|
include Timestamps
|
19
19
|
|
20
|
+
# Returns the list of associations that must be eager loaded to efficiently render a version
|
21
|
+
# when this decorator is used in a collection (eg. VersionsDecorator)
|
22
|
+
# The associations that need to be eager loaded for the VersionDecorator
|
23
|
+
# are hand coded
|
24
|
+
# @return <Array>
|
25
|
+
def self.eager_load_associations
|
26
|
+
[
|
27
|
+
:pacticipant,
|
28
|
+
:pact_publications,
|
29
|
+
{ branch_versions: [:version, :branch_head, { branch: :pacticipant }] },
|
30
|
+
{ tags: :head_tag }
|
31
|
+
]
|
32
|
+
end
|
33
|
+
|
20
34
|
link :self do | options |
|
21
35
|
{
|
22
36
|
title: "Version",
|
@@ -31,7 +31,7 @@ module PactBroker
|
|
31
31
|
end
|
32
32
|
|
33
33
|
def versions
|
34
|
-
@versions ||= version_service.find_pacticipant_versions_in_reverse_order(pacticipant_name, { branch_name: identifier_from_path[:branch_name] }, pagination_options)
|
34
|
+
@versions ||= version_service.find_pacticipant_versions_in_reverse_order(pacticipant_name, { branch_name: identifier_from_path[:branch_name] }, pagination_options, decorator_class(:versions_decorator).eager_load_associations)
|
35
35
|
end
|
36
36
|
|
37
37
|
def policy_name
|
@@ -15,6 +15,10 @@ module PactBroker
|
|
15
15
|
[["application/hal+json", :to_json]]
|
16
16
|
end
|
17
17
|
|
18
|
+
# TODO drop support for GET in next major version.
|
19
|
+
# GET was only used by the very first Ruby Pact clients that supported the 'pacts for verification'
|
20
|
+
# feature, until it became clear that the parameters for the request were going to get nested and complex,
|
21
|
+
# at which point the POST was added.
|
18
22
|
def allowed_methods
|
19
23
|
["GET", "POST", "OPTIONS"]
|
20
24
|
end
|
@@ -32,6 +36,7 @@ module PactBroker
|
|
32
36
|
end
|
33
37
|
end
|
34
38
|
|
39
|
+
# For this endoint, the POST is a "read" action (used for Pactflow)
|
35
40
|
def read_methods
|
36
41
|
super + %w{POST}
|
37
42
|
end
|
@@ -31,7 +31,7 @@ module PactBroker
|
|
31
31
|
end
|
32
32
|
|
33
33
|
def versions
|
34
|
-
@versions ||= version_service.
|
34
|
+
@versions ||= version_service.find_pacticipant_versions_in_reverse_order(pacticipant_name, {}, pagination_options, decorator_class(:versions_decorator).eager_load_associations)
|
35
35
|
end
|
36
36
|
|
37
37
|
def policy_name
|
data/lib/pact_broker/app.rb
CHANGED
@@ -27,6 +27,7 @@ require "pact_broker/config/basic_auth_configuration"
|
|
27
27
|
require "pact_broker/api/authorization/resource_access_policy"
|
28
28
|
require "pact_broker/api/middleware/http_debug_logs"
|
29
29
|
require "pact_broker/application_context"
|
30
|
+
require "pact_broker/db/advisory_lock"
|
30
31
|
|
31
32
|
module PactBroker
|
32
33
|
|
@@ -101,22 +102,19 @@ module PactBroker
|
|
101
102
|
|
102
103
|
def prepare_database
|
103
104
|
logger.info "Database schema version is #{PactBroker::DB.version(configuration.database_connection)}"
|
105
|
+
lock = PactBroker::DB::AdvisoryLock.new(configuration.database_connection, :migrate, :pg_advisory_lock)
|
104
106
|
if configuration.auto_migrate_db
|
105
|
-
|
106
|
-
|
107
|
-
logger.info "Skipping database migrations as the latest migration has already been applied"
|
108
|
-
else
|
109
|
-
logger.info "Migrating database schema"
|
110
|
-
PactBroker::DB.run_migrations configuration.database_connection, migration_options
|
111
|
-
logger.info "Database schema version is now #{PactBroker::DB.version(configuration.database_connection)}"
|
107
|
+
lock.with_lock do
|
108
|
+
ensure_all_database_migrations_are_applied
|
112
109
|
end
|
113
110
|
else
|
114
111
|
logger.info "Skipping database schema migrations as database auto migrate is disabled"
|
115
112
|
end
|
116
113
|
|
117
114
|
if configuration.auto_migrate_db_data
|
118
|
-
|
119
|
-
|
115
|
+
lock.with_lock do
|
116
|
+
run_data_migrations
|
117
|
+
end
|
120
118
|
else
|
121
119
|
logger.info "Skipping data migrations"
|
122
120
|
end
|
@@ -125,6 +123,23 @@ module PactBroker
|
|
125
123
|
PactBroker::Webhooks::Service.fail_retrying_triggered_webhooks
|
126
124
|
end
|
127
125
|
|
126
|
+
def ensure_all_database_migrations_are_applied
|
127
|
+
migration_options = { allow_missing_migration_files: configuration.allow_missing_migration_files }
|
128
|
+
|
129
|
+
if PactBroker::DB.is_current?(configuration.database_connection, migration_options)
|
130
|
+
logger.info "Skipping database migrations as the latest migration has already been applied"
|
131
|
+
else
|
132
|
+
logger.info "Migrating database schema"
|
133
|
+
PactBroker::DB.run_migrations(configuration.database_connection, migration_options)
|
134
|
+
logger.info "Database schema version is now #{PactBroker::DB.version(configuration.database_connection)}"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def run_data_migrations
|
139
|
+
logger.info "Migrating data"
|
140
|
+
PactBroker::DB.run_data_migrations(configuration.database_connection)
|
141
|
+
end
|
142
|
+
|
128
143
|
def load_configuration_from_database
|
129
144
|
configuration.load_from_database!
|
130
145
|
end
|
@@ -35,7 +35,7 @@ module PactBroker
|
|
35
35
|
version, version_notices = create_version(parsed_contracts)
|
36
36
|
tags = create_tags(parsed_contracts, version)
|
37
37
|
pacts, pact_notices = create_pacts(parsed_contracts, base_url)
|
38
|
-
|
38
|
+
create_or_update_integrations(pacts)
|
39
39
|
notices = version_notices + pact_notices
|
40
40
|
ContractsPublicationResults.from_hash(
|
41
41
|
pacticipant: version.pacticipant,
|
@@ -304,7 +304,11 @@ module PactBroker
|
|
304
304
|
PactBroker::Api::PactBrokerUrls.triggered_webhook_logs_url(triggered_webhook, base_url)
|
305
305
|
end
|
306
306
|
|
307
|
-
|
307
|
+
# Creating/updating the integrations all at once at the end of the transaction instead
|
308
|
+
# of one by one, as each pact is created, reduces the amount of time that
|
309
|
+
# a lock is held on the integrations table, therefore reducing contention
|
310
|
+
# and potential for deadlocks when there are many pacts being published at once.
|
311
|
+
def create_or_update_integrations(pacts)
|
308
312
|
integration_service.handle_bulk_contract_data_published(pacts)
|
309
313
|
end
|
310
314
|
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require "pact_broker/logging"
|
2
|
+
|
3
|
+
# Uses a Postgres advisory lock to ensure that a given block of code can only have ONE
|
4
|
+
# thread in excution at a time against a given database.
|
5
|
+
# When the database is not Postgres, the block will yield without any locks, allowing
|
6
|
+
# this class to be used safely with other database types, but without the locking
|
7
|
+
# functionality.
|
8
|
+
#
|
9
|
+
# This is a wrapper around the actual implementation code in the Sequel extension from https://github.com/yuryroot/sequel-pg_advisory_lock
|
10
|
+
# which was copied into this codebase and modified for usage in this codebase.
|
11
|
+
#
|
12
|
+
# See https://www.postgresql.org/docs/16/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS for docs on lock types
|
13
|
+
#
|
14
|
+
|
15
|
+
module PactBroker
|
16
|
+
module DB
|
17
|
+
class AdvisoryLock
|
18
|
+
include PactBroker::Logging
|
19
|
+
|
20
|
+
def initialize(database_connection, name, type = :pg_try_advisory_lock)
|
21
|
+
@database_connection = database_connection
|
22
|
+
@name = name
|
23
|
+
@type = type
|
24
|
+
@lock_obtained = false
|
25
|
+
register_advisory_lock if postgres?
|
26
|
+
end
|
27
|
+
|
28
|
+
def with_lock
|
29
|
+
if postgres?
|
30
|
+
@database_connection.with_advisory_lock(@name) do
|
31
|
+
logger.debug("Lock #{@name} obtained")
|
32
|
+
@lock_obtained = true
|
33
|
+
yield
|
34
|
+
end
|
35
|
+
else
|
36
|
+
logger.warn("Executing block without lock as this is not a Postgres database")
|
37
|
+
@lock_obtained = true
|
38
|
+
yield
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def lock_obtained?
|
43
|
+
@lock_obtained
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def postgres?
|
49
|
+
@database_connection.adapter_scheme.to_s =~ /postgres/
|
50
|
+
end
|
51
|
+
|
52
|
+
def register_advisory_lock
|
53
|
+
@database_connection.extension :pg_advisory_lock
|
54
|
+
@database_connection.register_advisory_lock(@name, @type)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -4,9 +4,10 @@ require "pact_broker/versions/eager_loaders"
|
|
4
4
|
|
5
5
|
module PactBroker
|
6
6
|
module Domain
|
7
|
-
|
7
|
+
class Version < Sequel::Model
|
8
|
+
VERSION_COLUMNS = Sequel::Model.db.schema(:versions).collect(&:first) - [:branch] # do not include the branch column, as we now have a branches table
|
9
|
+
set_dataset(Sequel::Model.db[:versions].select(*VERSION_COLUMNS.collect{ | column | Sequel.qualify(:versions, column) }))
|
8
10
|
|
9
|
-
class Version < Sequel::Model(Sequel::Model.db[:versions].select(*VERSION_COLUMNS.collect{ | column | Sequel.qualify(:versions, column) }))
|
10
11
|
set_primary_key :id
|
11
12
|
|
12
13
|
plugin :timestamps, update_on_create: true
|
@@ -7,7 +7,17 @@ require "pact_broker/verifications/latest_verification_for_consumer_and_provider
|
|
7
7
|
|
8
8
|
module PactBroker
|
9
9
|
module Integrations
|
10
|
-
|
10
|
+
# The columns are explicitly specified for the Integration object so that the consumer_name and provider_name columns aren't included
|
11
|
+
# in the model.
|
12
|
+
# Those columns exist in the integrations table because the integrations table used to be an integrations view based on the
|
13
|
+
# pact_publications table, and those columns existed in the view.
|
14
|
+
# When the view was migrated to be a table (in db/migrations/20211102_create_table_temp_integrations.rb and the following migrations)
|
15
|
+
# the columns had to be maintained for backwards compatiblity.
|
16
|
+
# They are not used by the current code, however.
|
17
|
+
class Integration < Sequel::Model
|
18
|
+
INTEGRATION_COLUMNS = Sequel::Model.db.schema(:integrations).collect(&:first) - [:consumer_name, :provider_name]
|
19
|
+
set_dataset(Sequel::Model.db[:integrations].select(*INTEGRATION_COLUMNS))
|
20
|
+
|
11
21
|
set_primary_key :id
|
12
22
|
plugin :insert_ignore, identifying_columns: [:consumer_id, :provider_id]
|
13
23
|
associate(:many_to_one, :consumer, :class => "PactBroker::Domain::Pacticipant", :key => :consumer_id, :primary_key => :id)
|
@@ -28,6 +28,20 @@ module PactBroker
|
|
28
28
|
nil
|
29
29
|
end
|
30
30
|
|
31
|
+
# Ensure an Integration exists for each consumer/provider pair.
|
32
|
+
# Using SELECT ... INSERT IGNORE rather than just INSERT IGNORE so that we do not
|
33
|
+
# need to lock the table at all when the integrations already exist, which will
|
34
|
+
# be the most common use case. New integrations get created incredibly rarely.
|
35
|
+
# The INSERT IGNORE is used rather than just INSERT to handle race conditions
|
36
|
+
# when requests come in parallel.
|
37
|
+
# @param [Array<Object>] where each object has a consumer and a provider
|
38
|
+
def create_for_pacts(objects_with_consumer_and_provider)
|
39
|
+
published_integrations = objects_with_consumer_and_provider.collect{ |i| { consumer_id: i.consumer.id, provider_id: i.provider.id } }
|
40
|
+
existing_integrations = Sequel::Model.db[:integrations].select(:consumer_id, :provider_id).where(Sequel.|(*published_integrations) ).all
|
41
|
+
new_integrations = (published_integrations - existing_integrations).collect{ |i| i.merge(created_at: Sequel.datetime_class.now, contract_data_updated_at: Sequel.datetime_class.now) }
|
42
|
+
Integration.dataset.insert_ignore.multi_insert(new_integrations)
|
43
|
+
end
|
44
|
+
|
31
45
|
def delete(consumer_id, provider_id)
|
32
46
|
Integration.where(consumer_id: consumer_id, provider_id: provider_id).delete
|
33
47
|
end
|
@@ -36,18 +50,43 @@ module PactBroker
|
|
36
50
|
# @param [PactBroker::Domain::Pacticipant, nil] consumer the consumer for the integration, or nil if for a provider-only event (eg. Pactflow provider contract published)
|
37
51
|
# @param [PactBroker::Domain::Pacticipant] provider the provider for the integration
|
38
52
|
def set_contract_data_updated_at(consumer, provider)
|
39
|
-
|
40
|
-
.where({ consumer_id: consumer&.id, provider_id: provider.id }.compact )
|
41
|
-
.update(contract_data_updated_at: Sequel.datetime_class.now)
|
53
|
+
set_contract_data_updated_at_for_multiple_integrations([OpenStruct.new(consumer: consumer, provider: provider)])
|
42
54
|
end
|
43
55
|
|
44
56
|
|
45
|
-
# Sets the contract_data_updated_at for the integrations as specified by an array of objects which each have a consumer and provider
|
46
|
-
#
|
57
|
+
# Sets the contract_data_updated_at for the integrations as specified by an array of objects which each have a consumer and provider.
|
58
|
+
#
|
59
|
+
# The contract_data_updated_at attribute is only ever used for ordering the list of integrations on the index page of the *Pact Broker* UI,
|
60
|
+
# so that the most recently updated integrations (the ones you're likely working on) are showed at the top of the first page.
|
61
|
+
# There is often contention around updating it however, which can cause deadlocks, and slow down API responses.
|
62
|
+
# Because it's not a critical field (eg. it won't change any can-i-deploy results), the easiest way to reduce this contention
|
63
|
+
# is to just not update it if the row is locked, because if it is locked, the value of contract_data_updated_at is already
|
64
|
+
# going to be a date from a few seconds ago, which is perfectly fine for the purposes for which we are using the value.
|
65
|
+
#
|
66
|
+
# Notes on SKIP LOCKED:
|
67
|
+
# SKIP LOCKED is only supported by Postgres.
|
68
|
+
# When executing SELECT ... FOR UPDATE SKIP LOCKED, the SELECT will run immediately, not waiting for any other transactions,
|
69
|
+
# and only return rows that are not already locked by another transaction.
|
70
|
+
# The FOR UPDATE is required to make it work this way - SKIP LOCKED on its own does not work.
|
71
|
+
#
|
72
|
+
# @param [Array<Object>] where each object MAY have a consumer and does have a provider (for Pactflow provider contract published there is no consumer)
|
47
73
|
def set_contract_data_updated_at_for_multiple_integrations(objects_with_consumer_and_provider)
|
48
|
-
consumer_and_provider_ids = objects_with_consumer_and_provider.collect{ | object |
|
74
|
+
consumer_and_provider_ids = objects_with_consumer_and_provider.collect{ | object | { consumer_id: object.consumer&.id, provider_id: object.provider.id }.compact }.uniq
|
75
|
+
|
76
|
+
# MySQL doesn't support an UPDATE with a subquery. FFS. Really need to do a major version release and delete the support code.
|
77
|
+
criteria = if Integration.dataset.supports_skip_locked?
|
78
|
+
integration_ids_to_update = Integration
|
79
|
+
.select(:id)
|
80
|
+
.where(Sequel.|(*consumer_and_provider_ids))
|
81
|
+
.for_update
|
82
|
+
.skip_locked
|
83
|
+
{ id: integration_ids_to_update }
|
84
|
+
else
|
85
|
+
Sequel.|(*consumer_and_provider_ids)
|
86
|
+
end
|
87
|
+
|
49
88
|
Integration
|
50
|
-
.where(
|
89
|
+
.where(criteria)
|
51
90
|
.update(contract_data_updated_at: Sequel.datetime_class.now)
|
52
91
|
end
|
53
92
|
end
|
@@ -21,6 +21,7 @@ module PactBroker
|
|
21
21
|
# @param [PactBroker::Domain::Pacticipant] consumer or nil
|
22
22
|
# @param [PactBroker::Domain::Pacticipant] provider
|
23
23
|
def self.handle_contract_data_published(consumer, provider)
|
24
|
+
integration_repository.create_for_pact(consumer.id, provider.id)
|
24
25
|
integration_repository.set_contract_data_updated_at(consumer, provider)
|
25
26
|
end
|
26
27
|
|
@@ -28,6 +29,7 @@ module PactBroker
|
|
28
29
|
# Callback to invoke when a batch of contract data is published (eg. the publish contracts endpoint)
|
29
30
|
# @param [Array<Object>] where each object has a consumer and a provider
|
30
31
|
def self.handle_bulk_contract_data_published(objects_with_consumer_and_provider)
|
32
|
+
integration_repository.create_for_pacts(objects_with_consumer_and_provider)
|
31
33
|
integration_repository.set_contract_data_updated_at_for_multiple_integrations(objects_with_consumer_and_provider)
|
32
34
|
end
|
33
35
|
|
@@ -50,7 +50,7 @@ en:
|
|
50
50
|
contract:
|
51
51
|
pact_published: Pact successfully published for %{consumer_name} version %{consumer_version_number} and provider %{provider_name}.
|
52
52
|
same_pact_content_published: Pact successfully republished for %{consumer_name} version %{consumer_version_number} and provider %{provider_name} with no content changes.
|
53
|
-
pact_modified_for_same_version: Pact with changed content published over existing content for %{consumer_name} version %{consumer_version_number} and provider %{provider_name}. This is not recommended in normal
|
53
|
+
pact_modified_for_same_version: Pact with changed content published over existing content for %{consumer_name} version %{consumer_version_number} and provider %{provider_name}. This is not recommended in normal circumstances and may indicate that you have not configured your Pact pipeline correctly. For more information see https://docs.pact.io/go/versioning
|
54
54
|
pact_merged: Pact content merged with existing content for %{consumer_name} version %{consumer_version_number} and provider %{provider_name}.
|
55
55
|
events:
|
56
56
|
pact_published_unchanged_with_single_tag: pact content is the same as previous version with tag %{tag_name} and no new tags were applied
|