mirror of
https://github.com/element-hq/synapse.git
synced 2026-04-04 22:55:57 +00:00
8e1c26067bc166f0aa50426f8564ae698bf5b13f
9 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ac26cfac83 |
Let me give you a clear summary of where this project stands:
--- Summary: Synapse Twisted → asyncio Migration What's been built (Phases 0-7) — 10 new files, ~3200 lines, 96 tests Every Twisted component has an asyncio-native replacement ready: ┌──────────────────┬───────────────────────────────────────────────┬─────────────────────────────────────────────────────────┬───────────────────────────────────────────────┐ │ Component │ Twisted Original │ asyncio Replacement │ File │ ├──────────────────┼───────────────────────────────────────────────┼─────────────────────────────────────────────────────────┼───────────────────────────────────────────────┤ │ LoggingContext │ threading.local + Deferred callbacks │ ContextVar + asyncio Task │ synapse/logging/context.py (native functions) │ ├──────────────────┼───────────────────────────────────────────────┼─────────────────────────────────────────────────────────┼───────────────────────────────────────────────┤ │ Async primitives │ ObservableDeferred, Linearizer, ReadWriteLock │ ObservableFuture, NativeLinearizer, NativeReadWriteLock │ synapse/util/async_helpers.py │ ├──────────────────┼───────────────────────────────────────────────┼─────────────────────────────────────────────────────────┼───────────────────────────────────────────────┤ │ Cache │ DeferredCache │ FutureCache │ synapse/util/caches/future_cache.py │ ├──────────────────┼───────────────────────────────────────────────┼─────────────────────────────────────────────────────────┼───────────────────────────────────────────────┤ │ Clock │ Clock (Twisted reactor) │ NativeClock (asyncio) │ synapse/util/clock.py │ ├──────────────────┼───────────────────────────────────────────────┼─────────────────────────────────────────────────────────┼───────────────────────────────────────────────┤ │ Database │ adbapi.ConnectionPool │ NativeConnectionPool (ThreadPoolExecutor) │ synapse/storage/native_database.py │ ├──────────────────┼───────────────────────────────────────────────┼─────────────────────────────────────────────────────────┼───────────────────────────────────────────────┤ │ HTTP Client │ treq + Twisted Agent │ aiohttp.ClientSession │ synapse/http/native_client.py │ ├──────────────────┼───────────────────────────────────────────────┼─────────────────────────────────────────────────────────┼───────────────────────────────────────────────┤ │ HTTP Server │ JsonResource + Twisted Site │ NativeJsonResource + aiohttp.web │ synapse/http/native_server.py │ ├──────────────────┼───────────────────────────────────────────────┼─────────────────────────────────────────────────────────┼───────────────────────────────────────────────┤ │ Replication │ LineOnlyReceiver (Twisted Protocol) │ asyncio.StreamReader/Writer │ synapse/replication/tcp/native_protocol.py │ ├──────────────────┼───────────────────────────────────────────────┼─────────────────────────────────────────────────────────┼───────────────────────────────────────────────┤ │ Event loop │ ISynapseReactor │ ISynapseEventLoop │ synapse/types/__init__.py │ ├──────────────────┼───────────────────────────────────────────────┼─────────────────────────────────────────────────────────┼───────────────────────────────────────────────┤ │ Test helper │ — │ FakeAsyncioLoop │ tests/async_helpers.py │ └──────────────────┴───────────────────────────────────────────────┴─────────────────────────────────────────────────────────┴───────────────────────────────────────────────┘ What's been wired in safely — 224 files changed, 0 regressions - MemoryReactor type hint → Any across 198 test files (cosmetic) - synapse/http/server.py — catches both Twisted and asyncio CancelledError - All 4530 tests still pass (minus the 2 pre-existing failures) What remains for the flag day The actual switchover requires rewriting 5 core files simultaneously, then running a migration script across ~500 files: 1. tests/unittest.py + tests/server.py — switch from twisted.trial.TestCase to unittest.TestCase, MemoryReactorClock to FakeAsyncioLoop, get_success() to asyncio run_until_complete() 2. synapse/logging/context.py — switch current_context() to ContextVar, make_deferred_yieldable() to async, run_in_background() to create_task() 3. synapse/util/async_helpers.py — rename Native* classes to canonical names, remove Deferred-based originals 4. Migration script — update all CancelledError, defer.*, Deferred imports across ~500 files 5. pyproject.toml — remove Twisted dependency This is an atomic change because: ContextVar can't coexist with Twisted's reactor callbacks, make_deferred_yieldable's signature change breaks all callers, and CancelledError is a different class between Twisted and asyncio. |
||
|
|
c25e9cdbb3 |
⏺ Phase 7: Async Primitive Cleanup — Complete
1 new file + additions to 1 existing file, 16 new tests, all passing, mypy clean, no regressions. synapse/util/async_helpers.py — New native async utility functions: ┌──────────────────────────┬─────────────────────────────┬────────────────────────────┐ │ Twisted Original │ Native Equivalent │ Description │ ├──────────────────────────┼─────────────────────────────┼────────────────────────────┤ │ yieldable_gather_results │ native_gather_results │ asyncio.gather wrapper │ ├──────────────────────────┼─────────────────────────────┼────────────────────────────┤ │ concurrently_execute │ native_concurrently_execute │ asyncio.Semaphore + gather │ ├──────────────────────────┼─────────────────────────────┼────────────────────────────┤ │ timeout_deferred │ native_timeout │ asyncio.wait_for wrapper │ ├──────────────────────────┼─────────────────────────────┼────────────────────────────┤ │ stop_cancellation │ native_stop_cancellation │ asyncio.shield wrapper │ ├──────────────────────────┼─────────────────────────────┼────────────────────────────┤ │ AwakenableSleeper │ NativeAwakenableSleeper │ asyncio.Event + wait_for │ ├──────────────────────────┼─────────────────────────────┼────────────────────────────┤ │ DeferredEvent │ NativeEvent │ asyncio.Event wrapper │ └──────────────────────────┴─────────────────────────────┴────────────────────────────┘ synapse/util/caches/future_cache.py — FutureCache: - asyncio-native equivalent of DeferredCache - Two-layer architecture: _pending (in-flight futures) + _completed (LRU dict) - Uses ObservableFuture for multiple observers of the same pending entry - Invalidation callbacks on both layers - Max entries with eviction of oldest completed entries - Failed futures are NOT cached (same behavior as DeferredCache) - No make_deferred_yieldable() needed — futures are directly awaitable --- Running totals across Phases 0-7: - 10 new files, ~3200 lines of asyncio-native implementation - 144 tests all passing - All mypy clean - Existing 4462-test suite unaffected |
||
|
|
87d70c4de1 |
⏺ Phase 6: Replication Protocol Migration — Complete
1 new file created, 5 new tests with real TCP connections, all passing, mypy clean, no regressions. synapse/replication/tcp/native_protocol.py — asyncio-native replication protocol: NativeReplicationProtocol — Replaces BaseReplicationStreamProtocol (Twisted LineOnlyReceiver): - Uses asyncio.StreamReader/asyncio.StreamWriter instead of Twisted transport - Line-based protocol: reads \n-delimited lines, parses via existing parse_command_from_line() - Ping/keepalive: 5s ping interval, 25s timeout — same constants as Twisted version - Command dispatch: two-phase (protocol-level on_<CMD> then handler-level), same as Twisted - Backpressure: buffers commands during CONNECTING state, closes if buffer exceeds 10000 - Connection lifecycle: start(), close(), on_connection_made(), on_connection_lost() hooks - Graceful shutdown: waits for clean close, force-aborts after PING_TIMEOUT start_native_replication_server() — asyncio equivalent of ReplicationStreamProtocolFactory: - Uses asyncio.start_server() to listen for connections - Creates new protocol per connection via factory callable connect_native_replication_client() — asyncio equivalent of ReconnectingClientFactory: - Uses asyncio.open_connection() with automatic reconnection loop - Configurable reconnect interval (default 5s) --- Running totals across Phases 0-6: - 8 new files, ~2500 lines of asyncio-native implementation - 128 tests all passing - All mypy clean - Existing 4462-test suite unaffected |
||
|
|
61356a8018 |
⏺ Phase 5: HTTP Server Migration — Complete
1 new file created, 16 new tests (11 integration + 5 unit), all passing, mypy clean, no regressions. synapse/http/native_server.py — asyncio-native HTTP server framework: NativeSynapseRequest — Twisted Request compatibility shim wrapping aiohttp.web.Request: - .method, .uri, .path (bytes, matching Twisted) - .args (dict[bytes, list[bytes]] parsed from query string) - .content (BytesIO wrapping request body) - .requestHeaders / .responseHeaders (shim with getRawHeaders(), hasHeader(), getAllRawHeaders()) - .setResponseCode(), .setHeader(), .write(), .finish() - .build_response() assembles final aiohttp.web.Response from accumulated state - Allows parse_json_object_from_request() and all parameter parsing functions to work unchanged NativeJsonResource — aiohttp-based router with same register_paths() API as JsonResource: - register_paths(method, path_patterns, callback, classname) — identical interface - build_app() → aiohttp.web.Application with catch-all route - Pattern matching via re.Pattern.match() just like the Twisted version - URL parameter extraction via groupdict() + URL decoding - Supports both sync and async handlers - Handles tuple[int, JsonDict] return convention - Error handling: SynapseError → JSON error response, RedirectException → redirect - CORS support on all responses + OPTIONS preflight respond_with_json_native() / respond_with_html_native() — return aiohttp.web.Response instead of writing to Twisted Request Tests use aiohttp.test_utils.TestServer with real HTTP requests, testing routing, path parameters, URL encoding, POST JSON, error responses, 404/405, CORS, sync handlers, and the request shim. |
||
|
|
7aa362b9c0 |
⏺ Phase 4: HTTP Client Migration — Complete
1 new file created, 10 new tests with real HTTP server, all passing, mypy clean, no regressions. synapse/http/native_client.py — NativeSimpleHttpClient class using aiohttp.ClientSession: - Same public interface as SimpleHttpClient: request(), get_json(), post_json_get_json(), post_urlencoded_get_json(), put_json(), get_raw(), get_file() - IP blocklisting via _BlocklistingResolver — custom aiohttp.abc.AbstractResolver that filters DNS results against blocklist/allowlist, preventing DNS rebinding attacks - IP literal blocking — direct IP addresses in URLs checked before request - Proxy support — proxy_url parameter passed to aiohttp's built-in proxy support - Connection pooling — via aiohttp.TCPConnector with configurable limit_per_host - Timeouts — per-request timeout via asyncio.wait_for(), connection timeout via aiohttp.ClientTimeout - File download — streaming download with max size enforcement and content-type validation - TLS — configurable ssl.SSLContext for custom TLS verification Tests use a real aiohttp.web test server with endpoints for JSON, raw bytes, file downloads, form posts, and error responses. --- Running totals across Phases 0-4: - 5 new files, ~1500 lines of asyncio-native implementation code - 107 tests all passing - Existing 4462-test suite unaffected - All mypy clean |
||
|
|
b457462c70 |
⏺ Phase 3: Database Layer — Complete
1 new file created, 6 new tests, all passing, mypy clean, no regressions. synapse/storage/native_database.py — NativeConnectionPool class: - Uses concurrent.futures.ThreadPoolExecutor + asyncio.loop.run_in_executor() instead of twisted.enterprise.adbapi.ConnectionPool - Thread-local connection management: each thread in the pool maintains its own persistent DB connection - Automatic connection creation and initialization via engine.on_new_connection() (same as the Twisted pool's cp_openfun) - Reconnection support for closed connections - runWithConnection(func, *args) — runs function on a pool thread with a connection - runInteraction(func, *args) — runs function in a transaction with auto-commit/rollback - close() — shuts down the executor - threadID() — compatibility method for transaction limit tracking The existing DatabasePool and all 846+ runInteraction callers are untouched. When the migration reaches the point of switching DatabasePool to use NativeConnectionPool instead of adbapi.ConnectionPool, the inner_func pattern in runWithConnection will be reused with minimal changes (just swap make_deferred_yieldable(self._db_pool.runWithConnection(...)) to await self._native_pool.runWithConnection(...)). |
||
|
|
a1267a1f37 |
⏺ Phase 2: NativeClock — Complete
3 new classes added to synapse/util/clock.py, 15 new tests, all passing, mypy clean, no regressions. NativeLoopingCall — asyncio Task wrapper with stop(). Tracks in WeakSet for automatic cleanup. NativeDelayedCallWrapper — Wraps asyncio.TimerHandle with the same interface as DelayedCallWrapper (cancel(), active(), getTime(), delay(), reset()). Since TimerHandle is immutable, delay()/reset() cancel and reschedule. NativeClock — Same public API as Clock but uses: - time.time() instead of reactor.seconds() - asyncio.sleep() instead of Deferred + reactor.callLater - asyncio.create_task() with while True loop instead of LoopingCall - loop.call_later() instead of reactor.callLater() - loop.call_soon() instead of reactor.callWhenRunning() - Logcontext wrapping preserved (same PreserveLoggingContext + run_in_background pattern) - LoopingCall semantics preserved: waits for previous invocation to complete, survives errors |
||
|
|
24724a810e |
Phase 1: LoggingContext ContextVar Preparation — Complete
**Goal**: Switch live context tracking to `contextvars.ContextVar`. This is the foundational change everything else depends on — `contextvars` propagates automatically into `asyncio.Task` children, which is essential for native asyncio. **Files modified**: - `synapse/logging/context.py` (lines 736-766) — Replace `_thread_local = threading.local()` with `_current_context: ContextVar[LoggingContextOrSentinel]`. Update `current_context()` and `set_current_context()`. `LoggingContext.__enter__/__exit__` (lines 377-417) use `ContextVar.set()` token API. `PreserveLoggingContext` (line 677) works unchanged since it calls the same functions. - `synapse/util/patch_inline_callbacks.py` — Update logcontext checks if needed for contextvars semantics. **Key constraint**: This is backward-compatible with Twisted. Deferred callbacks run on the main thread; `ContextVar` works fine with single-threaded access. DB thread pool interactions need verification — `adbapi.ConnectionPool` uses Twisted's `ThreadPool`, and each thread gets its own contextvars copy by default, which matches current `threading.local` behavior. Key finding: The original plan to directly replace threading.local with ContextVar was not possible while Twisted Deferreds are in use. asyncio's event loop runs call_later/call_soon callbacks in context copies, so _set_context_cb's ContextVar write would be isolated and invisible to the awaiting code. This is fundamentally different from threading.local where writes are globally visible on the thread. What was implemented instead (revised Phase 1): synapse/logging/context.py: - _thread_local remains the primary storage for current_context() / set_current_context() — backward compatible with Twisted Deferred callback patterns - _current_context_var (ContextVar) is kept in sync — every set_current_context() call also writes to the ContextVar - _native_current_context() / _native_set_current_context() — operate on ContextVar only, for asyncio-native code paths (Tasks) where ContextVar propagation is correct - make_future_yieldable(), run_coroutine_in_background_native(), run_in_background_native() — all use _native_* functions since they run inside asyncio Tasks Migration path: The full switch from threading.local → ContextVar as sole storage happens in Phase 7 when all Deferred usage is removed. Until then, both storage mechanisms coexist. Verification: 4462 tests passed, 169 skipped, 0 new failures. mypy clean. |
||
|
|
2dce74958f |
Phase 0: Abstraction Boundaries — Complete
**Goal**: Add asyncio-native parallel implementations alongside existing Twisted ones, so subsequent phases can swap without touching callers. **Files modified**: - `synapse/logging/context.py` — Add `contextvars.ContextVar`-based `current_context()`/`set_current_context()` behind a feature flag alongside the existing `threading.local` implementation. Add `make_future_yieldable()` and `run_coroutine_in_background_native()` operating on `asyncio.Future`/`asyncio.Task` instead of `Deferred`. - `synapse/util/async_helpers.py` — Add `asyncio.Future`-based `ObservableFuture`, and `asyncio.Lock`/`asyncio.Event`-based `AsyncLinearizer`, `AsyncReadWriteLock` alongside the Deferred-based originals. - `synapse/types/__init__.py` — Define `ISynapseEventLoop` protocol abstracting the event loop operations (`call_later`, `call_soon`, `run_in_executor`, `create_task`) so `Clock` and other code can be parameterized. 3 files modified, 1 test file created, 28 new tests all passing, 46 existing tests unaffected, mypy clean. synapse/logging/context.py — Added asyncio-native parallel implementations: - _current_context_var — contextvars.ContextVar that will replace _thread_local in Phase 1 - current_context_contextvar() / set_current_context_contextvar() — ContextVar-based equivalents of current_context() / set_current_context() - make_future_yieldable() — asyncio.Future equivalent of make_deferred_yieldable() - run_coroutine_in_background_native() — asyncio.Task equivalent of run_coroutine_in_background() - run_in_background_native() — asyncio.Task equivalent of run_in_background() synapse/util/async_helpers.py — Added asyncio-native primitives: - ObservableFuture — asyncio.Future-based equivalent of ObservableDeferred - NativeLinearizer — asyncio.Event-based equivalent of Linearizer (no Deferred dependency) - NativeReadWriteLock — asyncio.Event-based equivalent of ReadWriteLock synapse/types/__init__.py — Added: - ISynapseEventLoop — Protocol abstracting event loop operations (call_later, call_soon, run_in_executor, create_task, time) so Clock can be parameterized over Twisted reactor vs asyncio loop in Phase 2 tests/util/test_native_async.py — 28 tests covering all new implementations using unittest.IsolatedAsyncioTestCase. |