FEATURE: Add a sidekiq job for syncing S3 ACLs (#16449)

Sometimes we need to update a _lot_ of ACLs on S3 (such as when secure media
is enabled), and since it takes ~1s per upload to update the ACL, this is
best spread out over many jobs instead of having to do the whole thing serially.

In future, it will be better to have a job that can be run based on
a column on uploads (e.g. acl_stale) so we can track progress, similar
to how we can set the baked_version to nil to rebake posts.
This commit is contained in:
Martin Brennan 2022-04-12 14:26:42 +10:00 committed by GitHub
parent 131a4674e3
commit 9f2138dc92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 74 additions and 4 deletions

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
module Jobs
# Sometimes we need to update a _lot_ of ACLs on S3 (such as when secure media
# is enabled), and since it takes ~1s per upload to update the ACL, this is
# best spread out over many jobs instead of having to do the whole thing serially.
class SyncAclsForUploads < ::Jobs::Base
def execute(args)
return if !Discourse.store.external?
return if !args.key?(:upload_ids)
# TODO (martin) Change the logging here to debug after acl_stale implemented.
#
# Note...these log messages are set to warn to ensure this is working
# as intended in initial production trials, this will be set to debug
# after an acl_stale column is added to uploads.
time = Benchmark.measure do
Rails.logger.warn("Syncing ACL for upload ids: #{args[:upload_ids].join(", ")}")
Upload.includes(:optimized_images).where(id: args[:upload_ids]).find_in_batches do |uploads|
uploads.each do |upload|
Discourse.store.update_upload_ACL(upload, optimized_images_preloaded: true)
end
end
Rails.logger.warn("Completed syncing ACL for upload ids in #{time.to_s}. IDs: #{args[:upload_ids].join(", ")}")
end
end
end
end

View File

@ -279,13 +279,24 @@ module FileStore
end
end
def update_upload_ACL(upload)
def update_upload_ACL(upload, optimized_images_preloaded: false)
key = get_upload_key(upload)
update_ACL(key, upload.secure?)
upload.optimized_images.find_each do |optimized_image|
optimized_image_key = get_path_for_optimized_image(optimized_image)
update_ACL(optimized_image_key, upload.secure?)
# if we do find_each when the images have already been preloaded with
# includes(:optimized_images), then the optimized_images are fetched
# from the database again, negating the preloading if this operation
# is done on a large amount of uploads at once (see Jobs::SyncAclsForUploads)
if optimized_images_preloaded
upload.optimized_images.each do |optimized_image|
optimized_image_key = get_path_for_optimized_image(optimized_image)
update_ACL(optimized_image_key, upload.secure?)
end
else
upload.optimized_images.find_each do |optimized_image|
optimized_image_key = get_path_for_optimized_image(optimized_image)
update_ACL(optimized_image_key, upload.secure?)
end
end
true

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
require 'rails_helper'
describe Jobs::SyncAclsForUploads do
let(:upload1) { Fabricate(:upload) }
let(:upload2) { Fabricate(:upload) }
let(:upload3) { Fabricate(:secure_upload) }
let(:upload_ids) { [upload1.id, upload2.id, upload3.id] }
def run_job
described_class.new.execute(upload_ids: upload_ids)
end
it "does nothing if not using external storage" do
Upload.expects(:where).never
run_job
end
context "external storage enabled" do
before do
setup_s3
stub_s3_store
end
it "runs update_upload_ACL for each upload" do
Discourse.store.expects(:update_upload_ACL).times(3)
run_job
end
end
end