gitlab-org--gitlab-foss/app/models/push_event.rb
Yorick Peterse 83355336dd
Rework how recent push events are retrieved
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
2017-09-08 00:46:16 +02:00

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