pub trait AccountDataLock: AccountDataQueries {
// Provided methods
async fn lock_room_account_data(
&mut self,
user_id: &UserId,
) -> Result<(), DatabaseError> { ... }
async fn fetch_room_account_data_for_update<C>(
&mut self,
user_id: &UserId,
room_id: &RoomId,
event_type: &RoomAccountDataEventType,
) -> Result<Option<RoomAccountDataEvent<C>>, DatabaseError>
where C: RoomAccountDataEventContent + DeserializeOwned,
Self: Sized { ... }
}Expand description
Extension trait to acquire a per-user advisory lock for account data writes, held for the duration of the current transaction.
Provided Methods§
Sourceasync fn lock_room_account_data(
&mut self,
user_id: &UserId,
) -> Result<(), DatabaseError>
async fn lock_room_account_data( &mut self, user_id: &UserId, ) -> Result<(), DatabaseError>
Acquire a transaction-scoped, per-user advisory lock for account data writes.
This serializes every writer performing a read-modify-write on a
user’s account data (e.g. the tag handlers, which read the m.tag
blob, modify it in Rust, and upsert it back). It must be called
before the read that a subsequent write depends on, so that the
whole read-then-write sequence is atomic with respect to other
writers. Without it, two concurrent writers can both read the same
blob and the loser’s update is silently overwritten (lost update,
see issue #688).
pg_advisory_xact_lock is preferred over SELECT ... FOR UPDATE on
the room_account_data row: FOR UPDATE only locks rows that already
exist, so two concurrent writers inserting the first row for a
(user, room, type) key would still race. The advisory lock also
releases automatically on commit or rollback and is re-entrant within
the same session.
The two-key (room_account_data_oid, users.nid) form is deliberate —
do NOT reduce it to pg_advisory_xact_lock(nid::bigint). A small
nid in the bigint form is stored as classid = 0, the space
max_safe_seq scans for in-flight stream id values (see
00_locking_sequence.sql). Since account data writes allocate a
stream id in the same transaction, a classid = 0 lock would collapse
the sync watermark to the user’s nid and stall /sync. Namespacing by
table OID keeps the lock out of that scan.
Locking on users.nid serializes all account data writes of one user
(across rooms and data types). That is coarser than strictly needed,
but account data writes are low-frequency and this keeps the lock key
a plain int4 without hashing.
If the user does not exist no lock is taken — handlers only run for authenticated (i.e. existing) users.
Sourceasync fn fetch_room_account_data_for_update<C>(
&mut self,
user_id: &UserId,
room_id: &RoomId,
event_type: &RoomAccountDataEventType,
) -> Result<Option<RoomAccountDataEvent<C>>, DatabaseError>where
C: RoomAccountDataEventContent + DeserializeOwned,
Self: Sized,
async fn fetch_room_account_data_for_update<C>(
&mut self,
user_id: &UserId,
room_id: &RoomId,
event_type: &RoomAccountDataEventType,
) -> Result<Option<RoomAccountDataEvent<C>>, DatabaseError>where
C: RoomAccountDataEventContent + DeserializeOwned,
Self: Sized,
Retrieve room account data for a user in a specific room, taking the
per-user account data lock (Self::lock_room_account_data) first.
Use this instead of fetch_room_account_data whenever the fetched
value is modified and written back within the same transaction
(read-modify-write), so concurrent writers are serialized and cannot
overwrite each other’s updates (#688).
Dyn Compatibility§
This trait is not dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".
Implementations on Foreign Types§
impl AccountDataLock for TransactedDb<'_>
The lock is transaction-scoped (pg_advisory_xact_lock releases on commit
or rollback), so it is only implemented for TransactedDb: taking it on
a plain connection would release it again right after the statement,
providing no protection.