Files
synapse/rust/src/storage/events.rs
T
Erik JohnstonandClaude Opus 5.5 a78b02b55c Load events in Rust for Python too, on the native driver
With the main database on the native Postgres driver, every event load
goes through the Rust `EventLoader`. `EventsWorkerStore` sends the events
its own lookups miss to `RustStore.load_events`, and `_get_events_from_db`
to `reload_events`. Loads from Python, sync and state resolution then
coalesce in the shared event cache, and an invalidation during a load
stops it caching a stale entry. Python's fetch threads and in-flight map
stay only for other databases.

- `EventLoader::load` goes through `Cache::get_or_load_many`. The extra
  redaction events it reads are returned but no longer cached.
- `PoolQuerier` checks its connection out before claiming any loads, so
  readers that hold connections while they wait cannot starve a load.
- An unusable event raises `InvalidEventError` or
  `DatabaseCorruptionError`, as from Python, which some callers catch.
- Cancelling a Python caller leaves the load running for the others, and
  `evt_db_fetch_count` counts only what a caller read itself.
- `RustStore` holds the `DatabasePool` weakly and reads its pool per call.

Tests: trial makes the loads block, as it does Python's transactions
(`_BlockingRustCalls`, renamed from `_BlockingRustSyncHandler`). The
cancellation and outage tests get Rust variants. Two tests stop depending
on how far requests advance the clock or on which `DatabasePool` object
a worker store keeps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VpBQxvqZTUvHCMm3u6CR7J
2026-10-16 09:34:35 +00:00

885 lines
32 KiB
Rust

/*
* This file is licensed under the Affero General Public License (AGPL) version 3.
*
* Copyright (C) 2026 Element Creations Ltd.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* See the GNU Affero General Public License for more details:
* <https://www.gnu.org/licenses/agpl-3.0.html>.
*/
//! Loading events from the database, redactions included.
//!
//! Port of the read side of `EventsWorkerStore`: [`EventLoader::load`] is
//! `_get_events_from_db` behind `get_unredacted_events_from_cache_or_db`,
//! and [`EventLoader::get_events`] is `get_events_as_list`. Events read
//! from the database go into the [`EventCache`] as a [`CachedEvent`], which
//! holds the event as persisted and, when a valid redaction of it was found,
//! the redacted form, as Python's `EventCacheEntry` does. A reader picks
//! between them with a [`RedactBehaviour`].
//!
//! The loader runs its statements through a [`Querier`], so it works inside
//! the state resolution's long transaction as well as on a connection the
//! sync store has borrowed for one batch.
use std::{
collections::{HashMap, HashSet},
sync::{
atomic::{AtomicU64, Ordering},
Arc, Mutex, OnceLock,
},
time::{Duration, Instant},
};
use anyhow::Context;
use async_trait::async_trait;
use log::{debug, error, info, warn};
use pyo3::{Py, PyResult, Python};
use tokio_postgres::{types::ToSql, Row};
use crate::caches::lock_ignoring_poison;
use crate::event_auth::get_domain_from_id;
use crate::events::{
constants::event_type::{M_ROOM_CREATE, M_ROOM_MEMBER, M_ROOM_REDACTION},
internal_metadata::EventInternalMetadata,
redact_event_py, Event,
};
use crate::room_versions::{EventFormatVersions, RoomVersion};
use crate::storage::caches::EventCache;
/// Runs statements on one database connection.
///
/// Implemented by the state resolution's transaction and by the sync
/// store's borrowed connections, so [`EventLoader`] serves both.
#[async_trait]
pub trait Querier: Send + Sync {
async fn query(&self, sql: &str, params: &[&(dyn ToSql + Sync)]) -> anyhow::Result<Vec<Row>>;
}
/// An event as the event cache holds it: the event as persisted, rejected
/// or not, and its redacted form if a valid redaction was known when it
/// was loaded. The auth event IDs are worked out on first use and kept, as
/// state resolution reads them many times and most other readers never do.
///
/// An event handed in from Python shares the Python object's data,
/// including its internal metadata and unsigned data.
#[derive(Clone)]
pub struct CachedEvent(Arc<CachedEventInner>);
struct CachedEventInner {
event: Event,
/// The event with the redaction applied, when one was found.
redacted: Option<Event>,
auth_event_ids: OnceLock<Box<[Arc<str>]>>,
}
impl CachedEvent {
/// An event without a known redaction: one from memory rather than the
/// database.
pub fn new(event: Event) -> Self {
Self::with_redaction(event, None)
}
pub fn with_redaction(event: Event, redacted: Option<Event>) -> Self {
CachedEvent(Arc::new(CachedEventInner {
event,
redacted,
auth_event_ids: OnceLock::new(),
}))
}
/// The event as persisted.
pub fn event(&self) -> &Event {
&self.0.event
}
/// The event with its redaction applied, if it has a valid one.
pub fn redacted(&self) -> Option<&Event> {
self.0.redacted.as_ref()
}
/// The IDs of the event's auth events.
///
/// Fails only for an event whose format derives them from internal
/// metadata that was never filled in. A failure is not kept, so a later
/// call tries again.
pub fn auth_event_ids(&self) -> anyhow::Result<&[Arc<str>]> {
if let Some(ids) = self.0.auth_event_ids.get() {
return Ok(ids);
}
let ids = auth_event_ids_of(&self.0.event)?;
Ok(self.0.auth_event_ids.get_or_init(|| ids))
}
/// The event as `behaviour` wants it: `None` for [`RedactBehaviour::Block`]
/// on a redacted event.
pub fn select(&self, behaviour: RedactBehaviour) -> Option<&Event> {
match (self.redacted(), behaviour) {
(Some(_), RedactBehaviour::Block) => None,
(Some(redacted), RedactBehaviour::Redact) => Some(redacted),
_ => Some(self.event()),
}
}
/// The event as a new Python object sharing this event's data.
pub fn to_py(&self, py: Python<'_>) -> PyResult<Py<Event>> {
Py::new(py, self.0.event.shallow_clone())
}
/// The redacted form as a new Python object, if there is one.
pub fn redacted_to_py(&self, py: Python<'_>) -> PyResult<Option<Py<Event>>> {
self.0
.redacted
.as_ref()
.map(|redacted| Py::new(py, redacted.shallow_clone()))
.transpose()
}
}
impl std::ops::Deref for CachedEvent {
type Target = Event;
fn deref(&self) -> &Event {
self.event()
}
}
fn auth_event_ids_of(event: &Event) -> anyhow::Result<Box<[Arc<str>]>> {
let ids = event
.auth_event_ids()
.map_err(|e| anyhow::anyhow!("{e}"))
.with_context(|| format!("auth event IDs of {}", event.event_id()))?;
Ok(ids.into_iter().map(Arc::from).collect())
}
/// Why an event in the database cannot be used. Each kind is a Python
/// error of the same name, which some callers catch to skip a broken room.
#[derive(Debug)]
pub enum UnusableEvent {
/// `InvalidEventError`.
Invalid(String),
/// `DatabaseCorruptionError`: the event ID computed from the stored
/// event doesn't match the stored ID.
Corrupt {
room_id: String,
persisted_event_id: String,
computed_event_id: String,
},
}
impl std::fmt::Display for UnusableEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UnusableEvent::Invalid(message) => f.write_str(message),
UnusableEvent::Corrupt {
room_id,
persisted_event_id,
computed_event_id,
} => write!(
f,
"Database corruption: event {persisted_event_id} in room {room_id} has event ID {computed_event_id}"
),
}
}
}
impl std::error::Error for UnusableEvent {}
/// What to do with a redacted event. `EventRedactBehaviour`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RedactBehaviour {
/// The full event body.
AsIs,
/// The event with its body redacted.
Redact,
/// Leave the event out.
Block,
}
impl RedactBehaviour {
/// Parse the name of a member of Python's `EventRedactBehaviour`.
pub fn parse(name: &str) -> Option<Self> {
match name {
"as_is" => Some(RedactBehaviour::AsIs),
"redact" => Some(RedactBehaviour::Redact),
"block" => Some(RedactBehaviour::Block),
_ => None,
}
}
}
/// What an [`EventLoader`] did, for metrics.
#[derive(Debug, Default, Clone, Copy)]
pub struct EventLoadStats {
/// Events loaded from the database.
pub fetched: u64,
/// Events found in the event cache.
pub cached: u64,
/// Time spent building events from rows: parsing the JSON and computing
/// the event IDs.
pub parse_time: Duration,
}
impl EventLoadStats {
fn add(&mut self, other: &EventLoadStats) {
self.fetched += other.fetched;
self.cached += other.cached;
self.parse_time += other.parse_time;
}
}
/// `ids` without repeats, in order.
fn dedup(ids: &[Arc<str>]) -> Vec<Arc<str>> {
let mut seen = HashSet::with_capacity(ids.len());
ids.iter()
.filter(|id| seen.insert(Arc::clone(id)))
.cloned()
.collect()
}
/// Loads events through `querier`, consulting `cache` first and filling it
/// with what it loads.
pub struct EventLoader<'a> {
querier: &'a dyn Querier,
cache: Option<&'a EventCache>,
}
/// How many event IDs to send per statement.
const EVENT_BATCH_SIZE: usize = 500;
const LOAD_EVENTS_SQL: &str = "
SELECT e.event_id, e.stream_ordering, e.received_ts, e.instance_name,
ej.internal_metadata, ej.json, ej.format_version, r.room_version,
rej.reason, e.outlier
FROM events AS e
JOIN event_json AS ej USING (event_id)
LEFT JOIN rooms AS r ON r.room_id = e.room_id
LEFT JOIN rejections AS rej USING (event_id)
WHERE e.event_id = ANY($1)
";
const LOAD_REDACTIONS_SQL: &str = "
SELECT event_id, redacts, recheck FROM redactions WHERE redacts = ANY($1)
";
const LOAD_BAN_REDACTIONS_SQL: &str = "
SELECT room_id, user_id, redacting_event_id, redact_end_ordering
FROM room_ban_redactions
WHERE room_id = ANY($1) AND user_id = ANY($2)
";
/// A row of [`LOAD_EVENTS_SQL`] with its redactions, as `_EventRow`.
struct FetchedEvent {
event: Event,
/// Redaction events that claim to redact this one and must be checked
/// against it: room v3 and later redactions whose sender could not be
/// verified when they were persisted, and MSC4293 ban redactions.
unconfirmed_redactions: Vec<Arc<str>>,
/// Redactions already known to be valid.
confirmed_redactions: Vec<Arc<str>>,
}
impl<'a> EventLoader<'a> {
pub fn new(querier: &'a dyn Querier, cache: Option<&'a EventCache>) -> Self {
EventLoader { querier, cache }
}
/// `get_unredacted_events_from_cache_or_db` with `allow_rejected`:
/// the events by ID, rejected ones included, from the cache or the
/// database. Events that are not found are left out. The result can
/// hold events that were not asked for: the redaction events read to
/// check an unconfirmed redaction.
///
/// With a cache, an event another caller is loading is waited for
/// rather than read again, and the rest are read in one go. Those that
/// were asked for are cached, unless invalidated while being read. The
/// extra redaction events are not, as nothing guards them against an
/// invalidation during the read.
pub async fn load(
&self,
ids: &[Arc<str>],
stats: &mut EventLoadStats,
) -> anyhow::Result<HashMap<Arc<str>, CachedEvent>> {
let Some(cache) = self.cache else {
let fetched = self.fetch_from_db(dedup(ids), stats).await?;
stats.fetched += fetched.len() as u64;
return Ok(fetched);
};
// `get_or_load_many` takes a closure it may call more than once, so
// the closure cannot hold `stats` itself.
let load_stats = Mutex::new(EventLoadStats::default());
let loaded_requested = AtomicU64::new(0);
let events = cache
.get_or_load_many(ids, |missing| {
let load_stats = &load_stats;
let loaded_requested = &loaded_requested;
async move {
let mut batch_stats = EventLoadStats::default();
let fetched = self
.fetch_from_db(missing.clone(), &mut batch_stats)
.await?;
let requested = missing.iter().filter(|id| fetched.contains_key(*id));
loaded_requested.fetch_add(requested.count() as u64, Ordering::Relaxed);
batch_stats.fetched += fetched.len() as u64;
lock_ignoring_poison(load_stats).add(&batch_stats);
Ok(fetched)
}
})
.await
.map_err(anyhow::Error::new)?;
let load_stats = lock_ignoring_poison(&load_stats);
stats.add(&load_stats);
let requested_found = ids.iter().filter(|id| events.contains_key(*id)).count() as u64;
stats.cached += requested_found.saturating_sub(loaded_requested.into_inner());
Ok(events)
}
/// [`EventLoader::load`] reading every event from the database, as
/// `_get_events_from_db`: for callers that must not trust the cache.
/// What it reads replaces what was cached.
pub async fn reload(
&self,
ids: &[Arc<str>],
stats: &mut EventLoadStats,
) -> anyhow::Result<HashMap<Arc<str>, CachedEvent>> {
if let Some(cache) = self.cache {
// Invalidating also stops loads already in flight from caching
// what they read before ours.
for id in ids {
drop(cache.invalidate(id));
}
}
self.load(ids, stats).await
}
/// `get_events_as_list`: the events in the order of `ids`, each as
/// `behaviour` wants it, leaving out those that are not found, those
/// that were rejected unless `allow_rejected`, and redaction events
/// whose target is missing or which turn out not to be authorised. With
/// `prev_content`, a state event carries the content and sender of the
/// state it replaced in its unsigned data.
pub async fn get_events(
&self,
ids: &[Arc<str>],
behaviour: RedactBehaviour,
allow_rejected: bool,
prev_content: bool,
stats: &mut EventLoadStats,
) -> anyhow::Result<Vec<Event>> {
let events = self
.get_events_without_prev_content(ids, behaviour, allow_rejected, stats)
.await?;
if prev_content {
self.add_prev_content(&events, stats).await?;
}
Ok(events)
}
/// [`EventLoader::get_events`] without the previous content, which it
/// loads through this.
async fn get_events_without_prev_content(
&self,
ids: &[Arc<str>],
behaviour: RedactBehaviour,
allow_rejected: bool,
stats: &mut EventLoadStats,
) -> anyhow::Result<Vec<Event>> {
if ids.is_empty() {
return Ok(Vec::new());
}
let entries = self.load(ids, stats).await?;
// A redaction is only served once its target is known to exist and
// the redaction to be authorised, which can mean rechecking it now.
let mut redacted_ids: Vec<Arc<str>> = Vec::new();
if !allow_rejected {
for id in ids {
let Some(entry) = entries.get(id) else {
continue;
};
if entry.type_() == M_ROOM_REDACTION {
if let Some(redacts) = entry.redacts().and_then(|v| v.as_str()) {
redacted_ids.push(Arc::from(redacts));
}
}
}
}
let originals = if redacted_ids.is_empty() {
HashMap::new()
} else {
self.load(&redacted_ids, stats).await?
};
let mut events = Vec::with_capacity(ids.len());
for id in ids {
let Some(entry) = entries.get(id) else {
continue;
};
if !allow_rejected {
if entry.rejected_reason().is_some() {
continue;
}
if entry.type_() == M_ROOM_REDACTION && !redaction_is_servable(entry, &originals)? {
continue;
}
}
let Some(event) = entry.select(behaviour) else {
continue;
};
events.push(event.shallow_clone());
}
Ok(events)
}
/// The `get_prev_content` part of `get_events_as_list`: give each state
/// event the content and sender of the state it replaced, unless it has
/// them already. The unsigned data is shared with any Python object of
/// the event, so this shows wherever the event is held, as in Python.
async fn add_prev_content(
&self,
events: &[Event],
stats: &mut EventLoadStats,
) -> anyhow::Result<()> {
let mut wanted: Vec<(usize, Arc<str>)> = Vec::new();
for (i, event) in events.iter().enumerate() {
let unsigned = event.unsigned_ref();
if let Some(replaces) = unsigned.replaces_state().map_err(py_err)? {
if !unsigned.has_prev_content_and_sender().map_err(py_err)? {
wanted.push((i, Arc::from(replaces)));
}
}
}
if wanted.is_empty() {
return Ok(());
}
let ids: Vec<Arc<str>> = wanted.iter().map(|(_, id)| Arc::clone(id)).collect();
let prevs = self
.get_events_without_prev_content(&ids, RedactBehaviour::Redact, false, stats)
.await?;
let prevs: HashMap<&str, &Event> = prevs.iter().map(|e| (e.event_id(), e)).collect();
for (i, prev_id) in wanted {
if let Some(prev) = prevs.get(&*prev_id) {
let content = serde_json::to_value(prev.content())
.context("serializing the previous content")?;
events[i]
.unsigned_ref()
.set_prev_content_and_sender(content, prev.sender())
.map_err(py_err)?;
}
}
Ok(())
}
/// `_get_events_from_db`: load `ids` and whatever redaction events are
/// needed to check their unconfirmed redactions, and decide each
/// event's redaction.
async fn fetch_from_db(
&self,
ids: Vec<Arc<str>>,
stats: &mut EventLoadStats,
) -> anyhow::Result<HashMap<Arc<str>, CachedEvent>> {
let mut fetched: HashMap<Arc<str>, FetchedEvent> = HashMap::with_capacity(ids.len());
let mut tried: HashSet<Arc<str>> = HashSet::with_capacity(ids.len());
let mut to_fetch = ids;
while !to_fetch.is_empty() {
let mut rows = self.fetch_rows(&to_fetch, stats).await?;
// A redaction whose sender could not be verified at persist time
// is checked here, which needs the redaction event itself.
let mut redaction_ids: HashSet<Arc<str>> = HashSet::new();
for id in to_fetch.drain(..) {
if let Some(row) = rows.remove(&id) {
if row.confirmed_redactions.is_empty() {
redaction_ids.extend(row.unconfirmed_redactions.iter().cloned());
}
fetched.insert(Arc::clone(&id), row);
}
tried.insert(id);
}
to_fetch = redaction_ids
.into_iter()
.filter(|id| !tried.contains(id))
.collect();
if !to_fetch.is_empty() {
debug!("Also fetching redaction events {to_fetch:?}");
}
}
let mut redactions: Vec<(Arc<str>, Option<Event>)> = Vec::with_capacity(fetched.len());
for (event_id, row) in &fetched {
let redacted = maybe_redact(
&row.event,
&row.unconfirmed_redactions,
&row.confirmed_redactions,
&fetched,
)?;
redactions.push((Arc::clone(event_id), redacted));
}
let mut events = HashMap::with_capacity(fetched.len());
for (event_id, redacted) in redactions {
let row = fetched.remove(&event_id).expect("built from the same map");
events.insert(event_id, CachedEvent::with_redaction(row.event, redacted));
}
Ok(events)
}
/// `_fetch_event_rows`: the events' rows with their redactions, parsed.
/// Events that are not found, or whose row cannot be read, are left out.
async fn fetch_rows(
&self,
ids: &[Arc<str>],
stats: &mut EventLoadStats,
) -> anyhow::Result<HashMap<Arc<str>, FetchedEvent>> {
let mut fetched = HashMap::with_capacity(ids.len());
for batch in ids.chunks(EVENT_BATCH_SIZE) {
let batch_ids: Vec<&str> = batch.iter().map(|id| &**id).collect();
let rows = self.querier.query(LOAD_EVENTS_SQL, &[&batch_ids]).await?;
let start = Instant::now();
let mut batch_events: HashMap<Arc<str>, FetchedEvent> =
HashMap::with_capacity(rows.len());
for row in rows {
if let Some((event_id, event)) = event_from_row(&row)? {
batch_events.insert(
event_id,
FetchedEvent {
event,
unconfirmed_redactions: Vec::new(),
confirmed_redactions: Vec::new(),
},
);
}
}
stats.parse_time += start.elapsed();
let rows = self
.querier
.query(LOAD_REDACTIONS_SQL, &[&batch_ids])
.await?;
for row in rows {
let redacter: String = row.try_get(0)?;
let redacted: String = row.try_get(1)?;
let recheck: Option<bool> = row.try_get(2)?;
if let Some(event) = batch_events.get_mut(&*redacted) {
let list = if recheck.unwrap_or(false) {
&mut event.unconfirmed_redactions
} else {
&mut event.confirmed_redactions
};
list.push(Arc::from(redacter));
}
}
// MSC4293: a ban or kick can redact the user's events in the
// room up to it.
let mut room_ids: Vec<&str> = Vec::new();
let mut user_ids: Vec<&str> = Vec::new();
for event in batch_events.values() {
room_ids.push(event.event.room_id());
user_ids.push(event.event.sender());
}
room_ids.sort_unstable();
room_ids.dedup();
user_ids.sort_unstable();
user_ids.dedup();
if !room_ids.is_empty() {
let rows = self
.querier
.query(LOAD_BAN_REDACTIONS_SQL, &[&room_ids, &user_ids])
.await?;
for row in rows {
let room_id: String = row.try_get(0)?;
let user_id: String = row.try_get(1)?;
let redacting_event_id: String = row.try_get(2)?;
let redact_end_ordering: Option<i64> = row.try_get(3)?;
let redacting_event_id: Arc<str> = Arc::from(redacting_event_id);
for event in batch_events.values_mut() {
if event.event.room_id() != room_id || event.event.sender() != user_id {
continue;
}
// The redaction ends at the membership event that
// ends it: events from then on are not redacted.
// Backfilled events have negative stream orderings,
// so they always are.
if let Some(end) = redact_end_ordering.filter(|end| *end != 0) {
if stream_ordering_of(&event.event) >= end {
continue;
}
}
event
.unconfirmed_redactions
.push(Arc::clone(&redacting_event_id));
}
}
}
fetched.extend(batch_events);
}
Ok(fetched)
}
}
fn py_err(e: pyo3::PyErr) -> anyhow::Error {
anyhow::anyhow!("{e}")
}
fn stream_ordering_of(event: &Event) -> i64 {
event
.internal_metadata()
.stream_ordering()
.map(i64::from)
.unwrap_or(0)
}
/// The part of `get_events_as_list` that decides whether a redaction event
/// is served, given `originals`, the events it may redact: its target must
/// be known and not rejected, not the create event, in the same room, and
/// from the redaction's own server when the redaction has not been
/// authorised yet. An authorised redaction is marked as such, so the check
/// is not repeated.
fn redaction_is_servable(
redaction: &CachedEvent,
originals: &HashMap<Arc<str>, CachedEvent>,
) -> anyhow::Result<bool> {
let Some(redacts) = redaction.redacts().and_then(|v| v.as_str()) else {
// A redacted redaction has no `redacts` key: withhold it.
debug!(
"Withholding redaction event {} as we don't have redacts key",
redaction.event_id()
);
return Ok(false);
};
let original = originals
.get(redacts)
.filter(|original| original.rejected_reason().is_none());
let Some(original) = original else {
// The redaction is assumed unauthorised until its target turns up,
// when it is rechecked.
debug!(
"Withholding redaction event {} since we don't (yet) have the original {redacts}",
redaction.event_id()
);
return Ok(false);
};
if original.type_() == M_ROOM_CREATE {
info!(
"Withholding redaction {} of create event {redacts}",
redaction.event_id()
);
return Ok(false);
}
if original.room_id() != redaction.room_id() {
info!(
"Withholding redaction {} of event {redacts} from a different room",
redaction.event_id()
);
return Ok(false);
}
if redaction
.internal_metadata()
.need_to_check_redaction()
.map_err(py_err)?
{
let original_domain = domain_of(original.sender())?;
let redaction_domain = domain_of(redaction.sender())?;
if original_domain != redaction_domain {
info!(
"Withholding redaction {} whose sender domain {redaction_domain} doesn't match that of redacted event {redacts} {original_domain}",
redaction.event_id()
);
return Ok(false);
}
redaction
.internal_metadata()
.set_recheck_redaction(false)
.map_err(py_err)?;
}
Ok(true)
}
/// `_maybe_redact_event_row`: the redacted form of `event` if one of the
/// redactions claiming it is valid. A confirmed redaction is taken as it
/// is. An unconfirmed one must be among `fetched`, not rejected, in the
/// same room, and when it has not been authorised yet, from the sender's
/// own server, in which case it is marked authorised.
fn maybe_redact(
event: &Event,
unconfirmed_redactions: &[Arc<str>],
confirmed_redactions: &[Arc<str>],
fetched: &HashMap<Arc<str>, FetchedEvent>,
) -> anyhow::Result<Option<Event>> {
if event.type_() == M_ROOM_CREATE {
// Redactions of create events are ignored.
return Ok(None);
}
if let Some(redaction_id) = confirmed_redactions.first() {
return Ok(Some(redact(event, redaction_id)?));
}
for redaction_id in unconfirmed_redactions {
let redaction = fetched
.get(redaction_id)
.map(|row| &row.event)
.filter(|redaction| redaction.rejected_reason().is_none());
let Some(redaction) = redaction else {
debug!(
"{} was redacted by {redaction_id} but redaction not found/authed",
event.event_id()
);
continue;
};
if redaction.room_id() != event.room_id() {
debug!(
"{} was redacted by {redaction_id} but redaction was in a different room!",
event.event_id()
);
continue;
}
// From room version 3, a redaction received before its target is
// checked on read instead.
if redaction
.internal_metadata()
.need_to_check_redaction()
.map_err(py_err)?
{
if domain_of(redaction.sender())? == domain_of(event.sender())? {
redaction
.internal_metadata()
.set_recheck_redaction(false)
.map_err(py_err)?;
} else {
debug!(
"{} was redacted by {redaction_id} but the senders don't match",
event.event_id()
);
continue;
}
}
debug!("Redacting {} due to {redaction_id}", event.event_id());
return Ok(Some(redact(event, redaction_id)?));
}
Ok(None)
}
/// `prune_event` plus `redacted_by`. The `redacted_because` of the
/// unsigned data is added when the event is serialized, as in Python.
fn redact(event: &Event, redaction_id: &str) -> anyhow::Result<Event> {
let redacted = redact_event_py(event)
.map_err(py_err)
.with_context(|| format!("redacting {}", event.event_id()))?;
redacted
.internal_metadata()
.set_redacted_by(Some(redaction_id.to_owned()))
.map_err(py_err)?;
Ok(redacted)
}
fn domain_of(user_id: &str) -> anyhow::Result<&str> {
get_domain_from_id(user_id).map_err(|e| anyhow::anyhow!("{e}"))
}
/// Build an [`Event`] from a row of [`LOAD_EVENTS_SQL`], the way
/// `_get_events_from_db` does.
///
/// Returns `None` for rows the Python skips: an unknown room version, an
/// event in the wrong format for its room, or JSON that does not parse.
fn event_from_row(row: &Row) -> anyhow::Result<Option<(Arc<str>, Event)>> {
let event_id: String = row.try_get(0)?;
let stream_ordering: Option<i64> = row.try_get(1)?;
let received_ts: Option<i64> = row.try_get(2)?;
let instance_name: Option<String> = row.try_get(3)?;
let internal_metadata: String = row.try_get(4)?;
let json: String = row.try_get(5)?;
let format_version: Option<i32> = row.try_get(6)?;
let room_version_id: Option<String> = row.try_get(7)?;
let rejected_reason: Option<String> = row.try_get(8)?;
let outlier: bool = row.try_get(9)?;
// Events stored before Synapse had event format versions are v1.
let format_version = format_version.unwrap_or(EventFormatVersions::ROOM_V1_V2);
let room_version_id = room_version_id.filter(|id| !id.is_empty());
let room_version: &'static RoomVersion = match room_version_id.as_deref() {
// Only out-of-band membership events from before
// matrix-org/synapse#6983 have no `rooms` entry. Those rooms are v5 or
// earlier, which all redact the same way.
None => match format_version {
EventFormatVersions::ROOM_V1_V2 => &RoomVersion::V1,
EventFormatVersions::ROOM_V3 => &RoomVersion::V3,
_ => &RoomVersion::V5,
},
Some(id) => match id.parse::<&'static RoomVersion>() {
Ok(room_version) => room_version,
Err(_) => {
warn!("Event {event_id} has unknown room version {id}");
return Ok(None);
}
},
};
if room_version_id.is_some() && room_version.event_format != format_version {
error!(
"Event {event_id} with room version {room_version} has wrong format: expected {}, was {format_version}",
room_version.event_format
);
return Ok(None);
}
let metadata = match EventInternalMetadata::from_json_str(&internal_metadata) {
Ok(metadata) => metadata,
Err(e) => {
error!("Unable to parse internal_metadata from event: {event_id}: {e:#}");
return Ok(None);
}
};
metadata.set_outlier_flag(outlier).map_err(py_err)?;
metadata
.set_persisted_position(
stream_ordering,
received_ts,
// A null instance name is a row from before sharded persisters.
Some(instance_name.unwrap_or_else(|| "master".to_owned())),
)
.map_err(py_err)?;
let event = match Event::from_json_str(
&json,
room_version,
metadata,
rejected_reason.map(String::into_boxed_str),
) {
Ok(event) => event,
Err(e) => {
error!("Unable to parse event from database {event_id}: {e:#}");
return Ok(None);
}
};
if room_version_id.is_none() && event.type_() != M_ROOM_MEMBER {
return Err(UnusableEvent::Invalid(format!(
"Room {} for event {event_id} is unknown",
event.room_id()
))
.into());
}
// If the content was modified in the database, the computed event ID
// will not match.
if event.event_id() != event_id {
return Err(UnusableEvent::Corrupt {
room_id: event.room_id().to_owned(),
persisted_event_id: event_id,
computed_event_id: event.event_id().to_owned(),
}
.into());
}
Ok(Some((Arc::from(event_id), event)))
}