pact_broker 2.109.1 → 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 +17 -0
- 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/pacts/pact_publication_dataset_module.rb +56 -3
- data/lib/pact_broker/pacts/pact_publication_selector_dataset_module.rb +1 -0
- data/lib/pact_broker/pacts/pacts_for_verification_repository.rb +4 -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/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,20 @@
|
|
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
|
+
|
1
18
|
<a name="v2.109.1"></a>
|
2
19
|
### v2.109.1 (2024-02-21)
|
3
20
|
|
@@ -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
|
|
@@ -57,7 +57,13 @@ module PactBroker
|
|
57
57
|
end
|
58
58
|
end
|
59
59
|
|
60
|
-
#
|
60
|
+
# Returns the latest pact for each branch, returning a pact for every branch, even if
|
61
|
+
# the most recent version of that branch does not have a pact.
|
62
|
+
# This is different from for_all_branch_heads, which will find the branch head versions,
|
63
|
+
# and return the pacts associated with those versions.
|
64
|
+
# This method should not be used for 'pacts for verification', because it will return
|
65
|
+
# a pact for branches where that integration should no longer exist.
|
66
|
+
# @return [Dataset<PactBroker::Pacts::PactPublication>]
|
61
67
|
def latest_by_consumer_branch
|
62
68
|
branch_versions_join = {
|
63
69
|
Sequel[:pact_publications][:consumer_version_id] => Sequel[:branch_versions][:version_id]
|
@@ -112,10 +118,29 @@ module PactBroker
|
|
112
118
|
.limit(1)
|
113
119
|
end
|
114
120
|
|
115
|
-
#
|
121
|
+
# Returns the pacts (if they exist) for all the branch heads.
|
122
|
+
# If the version for the branch head does not have a pact, then no pact is returned,
|
123
|
+
# (unlike latest_by_consumer_branch)
|
124
|
+
# This is much more performant than latest_by_consumer_branch and should be used
|
125
|
+
# for the 'pacts for verification' response
|
126
|
+
# @return [Dataset<PactBroker::Pacts::PactPublication>]
|
127
|
+
def for_all_branch_heads
|
128
|
+
base_query = self
|
129
|
+
base_query = base_query.join(:branch_heads, { Sequel[:bh][:version_id] => Sequel[:pact_publications][:consumer_version_id] }, { table_alias: :bh })
|
130
|
+
|
131
|
+
if no_columns_selected?
|
132
|
+
base_query = base_query.select_all_qualified.select_append(Sequel[:bh][:branch_name].as(:branch_name))
|
133
|
+
end
|
134
|
+
|
135
|
+
base_query.remove_overridden_revisions
|
136
|
+
end
|
137
|
+
|
138
|
+
# Return the pacts (if they exist) for the branch heads of the given branch names
|
116
139
|
# This uses the new logic of finding the branch head and returning any associated pacts,
|
117
140
|
# rather than the old logic of returning the pact for the latest version
|
118
141
|
# on the branch that had a pact.
|
142
|
+
# @param [String] branch_name
|
143
|
+
# @return [Sequel::Dataset<PactBroker::Pacts::PactPublication>]
|
119
144
|
def for_branch_heads(branch_name)
|
120
145
|
branch_head_join = {
|
121
146
|
Sequel[:pact_publications][:consumer_version_id] => Sequel[:branch_heads][:version_id],
|
@@ -174,7 +199,10 @@ module PactBroker
|
|
174
199
|
# The latest pact publication for each tag
|
175
200
|
# This uses the old logic of "the latest pact for a version that has a tag" (which always returns a pact)
|
176
201
|
# rather than "the pact for the latest version with a tag"
|
177
|
-
#
|
202
|
+
#
|
203
|
+
# For 'pacts for verification' this has been replaced by for_all_tag_heads
|
204
|
+
# This should only be used for the UI
|
205
|
+
# @return [Sequel::Dataset<PactBroker::Pacts::PactPublication>]
|
178
206
|
def latest_by_consumer_tag
|
179
207
|
tags_join = {
|
180
208
|
Sequel[:pact_publications][:consumer_version_id] => Sequel[:tags][:version_id],
|
@@ -203,6 +231,7 @@ module PactBroker
|
|
203
231
|
# This uses the old logic of "the latest pact for a version that has a tag" (which always returns a pact)
|
204
232
|
# rather than "the pact for the latest version with a tag"
|
205
233
|
# Need to see about updating this.
|
234
|
+
# @return [Sequel::Dataset<PactBroker::Pacts::PactPublication>]
|
206
235
|
def latest_for_consumer_tag(tag_name)
|
207
236
|
tags_join = {
|
208
237
|
Sequel[:pact_publications][:consumer_version_id] => Sequel[:tags][:version_id],
|
@@ -253,6 +282,30 @@ module PactBroker
|
|
253
282
|
.remove_overridden_revisions_from_complete_query
|
254
283
|
end
|
255
284
|
|
285
|
+
# The pacts for the latest versions for each tag.
|
286
|
+
# Will not return a pact if the pact is no longer published for a particular tag
|
287
|
+
# NEW LOGIC
|
288
|
+
# @return [Sequel::Dataset<PactBroker::Pacts::PactPublication>]
|
289
|
+
def for_all_tag_heads
|
290
|
+
head_tags = PactBroker::Domain::Tag
|
291
|
+
.select_group(:pacticipant_id, :name)
|
292
|
+
.select_append{ max(version_order).as(:latest_version_order) }
|
293
|
+
|
294
|
+
head_tags_join = {
|
295
|
+
Sequel[:pact_publications][:consumer_id] => Sequel[:head_tags][:pacticipant_id],
|
296
|
+
Sequel[:pact_publications][:consumer_version_order] => Sequel[:head_tags][:latest_version_order]
|
297
|
+
}
|
298
|
+
|
299
|
+
base_query = self
|
300
|
+
if no_columns_selected?
|
301
|
+
base_query = base_query.select_all_qualified.select_append(Sequel[:head_tags][:name].as(:tag_name))
|
302
|
+
end
|
303
|
+
|
304
|
+
base_query
|
305
|
+
.join(head_tags, head_tags_join, { table_alias: :head_tags })
|
306
|
+
.remove_overridden_revisions_from_complete_query
|
307
|
+
end
|
308
|
+
|
256
309
|
def in_environments
|
257
310
|
currently_deployed_join = {
|
258
311
|
Sequel[:pact_publications][:consumer_version_id] => Sequel[:currently_deployed_version_ids][:version_id]
|
@@ -15,6 +15,7 @@ module PactBroker
|
|
15
15
|
|
16
16
|
# Do the "latest" logic last so that the provider/consumer criteria get included in the "latest" query before the join, rather than after
|
17
17
|
query = query.latest_for_main_branches if selector.latest_for_main_branch?
|
18
|
+
query = query.for_all_branch_heads if selector.latest_for_each_branch?
|
18
19
|
query = query.latest_for_consumer_branch(selector.branch) if selector.latest_for_branch?
|
19
20
|
query = query.for_latest_consumer_versions_with_tag(selector.tag) if selector.latest_for_tag?
|
20
21
|
query = query.overall_latest if selector.overall_latest?
|
@@ -64,7 +64,7 @@ module PactBroker
|
|
64
64
|
provider_tags_names,
|
65
65
|
wip_start_date,
|
66
66
|
explicitly_specified_verifiable_pacts,
|
67
|
-
:
|
67
|
+
:for_all_tag_heads
|
68
68
|
)
|
69
69
|
|
70
70
|
wip_by_consumer_branches = find_wip_pact_versions_for_provider_by_provider_tags(
|
@@ -72,7 +72,7 @@ module PactBroker
|
|
72
72
|
provider_tags_names,
|
73
73
|
wip_start_date,
|
74
74
|
explicitly_specified_verifiable_pacts,
|
75
|
-
:
|
75
|
+
:for_all_branch_heads
|
76
76
|
)
|
77
77
|
|
78
78
|
deduplicate_verifiable_pacts(wip_by_consumer_tags + wip_by_consumer_branches).sort
|
@@ -229,8 +229,8 @@ module PactBroker
|
|
229
229
|
provider = pacticipant_repository.find_by_name(provider_name)
|
230
230
|
wip_start_date = options.fetch(:include_wip_pacts_since)
|
231
231
|
|
232
|
-
potential_wip_by_consumer_branch = PactPublication.for_provider(provider).created_after(wip_start_date).
|
233
|
-
potential_wip_by_consumer_tag = PactPublication.for_provider(provider).created_after(wip_start_date).
|
232
|
+
potential_wip_by_consumer_branch = PactPublication.for_provider(provider).created_after(wip_start_date).for_all_branch_heads
|
233
|
+
potential_wip_by_consumer_tag = PactPublication.for_provider(provider).created_after(wip_start_date).for_all_tag_heads
|
234
234
|
|
235
235
|
log_debug_for_wip do
|
236
236
|
log_pact_publications_from_query("Potential WIP pacts for provider branch #{provider_version_branch} created after #{wip_start_date} by consumer branch", potential_wip_by_consumer_branch)
|
@@ -33,8 +33,8 @@ module PactBroker
|
|
33
33
|
scope
|
34
34
|
end
|
35
35
|
|
36
|
-
|
37
|
-
|
36
|
+
# @return [PactBroker::Domain::Pact]
|
37
|
+
def create(params)
|
38
38
|
pact_version = find_or_create_pact_version(
|
39
39
|
params.fetch(:consumer_id),
|
40
40
|
params.fetch(:provider_id),
|
@@ -31,6 +31,8 @@ module PactBroker
|
|
31
31
|
def type
|
32
32
|
if latest_for_main_branch?
|
33
33
|
:latest_for_main_branch
|
34
|
+
elsif latest_for_each_branch?
|
35
|
+
:latest_for_each_branch
|
34
36
|
elsif latest_for_branch?
|
35
37
|
:latest_for_branch
|
36
38
|
elsif matching_branch?
|
@@ -265,12 +267,16 @@ module PactBroker
|
|
265
267
|
# Not sure if the fallback_tag logic is needed
|
266
268
|
def latest_for_branch? potential_branch = nil
|
267
269
|
if potential_branch
|
268
|
-
|
270
|
+
latest == true && branch == potential_branch
|
269
271
|
else
|
270
|
-
|
272
|
+
latest == true && branch.is_a?(String)
|
271
273
|
end
|
272
274
|
end
|
273
275
|
|
276
|
+
def latest_for_each_branch?
|
277
|
+
latest == true && branch == true
|
278
|
+
end
|
279
|
+
|
274
280
|
def all_for_tag_and_consumer?
|
275
281
|
!!(tag && !latest? && consumer)
|
276
282
|
end
|
@@ -1,3 +1,8 @@
|
|
1
|
+
# This task is used to clean up old data in a Pact Broker database
|
2
|
+
# to stop performance issues from slowing down responses when there is
|
3
|
+
# too much data.
|
4
|
+
# See https://docs.pact.io/pact_broker/administration/maintenance
|
5
|
+
|
1
6
|
module PactBroker
|
2
7
|
module DB
|
3
8
|
class CleanTask < ::Rake::TaskLib
|
@@ -7,11 +12,13 @@ module PactBroker
|
|
7
12
|
attr_accessor :version_deletion_limit
|
8
13
|
attr_accessor :logger
|
9
14
|
attr_accessor :dry_run
|
15
|
+
attr_accessor :use_lock # allow disabling of postgres lock if it is causing problems
|
10
16
|
|
11
17
|
def initialize &block
|
12
18
|
require "pact_broker/db/clean_incremental"
|
13
19
|
@version_deletion_limit = 1000
|
14
20
|
@dry_run = false
|
21
|
+
@use_lock = true
|
15
22
|
@keep_version_selectors = PactBroker::DB::CleanIncremental::DEFAULT_KEEP_SELECTORS
|
16
23
|
rake_task(&block)
|
17
24
|
end
|
@@ -28,42 +35,77 @@ module PactBroker
|
|
28
35
|
namespace :db do
|
29
36
|
desc "Clean unnecessary pacts and verifications from database"
|
30
37
|
task :clean do | _t, _args |
|
31
|
-
|
32
38
|
instance_eval(&block)
|
33
39
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
40
|
+
with_lock do
|
41
|
+
perform_clean
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def perform_clean
|
49
|
+
require "pact_broker/db/clean_incremental"
|
50
|
+
require "pact_broker/error"
|
51
|
+
require "yaml"
|
52
|
+
require "benchmark"
|
38
53
|
|
39
|
-
|
54
|
+
raise PactBroker::Error.new("You must specify the version_deletion_limit") unless version_deletion_limit
|
40
55
|
|
41
|
-
|
56
|
+
if keep_version_selectors.nil? || keep_version_selectors.empty?
|
57
|
+
raise PactBroker::Error.new("You must specify which versions to keep")
|
58
|
+
else
|
59
|
+
add_defaults_to_keep_selectors
|
60
|
+
output "Deleting oldest #{version_deletion_limit} versions, keeping versions that match the configured selectors", keep_version_selectors.collect(&:to_hash)
|
61
|
+
end
|
42
62
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
63
|
+
start_time = Time.now
|
64
|
+
results = PactBroker::DB::CleanIncremental.call(database_connection,
|
65
|
+
keep: keep_version_selectors,
|
66
|
+
limit: version_deletion_limit,
|
67
|
+
logger: logger,
|
68
|
+
dry_run: dry_run
|
69
|
+
)
|
70
|
+
end_time = Time.now
|
71
|
+
elapsed_seconds = (end_time - start_time).to_i
|
72
|
+
output "Results (#{elapsed_seconds} seconds)", results
|
73
|
+
end
|
49
74
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
75
|
+
# Use a Postgres advisory lock to ensure that only one clean can run at a time.
|
76
|
+
# This allows a cron schedule to be used on the Pact Broker Docker image when deployed
|
77
|
+
# on a multi-instance architecture, without all the instances stepping on each other's toes.
|
78
|
+
#
|
79
|
+
# Any tasks that attempt to run while a clean job is running will skip the clean
|
80
|
+
# and exit with a message and a success code.
|
81
|
+
#
|
82
|
+
# To test that the lock works, run:
|
83
|
+
# script/docker/db-start.sh
|
84
|
+
# script/docker/db-migrate.sh
|
85
|
+
# for i in {0..3}; do PACT_BROKER_TEST_DATABASE_URL=postgres://postgres:postgres@localhost/postgres bundle exec rake pact_broker:db:clean &; done;
|
86
|
+
#
|
87
|
+
# There will be 3 messages saying "Clean was not performed" and output from one thread showing the clean is being done.
|
88
|
+
def with_lock
|
89
|
+
if use_lock
|
90
|
+
require "pact_broker/db/advisory_lock"
|
91
|
+
|
92
|
+
lock = PactBroker::DB::AdvisoryLock.new(database_connection, :clean, :pg_try_advisory_lock)
|
93
|
+
results = lock.with_lock do
|
94
|
+
yield
|
95
|
+
end
|
96
|
+
|
97
|
+
if !lock.lock_obtained?
|
98
|
+
output("Clean was not performed as a clean is already in progress. Exiting.")
|
61
99
|
end
|
100
|
+
results
|
101
|
+
else
|
102
|
+
yield
|
62
103
|
end
|
63
104
|
end
|
64
105
|
|
65
|
-
def output
|
66
|
-
|
106
|
+
def output(string, payload = {})
|
107
|
+
prefix = dry_run ? "[DRY RUN] " : ""
|
108
|
+
logger ? logger.info("#{prefix}#{string}") : puts("#{prefix}#{string} #{payload.to_json}")
|
67
109
|
end
|
68
110
|
|
69
111
|
def add_defaults_to_keep_selectors
|
@@ -43,7 +43,6 @@ module PactBroker
|
|
43
43
|
include PactBroker::Services
|
44
44
|
using PactBroker::StringRefinements
|
45
45
|
|
46
|
-
|
47
46
|
attr_reader :pacticipant
|
48
47
|
attr_reader :consumer
|
49
48
|
attr_reader :provider
|
@@ -192,8 +191,11 @@ module PactBroker
|
|
192
191
|
self
|
193
192
|
end
|
194
193
|
|
194
|
+
# Create an Integration object for the current consumer and provider
|
195
|
+
# @return [PactBroker::Test::TestDataBuilder]
|
195
196
|
def create_integration
|
196
|
-
PactBroker::Integrations::Repository.new.create_for_pact(consumer.id, provider.id)
|
197
|
+
@integration = PactBroker::Integrations::Repository.new.create_for_pact(consumer.id, provider.id)
|
198
|
+
set_created_at_if_set(@now, :integrations, { consumer_id: consumer.id, provider_id: provider.id })
|
197
199
|
self
|
198
200
|
end
|
199
201
|
|
@@ -280,7 +282,9 @@ module PactBroker
|
|
280
282
|
self
|
281
283
|
end
|
282
284
|
|
283
|
-
|
285
|
+
# Creates a pact (and integration if one does not already exist) from the given params
|
286
|
+
# @return [PactBroker::Test::TestDataBuilder]
|
287
|
+
def create_pact(params = {})
|
284
288
|
params.delete(:comment)
|
285
289
|
json_content = params[:json_content] || default_json_content
|
286
290
|
pact_version_sha = params[:pact_version_sha] || generate_pact_version_sha(json_content)
|
@@ -293,6 +297,7 @@ module PactBroker
|
|
293
297
|
json_content: prepare_json_content(json_content),
|
294
298
|
version: @consumer_version
|
295
299
|
)
|
300
|
+
integration_service.handle_bulk_contract_data_published([@pact])
|
296
301
|
pact_versions_count_after = PactBroker::Pacts::PactVersion.count
|
297
302
|
set_created_at_if_set(params[:created_at], :pact_publications, id: @pact.id)
|
298
303
|
set_created_at_if_set(params[:created_at], :pact_versions, sha: @pact.pact_version_sha) if pact_versions_count_after > pact_versions_count_before
|
data/lib/pact_broker/version.rb
CHANGED
@@ -57,29 +57,11 @@ module PactBroker
|
|
57
57
|
.single_record
|
58
58
|
end
|
59
59
|
|
60
|
-
|
61
|
-
# Newer "find all" implementations for other models pass the relations to eager load in
|
62
|
-
# from the decorator via the resource.
|
63
|
-
def find_all_pacticipant_versions_in_reverse_order name, pagination_options = {}
|
64
|
-
pacticipant = pacticipant_repository.find_by_name!(name)
|
65
|
-
query = PactBroker::Domain::Version
|
66
|
-
.where(pacticipant: pacticipant)
|
67
|
-
.eager(:pacticipant)
|
68
|
-
.eager(branch_versions: [:version, :branch_head, { branch: :pacticipant }])
|
69
|
-
.eager(tags: :head_tag)
|
70
|
-
.eager(:pact_publications)
|
71
|
-
.reverse_order(:order)
|
72
|
-
query.all_with_pagination_options(pagination_options)
|
73
|
-
end
|
74
|
-
|
75
|
-
def find_pacticipant_versions_in_reverse_order(pacticipant_name, options = {}, pagination_options = {})
|
60
|
+
def find_pacticipant_versions_in_reverse_order(pacticipant_name, options = {}, pagination_options = {}, eager_load_associations = [])
|
76
61
|
pacticipant = pacticipant_repository.find_by_name!(pacticipant_name)
|
77
62
|
query = PactBroker::Domain::Version
|
78
63
|
.where(pacticipant: pacticipant)
|
79
|
-
.eager(
|
80
|
-
.eager(branch_versions: [:version, :branch_head, { branch: :pacticipant }])
|
81
|
-
.eager(tags: :head_tag)
|
82
|
-
.eager(:pact_publications)
|
64
|
+
.eager(*eager_load_associations)
|
83
65
|
.reverse_order(:order)
|
84
66
|
|
85
67
|
if options[:branch_name]
|
@@ -26,12 +26,8 @@ module PactBroker
|
|
26
26
|
version_repository.find_latest_by_pacticipant_name_and_branch_name(pacticipant_name, branch_name)
|
27
27
|
end
|
28
28
|
|
29
|
-
def self.
|
30
|
-
version_repository.
|
31
|
-
end
|
32
|
-
|
33
|
-
def self.find_pacticipant_versions_in_reverse_order(pacticipant_name, options, pagination_options = {})
|
34
|
-
version_repository.find_pacticipant_versions_in_reverse_order(pacticipant_name, options, pagination_options)
|
29
|
+
def self.find_pacticipant_versions_in_reverse_order(pacticipant_name, options, pagination_options = {}, eager_load_associations = [])
|
30
|
+
version_repository.find_pacticipant_versions_in_reverse_order(pacticipant_name, options, pagination_options, eager_load_associations)
|
35
31
|
end
|
36
32
|
|
37
33
|
def self.create_or_overwrite(pacticipant_name, version_number, version)
|
@@ -2,14 +2,14 @@ require "pact_broker/dataset"
|
|
2
2
|
|
3
3
|
module PactBroker
|
4
4
|
module Webhooks
|
5
|
-
class Execution < Sequel::Model(
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
5
|
+
class Execution < Sequel::Model(:webhook_executions)
|
6
|
+
# Ignore the columns that were used before the TriggeredWebhook class existed.
|
7
|
+
# It used to go Webhook -> Execution, and now it goes Webhook -> TriggeredWebhook -> Execution
|
8
|
+
# If we ever release a major version where we drop unused columns, those columns could be deleted.
|
9
|
+
EXECUTION_COLUMNS = Sequel::Model.db.schema(:webhook_executions).collect(&:first) - [:webhook_id, :pact_publication_id, :consumer_id, :provider_id]
|
10
|
+
|
11
|
+
set_dataset(Sequel::Model.db[:webhook_executions].select(*EXECUTION_COLUMNS))
|
12
|
+
|
13
13
|
set_primary_key :id
|
14
14
|
plugin :timestamps
|
15
15
|
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# Copied with thanks from https://github.com/yuryroot/sequel-pg_advisory_lock/blob/d7509aa/lib/sequel/extensions/pg_advisory_lock.rb
|
2
|
+
# The reason this is copy/pasted and modified is that I wanted to allow exact duplicate
|
3
|
+
# locks to be registered because different threads running the same code
|
4
|
+
# should not cause a Sequel::Error to be raised.
|
5
|
+
# Also, I wanted it to use Concurrent::Hash for multi-threaded environments.
|
6
|
+
|
7
|
+
require "sequel"
|
8
|
+
require "zlib"
|
9
|
+
require "concurrent/hash"
|
10
|
+
|
11
|
+
module Sequel
|
12
|
+
module Postgres
|
13
|
+
module PgAdvisoryLock
|
14
|
+
|
15
|
+
SESSION_LEVEL_LOCKS = [
|
16
|
+
:pg_advisory_lock,
|
17
|
+
:pg_try_advisory_lock
|
18
|
+
].freeze
|
19
|
+
|
20
|
+
TRANSACTION_LEVEL_LOCKS = [
|
21
|
+
:pg_advisory_xact_lock,
|
22
|
+
:pg_try_advisory_xact_lock
|
23
|
+
].freeze
|
24
|
+
|
25
|
+
LOCK_FUNCTIONS = (SESSION_LEVEL_LOCKS + TRANSACTION_LEVEL_LOCKS).freeze
|
26
|
+
|
27
|
+
DEFAULT_LOCK_FUNCTION = :pg_advisory_lock
|
28
|
+
UNLOCK_FUNCTION = :pg_advisory_unlock
|
29
|
+
|
30
|
+
class LockAlreadyRegistered < Sequel::Error; end
|
31
|
+
|
32
|
+
def registered_advisory_locks
|
33
|
+
@registered_advisory_locks ||= Concurrent::Hash.new
|
34
|
+
end
|
35
|
+
|
36
|
+
def with_advisory_lock(name, id = nil)
|
37
|
+
options = registered_advisory_locks.fetch(name.to_sym)
|
38
|
+
|
39
|
+
lock_key = options.fetch(:key)
|
40
|
+
function_params = [lock_key, id].compact
|
41
|
+
|
42
|
+
lock_function = options.fetch(:lock_function)
|
43
|
+
transaction_level_lock = TRANSACTION_LEVEL_LOCKS.include?(lock_function)
|
44
|
+
|
45
|
+
if transaction_level_lock
|
46
|
+
# TODO: It's allowed to specify additional options (in particular, :server)
|
47
|
+
# while opening database transaction.
|
48
|
+
# That's why this check must be smarter.
|
49
|
+
unless in_transaction?
|
50
|
+
raise Error, "Transaction must be manually opened before using transaction level lock '#{lock_function}'"
|
51
|
+
end
|
52
|
+
|
53
|
+
if get(Sequel.function(lock_function, *function_params))
|
54
|
+
yield
|
55
|
+
end
|
56
|
+
else
|
57
|
+
synchronize do
|
58
|
+
if get(Sequel.function(lock_function, *function_params))
|
59
|
+
begin
|
60
|
+
result = yield
|
61
|
+
ensure
|
62
|
+
get(Sequel.function(UNLOCK_FUNCTION, *function_params))
|
63
|
+
result
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Beth: not sure how much extra value this registration provides.
|
71
|
+
# It turns the name into a number, and makes sure the name/number is unique,
|
72
|
+
# and that you don't try and use a different lock function with the same name.
|
73
|
+
def register_advisory_lock(name, lock_function = DEFAULT_LOCK_FUNCTION)
|
74
|
+
name = name.to_sym
|
75
|
+
|
76
|
+
if registered_advisory_locks.key?(name) && registered_advisory_locks[name][:lock_function] != lock_function
|
77
|
+
raise LockAlreadyRegistered, "Lock with name :#{name} is already registered with a different lock function (#{registered_advisory_locks[name][:lock_function]})"
|
78
|
+
end
|
79
|
+
|
80
|
+
key = advisory_lock_key_for(name)
|
81
|
+
name_for_key = registered_advisory_locks.keys.find { |n| registered_advisory_locks[n].fetch(:key) == key }
|
82
|
+
if name_for_key && name_for_key != name
|
83
|
+
raise Error, "Lock key #{key} is already taken"
|
84
|
+
end
|
85
|
+
|
86
|
+
function = lock_function.to_sym
|
87
|
+
unless LOCK_FUNCTIONS.include?(function)
|
88
|
+
raise Error, "Invalid lock function :#{function}"
|
89
|
+
end
|
90
|
+
|
91
|
+
registered_advisory_locks[name] = { key: key, lock_function: function }
|
92
|
+
end
|
93
|
+
|
94
|
+
def advisory_lock_key_for(lock_name)
|
95
|
+
Zlib.crc32(lock_name.to_s) % 2 ** 31
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
Database.register_extension(:pg_advisory_lock, Postgres::PgAdvisoryLock)
|
101
|
+
end
|
data/pact_broker.gemspec
CHANGED
@@ -50,7 +50,7 @@ Gem::Specification.new do |gem|
|
|
50
50
|
gem.license = "MIT"
|
51
51
|
|
52
52
|
gem.add_runtime_dependency "json", "~> 2.3"
|
53
|
-
gem.add_runtime_dependency "psych", "~>
|
53
|
+
gem.add_runtime_dependency "psych", "~> 5.0"
|
54
54
|
gem.add_runtime_dependency "roar", "~> 1.1"
|
55
55
|
gem.add_runtime_dependency "dry-validation", "~> 1.8"
|
56
56
|
gem.add_runtime_dependency "reform", "~> 2.6"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pact_broker
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.110.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bethany Skurrie
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2024-
|
13
|
+
date: 2024-05-10 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: json
|
@@ -32,14 +32,14 @@ dependencies:
|
|
32
32
|
requirements:
|
33
33
|
- - "~>"
|
34
34
|
- !ruby/object:Gem::Version
|
35
|
-
version: '
|
35
|
+
version: '5.0'
|
36
36
|
type: :runtime
|
37
37
|
prerelease: false
|
38
38
|
version_requirements: !ruby/object:Gem::Requirement
|
39
39
|
requirements:
|
40
40
|
- - "~>"
|
41
41
|
- !ruby/object:Gem::Version
|
42
|
-
version: '
|
42
|
+
version: '5.0'
|
43
43
|
- !ruby/object:Gem::Dependency
|
44
44
|
name: roar
|
45
45
|
requirement: !ruby/object:Gem::Requirement
|
@@ -582,6 +582,9 @@ files:
|
|
582
582
|
- docs/api/PACTICIPANTS.md
|
583
583
|
- docs/api/PAGINATION.md
|
584
584
|
- docs/api/WEBHOOKS.md
|
585
|
+
- docs/developer/design_pattern_for_eager_loading_collections.md
|
586
|
+
- docs/developer/matrix.md
|
587
|
+
- docs/developer/rack.md
|
585
588
|
- lib/pact/doc/doc_file.rb
|
586
589
|
- lib/pact/doc/generate.rb
|
587
590
|
- lib/pact/doc/generator.rb
|
@@ -812,6 +815,7 @@ files:
|
|
812
815
|
- lib/pact_broker/dataset/page.rb
|
813
816
|
- lib/pact_broker/date_helper.rb
|
814
817
|
- lib/pact_broker/db.rb
|
818
|
+
- lib/pact_broker/db/advisory_lock.rb
|
815
819
|
- lib/pact_broker/db/clean.rb
|
816
820
|
- lib/pact_broker/db/clean/selector.rb
|
817
821
|
- lib/pact_broker/db/clean_incremental.rb
|
@@ -1124,6 +1128,7 @@ files:
|
|
1124
1128
|
- lib/rack/pact_broker/ui_request_filter.rb
|
1125
1129
|
- lib/rack/pact_broker/use_when.rb
|
1126
1130
|
- lib/semantic_logger/formatters/short.rb
|
1131
|
+
- lib/sequel/extensions/pg_advisory_lock.rb
|
1127
1132
|
- lib/sequel/extensions/statement_timeout.rb
|
1128
1133
|
- lib/sequel/plugins/age.rb
|
1129
1134
|
- lib/sequel/plugins/insert_ignore.rb
|
@@ -1260,7 +1265,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
1260
1265
|
- !ruby/object:Gem::Version
|
1261
1266
|
version: '0'
|
1262
1267
|
requirements: []
|
1263
|
-
rubygems_version: 3.5.
|
1268
|
+
rubygems_version: 3.5.10
|
1264
1269
|
signing_key:
|
1265
1270
|
specification_version: 4
|
1266
1271
|
summary: See description
|