Repository navigation
Deliver realm events to synapse through one bounded, paced queue per process #6899
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1975634
102f4b8
4774110
fb6cdcc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -55,6 +55,12 @@ import type { | |
| } from '@cardstack/base/matrix-event'; | ||
| import { APP_BOXEL_REALM_EVENT_TYPE } from '@cardstack/runtime-common/matrix-constants'; | ||
| import { createJWT, verifyJWT } from './jwt.ts'; | ||
| import { | ||
| isBackPressure, | ||
| realmEventDispatcher, | ||
| type RealmEventDispatcher, | ||
| type RealmEventSend, | ||
| } from './realm-event-dispatcher.ts'; | ||
|
|
||
| const realmEventsLog = logger('realm:events'); | ||
|
|
||
|
|
@@ -88,10 +94,20 @@ function lastModifiedSeconds(stat: Stats): number { | |
| export class NodeAdapter implements RealmAdapter { | ||
| private realmDir: string; | ||
| private enableFileWatcher?: boolean; | ||
|
|
||
| constructor(realmDir: string, enableFileWatcher?: boolean) { | ||
| private dispatcher: RealmEventDispatcher; | ||
| // Set when the realm's last room lookup found no session rooms. The lookup | ||
| // waits and retries only when this is clear, so a realm nobody has open | ||
| // pays the wait once rather than on every queued event. | ||
| private lastLookupFoundNoRooms = false; | ||
|
|
||
| constructor( | ||
| realmDir: string, | ||
| enableFileWatcher?: boolean, | ||
| dispatcher: RealmEventDispatcher = realmEventDispatcher(), | ||
| ) { | ||
| this.realmDir = realmDir; | ||
| this.enableFileWatcher = enableFileWatcher; | ||
| this.dispatcher = dispatcher; | ||
| } | ||
|
|
||
| get dir(): string { | ||
|
|
@@ -421,6 +437,9 @@ export class NodeAdapter implements RealmAdapter { | |
| }; | ||
| } | ||
|
|
||
| // Resolves once the event has reached every session room subscribed to the | ||
| // realm, or has been merged or dropped by the process's realm event line | ||
| // (see realm-event-dispatcher.ts). Never rejects: a failed send is logged. | ||
| async broadcastRealmEvent( | ||
| event: RealmEventContent, | ||
| realmUrl: string, | ||
|
|
@@ -438,33 +457,70 @@ export class NodeAdapter implements RealmAdapter { | |
| ); | ||
| return; | ||
| } | ||
| await this.dispatcher.broadcast({ | ||
| realmURL: realmUrl, | ||
| event: eventWithRealmURL, | ||
| newTxnId: () => matrixClient.nextTxnId(), | ||
| prepare: (content) => | ||
| this.prepareRoomSends(content, realmUrl, matrixClient, dbAdapter), | ||
| }); | ||
| } | ||
|
|
||
| private async prepareRoomSends( | ||
| event: RealmEventContent, | ||
| realmUrl: string, | ||
| matrixClient: MatrixClient, | ||
| dbAdapter: DBAdapter, | ||
| ): Promise<RealmEventSend[]> { | ||
| if (dbAdapter.isClosed) { | ||
| return []; | ||
| } | ||
| // A logged-in client reuses its session; this logs in only on the first | ||
| // broadcast, or after the session was revoked. | ||
| try { | ||
| await matrixClient.login(); | ||
| if (!matrixClient.getUserId()) { | ||
| realmEventsLog.error( | ||
| 'Matrix client has no user ID after login, unable to broadcast realm event', | ||
| event, | ||
| ); | ||
| return; | ||
| return []; | ||
| } | ||
| } catch (e) { | ||
| // An overloaded or unreachable homeserver is back-pressure: the | ||
| // dispatcher pauses and prepares the event again rather than skip it. | ||
| if (isBackPressure(e)) { | ||
| throw e; | ||
| } | ||
| realmEventsLog.error('Error logging into matrix. Skipping broadcast', e); | ||
| return; | ||
| return []; | ||
| } | ||
|
|
||
| let dmRooms = await this.waitForSessionRooms(dbAdapter, realmUrl); | ||
| let dmRooms = await this.waitForSessionRooms( | ||
| dbAdapter, | ||
| realmUrl, | ||
| this.lastLookupFoundNoRooms ? 0 : undefined, | ||
| ); | ||
| this.lastLookupFoundNoRooms = Object.keys(dmRooms).length === 0; | ||
|
|
||
| realmEventsLog.debug('Sending to dm rooms', Object.values(dmRooms)); | ||
|
|
||
| for (let userId of Object.keys(dmRooms)) { | ||
| let roomId = dmRooms[userId]; | ||
| try { | ||
| return Object.entries(dmRooms).map(([userId, roomId]) => ({ | ||
| roomId, | ||
| send: async (txnId: string) => { | ||
| // A send can start while a revoked session is being replaced, when | ||
| // the client holds no token; wait for the new one rather than fail. | ||
| if (!matrixClient.isLoggedIn()) { | ||
| await matrixClient.login(); | ||
| } | ||
|
Comment on lines
+510
to
+515
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Claude Code 🤖] The back-pressure classification doesn't cover the login this closure now waits on. Throwing Pre-existing, now load-bearing. Non-blocking.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Claude Code 🤖] Fixed in That covers both paths:
New tests in |
||
| await matrixClient.sendEvent( | ||
| roomId, | ||
| APP_BOXEL_REALM_EVENT_TYPE, | ||
| eventWithRealmURL, | ||
| event, | ||
| txnId, | ||
| ); | ||
|
Comment on lines
+510
to
521
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Claude Code 🤖] Retrying with the same transaction id holds across the 401 re-login, because the device stays An
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Claude Code 🤖] Fixed in |
||
| } catch (e) { | ||
| }, | ||
| onFailure: async (e: unknown) => { | ||
| if (isRealmServerNotInRoomError(e, roomId)) { | ||
| try { | ||
| let cleared = await clearSessionRoom(dbAdapter, userId, roomId); | ||
|
|
@@ -483,15 +539,18 @@ export class NodeAdapter implements RealmAdapter { | |
| e, | ||
| ); | ||
| } | ||
| continue; | ||
| return; | ||
| } | ||
| // The error's message carries the status and the homeserver's answer, | ||
| // which is all a failed send has to say; its stack is the same for | ||
| // every room. | ||
| realmEventsLog.error( | ||
| `Unable to send event in room ${roomId} for user ${userId}`, | ||
| event, | ||
| e, | ||
| `Unable to send event in room ${roomId} for user ${userId}: ${ | ||
| e instanceof Error ? e.message : String(e) | ||
| }`, | ||
| ); | ||
| } | ||
| } | ||
| }, | ||
| })); | ||
| } | ||
|
|
||
| private async waitForSessionRooms( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The rule queries a trailing five-minute window and then reduces it with
max, while also requiring the resulting condition for five minutes. Consequently, a backlog that clears after roughly a minute can remain above threshold solely because its old samples stay in the lookback window long enough to satisfyfor: 5m, producing a stale alert despite the summary claiming five continuous minutes of backlog. Query only the current sampling interval before applyingfor, or aggregate the window in a way that requires every interval to exceed the threshold.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Claude Code 🤖] Fixed in
102f4b8. The rule now reduces the 5-minute window withminand usesfor: 0s, like its sibling rules. It fires only when every 1-minute bin in the window is over 60s, so a backlog that clears stops matching within a minute. The summary says the same.