Geräte-Routing, Wiederverbindung und Wiederherstellung
Registrierungsantworten und Ablehnungen, die zwei Watchdogs einer toten Verbindung, die Wiederverbindungsfrist, wie Timeline-Events durch zwei begrenzte Puffer nur interessierte Telefone erreichen, die anderen Relay-Events, die Push-Registrierung und warum das Relay als ein Prozess läuft.
Gegen den Quellcode geprüft am 5. Oktober 2026
Auf dieser Seite
Registrierung und Erkennung
| Client | WebSocket-Header | Gegengeprüft mit |
|---|---|---|
| Desktop | X-Client-Type: desktop | Eine Desktop-Gerätezeile, die dasselbe Konto über HTTP registriert hat. |
| Telefon | X-Client-Type: mobile, dazu X-Target-Desktop-Device-ID für den ausgewählten Desktop | Eine mobile Gerätezeile. Das Relay liest den Ziel-Desktop bei jeder Registrierung aus dem register-Payload, sonst aus dem Header, nie aus einer gespeicherten Session. |
Der erste Frame auf dem Socket ist die register-Nachricht. Das Relay antwortet mit registered samt sessionId, resumeToken, expiresAt und relayInstanceId. Eine register-Nachricht mit gültigem Resume-Token erhält resumed mit derselben sessionId, mit expiresAt und relayInstanceId und ohne neues Token. Wenn sich ein Desktop registriert, sendet ihm das Relay außerdem die aktuellen Interesse-Leases seiner Telefone.
Eine abgelehnte Registrierung erhält einen Fehler-Frame mit einem dieser Codes.
| Code | Ursache |
|---|---|
| invalid | Der Client-Typ ist weder desktop noch mobile. |
| invalid | Die Geräte-ID ist keine UUID. |
| device | Es gibt keine Gerätezeile. Das Gerät muss sich zuerst über HTTP registrieren. |
| device | Die Gerätezeile gehört zu einem anderen Konto. |
| device | Der Client-Typ weicht vom Typ der Gerätezeile ab. |
| desktop | Die Desktop-Zeile ist für das Relay nicht zugelassen. |
| desktop | Das Gerät wurde im Konto vergessen. |
| protocol | Die Relay-Protokollversion fehlt oder liegt unter der Untergrenze. |
| invalid | Die Relay-Protokollversion lässt sich nicht parsen. |
| invalid | Das Resume-Token ist unbekannt oder abgelaufen. Der Client registriert sich erneut ohne Token. |
| relay | Der Prozess läuft für ein Deployment im Drain. |
| relay | Das Relay konnte seine Protokollrichtlinie oder die Gerätezeile nicht lesen. Der Fehler liegt beim Server. |
Wiederverbindungsfrist und Wiederverbindungs-Snapshot
- Desktop-WatchdogINBOUND_ACTIVITY_TIMEOUT_SECS = 45
Alle 15 Sekunden prüft der Desktop die Zeit seit dem letzten eingehenden Frame und sendet dann einen Heartbeat, den das Relay mit einem Pong beantwortet. Eine Lücke von mehr als 45 Sekunden beendet die Verbindung. Die Prüfung läuft im Takt von 15 Sekunden. Deshalb gibt der Desktop eine tote Verbindung spätestens 60 Sekunden nach ihrem letzten Frame auf.
- Wiederverbindungs-Backoffreconnect_backoff
Jede Wartezeit ist eine zufällige Zeit zwischen 100 Millisekunden und einer Obergrenze. Die Obergrenze beginnt bei 500 Millisekunden, verdoppelt sich pro fehlgeschlagenem Versuch und endet bei 30 Sekunden. Der Verbindungsaufbau und die Registrierungsantwort haben je ein Zeitlimit von 10 Sekunden. Der Fehlerzähler wird erst zurückgesetzt, wenn eine Verbindung 30 Sekunden lang bereit war.
- Relay-WatchdogCLIENT_TIMEOUT = 180 s
Alle 30 Sekunden prüft das Relay die Zeit seit dem letzten Frame des Sockets und sendet dann ein Ping. Mehr als 180 Sekunden Stille beenden den Socket. Das Schließen kommt also 180 bis 210 Sekunden nach dem letzten Frame. Ein Text-, Ping- oder Pong-Frame setzt die Uhr zurück.
- PräsenzfristDESKTOP_RECONNECT_GRACE_SECS = 90
Beginnt, wenn das Relay den Desktop-Socket schließt. Telefone sehen während der Frist „Wiederverbindung“. An ihrem Ende sendet das Relay device-status disconnected mit dem Grund reconnect_timeout an alle, sofern sich der Desktop nicht vorher neu registriert hat.
Ein Socket, der mit einem Close-Frame schließt, ist beiden Seiten sofort bekannt. Eine Verbindung, die lautlos stirbt, finden zwei unabhängige Watchdogs, und der des Desktops ist der schnellere. Der Desktop verbindet sich neu, lange bevor das Relay den alten Socket schließen würde. Im Normalfall sehen Telefone den Desktop deshalb nie als offline.
- Desktop-Anfragen-32011 · -32010
Für die geschlossene Verbindung erfasste Anfragen erhalten sofort -32011, „Desktop is reconnecting“, ebenso jede neue Anfrage während der Wartezeit. Danach erhalten neue Anfragen -32010. Einen stummen Socket betrachtet das Relay erst nach 180 Sekunden als geschlossen.
- Wiederverbindungsfristreconnect_timeout
Sie hält nur die Präsenz, und Telefone sehen den Desktop als reconnecting. Nach 90 Sekunden markiert das Relay ihn als offline und sendet device-status disconnected, sofern sich nicht zuvor eine neue Verbindung registriert hat.
- Telefon-Anfragen(user, desktop, id)
Der Eintrag überdauert den Socket des Telefons, sodass eine Antwort, die nach der erneuten Registrierung eintrifft, zugestellt wird. Eine finale Antwort, die in der Lücke eintrifft, wird verworfen, und der Aufruf des Telefons endet an seinem eigenen Timeout.
- Telefon-Leasechat:timeline-interest
An die Verbindung des Telefons gebunden, sodass sie mit dem Socket endet und nichts aus der Lücke aufbewahrt wird. Das Telefon sendet sie erneut, sobald das Relay registered oder resumed antwortet, pingt den Desktop und lädt das neueste Fenster.
Ein Client, der sich mit seinem Resume-Token registriert, durchläuft trotzdem die Protokoll- und Geräteeigentümer-Prüfungen. Der Relay-Session-Eintrag hinter dem Token lebt 24 Stunden, wird bei jedem RPC und jedem Heartbeat aufgefrischt und beim Abmelden ungültig. Ein Aufräumlauf entfernt abgelaufene Einträge alle 5 Minuten. iOS bewahrt das Token im Schlüsselbund auf.
Offline-, Reconnecting- und Timeout-Fehler bleiben unterschieden, und nur als retryable markierte Fehler erlauben eine Transportwiederholung. Ein Timeout beschreibt die Antwort, die der Aufrufer nicht bekommen hat. Er sagt nichts darüber, ob die Mutation gelaufen ist, also folgt der Client dem Wiederherstellungspfad der Operation, bevor er neue Arbeit anstößt.
| Status | Bedeutung und nächster Schritt |
|---|---|
| Verbindung wird wiederhergestellt | Das Telefon stellt seine Server-Verbindung wieder her und prüft danach den Desktop erneut. Geben Sie ihm einen Moment. |
| Offline oder getrennt | Prüfen Sie die Desktop-App, den Schlafzustand des Computers, das Konto, die Region und das Netzwerk. |
| Zeitüberschreitung der Anforderung | Die Antwort kam nicht rechtzeitig an. Die Anfrage kann trotzdem gelaufen sein, lesen Sie also den Session-Zustand, bevor Sie eine Mutation wiederholen. |
| Verbunden, aber Verlauf nicht verfügbar | Das Relay funktioniert, und das Lesen des Verlaufs ist fehlgeschlagen. Verwenden Sie die Aktion zum Neuladen in der Timeline. |
Timeline-Events erreichen nur interessierte Telefone
- Interesse-Leasechat:timeline-interest { appSessionId, active }
Vom Telefon für die Session auf seinem Bildschirm gesendet und alle 20 Sekunden erneuert. Das Relay speichert sie pro Telefon und leitet sie nie weiter. Sie enthält keine Timeline-Inhalte.
- Ende der LeaseTIMELINE_INTEREST_TTL = 60 s
Eine Lease endet 60 Sekunden nach ihrer letzten Erneuerung, oder sofort, wenn das Telefon active: false sendet, eine andere Session öffnet oder sein Socket geschlossen oder ersetzt wird. Wartende Updates für dieses Telefon verfallen mit ihr. Einen stummen Socket trennt das Relay erst nach 180 Sekunden, die Lease endet also zuerst.
- Frist auf dem Desktopchat:timeline-interest-updated { expiresAt }
Bei jeder Änderung einer Lease teilt das Relay dem Desktop pro Session mit, ob eine Lease besteht und wann sie endet, aber nie, welches Telefon sie hält. Der Desktop merkt sich diese Frist selbst und serialisiert die Timeline der Session nicht mehr, sobald sie abgelaufen ist.
- Relay-FilterauthorizationGeneration
Wird ausgegeben, wenn eine Session von keiner Lease zu einer wechselt. Jeder Frame muss den aktuellen Wert tragen. Das Relay entfernt ihn und sendet den Frame nur an Telefone, deren Lease beim letzten Senden noch lebt. Für die anderen wird nichts aufbewahrt, daher lädt ein zurückkehrendes Telefon einen Snapshot.
Die Lease ist nach Benutzer, Telefonroute, Ziel-Desktop und Basis-Session geschlüsselt. appSessionId wird getrimmt, auf 1.024 Bytes begrenzt und von einem angehängten skills-agent-Alias befreit. Aktualisierungen an den Desktop tragen außerdem eine Revision und die relayInstanceId, sodass der Desktop veraltete ignoriert und seinen Zustand löscht, wenn sie von einer anderen Instanz kommen.
- Desktop-WarteschlangeMAX_PENDING_EVENT_BYTES = 4 MiB
Eine Warteschlange pro Verbindung. Sie hält höchstens 64 Timeline-Schlüssel, 64 andere Events und 4 MiB. Ein Schlüssel besteht aus der Basis-Session und dem Thread. Ein neuerer Snapshot ersetzt den wartenden und behält dessen Platz in der Reihenfolge. Ein älterer oder identischer wird ignoriert. Wenn der Platz nicht reicht, wird der älteste wartende Snapshot verworfen.
- SendetaktRELAY_EVENT_SEND_INTERVAL = 350 ms
Der Socket-Schreiber nimmt pro Takt ein Event aus der Warteschlange. Steuernachrichten werden vor jedem Event geprüft und warten nicht auf den Takt. Jeder Frame-Versand hat ein Zeitlimit von 10 Sekunden.
- Relay-PufferTIMELINE_EVENT_FLUSH_INTERVAL = 250 ms
Ein Puffer pro Telefonverbindung mit höchstens 16 Schlüsseln und 1 MiB. Eine neuere Revision ersetzt die wartende, und der älteste Schlüssel wird verdrängt, wenn eine Grenze überschritten ist. Das Relay sendet ein Event pro Flush. Ein Frame wartet 250 Millisekunden oder den Rest der Pause nach dem vorherigen Versand. Diese Pause beträgt 250 Millisekunden pro angefangene 512 KiB des gesendeten Events.
- Übergrößen-Platz32 MiB − 16 KiB
Ein Snapshot über 512 KiB im Relay oder über 4 MiB auf dem Desktop wartet allein auf einem zusätzlichen Platz außerhalb der Byte-Grenze, und ein neuerer übergroßer ersetzt ihn. Der Desktop lehnt ein Event ab, dessen Frame 32 MiB minus 16 KiB überschreiten würde.
Ein Live-Update der Timeline ist ein vollständiger Snapshot des Live-Overlays mit settlementEpoch und revision. Dadurch ist jeder ältere Snapshot desselben Schlüssels überflüssig, und beide Puffer nutzen das. Die zwei Taktstufen bestimmen die Untergrenze der Live-Latenz: bis zu 350 Millisekunden auf dem Desktop und bis zu 250 Millisekunden im Relay.
| Fehler | Ergebnis |
|---|---|
| Desktop: keine aktuelle Lease für die Session | Das Event wird abgelehnt, bevor es serialisiert wird. Nichts kommt in die Warteschlange. |
| Desktop: Snapshot-Frame über 32 Mi | Der Snapshot wird verworfen. Der nächste kann trotzdem durchgehen. |
| Desktop: anderes Event über 32 Mi | Der Desktop beendet die Verbindung und verbindet sich neu, und Telefone gleichen ab. |
| Desktop: kein Platz für ein geordnetes Timeline-Event oder zwei verschiedene Snapshots mit derselben Revision | Die Warteschlange wird geleert und geschlossen, und der Desktop trennt die Verbindung. |
| Relay: zwei verschiedene Snapshots mit derselben Revision | Der Puffer des Telefons wird geleert und sein Socket geschlossen. Das Telefon verbindet sich neu und gleicht ab. |
| Relay: mehr als 16 Schlüssel oder 1 Mi | Die ältesten Schlüssel werden verdrängt. Das Telefon erhält ihren Stand mit dem nächsten Snapshot dieser Schlüssel. |
Events neben RPC
Ein Event-Frame trägt einen eventType und einen Payload. Das Relay behandelt die Typen in der Tabelle besonders und sendet jeden anderen Typ an die Routen des Kontos. Ein Token eines angemeldeten Nutzers darf immer ein Event senden. Ein API-Schlüssel braucht die Scopes rpc, read und write und erhält ohne sie forbidden. Solange die Registrierung nicht abgeschlossen ist, antwortet das Relay mit registrationRequired.
| Event | Was das Relay tut |
|---|---|
| chat:timeline-updated | Wird nur vom Desktop angenommen. Das Relay sendet es an Telefone mit einer aktiven Lease für die Session. Ein Telefon, das es sendet, erhält forbidden. |
| chat:timeline-interest | Von einem Telefon gesendet. Das Relay speichert die Lease und leitet sie nie weiter. |
| chat:timeline-interest-updated | Vom Relay geschrieben. Es teilt dem Desktop den Zustand der Lease mit. Ein Client, der es sendet, erhält reserved |
| device-visibility-updated | Von einem Desktop oder einem Telefon gesendet. Das Relay speichert das Flag in der Gerätezeile und sendet es dann an alle. Ein Desktop, der visible: false meldet, wird nach dem Senden geschlossen. |
| project-directory-updated | Von einem Desktop oder einem Telefon gesendet. Das Relay speichert das Verzeichnis in der Gerätezeile und sendet es dann an alle. |
| agent-notification-requested | Vom Desktop gesendet. Das Relay sendet es an alle und verschickt einen sichtbaren Push. Den Push-Teil ignoriert es, wenn ein Telefon das Event sendet. |
| device-status | Vom Relay geschrieben. Es meldet Präsenzänderungen eines Desktops, etwa disconnected mit dem Grund reconnect_ |
| device-unlinked | Vom Relay geschrieben. Es geht an jede Route des Kontos, wenn ein Gerät vergessen wird. Danach schließt das Relay die Routen dieses Geräts. Ein Client, der es sendet, erhält reserved |
Push-Registrierung und Benachrichtigungen
Beide Telefone registrieren im angemeldeten Zustand ein Push-Token mit PUT api/devices/push-token. Der Server sendet genau zwei Arten von Push. Die erste ist die stille Datennachricht desktop_online, gesendet, wenn sich Ihr Desktop beim Relay registriert. Sie trägt die relayInstanceId und die Verbindungsgeneration, und beide Apps stellen damit ihre Verbindung wieder her, bevor Sie sie öffnen. Die zweite ist der sichtbare Hinweis, den das Benachrichtigungs-Tool des Agenten schreibt. Der Desktop sendet ihn als Event agent-notification-requested mit sessionId, title und body, und das Relay gibt ihn an APNs und FCM weiter.
Warum das Relay als ein Prozess läuft
- ProzessspeicherDeviceConnectionManager
Hält jede Verbindungsroute, jeden ausstehenden Anfrage-Eintrag, die Wiederverbindungs-Präsenz und jede Interesse-Lease. RelaySessionStore hält die Resume-Sessions. Ein zweiter Prozess sähe nichts davon, darum läuft das Relay als ein einziger Prozess.
- DrainrelayDraining
Unumkehrbar. Ab seinem Beginn weist der Prozess Registrierungen ab. Er wartet höchstens 60 Sekunden auf laufende Routing-Arbeit, löscht dann seinen Speicher, ohne offene Anfragen zu beantworten, und schließt jeden Geräte-Socket.
- Instanz-IDrelayInstanceId
Eine neue UUID pro Prozess, gesendet mit jeder Registrierungsantwort. Telefone ignorieren Präsenz-Events einer anderen Instanz, und der Desktop löscht seinen Interesse-Zustand, wenn Aktualisierungen von einer anderen kommen.
Mit zwei Prozessen hinter einem Load Balancer sähe ein Telefon, dessen Socket auf dem Prozess ohne den Socket seines Desktops gelandet ist, diesen Desktop als offline.
In Produktion gibt es deshalb genau eine Routing-Autorität pro Region. Ein Blue-Green-Deploy startet den neuen Prozess, drainiert den alten und leitet dann den Verkehr zum neuen. Pro Verbindung begrenzt das Relay Frames auf 32 MiB, drosselt mit einem Token-Bucket von 300 Burst und 150 pro Sekunde, pingt alle 30 Sekunden und trennt einen Client nach 180 Sekunden Stille.
Ein Text-Frame über dem Ratenlimit wird verworfen, bevor das Relay ihn validiert. Ist der Frame eine rpc.request mit einer Anfrage-ID und einem gültigen traceparent, antwortet das Relay mit dem Fehler -32028, der Aufruf scheitert also sofort, statt auf das Zeitlimit des Clients zu warten. Jeder andere verworfene Frame erhält keine Antwort. Die Antworten haben einen eigenen Token-Bucket derselben Größe. Jeder verworfene Frame nimmt daraus ein Token, und ohne Token liest das Relay nichts aus dem Frame und sendet nichts.