Files
danbooru/app/models/bulk_update_request.rb
evazion 9ba84efc07 BURs: process BURs sequentially in a single job.
Change the way BURs are processed. Before, we spawned a background job
for each line of the BUR, then processed each job sequentially. Now, we
process the entire BUR sequentially in a single background job.

This means that:

* BURs are truly sequential now. Before certain things like removing
  aliases weren't actually performed in a background job, so they were
  performed out-of-order before everything else in the BUR.

* Before, if an alias or implication line failed, then subsequent alias
  or implication lines would still be processed. This was because each
  alias or implication line was queued as a separate job, so a failure
  of one job didn't block another. Now, if any alias or implication
  fails, the entire BUR will fail and stop processing after that line.
  This may be good or bad, depending on whether we actually need the BUR
  to be processed in order or not.

* Before, BURs were processed inside a database transaction (except for the
  actual updating of posts). Now they're not. This is because we can't
  afford to hold transactions open while processing long-running aliases
  or implications. This means that if BUR fails in the middle when it is
  initially approved, it will be left in a half-complete state. Before
  it would be rolled back and left in a pending state with no changes
  performed.

* Before, only one BUR at a time could be processed. If multiple BURs
  were approved at the same time, then they would queue up and be
  processed one at a time. Now, multiple BURs can be processed at the
  same time. This may be undesirable when processing large BURs, or BURs
  that must be approved in a specific order.

* Before, large tag category changes could time out. This was because
  they weren't actually performed in a background job. Now they are, so
  they shouldn't time out.
2021-09-20 01:12:14 -05:00

130 lines
3.6 KiB
Ruby

class BulkUpdateRequest < ApplicationRecord
attr_accessor :title, :reason
belongs_to :user
belongs_to :forum_topic, optional: true
belongs_to :forum_post, optional: true
belongs_to :approver, optional: true, class_name: "User"
validates :reason, presence: true, on: :create
validates :script, presence: true
validates :title, presence: true, if: ->(rec) { rec.forum_topic_id.blank? }
validates :forum_topic, presence: true, if: ->(rec) { rec.forum_topic_id.present? }
validates :status, inclusion: { in: %w[pending approved rejected] }
validate :validate_script, if: :script_changed?
before_save :update_tags, if: :script_changed?
after_create :create_forum_topic
scope :pending_first, -> { order(Arel.sql("(case status when 'pending' then 0 when 'approved' then 1 else 2 end)")) }
scope :pending, -> {where(status: "pending")}
scope :approved, -> { where(status: "approved") }
scope :rejected, -> { where(status: "rejected") }
scope :has_topic, -> { where.not(forum_topic: nil) }
module SearchMethods
def default_order
pending_first.order(id: :desc)
end
def search(params = {})
q = search_attributes(params, :id, :created_at, :updated_at, :script, :tags, :user, :forum_topic, :forum_post, :approver)
q = q.text_attribute_matches(:script, params[:script_matches])
if params[:status].present?
q = q.where(status: params[:status].split(","))
end
params[:order] ||= "status_desc"
case params[:order]
when "id_desc"
q = q.order(id: :desc)
when "id_asc"
q = q.order(id: :asc)
when "updated_at_desc"
q = q.order(updated_at: :desc)
when "updated_at_asc"
q = q.order(updated_at: :asc)
else
q = q.apply_default_order(params)
end
q
end
end
module ApprovalMethods
def forum_updater
@forum_updater ||= ForumUpdater.new(forum_topic)
end
def approve!(approver)
transaction do
CurrentUser.scoped(approver) do
processor.validate!(:approval)
update!(status: "approved", approver: approver)
processor.process_later!
forum_updater.update("The #{bulk_update_request_link} (forum ##{forum_post.id}) has been approved by @#{approver.name}.")
end
end
end
def create_forum_topic
CurrentUser.scoped(user) do
body = "[bur:#{id}]\n\n#{reason}"
self.forum_topic = ForumTopic.create(title: title, category_id: 1, creator: user) unless forum_topic.present?
self.forum_post = forum_topic.forum_posts.create(body: body, creator: user) unless forum_post.present?
save
end
end
def reject!(rejector = User.system)
transaction do
update!(status: "rejected")
forum_updater.update("The #{bulk_update_request_link} (forum ##{forum_post.id}) has been rejected by @#{rejector.name}.")
end
end
def bulk_update_request_link
%{"bulk update request ##{id}":#{Routes.bulk_update_requests_path(search: { id: id })}}
end
end
def validate_script
if processor.invalid?(:request)
errors.add(:base, processor.errors.full_messages.join("; "))
end
end
extend SearchMethods
include ApprovalMethods
def update_tags
self.tags = processor.affected_tags
end
def processor
@processor ||= BulkUpdateRequestProcessor.new(self)
end
def is_tag_move_allowed?
processor.is_tag_move_allowed?
end
def is_pending?
status == "pending"
end
def is_approved?
status == "approved"
end
def is_rejected?
status == "rejected"
end
def self.available_includes
[:user, :forum_topic, :forum_post, :approver]
end
end