gitlab-org--gitlab-foss/app/uploaders/records_uploads.rb
Sean McGivern ec85debaf5 Speed up avatar URLs with object storage
With object storage enabled, calling `#filename` on an upload does this:

1. Call the `#filename` method on the CarrierWave object.
2. Generate the URL for that object.
3. If the uploader isn't public, do so by generating an authenticated
   URL, including signing that request.

That's all correct behaviour, but for the case where we use `#filename`,
it's typically to generate a GitLab URL. That URL doesn't need to be
signed because we do our own auth.

Signing the URLs can be very expensive, especially in batch (say, we
need to get the avatar URLs for 150 users in one request). It's all
unnecessary work. If we used the `RecordsUploads` concern, we have
already recorded a `path` in the database. That `path` is actually
generated from CarrierWave's `#filename` at upload time, so we don't
need to recompute it - we can just use it and strip off the prefix if
it's available.

On a sample users autocomplete URL, at least 10% of the time before this
change went to signing URLs. After this change, we spend no time in URL
signing, and still get the correct results.
2019-04-04 11:32:42 +01:00

83 lines
2.2 KiB
Ruby

# frozen_string_literal: true
module RecordsUploads
module Concern
extend ActiveSupport::Concern
attr_accessor :upload
included do
after :store, :record_upload
before :remove, :destroy_upload
end
# After storing an attachment, create a corresponding Upload record
#
# NOTE: We're ignoring the argument passed to this callback because we want
# the `SanitizedFile` object from `CarrierWave::Uploader::Base#file`, not the
# `Tempfile` object the callback gets.
#
# Called `after :store`
# rubocop: disable CodeReuse/ActiveRecord
def record_upload(_tempfile = nil)
return unless model
return unless file && file.exists?
# MySQL InnoDB may encounter a deadlock if a deletion and an
# insert is in the same transaction due to its next-key locking
# algorithm, so we need to skip the transaction.
# https://gitlab.com/gitlab-org/gitlab-ce/issues/55161#note_131556351
if Gitlab::Database.mysql?
readd_upload
else
Upload.transaction { readd_upload }
end
end
def readd_upload
uploads.where(path: upload_path).delete_all
upload.delete if upload
self.upload = build_upload.tap(&:save!)
end
# rubocop: enable CodeReuse/ActiveRecord
def upload_path
File.join(store_dir, filename.to_s)
end
def filename
upload&.path ? File.basename(upload.path) : super
end
private
# rubocop: disable CodeReuse/ActiveRecord
def uploads
Upload.order(id: :desc).where(uploader: self.class.to_s)
end
# rubocop: enable CodeReuse/ActiveRecord
def build_upload
Upload.new(
uploader: self.class.to_s,
size: file.size,
path: upload_path,
model: model,
mount_point: mounted_as
)
end
# Before removing an attachment, destroy any Upload records at the same path
#
# Called `before :remove`
# rubocop: disable CodeReuse/ActiveRecord
def destroy_upload(*args)
return unless file && file.exists?
self.upload = nil
uploads.where(path: upload_path).delete_all
end
# rubocop: enable CodeReuse/ActiveRecord
end
end