Commit Graph
861 Commits
Author SHA1 Message Date
Erik JohnstonandClaude Opus 5.5 47367a14a2 Make the Rust event cache EventsWorkerStore's event cache
`EventsWorkerStore._get_event_cache` is now the Rust `EventCache` that the
sync handler and state resolution read, so each event is held once.

The cache keeps Rust `CachedEvent` values. Python gets them through
`RustEventCache`, a `RustCache` subclass with the `AsyncLruCache` names the
rest of Synapse uses, and each hit wraps the cached event in a new `Event`
object. The wrapper shares the cached event's internal metadata, unsigned
data and signatures, so a change Python makes, such as adding
`prev_content` or flipping `outlier`, lands in the one copy the Rust
readers see. Python's `_event_ref` weak map is checked before the cache,
so while anything holds an event, a lookup returns that same object, as
before. A hit there marks the cache entry as read, and puts the event back
if it was evicted.

- The cache is sized from `caches.event_cache_size` and named
  `*getEvent*`, so its metrics and `per_cache_factors` keep their names.
  The Rust loader now counts its misses too.
- Invalidating an event drops its chain position as well, and a room
  purge drops the room's events through the room index, plus the chain
  cover and state group caches, as `RustStore.invalidate_event` and
  `clear_event_cache` did. Those two calls are gone.
- `EventsWorkerStore` and `RustWorkerStore` run their `__init__` bodies
  in a different order in `DataStore` and `GenericWorkerStore`, as they
  sit differently in each MRO. So whichever runs first builds the
  `RustStore`. A datastore without `RustWorkerStore`, such as the port
  script's, gets a standalone `RustEventCache`.
- The external cache stubs (`get_external`, `set_external`,
  `_invalidate_async_get_event_cache` and its room variant) are dropped.
  They were documented no-ops.
- The sync shadow diff still ignores `prev_content` on state events.
  Python keeps the objects it holds after the cache evicts them, so a Rust
  reader can still load a second copy without it. Trial's cache of one
  event makes that routine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VpBQxvqZTUvHCMm3u6CR7J
2026-10-15 12:14:54 +00:00
Erik JohnstonandClaude Fable 5.1 f7c9f55d8f Add the Rust sync store
The queries of the per-room sync loop, on the native Postgres driver, each
a port of the store method it is named after. A `SyncReader` is made per
request and borrows a pooled connection for one group of statements at a
time, so that a sync worker with hundreds of requests in flight does not
pin a connection per room; a group that needs one snapshot runs in a
`REPEATABLE READ READ ONLY` transaction. State groups are loaded whole,
with the recursive CTE sent as one simple query per batch, and cached as
full maps.

The caches that copy a `@cached` function (rooms for user, room summary,
partial state rooms, erased users, retention policies, event to state
group, unread counts) live on the Rust store and are attached to the
Python cache each names by the new `RustWorkerStore`, which also takes
over building the Rust store. Unread counts are invalidated by room as
well as by room and user, as the Python tree cache is.

`tests/storage/test_sync_store.py` runs every query and the Python
method against the same database, with multi-writer tokens, redactions
of every kind, and the summary rotation of the unread counts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194eNQP5weBtVMg8NKtc6VU
2026-10-14 15:11:51 +00:00
Erik JohnstonandClaude Fable 5.1 4d002f82eb Move is_feature_enabled onto a cache that lives in Rust
The first Rust database function to get a cache, and the first Python
`@cached` function to be replaced by one. The Rust `/versions` handler
calls `Store::is_feature_enabled_for_user` on every authenticated request,
and until now each call ran a query. The Python `is_feature_enabled` had a
cache of the same rows under another name, which every writer would have
had to invalidate alongside. Now there is one cache, keyed on
`(user_id, feature)`, and both sides read through it.

The main datastore builds the Rust `Store` in its `__init__`, over its own
`db_pool`, and `hs.get_rust_store()` hands it to the Rust handlers: the
datastore owns the store, the handlers borrow it. The store's
`is_feature_enabled_for_user` reaches Python as a callable cache object,
bound on the datastore under that name. Calling it answers a hit with an
already-fired Deferred, without leaving the reactor thread, and only a
miss goes through tokio. Being a `RustCache`, it is also what
`_invalidate_cache_and_stream`, replication and `register_cache` take, so
`set_features_for_user` invalidates it with the one call it already made,
and the name is spelled once in Rust, where the cache is built.

`is_feature_enabled` keeps its global-config short cut in Python and
delegates the per-user lookup.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ULdieJyQoHNzeHq42H2M1p
2026-10-14 12:42:31 +00:00
Erik JohnstonandClaude Fable 5.1 368465b323 Cache the chain cover index on the Rust state resolution handler
With the event cache warm, the chain cover queries were what was left of
the database work in a resolution: the position of every event in the
state sets, and the links out of every chain they are on. Both can be
cached.

A chain position never changes once assigned, so it is cached before
the transaction commits, and dropped with the event in case the event is
purged and received again. The links out of a chain only grow, at the
end: an event appended at sequence number n adds links out of (chain, n)
and changes nothing below. So links read while the chain was known to
reach n are complete for every walk that starts at or below n, and the
cache keeps that bound with them. A walk that needs a chain beyond its
bound refetches it. Both caches go on a room purge, which deletes the
room's chains and links.

The chain cover walk now gathers links first (from the cache, following
the links a backwards walk would follow so reached chains are gathered
too, and from the database for the rest) and then walks them once. The
Python walks each batch of its recursive query as it arrives, but a batch
is closed under reachability, so walking the union comes to the same
thing. Fetching only what a sequence-aware walk reaches, rather than the
full reachability closure, leaves the auth difference and the v2.1
conflicted subgraph unchanged: any forwards path that ends inside a
backwards-reachable range runs through chains the backwards walks
reached, below their bounds. The trial comparison against the Python
runs every case through a handler's caches too.

The three store-side caches are bundled as StateResCaches, so the store
and the invalidation hooks treat them as one. The integer-keyed maps in
the chain cover code and the interner use FxHash, and the reverse
position map is one flat map, which together are worth 2 ms on the warm
path of the benchmark. The stats gained the cache hit counts and
timings for the auth chain difference, event loading and the
connection.

On the benchmark room (2,000 members, 1,000 conflicted events) a warm
resolution runs no chain queries and takes 14 to 20 ms end to end, from
27 to 32 with the event cache alone; Python warm is 38 to 41.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ULdieJyQoHNzeHq42H2M1p
2026-10-13 16:52:05 +00:00
Erik JohnstonandClaude Opus 5.5 a446b378dc Test the Rust state res store against the Python one
When the tests run on the native driver, the auth difference tests also
check that the Rust chain cover port gives the same auth difference and
conflicted subgraph as _get_auth_chain_difference_using_cover_index_txn,
and a new test compares events loaded by Rust with those loaded by
Python.

    SYNAPSE_TEST_RUST_DRIVER=1 POSTGRES=1 run-trial.sh tests.storage.test_event_federation

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ULdieJyQoHNzeHq42H2M1p
2026-10-13 10:49:24 +00:00
Erik JohnstonandClaude Opus 5.5 34fdef7a9d Use the Rust auth rules from Python
check_state_dependent_auth_rules and auth_types_for_event now call the
Rust implementation, so every caller (federation, sticky events, state
res v1 and v2) uses it. The Python helpers that only they used are gone.
The ed25519 check for third-party invites stays in Python, as a helper
the Rust rule calls.

auth_types_for_event returns its pairs in the order the Python added
them. Event creation lists the auth events in the set's iteration
order, so a Rust HashSet's random order changed event IDs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ULdieJyQoHNzeHq42H2M1p
2026-10-13 10:49:24 +00:00
Erik JohnstonandClaude Fable 5.1 4636443dd2 Ask for the isolation level on BEGIN rather than setting the session default
The Rust pool set REPEATABLE READ as the session's default transaction
isolation, so autocommit statements ran at REPEATABLE READ too. psycopg2
applies its isolation level only to the transactions it opens, and
autocommit statements keep the server's default of READ COMMITTED.
Synapse's worker read/write lock release runs in autocommit mode and
relies on that: Complement's TestMSC3757OwnedState failed on the Rust
driver with a foreign-key violation while releasing a lock, and passed
on psycopg2.

Mirror psycopg2: the connection opens its implicit transactions with
`BEGIN ISOLATION LEVEL ...` (REPEATABLE READ unless the engine's
`set_isolation_level` says otherwise), and the pool no longer touches the
session default.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ULdieJyQoHNzeHq42H2M1p
2026-10-12 21:07:12 +00:00
Erik JohnstonandClaude Fable 5.1 066a2e9ab6 Route bulk VALUES statements through one LoggingTransaction helper
Five call sites repeated the same choice: `execute_values` with a
`VALUES ?` statement on psycopg2, or `execute_batch` with a row of
placeholders elsewhere. `execute_batch_values` makes that choice once,
given the statement and the row width, so the store code no longer
needs to know which driver has psycopg2's extras.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ULdieJyQoHNzeHq42H2M1p
2026-10-12 18:55:01 +00:00
Erik JohnstonandClaude Fable 5.1 a318c58f61 Make the Rust pool tests async and cover txn_limit
The pool tests were written with `inlineCallbacks`; trial runs `async`
test methods directly, so use `await`. Adds a test that `txn_limit`
replaces the pooled connection after the configured number of checkouts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ULdieJyQoHNzeHq42H2M1p
2026-10-12 18:43:26 +00:00
Erik JohnstonandClaude Fable 5.1 8c66441a95 Export connection-pool metrics for the Rust backend
Pool exhaustion was invisible until checkouts timed out. Expose
deadpool's occupancy from the Rust pool and publish it as gauges per
pool (max connections, open connections, idle connections, waiting
checkouts), plus a histogram of the time a checkout waits for a
connection.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ULdieJyQoHNzeHq42H2M1p
2026-10-12 18:41:08 +00:00
Erik JohnstonandClaude Fable 5.1 7d86758f01 Retry transactions whose cached plan went stale on the Rust backend
The Rust backend prepares and caches statements per connection. DDL that
changes a cached statement's result type, such as a schema delta applied
during a rolling restart, makes Postgres reject the stale plan with
SQLSTATE 0A000. Inside a transaction that error used to propagate as a
plain DatabaseError, which `new_transaction` does not retry, so other
workers saw transient failures until their caches turned over.

The cursor already drops the cached entry before the error propagates,
so treating 0A000 like a deadlock means the retried transaction prepares
a fresh plan.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ULdieJyQoHNzeHq42H2M1p
2026-10-12 18:36:20 +00:00
Erik JohnstonandClaude Fable 5.1 fc5e705f45 Pass the runtime when building the checkout-timeout test's pool
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ULdieJyQoHNzeHq42H2M1p
2026-10-12 18:31:24 +00:00
Erik JohnstonandClaude Opus 4.8 b6a59c3396 Route the Rust backend's bulk write paths off psycopg2's execute_values
The Rust shim has no `psycopg2.extras`, so the bulk write helpers that Synapse
runs via `execute_values` need a shim-compatible path. Add a
`uses_psycopg2_extras` engine capability flag (True for psycopg2, False for the
Rust and SQLite engines) and branch the write paths on it:

  - The bulk INSERT/DELETE helpers (`simple_insert_many_txn`,
    `simple_upsert_many_txn`, `simple_delete_many_batch_txn`, and the
    sliding-sync / relations / state-deletion inserts) use psycopg2's single
    multi-row `execute_values` on psycopg2, and `execute_batch` (the shim's
    pipelined `executemany`) on the Rust and SQLite backends.
  - `execute_values` itself is now psycopg2-only (it asserts
    `uses_psycopg2_extras`); no query path splices row values into SQL as
    literals.
  - The four VALUES-join / RETURNING queries that `execute_values(fetch=True)`
    served (in `event_federation` and `end_to_end_keys`) can't be expressed by
    `executemany`, so they use a parameterised `unnest(...)` form that works on
    all three backends.

Also cast the sequence name in `nextval(?::text)`: the Rust driver binds typed
parameters and can't produce a `regclass` from a name, whereas a text argument
coerces (this is the only parameterised sequence call, and Postgres-only SQL, so
psycopg2 and sqlite are unaffected).

psycopg2 behaviour and performance are unchanged; the Rust and SQLite bulk paths
now match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3G4M92AmwSSZCbmtMJU3d
2026-10-12 18:31:24 +00:00
Erik JohnstonandClaude Opus 4.8 f2b41a6005 Add a connection-checkout timeout to the native Rust Postgres pool
A checkout from the Rust connection pool (`ConnectionPool.connect`) previously
blocked indefinitely while waiting for a free slot or establishing a new
connection, so an unreachable or overloaded server could hang the worker
threads handling requests forever.

Bound the whole checkout with a configurable timeout: deadpool's `wait` and
`create` timeouts (enabled via its `rt_tokio_1` runtime feature, driven on our
own tokio runtime). Hitting it raises an OperationalError, which Synapse
already treats as a retryable operational error.

Configurable via the `pool_checkout_timeout` database option (milliseconds;
default 30s, `0` disables), read by the Rust engine and plumbed to both the
connection pool and the bootstrap (pool-of-one) connection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3G4M92AmwSSZCbmtMJU3d
2026-10-12 18:31:24 +00:00
Erik JohnstonandClaude Opus 4.8 c8d43aebc7 Add TLS support to the native Rust Postgres backend
The Rust backend connected with `NoTls`, so it couldn't talk to a Postgres that
requires (or, as libpq defaults, prefers) TLS — a blocker for most managed /
networked databases. Honour the same libpq keys psycopg2 already forwards, so an
existing `database.args` keeps working: `sslmode`, `sslrootcert`, `sslcert`,
`sslkey`, `sslpassword`.

Implemented on rustls (already in the tree via reqwest; pinned to the `ring`
provider since `aws-lc-rs` needs a C toolchain we don't have). `tokio-postgres`
only knows `sslmode` disable/prefer/require and does no verification itself, so a
new `postgres/tls.rs` drives both the handshake mode and the verifier:

  disable      -> no TLS
  allow/prefer -> encrypt opportunistically, no verification  (accept-any)
  require      -> encrypt, no verification (libpq's `require` does NOT verify)
  verify-ca    -> verify the chain, not the hostname          (WebPKI, name bypassed)
  verify-full  -> verify chain + hostname                     (WebPKI)

`sslrootcert` (or the system trust store) supplies the roots; `sslcert`/`sslkey`
add a client certificate for mutual TLS. The cert-path keys and the `verify-*`
modes are stripped from the DSN and passed to the pool as explicit params
(`rust_dbapi.split_ssl_params`), since `tokio-postgres` can't parse them.

Behaviour change: the default (no `sslmode`) is now libpq's `prefer` — attempt
TLS, fall back to plaintext — where it was previously no TLS. This matches
psycopg2; plaintext servers still connect via the fallback.

Caveats vs libpq/openssl: rustls is stricter (requires SANs, rejects a
non-CA self-signed root), and an encrypted client key (`sslpassword`) isn't
supported and is rejected with a clear error.

Validated end-to-end against an SSL Postgres: require / verify-ca / verify-full
connect and use TLS, verify-full with only the system roots correctly rejects an
unknown CA, and hostname is checked (verify-full) / ignored (verify-ca). Unit
tests cover the sslmode->verifier mapping; the Rust-backend storage suites still
pass with the new default (prefer falls back against the plaintext test server).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3G4M92AmwSSZCbmtMJU3d
2026-10-12 18:31:24 +00:00
Erik JohnstonandClaude Fable 5 8e4932ded5 Drop a stale type-ignore in test_rust_dbapi
`Cursor.fetchone` is annotated `-> Any` on this branch, so mypy flags the
ignore as unused and CI's lint would fail.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZdvuLkg7Lm7wDtPQzDnJx
2026-10-12 18:31:06 +00:00
Erik JohnstonandClaude Fable 5.1 4626c659f5 Build the reconnect test's pool through the shared runtime-aware helper
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ULdieJyQoHNzeHq42H2M1p
2026-10-12 18:31:06 +00:00
Erik JohnstonandClaude Opus 4.8 c20ce1b813 Support per-transaction isolation levels on the Rust backend
`new_transaction` sets a transaction's isolation level (for the receipts,
push-actions and purge paths, which ask for a specific level) by calling
`engine.attempt_to_set_isolation_level` before the transaction and resetting it
after. The Rust engine raised `NotImplementedError` there, so those transactions
errored out on the Rust backend.

The shim has no psycopg2-style `conn.set_isolation_level`, so implement it in
SQL: run `SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL <level>` on
the connection. That sets the session's default for *subsequent* transactions,
so it's committed before the caller's transaction begins (SET is transactional);
`new_transaction` resets it to the default (REPEATABLE READ, which the pool
already applies at connection setup) afterwards, scoping the override to the one
transaction — matching psycopg2's behaviour. The level name comes from a fixed
`IsolationLevel` map, not caller input.

Fixes the isolation-level failures in tests.storage.test_event_push_actions,
tests.storage.test_purge and tests.storage.test_receipts on the Rust backend
(20/20 pass); psycopg2 and sqlite are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3G4M92AmwSSZCbmtMJU3d
2026-10-12 18:31:06 +00:00
Erik JohnstonandClaude Opus 4.8 8753216c66 Let the test suite run against the Rust Postgres backend
Add SYNAPSE_TEST_RUST_DRIVER to run the Postgres test suite against the native
Rust driver (sets use_rust_driver on the test database config, off by default).
Teach the harness's make_fake_db_pool to build a RustConnectionPool for the Rust
engine, with its worker thread pool swapped for the threadless test pool so
queries run deterministically on the reactor during `pump` (the shim's block_on
waits there for the tokio workers). Test-database create/drop is admin work, so
it now goes directly over psycopg2 (always available in tests) rather than the
engine module's `connect`.

Running the suite this way surfaces a few real psycopg2-compatibility gaps:

  - `fixup_config_defaults` (was `fixup_default_host`) now also fills libpq's
    PGUSER / PGPASSWORD environment defaults, not just the host — tokio-postgres
    reads none of them from the environment, but Synapse's test setup relies on
    them (as psycopg2 does).
  - `build_dsn` skips `None`-valued args, matching psycopg2's handling of unset
    kwargs.

With these, a homeserver boots on the Rust backend under the test harness and
starts running real transactions and background updates. The remaining psycopg2
type-coercion / behaviour differences are addressed in the commits that follow;
psycopg2 and sqlite runs are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3G4M92AmwSSZCbmtMJU3d
2026-10-12 18:31:06 +00:00
Erik JohnstonandClaude Opus 4.8 b8419dc9fb Support reconnect on the Rust-backed connection
`DatabasePool`'s `inner_func` calls `conn.reconnect()` when a connection is
found closed, or to recycle one that has exceeded the per-connection
transaction limit (`txn_limit`). The Rust DBAPI2 adapter had no `reconnect`, so
those paths (off by default, but real) would have raised.

Add `reconnect` to the adapter: it returns the current connection to the pool
(or discards it if unusable) and checks out a fresh one. The adapter now holds
the pool it was checked out of, with an `owns_pool` flag distinguishing a shared
pool (RustConnectionPool — reconnect from it, don't close it) from a bootstrap
pool-of-one (`rust_dbapi.connect` — closed together with the connection).

Tested: reconnect swaps in a working connection; runWithConnection and the
bootstrap/adapter paths are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3G4M92AmwSSZCbmtMJU3d
2026-10-12 18:31:06 +00:00
Erik JohnstonandClaude Opus 4.8 1619f8481d Wire autocommit, executescript, and list-param binding for homeserver boot
A running homeserver exercises three shim capabilities beyond what the unit
tests reached; add them so it boots on the Rust backend end to end:

  - `prepare_database` reads `db_conn.autocommit`; add a readable `autocommit`
    getter to the shim `Connection` (and the adapter), mirroring psycopg2's
    property.
  - schema preparation runs multi-statement scripts via
    `engine.execute_script_file` -> `cursor.executescript`; route
    `LoggingTransaction.executescript` to the shim's multi-statement primitive
    for the Rust backend (as it already does for sqlite), and expose
    `executescript` on the DBAPI2 adapter cursor.
  - store loading binds Python lists as Postgres arrays for `column = ANY($1)` /
    `!= ALL($1)` (Synapse's `make_in_list_sql_clause`); add a `PgValue::Array`
    variant that classifies a `list` and encodes it as an array of the element
    column type, and accept array column types in `ToSql`.

With these, a homeserver boots on the Rust backend: it prepares the full schema
(176 tables), listens, `/health` returns OK, and DB-backed client endpoints
respond correctly.

Tested: array `to_sql` / classification and array-column `accepts` (Rust unit
tests); and, against a live Postgres, `= ANY($1)` list binding, adapter
`executescript`, and the `autocommit` getter. sqlite and psycopg2 homeserver
boots remain green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3G4M92AmwSSZCbmtMJU3d
2026-10-12 18:31:06 +00:00
Erik JohnstonandClaude Fable 5.1 318ce331c2 Thread the RustRuntime through the Rust backend's startup path
The Rust `ConnectionPool` takes the homeserver's `RustRuntime`, whose
tokio runtime drives the pool. `make_pool`, `rust_dbapi.connect` and
`make_conn` now take it too, and `DatabasePool` and `Databases` pass
`hs.get_rust_runtime()`. `make_conn` falls back to a standalone runtime
on the global reactor for the scripts and tests that have no homeserver
to take one from.

The adapter and engine tests build their pools through the shared
`_make_pool` helper for the same reason.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ULdieJyQoHNzeHq42H2M1p
2026-10-12 18:31:06 +00:00
Erik JohnstonandClaude Opus 4.8 96113be9be Wire the native Rust Postgres backend behind a use_rust_driver flag
Turn the Rust backend on with `database.use_rust_driver: true` (alongside
`name: psycopg2`). Keeping `name: psycopg2` means the engine is still selected as
Postgres and all the `isinstance(engine, PostgresEngine)` dialect checks hold;
the flag only swaps the driver implementation.

  - `create_engine` returns `RustPostgresEngine` instead of `Psycopg2Engine` when
    the flag is set.
  - `make_pool` gains a `clock` argument and, for the Rust engine, builds a
    `RustConnectionPool` (via `_make_rust_pool`) instead of an adbapi pool:
    it derives the libpq DSN from the config `args`, sizes threads/connections
    to `cp_max`, passes the engine's synchronous_commit / statement_timeout,
    starts the pool, and registers a shutdown hook via the clock. Its declared
    return type stays `adbapi.ConnectionPool` — `RustConnectionPool` provides the
    `_db_pool` subset `DatabasePool` uses (runWithConnection / threadID /
    running / threadpool) — so callers that lean on adbapi-specific methods keep
    type-checking.
  - `RustConnectionPool` takes synchronous_commit / statement_timeout_ms and
    threads them into its pooled connections' session setup.
  - `DatabasePool` passes its clock to `make_pool`; the test harness's
    `make_fake_db_pool` accepts the new argument.

Tested: create_engine honours the flag (and the Rust engine is still a
PostgresEngine, not a Psycopg2Engine); make_pool builds a started
RustConnectionPool that serves a query as `_db_pool`. psycopg2 and sqlite
homeserver boots remain green. A full homeserver boot on the Rust backend isn't
covered here because the test harness patches `make_pool` with a synchronous
adbapi fake.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3G4M92AmwSSZCbmtMJU3d
2026-10-12 18:31:01 +00:00
Erik JohnstonandClaude Opus 4.8 4bdf23c9d5 Wire the startup path for the Rust Postgres backend
Startup opens a bootstrap connection via `make_conn` (outside the runtime pool)
and validates it with `engine.check_database` before `prepare_database` runs.
Neither worked for the Rust backend: the Rust module has no `module.connect`,
and check_database/server_version were NotImplementedError stubs. Fill them in:

  - `rust_dbapi.build_dsn` turns the libpq-style `args` into a DSN string, and
    `rust_dbapi.connect` opens a standalone connection (a pool of one, kept
    alive by the returned Connection) for one-off bootstrap use.
  - `make_conn` routes the Rust engine through `rust_dbapi.connect` (with the
    engine's synchronous_commit / statement_timeout) instead of
    `engine.module.connect`; psycopg2 and sqlite are unchanged.
  - `RustPostgresEngine.check_database` reads the server version over a cursor
    (`SHOW server_version_num`) rather than psycopg2's `conn.server_version`,
    and applies the same version / encoding / collation / ctype checks;
    `server_version` is derived from the cached value.

Tested: build_dsn quoting; and, against a live Postgres, make_conn +
check_database + server_version + a query. psycopg2 and sqlite homeserver boots
(make_conn → check_database) remain green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3G4M92AmwSSZCbmtMJU3d
2026-10-12 18:31:01 +00:00
Erik JohnstonandClaude Opus 4.8 f8f32f8416 Present the _db_pool interface on RustConnectionPool
Make RustConnectionPool a drop-in for DatabasePool._db_pool so real
runInteraction traffic can flow through it (once make_pool is switched):

  - it now hands each function a DBAPI2-adapter connection (rust_dbapi.Connection)
    wrapping the pooled shim, so LoggingDatabaseConnection.cursor() yields a
    working LoggingTransaction and the engine's in_transaction / is_closed /
    set_autocommit operate on it;
  - the entry point is named `runWithConnection` (matching
    twisted.enterprise.adbapi.ConnectionPool, which database.py calls by that
    name), alongside `threadID` for the transaction-limit path. With
    `threadpool` and `running` already present, the pool covers the slice of the
    adbapi interface DatabasePool uses.

A new test drives a full transaction through the pool the way
DatabasePool.runWithConnection's inner_func does — engine.in_transaction check,
LoggingDatabaseConnection + LoggingTransaction, `?`→`$n` conversion, commit —
off the reactor thread, and gets the result back via the Deferred.

Still outstanding before make_pool can return it: `reconnect` on the connection
(transaction-limit / closed-connection paths) and the startup path
(make_conn / check_database).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3G4M92AmwSSZCbmtMJU3d
2026-10-12 18:31:01 +00:00
Erik JohnstonandClaude Opus 4.8 f83398c272 Make PostgresEngine a driver-agnostic base for psycopg2 and Rust
Previously RustPostgresEngine subclassed the psycopg2 PostgresEngine, which
dragged psycopg2 into the Rust path and conflated "is Postgres" with "is
psycopg2". Split the hierarchy so both drivers are siblings:

  - `PostgresEngine` (new `postgres_base` module) is now the driver-agnostic
    base holding the shared SQL-dialect and config logic (single_threaded,
    supports_using_any_list, row_id_name, get_db_locale, check_new_database,
    lock_table, synchronous_commit / statement_timeout). It has no driver
    dependency, so it always imports. Everything touching a live connection,
    the DBAPI2 exception module, or the placeholder style is left abstract.
  - `Psycopg2Engine(PostgresEngine)` holds the psycopg2 specifics (register_type
    / register_adapter, isolation-level map, conn.status/closed/server_version,
    `%s` placeholders, psycopg2 execute path, `uses_psycopg2_extras = True`).
  - `RustPostgresEngine(PostgresEngine)` is re-parented onto the base (no longer
    inherits psycopg2). It passes the Rust DBAPI2 module to the base and gets
    NotImplementedError stubs for the still-psycopg2-shaped check_database /
    server_version (part of the deferred startup wiring).

The base keeps the name `PostgresEngine`, so all ~91
`isinstance(engine, PostgresEngine)` checks across the storage layer (which mean
"emit Postgres SQL") hold for both drivers unchanged. `create_engine` now
returns `Psycopg2Engine` for `name == "psycopg2"`.

Verified: full lint clean; Rust engine/adapter tests pass; a psycopg2 homeserver
boots and runs (test_room_search under Postgres) and the sqlite path is
unaffected (test_room_search / test_event_federation under sqlite).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3G4M92AmwSSZCbmtMJU3d
2026-10-12 18:31:01 +00:00
Erik JohnstonandClaude Opus 4.8 75122834aa Add a DBAPI2 adapter over the Rust Postgres shim
Synapse's LoggingTransaction drives a cursor through the DBAPI2 spelling
(fetchone/fetchmany/fetchall, iteration, rowcount/description as properties),
but the Rust shim cursor exposes fetch_one/fetch_all/fetch_next_batch and
rowcount()/description() as methods. Rather than reshape the Rust API, add thin
Python `Connection`/`Cursor` wrappers that present the DBAPI2 shape and delegate
to the shim:

  - `Cursor` maps fetchone/fetchmany/fetchall/__iter__ and rowcount/description
    onto the shim, and tracks exhaustion so fetching past the end keeps
    returning "no more rows" (the shim raises instead);
  - `Connection.cursor()` returns the adapter cursor, and the transaction-control
    and engine-facing methods (commit/rollback/close, set_autocommit, is_closed,
    in_transaction) delegate straight through.

An end-to-end test drives a real LoggingTransaction backed by the adapter and
RustPostgresEngine: `?` placeholders are converted to `$n`, a query runs, and
rows come back via fetchone/iteration/description.

Not handled yet: execute_batch / execute_values (psycopg2 extras that
LoggingTransaction calls directly for PostgresEngine) still need a routing
change to reach a shim-backed implementation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3G4M92AmwSSZCbmtMJU3d
2026-10-12 18:31:01 +00:00
Erik JohnstonandClaude Opus 4.8 178985cf7a Add RustPostgresEngine for the native Rust Postgres backend
A database engine that drives the Rust Connection/Cursor shim instead of
psycopg2. It subclasses PostgresEngine to reuse the pure SQL-generation and
configuration behaviour, and overrides only the parts that touch a live
connection or are wired to psycopg2 internals:

  - `module` points at the Rust backend's DBAPI2 exception hierarchy, which the
    transaction driver catches on;
  - `convert_param_style` rewrites `?` to `$1, $2, ...` (the shim binds libpq
    positional placeholders, not psycopg2's `%s`);
  - `in_transaction` / `is_connection_closed` / `attempt_to_set_autocommit` call
    the shim's own methods;
  - `is_deadlock` matches the Rust `DatabaseError` and its `pgcode`;
  - `executescript` uses the shim's multi-statement primitive;
  - `on_new_connection` is a no-op — session setup belongs in the Rust pool.

Per-transaction isolation-level overrides raise NotImplementedError for now, and
`check_database` / `server_version` still read psycopg2 attributes; the engine
is not yet selectable via `create_engine`, so neither is reached. Wiring it into
`make_pool` (which also feeds session config to the pool) is the next step.

Tested directly: param-style rewriting, deadlock/pgcode matching, the module
pointer, and — against a live shim connection — in_transaction, is_closed and
autocommit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3G4M92AmwSSZCbmtMJU3d
2026-10-12 18:31:01 +00:00
Erik JohnstonandClaude Fable 5.1 8077de5b7e Commit or roll back open transactions when returning a pooled connection
Match adbapi's `_runWithConnection` contract in `RustConnectionPool._run`:
a transaction the function leaves open is committed when it returns and
rolled back when it raises, before the connection goes back to the pool.

The commit makes callers that rely on the pool's implicit commit durable,
and is a no-op after functions that commit themselves. The rollback
matters more: without it every raising transaction function hands a
mid-transaction connection back to the shim, which discards it (tearing
down the TCP session) rather than returning it to the pool for reuse.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ti5yoaVfjHYZ3LmzXFxSwq
2026-10-12 18:31:01 +00:00
Erik JohnstonandClaude Opus 4.8 139515e019 Add a threadpool bridge for running DB functions against pooled connections
`RustConnectionPool` runs synchronous Synapse transaction functions off the
reactor thread against the native Rust connection pool. Each
`run_with_connection(func, *args)` call runs, on a dedicated Twisted thread
pool, `func(conn, *args)` with a `Connection` checked out of the Rust pool, and
returns a Deferred that fires on the reactor with the result (or an errback if
it raised). It builds on `defer_to_threadpool`, so log contexts are preserved
across the hop, and returns the connection to the pool afterwards (clean →
reused, mid-transaction/broken → discarded, per the shim's disposal rules).

The Rust pool sizes 1:1 with the thread count, and `PyConnectionPool.close()`
(new) closes all idle connections so the owner can drop the server connections
deterministically. The bridge is lifecycle-owner-managed (start/close) and
takes no clock, keeping it trivially testable; slotting it into
`DatabasePool.make_pool` — which also needs engine-level support for the shim
connection (in_transaction, is_connection_closed, autocommit/isolation,
reconnect) — is left to a follow-up.

Tested against a live Postgres over the real reactor (Synapse's in-memory test
reactor mocks the DB thread pool out, so a plain Twisted trial TestCase is used):
result/return-value, arg forwarding, exception-to-errback, connection reuse
across calls, and concurrent checkouts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3G4M92AmwSSZCbmtMJU3d
2026-10-12 18:31:01 +00:00
MANI K 5e78aad7d7 Default request token errors to privacy-preserving responses (#20243)
## Summary

- default `request_token_inhibit_3pid_errors` to `true` to prevent
account enumeration
- document the new default and the explicit opt-out
- add an upgrade note and regression coverage for the default

Fixes #18415.
2026-10-09 13:26:31 +01:00
Olivier 'reivilibre 93368c7eb8 Prevent MSC4354 Sticky Events being sticky when they were redacted in the same persistence batch. (#20322)
Part of: MSC4354
Part of: #19641
Follows: #20232 (which applies already-persisted redactions to Sticky
Events being persisted)


In some cases we batch events together during persistence.

If one of the events in the batch is a redaction of another event, we
should apply it, which provides the same benefit as #20232 (namely: we
avoid potentially dodgy event content being persisted & the event
naturally loses its stickiness
so is not written to `sticky_events`)

---------

Signed-off-by: Olivier 'reivilibre <oliverw@matrix.org>
2026-10-09 12:30:58 +01:00
Olivier 'reivilibre 815d2e0c7d Remove the stickiness of MSC4354 Sticky Events when they are redacted after persistence. (#20323)
Part of: MSC4354
Part of: #19641
Follows: #20232 (redacted before persistence), #20322 (redacted during
persistence)

Original commit schedule, with full messages:

- Add tests for redaction of Sticky Events

- Add `delete_sticky_events_txn`

- Remove stickiness from validly redacted events at persistence


### Dev notes

Overview:

- [`#20232`](https://github.com/element-hq/synapse/pull/20232) (redacted
before persistence)
- [`#20322`](https://github.com/element-hq/synapse/pull/20322) (redacted
during persistence)
- [`#20323`](https://github.com/element-hq/synapse/pull/20323) (redacted
after persistence)

---------

Signed-off-by: Olivier 'reivilibre <oliverw@matrix.org>
2026-10-08 10:04:08 +01:00
Paul Chobert 42ddc70ea7 Remove the legacy mention push rules by default (#20220)
Part of #19415 (Support Matrix 1.17).

Companion test PRs:
- Complement: https://github.com/matrix-org/complement/pull/920
- Sytest: https://github.com/matrix-org/sytest/pull/1430

**What this does:** `experimental_features.msc4210_enabled` default
value changed `true`
([MSC4210](https://github.com/matrix-org/matrix-spec-proposals/pull/4210)).
The rest of the changes is fixing the tests, making sure test relying en
mentions sends the proper `m.mentions` attribute.

## Migration plan

Element Web has been writing both rule sets together since September
2023
([matrix-react-sdk#11667](https://github.com/matrix-org/matrix-react-sdk/pull/11667),
[element-web@0d367a7c7e](https://github.com/element-hq/element-web/commit/0d367a7c7e)),
so for its users `.m.rule.is_user_mention` and `.m.rule.is_room_mention`
already carry the same customisations as the legacy rules.

<strike>For everyone else, #20215 adds a background update that migrates
the overrides on the legacy rules to the intentional ones.</strike>

## Opt-out

```yaml
experimental_features:
  msc4210_enabled: false
```

---

### Pull Request Checklist

<!-- Please read
https://element-hq.github.io/synapse/latest/development/contributing_guide.html
before submitting your pull request -->

* [x] Pull request is based on the develop branch
* [x] Pull request includes a [changelog
file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog).
The entry should:
- Be a short description of your change which makes sense to users.
"Fixed a bug that prevented receiving messages from other servers."
instead of "Moved X method from `EventStore` to `EventWorkerStore`.".
  - Use markdown where necessary, mostly for `code blocks`.
  - End with either a period (.) or an exclamation mark (!).
  - Start with a capital letter.
- Feel free to credit yourself, by adding a sentence "Contributed by
@github_username." or "Contributed by [Your Name]." to the end of the
entry.
* [x] [Code
style](https://element-hq.github.io/synapse/latest/code_style.html) is
correct (run the
[linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters))
2026-10-08 11:03:30 +02:00
Olivier 'reivilibre 31ca874890 Add received_ts to the in-memory event internal metadata struct. (#20312)
Motivated by work on MSC4354, because the sticky events MSC specs that
`received_ts` is used to calculate the
start of an event's stickiness. (It would therefore be convenient to
have it available as part of event metadata, so an event can expose a
cheap helper method to check if it is sticky.)

This commit adds `received_ts` into the `event.internal_metadata` struct,
alongside `stream_ordering`.

I conceptually treat it as a sibling to `stream_ordering` and populate
it in all the same places where `stream_ordering` is populated.

Because this needs `received_ts` to be added to the HTTP replication
response for event persistence,
I also add a temporary compatibility measure that keeps rolling worker
upgrades working during this
transition.
I intend to remove the compatibility code in the next release.


I have manually tested that
```
SYNAPSE_POSTGRES=1 SYNAPSE_POSTGRES_USER=postgres SYNAPSE_POSTGRES_HOST=localhost SYNAPSE_POSTGRES_PASSWORD=postgres poetry run trial -j 6 tests.replication.test_sharded_event_persister
```
fails only when both the compatibility code is removed and the
`"received_ts"` key is commented out in the response (to omit it).
The compatibility code causes the test to pass even when `"received_ts"`
is commented out.

---------

Signed-off-by: Olivier 'reivilibre <oliverw@matrix.org>
2026-10-06 19:27:46 +01:00
Olivier 'reivilibreandEric Eastwood 7773f05596 Use sharded tokens in MSC4354: Sticky Events sync and sliding sync, preventing events appearing in multiple sync responses. (#20186)
Fixes: #19661
Part of: MSC4354

First introduced some helpers for querying a sharded stream, which were moved to
https://github.com/element-hq/synapse/pull/20108.

- SQL builder for 'give me rows between two sharded tokens'
- a function to return the 'new' from_token after a limited read


Then make the sticky events portions of both oldschool and sliding sync use
sharded tokens.

This prevents a 'torn sync' problem where, because the events and sticky
events streams are both sharded with the same writers:

1. persisting a sticky event leads to an event row and a `sticky_events`
row being created for event `e` (on writer `W`).
2. Suppose another writer is still persisting, so the position of the
stream for writer W advances, but the global
['linear'](https://element-hq.github.io/synapse/latest/development/synapse_architecture/streams.html?highlight=%E2%80%9Clinear%E2%80%9D#current-stream-id)
(I have been calling this 'baseline') position does not advance yet.
3. sync is sharding-aware for the regular events stream, so `e` appears
as a real-time timeline event, thus coming down sync
4. since sticky events were not sharding-aware, `e` is not detected as a
sticky event in this sync response. The sticky event position doesn't
advance in the sync token, either.
5. (assume the position advances because other writers have finished
writing and so the 'linear' position advances for the sticky events
stream)
6. the client then syncs again, using the new sync token. This time, `e`
appears in the sticky event section.

If we make sticky events sharding-aware in sync, then `e` would appear
for consideration in both the timeline section **and** the sticky event
section in the first sync.
It then gets deduplicated out of the sticky section since it is
appearing in the timeline section in the same sync.

---------

Signed-off-by: Olivier 'reivilibre <oliverw@matrix.org>
Co-authored-by: Eric Eastwood <erice@element.io>
2026-10-05 12:40:55 +01:00
Olivier 'reivilibreandEric Eastwood 9f0168aace Catch up MSC4354 Sticky Events to remote homeservers that have missed them. (#20165)
Part of: MSC4354 (Sticky Events)

Our locally-sent Sticky Events need to be reliably (within bounds of
possibility) sent to remote homeservers.

This adds the mechanism to make this happen.

Notably omits catching up historical sticky events to homeservers that only just
joined the room.

---------

Signed-off-by: Olivier 'reivilibre <oliverw@matrix.org>
Co-authored-by: Eric Eastwood <erice@element.io>
2026-10-05 10:52:46 +01:00
Olivier 'reivilibreandEric Eastwood d1874cdc37 Prevent MSC4354 Sticky Events being sticky when they were redacted prior to persistence. (#20232)
Part of: MSC4354, 
Part of: https://github.com/element-hq/synapse/issues/19641

This PR persists events in redacted form when they already have valid
redactions. That's not directly a sticky events-specific change (and I
feel it makes sense to redact immediately before persistence when we
already have the redaction).

This causes a nice effect which is that the event's stickiness is lost
immediately before persistence, because the `msc4354_sticky` field is
redacted by the redaction algorithm.

---------

Signed-off-by: Olivier 'reivilibre <oliverw@matrix.org>
Co-authored-by: Eric Eastwood <erice@element.io>
2026-09-29 15:22:34 +01:00
Paul ChobertandOlivier 'reivilibre db618e276d Fix appservice ephemeral read receipts being permanently skipped on large receipt bursts (#20108)
Fixes https://github.com/element-hq/synapse/issues/20096.

Part of https://github.com/element-hq/synapse/issues/18118

The spec requires read receipts to be delivered to interested
application services ([Pushing ephemeral
data](https://spec.matrix.org/v1.19/application-service-api/#pushing-ephemeral-data)):

> If the `receive_ephemeral` settings is enabled in the registration
file, homeservers MUST send ephemeral data that is relevant to the
application service via the transaction API, using the `ephemeral`
property of the request's body.
>
> `m.receipt`: MUST be sent to the application service under the same
rules as regular events, meaning that the application service must have
registered interest in the room itself, or in a user that is in the
room.

Ephemeral data delivery to application services was added in Matrix
v1.13 from
[MSC2409](https://github.com/matrix-org/matrix-spec-proposals/pull/2409)
(see the [v1.13
changelog](https://spec.matrix.org/v1.19/changelog/v1.13/#application-service-api)).

## Problem

When more than 100 read receipts are covered by a single receipt stream
update, only the last 100 are sent to appservices. The rest is
permanently skipped.


## Proposed Fix

- Looping over the receipts in batches of 100 items and updating the
cursor on every iteration
- Keep the fast-forward behavior for new appservices as intended in
https://github.com/matrix-org/synapse/pull/8744

---------

Co-authored-by: Olivier 'reivilibre <oliverw@element.io>
2026-09-29 12:06:15 +01:00
fbfa7bcfd1 Fix push badge count ignoring a room's notifications when another room's summary is up to date (#20237)
The count of unread notifications is computed in two phases:

1. From the summaries (`event_push_summary`) when those are up to date
with the user's last read receipt
2. Otherwise from counting the push actions (`event_push_actions`) after
that receipt

The problem in this process is that the threads whose summary is up to
date are identified with only their `thread_id`. But all main timelines
share the same `"main"` thread_id, so as soon as one room has an
up-to-date summary, phase 2 skips the main timeline of every room.

The fix identifies the up-to-date summaries with their `(room_id,
thread_id)` pair.

Authored by @sandhose. Found while investigating the
`TestThreadedReceipts` Complement flake (#15517, #18537), but it is a
bug on its own.

### Pull Request Checklist

<!-- Please read
https://element-hq.github.io/synapse/latest/development/contributing_guide.html
before submitting your pull request -->

* [x] Pull request is based on the develop branch
* [x] Pull request includes a [changelog
file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog).
The entry should:
- Be a short description of your change which makes sense to users.
"Fixed a bug that prevented receiving messages from other servers."
instead of "Moved X method from `EventStore` to `EventWorkerStore`.".
  - Use markdown where necessary, mostly for `code blocks`.
  - End with either a period (.) or an exclamation mark (!).
  - Start with a capital letter.
- Feel free to credit yourself, by adding a sentence "Contributed by
@github_username." or "Contributed by [Your Name]." to the end of the
entry.
* [x] [Code
style](https://element-hq.github.io/synapse/latest/code_style.html) is
correct (run the
[linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters))

---------

Co-authored-by: Quentin Gliech <quentingliech@gmail.com>
Co-authored-by: Devon Hudson <devonhudson@librem.one>
2026-09-23 16:08:43 +00:00
Erik Johnston f3ae564877 Report each ID generator's current position as a metric (#20097)
When a stream advances in the database but stops being replicated to a
process, that process's view of the stream freezes. Requests that wait
for it to catch up to a token issued by another worker then time out and
return empty responses indefinitely (see #20080), and nothing exported
said so.

Report `get_current_token` from every ID generator, on every process.
The value is comparable between processes, so a stream that has stopped
reaching one of them shows up as divergence with no client traffic
needed. It is also the position that `wait_for_stream_token` waits on,
so its divergence is the failure itself rather than a proxy for it.

Being a watermark over gapless runs of persisted IDs, it also catches a
single writer of a sharded stream going quiet, which a maximum across
writers would hide behind the writers still being replicated.
2026-09-22 13:16:35 +01:00
Erik Johnston 26e46786b3 Port LogContext to Rust (#19979)
Ports the logcontext classes to Rust, and gives tokio tasks a captured
logcontext so that work running in (or spawned from) Rust is attributed
to the request that caused it.

1. **Add characterization tests for logcontext error messages and the
filter** — pins the exact `logcontext_error` message shapes, the
abuse-detection code paths and `LoggingContextFilter`'s observable
behaviour, *against the existing Python implementation* (this commit is
green on its own). These are the behavioural contract the port has to
satisfy.
2. **Port `ContextResourceUsage` to a Rust pyclass** — self-contained:
the new `synapse_rust.logcontext` module, the class, its stub and the
re-export.
3. **Move the logcontext storage and `LoggingContext` to Rust** — the
core change; the commit message carries detailed design notes.
Highlights:
- The slot is typed `Option<Py<LoggingContext>>`, with `None`
representing the sentinel. `_Sentinel`/`SENTINEL_CONTEXT` stay pure
Python (unchanged); thin wrappers on
`current_context`/`set_current_context` convert at the boundary, and
pyo3's extraction enforces the type (`TypeError` otherwise).
- The accounting is native: one `getrusage(RUSAGE_THREAD)` read per
switch via libc, inline `stop`/`start` bookkeeping for base
`LoggingContext`s, Python dispatch only for subclasses
(`BackgroundProcessLoggingContext`) so their overrides run. The thread
id comes from `PyThread_get_thread_ident` (the exact
`threading.get_ident()`
     value) without calling into Python.
- The hot paths avoid per-operation allocation: names are `Py<PyString>`
(the per-log-record `str(context)`/`server_name` reads are INCREF-only),
error branches materialise strings only when hit.
4. **Attribute Rust-spawned work to the caller's logcontext** —
`create_deferred` captures the caller's context and scopes it onto the
spawned task via a tokio task-local (`LogContextHandle`);
`current_context()` gives the task-local read precedence, so
`LoggingContextFilter`/`pyo3-log` resolve the right context on worker
threads with no per-record stamping. `run_python_awaitable` restores the
captured context (via a `with_logcontext` helper, the Rust
`PreserveLoggingContext`) around Python called back from Rust, so e.g.
`runInteraction` from the Rust `/versions` handler accounts its DB usage
against the right request. Integration tests exercise both guarantees
through real production code paths.

Follow-up work on top of this (separate PR): porting
`BackgroundProcessLoggingContext` natively and removing further `Py<_>`
indirections. The fact that `BackgroundProcessLoggingContext` is a
subclass is what forces some of the warts in this PR: e.g. having to use
`Py<LoggingContext>` everywhere, etc.

We don't try (yet) to make this pure Rust, instead we see this as simply
maintaining the Python logcontext machinery when crossing, rather than
trying to make a Rust equivalent that can be used by pure Rust
dependencies. We probably do want to do that in future, as well as wire
up e.g. CPU recording on Rust side, but that is unnecessary for now.
2026-09-21 10:00:53 +01:00
Erik Johnston c595869fb7 Add cache to get_partial_filtered_current_state_ids (#20160)
For state filters that ask for concrete types. This allows us to cache
the common case of asking for a specific type/state key.

I noticed a bunch of queries in the jaeger traces that could be cached.
2026-09-18 12:11:09 +01:00
Jason LittleandPaul Chobert 7530874a12 Raise default room version to "12" (#20130)
Requires
* #19768 
* #19782 
* https://github.com/matrix-org/complement/pull/915

To meet the requirements for bumping Synapse to support [Matrix spec to
1.16](https://github.com/element-hq/synapse/issues/19414) the default
room version should be incremented to "12".

Other than the two separate Synapse PRs above(which are included here
but marked as "[diverted]" and should be removed prior to review) there
is only [one other real
change](https://github.com/element-hq/synapse/commit/c005e96c665b5a098a9b95d10f33faf224564be9)
to the code base itself to fix a `KeyError` during logging for a `/sync`
test against unknown room versions. Everything else should be on the
unit tests themselves.

Standard unit test running applies, should be nothing special to test
this outright.
`poetry run trial -jN tests` and similar for Postgresql.

Probably ok to review commit-by-commit

I took the liberty of writing a [room creating
helper](https://github.com/element-hq/synapse/commit/5ba81ad5d4a0a6787b7626bdb3293f633ea4097a)
for the two test series that try and test the sharding of the
`event_persister` workers. I'm not certain it stands up to scrutiny, but
at least does not do any funny mocking when producing room v12
appropriate room IDs.

I also took the liberty of writing an [assertion
helper](https://github.com/element-hq/synapse/pull/20130/commits/e408cef241d1b9c6a88f069eb5832de053790f9b)
for comparing lists of dicts for a select subset of keys/values. This is
used to compare stripped state selections while waiting on
https://github.com/element-hq/synapse/pull/19723 to be completed.

### Pull Request Checklist

<!-- Please read
https://element-hq.github.io/synapse/latest/development/contributing_guide.html
before submitting your pull request -->

* [x] Pull request is based on the develop branch
* [x] Pull request includes a [changelog
file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog).
The entry should:
- Be a short description of your change which makes sense to users.
"Fixed a bug that prevented receiving messages from other servers."
instead of "Moved X method from `EventStore` to `EventWorkerStore`.".
  - Use markdown where necessary, mostly for `code blocks`.
  - End with either a period (.) or an exclamation mark (!).
  - Start with a capital letter.
- Feel free to credit yourself, by adding a sentence "Contributed by
@github_username." or "Contributed by [Your Name]." to the end of the
entry.
* [x] [Code
style](https://element-hq.github.io/synapse/latest/code_style.html) is
correct (run the
[linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters))

---------

Co-authored-by: Paul Chobert <paul@chobert.fr>
2026-09-17 18:32:51 +00:00
Paul Chobert 8c11b13f63 Fix state events missing from MSC4222 state_after when the since token falls inside a persist batch (#20171)
This PR fixes the issue described as comment here:
https://github.com/element-hq/synapse/issues/18793#issuecomment-3502202379

In Element Call, this shows up as ghost participants: someone who left
the call keeps being displayed until a later state change refreshes the
room.

The bug is not specific to Element Call: any state event can be
affected, RTC membership just changes often enough to make it visible.

## What happens

Alice has a client syncing against a homeserver where events are
persisted by one worker (the event persister) and `/sync` is served by
another (the sync worker). Her client is parked in a long-poll: `GET
/sync?since=s99&timeout=30000`.

Bob joins a call at the same moment Carol sends a message. Carol's
message reaches the persister first; Bob's `m.call.member` arrives while
that write is still in flight, so the per-room persist queue groups them
into one transaction:

```
events (each gets its own stream ordering):
    stream_ordering 100:  m.room.message   Carol
    stream_ordering 101:  m.call.member    Bob        (state)

current_state_delta_stream (how state_after finds state changes):
    stream_id 100 ────►  (m.call.member, @bob) -> $bob_join_call
          ▲
          └─ stamped with the batch MINIMUM (100), not the event's own 101
              (see `_update_current_state_txn`)
```

The transaction commits: both events and the delta row are now in the
database, atomically.

The persister then announces the new events over replication, one RDATA
token per stream ordering — rows are only merged into one token when
they share a position, and 100 and 101 don't. So the sync worker's
events-stream position steps 99 → 100 → 101, and on reaching 100 it
pokes the notifier.

Alice's long-poll wakes at exactly that moment. Her response is built at
the worker's *current* position — `end = 100` — with RDATA 101 still in
the queue:

```
Sync A  (since=99, end=100):
  timeline:     events   99 < ordering ≤ 100  →  [Carol's message]
  state_after:  deltas   99 < stream_id ≤ 100 →  [$bob_join_call]  ← delivered EARLY
  next_batch:   s100                                               ← mid-batch token
```

No race on the client's side is needed: the server *hands out* the
mid-batch token as `next_batch`. Alice's client re-polls with it, as
every sync client does. The worker has meanwhile processed RDATA 101:

```
Sync B  (since=100, end=101):
  timeline:     events   100 < ordering ≤ 101  →  [Bob's m.call.member @101]  ✓
  state_after:  deltas   100 < stream_id ≤ 101 →  []     row is stamped 100   ✗
```

A state event in the timeline with an empty `state_after`. An MSC4222
client trusts `state_after` over timeline state events, so Alice's copy
of Bob's call membership never updates from this response.

On a single process this cannot happen: the batch's stream IDs are
released as a whole, so the position visible to `/sync` jumps 99 → 101
and `s100` is never handed out. Only a process that learns its position
from replication — any sync worker — ticks through the middle of a
batch.


### Pull Request Checklist

<!-- Please read
https://element-hq.github.io/synapse/latest/development/contributing_guide.html
before submitting your pull request -->

* [x] Pull request is based on the develop branch
* [x] Pull request includes a [changelog
file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog).
The entry should:
- Be a short description of your change which makes sense to users.
"Fixed a bug that prevented receiving messages from other servers."
instead of "Moved X method from `EventStore` to `EventWorkerStore`.".
  - Use markdown where necessary, mostly for `code blocks`.
  - End with either a period (.) or an exclamation mark (!).
  - Start with a capital letter.
- Feel free to credit yourself, by adding a sentence "Contributed by
@github_username." or "Contributed by [Your Name]." to the end of the
entry.
* [x] [Code
style](https://element-hq.github.io/synapse/latest/code_style.html) is
correct (run the
[linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters))
2026-09-16 11:07:25 +02:00
Olivier 'reivilibre 6825c98eb2 Add support for un-soft-failing MSC4354 Sticky Events when room state changes, making federation support more reliable. (#20204)
Part of: MSC4354

Experimental feature tracking issue:
https://github.com/element-hq/synapse/issues/19409

Related Complement tests currently in
https://github.com/matrix-org/complement/pull/806/files#diff-6c9d6d169485d0848c6b20dd9b43f6fe669a8a710e42f953d08fa25a99cc8f4cR509

---------

Signed-off-by: Olivier 'reivilibre <oliverw@matrix.org>
2026-09-14 11:41:21 +01:00
Paul Chobert bd21823818 Fix 500 error when setting a custom profile field for a user with no profile row (#20172)
Follow-up to #20149, which fixed the 500 when *getting* a profile field
for a user with no `profiles` row. The same crash was still reachable
when *setting* one via `PUT /_matrix/client/v3/profile/{userId}/{field}`
(as server admin). This PR splits that case in two:

* **The user exists but has no `profiles` row** (e.g. profile erased
upon deactivation):
* Before: `500 M_UNKNOWN` (`TypeError: cannot unpack non-sequence
NoneType` in the profile size check).
  * After: `200`, the profile row is recreated with the field set.
* **The user does not exist at all**:
  * Before: `500 M_UNKNOWN` (same crash).
* After: `404 M_NOT_FOUND`, without conjuring up an orphan profile row.

Fixing the crash also surfaced a latent SQLite-only bug where a field
set on a freshly created profile row was stored under the wrong key,
making it 404 on `GET` right after a successful `PUT`.



### Pull Request Checklist

<!-- Please read
https://element-hq.github.io/synapse/latest/development/contributing_guide.html
before submitting your pull request -->

* [x] Pull request is based on the develop branch
* [x] Pull request includes a [changelog
file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog).
The entry should:
- Be a short description of your change which makes sense to users.
"Fixed a bug that prevented receiving messages from other servers."
instead of "Moved X method from `EventStore` to `EventWorkerStore`.".
  - Use markdown where necessary, mostly for `code blocks`.
  - End with either a period (.) or an exclamation mark (!).
  - Start with a capital letter.
- Feel free to credit yourself, by adding a sentence "Contributed by
@github_username." or "Contributed by [Your Name]." to the end of the
entry.
* [x] [Code
style](https://element-hq.github.io/synapse/latest/code_style.html) is
correct (run the
[linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters))
2026-09-07 10:34:30 +02:00
Kegan Dougal 0088e43da7 MSC4242: State DAGs (fed client) (#20127)
Split out from https://github.com/element-hq/synapse/pull/19425

- Add federation support for MSC4242 fields in `/send_join` and
`/get_missing_events`
- Move the gate preventing joining MSC4242 rooms higher in the call
stack


Part of a series of 5x PRs to land the federation part of MSC4242
(storage, fedclient, serving, inbound-joins, inbound-pulls).


### Pull Request Checklist

<!-- Please read
https://element-hq.github.io/synapse/latest/development/contributing_guide.html
before submitting your pull request -->

* [x] Pull request is based on the develop branch
* [x] Pull request includes a [changelog
file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog).
The entry should:
- Be a short description of your change which makes sense to users.
"Fixed a bug that prevented receiving messages from other servers."
instead of "Moved X method from `EventStore` to `EventWorkerStore`.".
  - Use markdown where necessary, mostly for `code blocks`.
  - End with either a period (.) or an exclamation mark (!).
  - Start with a capital letter.
- Feel free to credit yourself, by adding a sentence "Contributed by
@github_username." or "Contributed by [Your Name]." to the end of the
entry.
* [x] [Code
style](https://element-hq.github.io/synapse/latest/code_style.html) is
correct (run the
[linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters))
2026-09-04 08:08:38 +00:00
Paul Chobert b6798a1353 Fix event_search background reindex skipping m.room.topic events (#20119)
Room topics are dropped from the search index whenever the
`event_search` background reindex runs (e.g. after a search index
rebuild, or when the background update is re-run on an upgraded
homeserver), making topics unsearchable even though the live write path
indexes them correctly.

The cause is a trailing comma in `_background_reindex_search`, which
turns the topic `value` into a 1-tuple instead of a string:


https://github.com/element-hq/synapse/blob/14c96c0f5444cbe28b6ac0361cf94216b8d352db/synapse/storage/databases/main/search.py#L211-L213

The downstream `if not isinstance(value, str): continue` guard then
silently skips *every* `m.room.topic` event, so no topic ever reaches
`event_search` during a reindex.

The regression was introduced in #18195, which added rich-text topic
support (MSC3765) to the reindex path.

### Problem Example

1. A room has topic "project roadmap".
2. An admin rebuilds the search index (or the `event_search` background
update re-runs).
3. `m.room.message` and `m.room.name` events are reindexed fine, but
every `m.room.topic` event is skipped.
4. Searching for "project roadmap" with key `content.topic` returns 0
results — the topic is permanently unsearchable until the event is sent
again.

---

### Pull Request Checklist

<!-- Please read
https://element-hq.github.io/synapse/latest/development/contributing_guide.html
before submitting your pull request -->

* [x] Pull request is based on the develop branch
* [x] Pull request includes a [changelog
file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog).
The entry should:
- Be a short description of your change which makes sense to users.
"Fixed a bug that prevented receiving messages from other servers."
instead of "Moved X method from `EventStore` to `EventWorkerStore`.".
  - Use markdown where necessary, mostly for `code blocks`.
  - End with either a period (.) or an exclamation mark (!).
  - Start with a capital letter.
- Feel free to credit yourself, by adding a sentence "Contributed by
@github_username." or "Contributed by [Your Name]." to the end of the
entry.
* [x] [Code
style](https://element-hq.github.io/synapse/latest/code_style.html) is
correct (run the
[linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters))
2026-09-02 14:27:38 +01:00
Kegan DougalandEric Eastwood 54bfa1a01b MSC4242: State DAGs (storage) (#19718)
Split out from https://github.com/element-hq/synapse/pull/19425

Add storage functions needed for federation support for MSC4242:
- `get_missing_events_state_dag`: will be used to satisfy
`/get_missing_events` requests.
 - `get_state_dag`: will be used to satisfy `/send_join` requests.


Part of a series of 5x PRs to land the federation part of MSC4242
(storage, fedclient, serving, inbound-joins, inbound-pulls).


### Pull Request Checklist

<!-- Please read
https://element-hq.github.io/synapse/latest/development/contributing_guide.html
before submitting your pull request -->

* [x] Pull request is based on the develop branch
* [x] Pull request includes a [changelog
file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog).
The entry should:
- Be a short description of your change which makes sense to users.
"Fixed a bug that prevented receiving messages from other servers."
instead of "Moved X method from `EventStore` to `EventWorkerStore`.".
  - Use markdown where necessary, mostly for `code blocks`.
  - End with either a period (.) or an exclamation mark (!).
  - Start with a capital letter.
- Feel free to credit yourself, by adding a sentence "Contributed by
@github_username." or "Contributed by [Your Name]." to the end of the
entry.
* [x] [Code
style](https://element-hq.github.io/synapse/latest/code_style.html) is
correct (run the
[linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters))

---------

Co-authored-by: Eric Eastwood <madlittlemods@gmail.com>
2026-09-02 09:12:49 +01:00