83355336dd
Whenever you push to a branch GitLab will show a button to create a merge request (should one not exist already). The underlying code to display this data was quite inefficient. For example, it involved multiple slow queries just to figure out what the most recent push event was. This commit changes the way this data is retrieved so it's much faster. This is achieved by caching the ID of the last push event on every push, which is then retrieved when loading certain pages. Database queries are only executed if necessary and the cached data is removed automatically once a merge request has been created, or 2 hours after being stored. A trade-off of this approach is that we _only_ track the last event. Previously if you were to push to branch A and B then create a merge request for branch B we'd still show the widget for branch A. As of this commit this is no longer the case, instead we will only show the widget for the branch you pushed to most recently. Once a merge request exists the widget is no longer displayed. Alternative solutions are either too complex and/or too slow, hence the decision was made to settle for this trade-off. Performance Impact ------------------ In the best case scenario (= a user didn't push anything for more than 2 hours) we perform a single Redis GET per page. Should there be cached data we will run a single (and lightweight) SQL query to get the event data from the database. If a merge request already exists we will run an additional DEL to remove the cache key. The difference in response timings can vary a bit per project. On GitLab.com the 99th percentile of time spent in User#recent_push hovers between 100 milliseconds and 1 second, while the mean hovers around 50 milliseconds. With the changes in this MR the expected time spent in User#recent_push is expected to be reduced down to just a few milliseconds. Fixes https://gitlab.com/gitlab-org/gitlab-ce/issues/35990
119 lines
3.2 KiB
Ruby
119 lines
3.2 KiB
Ruby
class PushEvent < Event
|
|
# This validation exists so we can't accidentally use PushEvent with a
|
|
# different "action" value.
|
|
validate :validate_push_action
|
|
|
|
# Authors are required as they're used to display who pushed data.
|
|
#
|
|
# We're just validating the presence of the ID here as foreign key constraints
|
|
# should ensure the ID points to a valid user.
|
|
validates :author_id, presence: true
|
|
|
|
# The project is required to build links to commits, commit ranges, etc.
|
|
#
|
|
# We're just validating the presence of the ID here as foreign key constraints
|
|
# should ensure the ID points to a valid project.
|
|
validates :project_id, presence: true
|
|
|
|
# These fields are also not used for push events, thus storing them would be a
|
|
# waste.
|
|
validates :target_id, absence: true
|
|
validates :target_type, absence: true
|
|
|
|
delegate :branch?, to: :push_event_payload
|
|
delegate :tag?, to: :push_event_payload
|
|
delegate :commit_from, to: :push_event_payload
|
|
delegate :commit_to, to: :push_event_payload
|
|
delegate :ref_type, to: :push_event_payload
|
|
delegate :commit_title, to: :push_event_payload
|
|
|
|
delegate :commit_count, to: :push_event_payload
|
|
alias_method :commits_count, :commit_count
|
|
|
|
# Returns events of pushes that either pushed to an existing ref or created a
|
|
# new one.
|
|
def self.created_or_pushed
|
|
actions = [
|
|
PushEventPayload.actions[:pushed],
|
|
PushEventPayload.actions[:created]
|
|
]
|
|
|
|
joins(:push_event_payload)
|
|
.where(push_event_payloads: { action: actions })
|
|
end
|
|
|
|
# Returns events of pushes to a branch.
|
|
def self.branch_events
|
|
ref_type = PushEventPayload.ref_types[:branch]
|
|
|
|
joins(:push_event_payload)
|
|
.where(push_event_payloads: { ref_type: ref_type })
|
|
end
|
|
|
|
# Returns PushEvent instances for which no merge requests have been created.
|
|
def self.without_existing_merge_requests
|
|
existing_mrs = MergeRequest.except(:order)
|
|
.select(1)
|
|
.where('merge_requests.source_project_id = events.project_id')
|
|
.where('merge_requests.source_branch = push_event_payloads.ref')
|
|
|
|
# For reasons unknown the use of #eager_load will result in the
|
|
# "push_event_payload" association not being set. Because of this we're
|
|
# using "joins" here, which does mean an additional query needs to be
|
|
# executed in order to retrieve the "push_event_association" when the
|
|
# returned PushEvent is used.
|
|
joins(:push_event_payload)
|
|
.where('NOT EXISTS (?)', existing_mrs)
|
|
.created_or_pushed
|
|
.branch_events
|
|
end
|
|
|
|
def self.sti_name
|
|
PUSHED
|
|
end
|
|
|
|
def push?
|
|
true
|
|
end
|
|
|
|
def push_with_commits?
|
|
!!(commit_from && commit_to)
|
|
end
|
|
|
|
def valid_push?
|
|
push_event_payload.ref.present?
|
|
end
|
|
|
|
def new_ref?
|
|
push_event_payload.created?
|
|
end
|
|
|
|
def rm_ref?
|
|
push_event_payload.removed?
|
|
end
|
|
|
|
def md_ref?
|
|
!(rm_ref? || new_ref?)
|
|
end
|
|
|
|
def ref_name
|
|
push_event_payload.ref
|
|
end
|
|
|
|
alias_method :branch_name, :ref_name
|
|
alias_method :tag_name, :ref_name
|
|
|
|
def commit_id
|
|
commit_to || commit_from
|
|
end
|
|
|
|
def last_push_to_non_root?
|
|
branch? && project.default_branch != branch_name
|
|
end
|
|
|
|
def validate_push_action
|
|
return if action == PUSHED
|
|
|
|
errors.add(:action, "the action #{action.inspect} is not valid")
|
|
end
|
|
end
|