Change local media attachments to perform heavy processing asynchronously (#13210)

Fix #9106
This commit is contained in:
Eugen Rochko 2020-03-08 23:56:18 +01:00 committed by GitHub
parent 764b89939f
commit 9660aa4543
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 165 additions and 17 deletions

View File

@ -3,25 +3,42 @@
class Api::V1::MediaController < Api::BaseController class Api::V1::MediaController < Api::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:media' } before_action -> { doorkeeper_authorize! :write, :'write:media' }
before_action :require_user! before_action :require_user!
before_action :set_media_attachment, except: [:create]
before_action :check_processing, except: [:create]
def create def create
@media = current_account.media_attachments.create!(media_params) @media_attachment = current_account.media_attachments.create!(media_attachment_params)
render json: @media, serializer: REST::MediaAttachmentSerializer render json: @media_attachment, serializer: REST::MediaAttachmentSerializer
rescue Paperclip::Errors::NotIdentifiedByImageMagickError rescue Paperclip::Errors::NotIdentifiedByImageMagickError
render json: file_type_error, status: 422 render json: file_type_error, status: 422
rescue Paperclip::Error rescue Paperclip::Error
render json: processing_error, status: 500 render json: processing_error, status: 500
end end
def show
render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
end
def update def update
@media = current_account.media_attachments.where(status_id: nil).find(params[:id]) @media_attachment.update!(media_attachment_params)
@media.update!(media_params) render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
render json: @media, serializer: REST::MediaAttachmentSerializer
end end
private private
def media_params def status_code_for_media_attachment
@media_attachment.not_processed? ? 206 : 200
end
def set_media_attachment
@media_attachment = current_account.media_attachments.unattached.find(params[:id])
end
def check_processing
render json: processing_error, status: 422 if @media_attachment.processing_failed?
end
def media_attachment_params
params.permit(:file, :description, :focus) params.permit(:file, :description, :focus)
end end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
class Api::V2::MediaController < Api::V1::MediaController
def create
@media_attachment = current_account.media_attachments.create!({ delay_processing: true }.merge(media_attachment_params))
render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: 202
rescue Paperclip::Errors::NotIdentifiedByImageMagickError
render json: file_type_error, status: 422
rescue Paperclip::Error
render json: processing_error, status: 500
end
end

View File

@ -230,12 +230,31 @@ export function uploadCompose(files) {
// Account for disparity in size of original image and resized data // Account for disparity in size of original image and resized data
total += file.size - f.size; total += file.size - f.size;
return api(getState).post('/api/v1/media', data, { return api(getState).post('/api/v2/media', data, {
onUploadProgress: function({ loaded }){ onUploadProgress: function({ loaded }){
progress[i] = loaded; progress[i] = loaded;
dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total)); dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
}, },
}).then(({ data }) => dispatch(uploadComposeSuccess(data, f))); }).then(({ status, data }) => {
// If server-side processing of the media attachment has not completed yet,
// poll the server until it is, before showing the media attachment as uploaded
if (status === 200) {
dispatch(uploadComposeSuccess(data, f));
} else if (status === 202) {
const poll = () => {
api(getState).get(`/api/v1/media/${data.id}`).then(response => {
if (response.status === 200) {
dispatch(uploadComposeSuccess(data, f));
} else if (response.status === 206) {
setTimeout(() => poll(), 1000);
}
}).catch(error => dispatch(uploadComposeFail(error)));
};
poll();
}
});
}).catch(error => dispatch(uploadComposeFail(error))); }).catch(error => dispatch(uploadComposeFail(error)));
}; };
}; };

View File

@ -74,7 +74,7 @@ module Attachmentable
self.class.attachment_definitions.each_key do |attachment_name| self.class.attachment_definitions.each_key do |attachment_name|
attachment = send(attachment_name) attachment = send(attachment_name)
next if attachment.blank? || attachment.queued_for_write[:original].blank? next if attachment.blank? || attachment.queued_for_write[:original].blank? || attachment.options[:preserve_files]
attachment.instance_write :file_name, SecureRandom.hex(8) + File.extname(attachment.instance_read(:file_name)) attachment.instance_write :file_name, SecureRandom.hex(8) + File.extname(attachment.instance_read(:file_name))
end end

View File

@ -19,12 +19,14 @@
# description :text # description :text
# scheduled_status_id :bigint(8) # scheduled_status_id :bigint(8)
# blurhash :string # blurhash :string
# processing :integer
# #
class MediaAttachment < ApplicationRecord class MediaAttachment < ApplicationRecord
self.inheritance_column = nil self.inheritance_column = nil
enum type: [:image, :gifv, :video, :unknown, :audio] enum type: [:image, :gifv, :video, :unknown, :audio]
enum processing: [:queued, :in_progress, :complete, :failed], _prefix: true
MAX_DESCRIPTION_LENGTH = 1_500 MAX_DESCRIPTION_LENGTH = 1_500
@ -156,6 +158,10 @@ class MediaAttachment < ApplicationRecord
remote_url.blank? remote_url.blank?
end end
def not_processed?
processing.present? && !processing_complete?
end
def needs_redownload? def needs_redownload?
file.blank? && remote_url.present? file.blank? && remote_url.present?
end end
@ -203,10 +209,18 @@ class MediaAttachment < ApplicationRecord
"#{x},#{y}" "#{x},#{y}"
end end
attr_writer :delay_processing
def delay_processing?
@delay_processing
end
after_commit :enqueue_processing, on: :create
after_commit :reset_parent_cache, on: :update after_commit :reset_parent_cache, on: :update
before_create :prepare_description, unless: :local? before_create :prepare_description, unless: :local?
before_create :set_shortcode before_create :set_shortcode
before_create :set_processing
before_post_process :set_type_and_extension before_post_process :set_type_and_extension
@ -277,6 +291,10 @@ class MediaAttachment < ApplicationRecord
end end
end end
def set_processing
self.processing = delay_processing? ? :queued : :complete
end
def set_meta def set_meta
meta = populate_meta meta = populate_meta
@ -322,9 +340,11 @@ class MediaAttachment < ApplicationRecord
}.compact }.compact
end end
def reset_parent_cache def enqueue_processing
return if status_id.nil? PostProcessMediaWorker.perform_async(id) if delay_processing?
end
Rails.cache.delete("statuses/#{status_id}") def reset_parent_cache
Rails.cache.delete("statuses/#{status_id}") if status_id.present?
end end
end end

View File

@ -12,7 +12,9 @@ class REST::MediaAttachmentSerializer < ActiveModel::Serializer
end end
def url def url
if object.needs_redownload? if object.not_processed?
nil
elsif object.needs_redownload?
media_proxy_url(object.id, :original) media_proxy_url(object.id, :original)
else else
full_asset_url(object.file.url(:original)) full_asset_url(object.file.url(:original))

View File

@ -101,6 +101,7 @@ class PostStatusService < BaseService
@media = @account.media_attachments.where(status_id: nil).where(id: @options[:media_ids].take(4).map(&:to_i)) @media = @account.media_attachments.where(status_id: nil).where(id: @options[:media_ids].take(4).map(&:to_i))
raise Mastodon::ValidationError, I18n.t('media_attachments.validations.images_and_video') if @media.size > 1 && @media.find(&:audio_or_video?) raise Mastodon::ValidationError, I18n.t('media_attachments.validations.images_and_video') if @media.size > 1 && @media.find(&:audio_or_video?)
raise Mastodon::ValidationError, I18n.t('media_attachments.validations.not_ready') if @media.any?(&:not_processed?)
end end
def language_from_option(str) def language_from_option(str)

View File

@ -9,8 +9,12 @@ class BackupWorker
backup_id = msg['args'].first backup_id = msg['args'].first
ActiveRecord::Base.connection_pool.with_connection do ActiveRecord::Base.connection_pool.with_connection do
begin
backup = Backup.find(backup_id) backup = Backup.find(backup_id)
backup&.destroy backup.destroy
rescue ActiveRecord::RecordNotFound
true
end
end end
end end

View File

@ -0,0 +1,34 @@
# frozen_string_literal: true
class PostProcessMediaWorker
include Sidekiq::Worker
sidekiq_options retry: 1, dead: false
sidekiq_retries_exhausted do |msg|
media_attachment_id = msg['args'].first
ActiveRecord::Base.connection_pool.with_connection do
begin
media_attachment = MediaAttachment.find(media_attachment_id)
media_attachment.processing = :failed
media_attachment.save
rescue ActiveRecord::RecordNotFound
true
end
end
Sidekiq.logger.error("Processing media attachment #{media_attachment_id} failed with #{msg['error_message']}")
end
def perform(media_attachment_id)
media_attachment = MediaAttachment.find(media_attachment_id)
media_attachment.processing = :in_progress
media_attachment.save
media_attachment.file.reprocess_original!
media_attachment.processing = :complete
media_attachment.save
rescue ActiveRecord::RecordNotFound
true
end
end

View File

@ -7,6 +7,7 @@ require 'rails/all'
Bundler.require(*Rails.groups) Bundler.require(*Rails.groups)
require_relative '../app/lib/exceptions' require_relative '../app/lib/exceptions'
require_relative '../lib/paperclip/attachment_extensions'
require_relative '../lib/paperclip/lazy_thumbnail' require_relative '../lib/paperclip/lazy_thumbnail'
require_relative '../lib/paperclip/gif_transcoder' require_relative '../lib/paperclip/gif_transcoder'
require_relative '../lib/paperclip/video_transcoder' require_relative '../lib/paperclip/video_transcoder'

View File

@ -853,6 +853,7 @@ en:
media_attachments: media_attachments:
validations: validations:
images_and_video: Cannot attach a video to a status that already contains images images_and_video: Cannot attach a video to a status that already contains images
not_ready: Cannot attach files that have not finished processing. Try again in a moment!
too_many: Cannot attach more than 4 files too_many: Cannot attach more than 4 files
migrations: migrations:
acct: Moved to acct: Moved to

View File

@ -343,7 +343,7 @@ Rails.application.routes.draw do
end end
end end
resources :media, only: [:create, :update] resources :media, only: [:create, :update, :show]
resources :blocks, only: [:index] resources :blocks, only: [:index]
resources :mutes, only: [:index] resources :mutes, only: [:index]
resources :favourites, only: [:index] resources :favourites, only: [:index]
@ -455,6 +455,7 @@ Rails.application.routes.draw do
end end
namespace :v2 do namespace :v2 do
resources :media, only: [:create]
get '/search', to: 'search#index', as: :search get '/search', to: 'search#index', as: :search
end end

View File

@ -0,0 +1,5 @@
class AddProcessingToMediaAttachments < ActiveRecord::Migration[5.2]
def change
add_column :media_attachments, :processing, :integer
end
end

View File

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2020_01_26_203551) do ActiveRecord::Schema.define(version: 2020_03_06_035625) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -459,6 +459,7 @@ ActiveRecord::Schema.define(version: 2020_01_26_203551) do
t.text "description" t.text "description"
t.bigint "scheduled_status_id" t.bigint "scheduled_status_id"
t.string "blurhash" t.string "blurhash"
t.integer "processing"
t.index ["account_id"], name: "index_media_attachments_on_account_id" t.index ["account_id"], name: "index_media_attachments_on_account_id"
t.index ["scheduled_status_id"], name: "index_media_attachments_on_scheduled_status_id" t.index ["scheduled_status_id"], name: "index_media_attachments_on_scheduled_status_id"
t.index ["shortcode"], name: "index_media_attachments_on_shortcode", unique: true t.index ["shortcode"], name: "index_media_attachments_on_shortcode", unique: true

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
module Paperclip
module AttachmentExtensions
# We overwrite this method to support delayed processing in
# Sidekiq. Since we process the original file to reduce disk
# usage, and we still want to generate thumbnails straight
# away, it's the only style we need to exclude
def process_style?(style_name, style_args)
if style_name == :original && instance.respond_to?(:delay_processing?) && instance.delay_processing?
false
else
style_args.empty? || style_args.include?(style_name)
end
end
def reprocess_original!
old_original_path = path(:original)
reprocess!(:original)
new_original_path = path(:original)
if new_original_path != old_original_path
@queued_for_delete << old_original_path
flush_deletes
end
end
end
end
Paperclip::Attachment.prepend(Paperclip::AttachmentExtensions)