Files
once-campfire/app/models/room.rb
T
Marcello Costagliola 7b16014f01 Delete a room's messages in a job, one transaction each
Room#destroy destroyed every message inside the room's own
transaction, which holds SQLite's write lock until the last one: on a
room with many messages, every other write in the app waited and
failed. The request now takes the room away from its members and
leaves the rest to Room::DestroyJob, which destroys the messages one
at a time, each in its own short transaction, and then the room.

An open room is closed in the request, so that someone who joins the
account before the job ends isn't given it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bj8KnxpTf9sj2Ysa8aLAVa
2026-10-06 13:57:05 +02:00

108 lines
3.0 KiB
Ruby

class Room < ApplicationRecord
has_many :memberships, dependent: :delete_all do
def grant_to(users)
room = proxy_association.owner
Membership.insert_all(Array(users).collect { |user| { room_id: room.id, user_id: user.id, involvement: room.default_involvement } })
end
def revoke_from(users)
destroy_by user: users
end
def revise(granted: [], revoked: [])
transaction do
grant_to(granted) if granted.present?
revoke_from(revoked) if revoked.present?
end
end
end
has_many :users, through: :memberships
has_many :messages, dependent: :destroy
belongs_to :creator, class_name: "User", default: -> { Current.user }
validate :direct_rooms_keep_their_type, on: :update
scope :opens, -> { where(type: "Rooms::Open") }
scope :closeds, -> { where(type: "Rooms::Closed") }
scope :directs, -> { where(type: "Rooms::Direct") }
scope :without_directs, -> { where.not(type: "Rooms::Direct") }
scope :ordered, -> { order("LOWER(name)") }
class << self
def create_for(attributes, users:)
transaction do
create!(attributes).tap do |room|
room.memberships.grant_to users
end
end
end
def original
order(:created_at).first
end
end
# Takes the room away from its members at once, and leaves its messages to Room::DestroyJob. Destroying them in
# the room's own transaction held the database's write lock, and stopped every other write, until the last one.
# An open room is closed first, so that no one who joins the account before the job runs is let in.
def destroy_later
room = open? ? becomes!(Rooms::Closed) : self
transaction do
room.save!
room.memberships.delete_all
end
Room::DestroyJob.perform_later(room)
end
# Each message is destroyed in its own transaction, so other writes get through in between. Any message posted
# meanwhile goes with the room.
def destroy_one_message_at_a_time
messages.find_each(&:destroy)
destroy
end
def receive(message)
unread_memberships(message)
push_later(message)
end
def open?
is_a?(Rooms::Open)
end
def closed?
is_a?(Rooms::Closed)
end
def direct?
is_a?(Rooms::Direct)
end
def default_involvement
"mentions"
end
private
# Open and closed rooms convert into each other freely. A direct room can't become
# either: its participants agreed to a private conversation, not to one whose
# audience someone else gets to widen afterwards.
def direct_rooms_keep_their_type
if type_changed? && type_was == "Rooms::Direct"
errors.add :type, "can't be changed for a direct room"
end
end
def unread_memberships(message)
memberships.visible.disconnected.where.not(user: message.creator).update_all(unread_at: message.created_at, updated_at: Time.current)
end
def push_later(message)
Room::PushMessageJob.perform_later(self, message)
end
end