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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/docs/developer/design_pattern_for_eager_loading_collections.md +23 -0
  4. data/docs/developer/matrix.md +95 -0
  5. data/docs/developer/rack.md +11 -0
  6. data/lib/pact_broker/api/contracts/consumer_version_selector_contract.rb +1 -1
  7. data/lib/pact_broker/api/decorators/base_decorator.rb +10 -1
  8. data/lib/pact_broker/api/decorators/version_decorator.rb +14 -0
  9. data/lib/pact_broker/api/resources/branch_versions.rb +1 -1
  10. data/lib/pact_broker/api/resources/provider_pacts_for_verification.rb +5 -0
  11. data/lib/pact_broker/api/resources/versions.rb +1 -1
  12. data/lib/pact_broker/app.rb +24 -9
  13. data/lib/pact_broker/contracts/service.rb +6 -2
  14. data/lib/pact_broker/db/advisory_lock.rb +58 -0
  15. data/lib/pact_broker/domain/version.rb +3 -2
  16. data/lib/pact_broker/integrations/integration.rb +11 -1
  17. data/lib/pact_broker/integrations/repository.rb +46 -7
  18. data/lib/pact_broker/integrations/service.rb +2 -0
  19. data/lib/pact_broker/pacts/pact_publication_dataset_module.rb +56 -3
  20. data/lib/pact_broker/pacts/pact_publication_selector_dataset_module.rb +1 -0
  21. data/lib/pact_broker/pacts/pacts_for_verification_repository.rb +4 -4
  22. data/lib/pact_broker/pacts/repository.rb +2 -2
  23. data/lib/pact_broker/pacts/selector.rb +8 -2
  24. data/lib/pact_broker/tasks/clean_task.rb +68 -26
  25. data/lib/pact_broker/test/test_data_builder.rb +8 -3
  26. data/lib/pact_broker/version.rb +1 -1
  27. data/lib/pact_broker/versions/repository.rb +2 -20
  28. data/lib/pact_broker/versions/service.rb +2 -6
  29. data/lib/pact_broker/webhooks/execution.rb +8 -8
  30. data/lib/sequel/extensions/pg_advisory_lock.rb +101 -0
  31. data/pact_broker.gemspec +1 -1
  32. metadata +10 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a4c49978b4312049fd2e394ce46bcf90f64125ff50556f9bb01f9610e1310546
4
- data.tar.gz: 43f24ab98f5ce7bd757a52a085d653668d83395b6889921e8dc0d4aac5982b73
3
+ metadata.gz: 0747ca988604f56bec96bb0511242e8b3e9e2c18c823ec53742a1c58f80eee16
4
+ data.tar.gz: fbdc8b9be85cf443ccd8ddcb58704a1aece12759e267aea1b4f79ba0d05fbf9c
5
5
  SHA512:
6
- metadata.gz: 3211f2cc4b2e9fbae7dd8a07bd1c49dc9d672fa46f6f4c83b6652b2266dc9498d4858a9a7714f91b09914d872e522affc5e96753cf3317e39228fecc620404c1
7
- data.tar.gz: 36a5bc3f3ecc827cd6e24fa21b4d4bc9f943ec75bfdb3db8cdcdd1399533739bf0a2c15c68df816ae995fa58987fa43d4da45dcb72a2d68fff12f7594a69906a
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(:str?)
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.find_all_pacticipant_versions_in_reverse_order(pacticipant_name, pagination_options)
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
@@ -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
- migration_options = { allow_missing_migration_files: configuration.allow_missing_migration_files }
106
- if PactBroker::DB.is_current?(configuration.database_connection, migration_options)
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
- logger.info "Migrating data"
119
- PactBroker::DB.run_data_migrations configuration.database_connection
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
- update_integrations(pacts)
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
- def update_integrations(pacts)
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
- VERSION_COLUMNS = [:id, :number, :repository_ref, :pacticipant_id, :order, :created_at, :updated_at, :build_url]
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
- class Integration < Sequel::Model(Sequel::Model.db[:integrations].select(:id, :consumer_id, :provider_id, :contract_data_updated_at))
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
- Integration
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
- # @param [Array<Object>] where each object has a consumer and a provider
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 | [object.consumer.id, object.provider.id] }.uniq
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([:consumer_id, :provider_id] => consumer_and_provider_ids)
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
- # TODO use the branch heads here
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
- # Return the pacts (if they exist) for the branch heads.
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
- # Need to see about updating this.
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
- :latest_by_consumer_tag
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
- :latest_by_consumer_branch
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).latest_by_consumer_branch
233
- potential_wip_by_consumer_tag = PactPublication.for_provider(provider).created_after(wip_start_date).latest_by_consumer_tag
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
- def create params
37
- integration_repository.create_for_pact(params.fetch(:consumer_id), params.fetch(:provider_id))
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
- !!(latest && branch == potential_branch)
270
+ latest == true && branch == potential_branch
269
271
  else
270
- !!(latest && !!branch)
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
- require "pact_broker/db/clean_incremental"
35
- require "pact_broker/error"
36
- require "yaml"
37
- require "benchmark"
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
- raise PactBroker::Error.new("You must specify the version_deletion_limit") unless version_deletion_limit
54
+ raise PactBroker::Error.new("You must specify the version_deletion_limit") unless version_deletion_limit
40
55
 
41
- prefix = dry_run ? "[DRY RUN] " : ""
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
- if keep_version_selectors.nil? || keep_version_selectors.empty?
44
- raise PactBroker::Error.new("You must specify which versions to keep")
45
- else
46
- add_defaults_to_keep_selectors
47
- output "#{prefix}Deleting oldest #{version_deletion_limit} versions, keeping versions that match the configured selectors", keep_version_selectors.collect(&:to_hash)
48
- end
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
- start_time = Time.now
51
- results = PactBroker::DB::CleanIncremental.call(database_connection,
52
- keep: keep_version_selectors,
53
- limit: version_deletion_limit,
54
- logger: logger,
55
- dry_run: dry_run
56
- )
57
- end_time = Time.now
58
- elapsed_seconds = (end_time - start_time).to_i
59
- output "Results (#{elapsed_seconds} seconds)", results
60
- end
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 string, payload = {}
66
- logger ? logger.info(string, payload) : puts("#{string} #{payload.to_json}")
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
- def create_pact params = {}
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
@@ -1,3 +1,3 @@
1
1
  module PactBroker
2
- VERSION = "2.109.1"
2
+ VERSION = "2.110.0"
3
3
  end
@@ -57,29 +57,11 @@ module PactBroker
57
57
  .single_record
58
58
  end
59
59
 
60
- # The eager loaded relations are hardcoded here to support the PactBroker::Api::Decorators::VersionDecorator
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(:pacticipant)
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.find_all_pacticipant_versions_in_reverse_order(name, pagination_options = {})
30
- version_repository.find_all_pacticipant_versions_in_reverse_order(name, pagination_options)
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
- Sequel::Model.db[:webhook_executions].select(
7
- Sequel[:webhook_executions][:id],
8
- :triggered_webhook_id,
9
- :success,
10
- :logs,
11
- Sequel[:webhook_executions][:created_at])
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", "~> 4.0" # TODO identify breaking changes and see if we can use 5
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.109.1
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-02-21 00:00:00.000000000 Z
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: '4.0'
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: '4.0'
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.6
1268
+ rubygems_version: 3.5.10
1264
1269
  signing_key:
1265
1270
  specification_version: 4
1266
1271
  summary: See description