class PostBulkCreator def initialize actor:, posts:, thumbnails: @actor_id = actor.id @posts = posts @thumbnails = thumbnails end def run results = Array.new(@posts.length) mutex = Mutex.new next_index = 0 workers = Array.new(2) do Thread.new do Rails.application.executor.wrap do ActiveRecord::Base.connection_pool.with_connection do actor = User.find(@actor_id) loop do index = nil begin index = mutex.synchronize do current = next_index next_index += 1 current end break if index >= @posts.length attributes = @posts[index] results[index] = create_row(actor, attributes, index) rescue StandardError => e Rails.logger.error( "post_bulk_creator_worker_failure #{ { error: e.class.name, message: e.message, index: }.to_json }") results[index] = { status: 'failed', recoverable: false, errors: { base: ['登録中にエラーが発生しました.'] }, base_errors: [] } end end end end end end workers.each(&:join) results.each_index do |index| next if results[index].present? results[index] = { status: 'failed', recoverable: false, errors: { base: ['登録中にエラーが発生しました.'] }, base_errors: [] } end { results: } end private def create_row actor, attributes, index preflight = PostCreatePreflight.new( attributes: attributes, thumbnail: thumbnail_for(index, attributes)).run if preflight[:existing_post_id].present? return { status: 'skipped', existing_post_id: preflight[:existing_post_id], existing_post: preflight[:existing_post] } end post = PostCreator.new( actor: actor, attributes: normalised_attributes(attributes, preflight, index)).create! result = { status: 'created', post: { id: post.id } } result[:field_warnings] = preflight[:field_warnings] if preflight[:field_warnings].present? result[:base_warnings] = preflight[:base_warnings] if preflight[:base_warnings].present? result rescue PostCreatePreflight::ValidationFailed => e { status: 'failed', recoverable: true, errors: e.fields, base_errors: e.base_errors } rescue ActiveRecord::RecordInvalid => e existing_post = existing_post_for_race(attributes, e.record) if existing_post.present? return { status: 'skipped', existing_post_id: existing_post[:id], existing_post: existing_post } end { status: 'failed', recoverable: true, errors: e.record.errors.to_hash, base_errors: e.record.errors[:base] } rescue ActiveRecord::RecordNotUnique => e if e.message.include?('index_posts_on_url') existing_post = existing_post_for_race(attributes) return { status: 'skipped', existing_post_id: existing_post[:id], existing_post: existing_post } if existing_post.present? end Rails.logger.error( "post_bulk_creator_record_not_unique #{ { error: e.class.name, message: e.message }.to_json }") { status: 'failed', recoverable: false, errors: { base: ['登録中にエラーが発生しました.'] }, base_errors: [] } rescue Tag::NicoTagNormalisationError { status: 'failed', recoverable: true, errors: { tags: ['ニコニコ・タグは直接指定できません.'] }, base_errors: [] } rescue Tag::DeprecatedTagNormalisationError { status: 'failed', recoverable: true, errors: { tags: ['廃止済みタグは付与できません.'] }, base_errors: [] } rescue PostCreator::VideoMsParseError { status: 'failed', recoverable: true, errors: { video_ms: ['動画時間の記法が不正です.'] }, base_errors: [] } rescue Post::RemoteThumbnailFetchFailed { status: 'failed', recoverable: true, errors: { thumbnail_base: ['サムネイル画像の取得に失敗しました.'] }, base_errors: [] } rescue ArgumentError => e { status: 'failed', recoverable: true, errors: { base: [e.message] }, base_errors: [] } rescue StandardError => e Rails.logger.error( "post_bulk_creator_failure #{ { error: e.class.name, message: e.message }.to_json }") { status: 'failed', recoverable: false, errors: { base: ['登録中にエラーが発生しました.'] }, base_errors: [] } end def normalised_attributes attributes, preflight, index { url: preflight[:url], title: preflight[:title], thumbnail_base: preflight[:thumbnail_base], thumbnail: thumbnail_for(index, attributes), tags: preflight[:tags], parent_post_ids: preflight[:parent_post_ids], original_created_from: preflight[:original_created_from], original_created_before: preflight[:original_created_before], duration: preflight[:duration], video_ms: preflight[:video_ms], direct_tag_specs: preflight[:direct_tag_specs], default_tag_specs: preflight[:default_tag_specs], snapshot_tag_specs: preflight[:snapshot_tag_specs], post_tag_specs: preflight[:post_tag_specs], tag_sections: preflight[:tag_sections], normalised_parent_post_ids: preflight[:normalised_parent_post_ids] } end def thumbnail_for index, attributes return nil if attributes['thumbnail_base'].present? || attributes[:thumbnail_base].present? @thumbnails[index] end def existing_post_for_race attributes, record = nil return nil if record.present? && !(record.errors.of_kind?(:url, :taken)) normal_url = PostUrlNormaliser.normalise(attributes['url'] || attributes[:url]) return nil if normal_url.blank? compact_existing_post(Post.with_attached_thumbnail.find_by(url: normal_url)) end def compact_existing_post post PostCompactRepr.base(post) end end