Commit Graph
6 Commits
Author SHA1 Message Date
Erik JohnstonandClaude Opus 4.8 b66644b4f7 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-07-11 09:06:48 +00:00
Erik JohnstonandClaude Opus 4.8 3ff94428a7 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-07-11 09:04:57 +00:00
Erik JohnstonandClaude Opus 4.8 504caa3184 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-07-11 09:04:57 +00:00
Erik JohnstonandClaude Opus 4.8 a1dcd4dfe4 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-07-11 09:04:57 +00:00
Erik JohnstonandClaude Opus 4.8 56e3472252 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-07-11 09:04:57 +00:00
Erik JohnstonandClaude Opus 4.8 969a473d58 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-07-11 09:04:57 +00:00