Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: reduce contention when updating the contract_data_updated_at field for integrations #671

Merged
merged 2 commits into from
Apr 2, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 32 additions & 7 deletions lib/pact_broker/integrations/repository.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,43 @@ def delete(consumer_id, provider_id)
# @param [PactBroker::Domain::Pacticipant, nil] consumer the consumer for the integration, or nil if for a provider-only event (eg. Pactflow provider contract published)
# @param [PactBroker::Domain::Pacticipant] provider the provider for the integration
def set_contract_data_updated_at(consumer, provider)
Integration
.where({ consumer_id: consumer&.id, provider_id: provider.id }.compact )
.update(contract_data_updated_at: Sequel.datetime_class.now)
set_contract_data_updated_at_for_multiple_integrations([OpenStruct.new(consumer: consumer, provider: provider)])
end


# Sets the contract_data_updated_at for the integrations as specified by an array of objects which each have a consumer and provider
# @param [Array<Object>] where each object has a consumer and a provider
# Sets the contract_data_updated_at for the integrations as specified by an array of objects which each have a consumer and provider.
#
# 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,
# so that the most recently updated integrations (the ones you're likely working on) are showed at the top of the first page.
# There is often contention around updating it however, which can cause deadlocks, and slow down API responses.
# Because it's not a critical field (eg. it won't change any can-i-deploy results), the easiest way to reduce this contention
# 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
# going to be a date from a few seconds ago, which is perfectly fine for the purposes for which we are using the value.
#
# Notes on SKIP LOCKED:
# SKIP LOCKED is only supported by Postgres.
# When executing SELECT ... FOR UPDATE SKIP LOCKED, the SELECT will run immediately, not waiting for any other transactions,
# and only return rows that are not already locked by another transaction.
# The FOR UPDATE is required to make it work this way - SKIP LOCKED on its own does not work.
#
# @param [Array<Object>] where each object MAY have a consumer and does have a provider (for Pactflow provider contract published there is no consumer)
def set_contract_data_updated_at_for_multiple_integrations(objects_with_consumer_and_provider)
consumer_and_provider_ids = objects_with_consumer_and_provider.collect{ | object | [object.consumer.id, object.provider.id] }.uniq
consumer_and_provider_ids = objects_with_consumer_and_provider.collect{ | object | { consumer_id: object.consumer&.id, provider_id: object.provider.id }.compact }.uniq

# MySQL doesn't support an UPDATE with a subquery. FFS. Really need to do a major version release and delete the support code.
criteria = if Integration.dataset.supports_skip_locked?
integration_ids_to_update = Integration
.select(:id)
.where(Sequel.|(*consumer_and_provider_ids))
.for_update
.skip_locked
{ id: integration_ids_to_update }
else
Sequel.|(*consumer_and_provider_ids)
end

Integration
.where([:consumer_id, :provider_id] => consumer_and_provider_ids)
.where(criteria)
.update(contract_data_updated_at: Sequel.datetime_class.now)
end
end
Expand Down