fix(realtime): dead-connection heartbeat + boot reconciliation by ArnabChatterjee20k · Pull Request #14717 · appwrite/appwrite · GitHub
Skip to content

fix(realtime): dead-connection heartbeat + boot reconciliation - #14717

Draft
ArnabChatterjee20k wants to merge 2 commits into
fix-realtime-presence-connections-counterfrom
fix-realtime-dead-connection-reconciliation
Draft

ArnabChatterjee20k wants to merge 2 commits into
fix-realtime-presence-connections-counterfrom
fix-realtime-dead-connection-reconciliation

Conversation

@ArnabChatterjee20k

Copy link
Copy Markdown
Member

Stacked on #14606 (per-socket presence counter) — base branch fix-realtime-presence-connections-counter. Merge that first.

Split out of #14606 so the per-socket connections counter can land independently. This carries the realtime dead-connection work that was previously merged into #14606 via #14661, plus follow-up fixes.

Contents

⚠️ Open issue to resolve here (do not merge until addressed)

hansi flagged (P1) that the hostname-based boot sweep is unsound on multi-replica self-hosted: the presence row is one shared row per user with hostname = last writer, and presence is upsert-once (no refresh), so a row last written by host A can still have a live hold on host B. When A restarts, the sweep can delete that row and emit offline while B is connected. The $updatedAt guard only protects writes during the sweep, not pre-existing cross-host holds.

A single shared row can't carry per-host hold information, so there's no targeted patch — options are to drop the boot sweep (rely on heartbeat + expiresAt) or add cross-host liveness. Tracking and resolving that here keeps it off the counter PR.

🤖 Generated with Claude Code

ArnabChatterjee20k and others added 2 commits October 21, 2026 15:34
The CE orphan sweep runs asynchronously while new connections are already
accepted. A reconnect increments the same per-user row in place, which keeps
its $createdAt, so the row still matched lessThan('$createdAt', $since) and was
deleted despite the fresh live hold.

Select candidates by $updatedAt (the field a reuse actually moves) and delete
through State::deleteOrphan, which re-reads under the same forUpdate row lock
the increment takes and bails when $updatedAt >= $since. Whichever grabs the
lock first wins: a mid-sweep reconnect advances $updatedAt and is skipped; a
sweep delete lets the reconnect create a fresh row. A live hold is never lost.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add StateTest coverage for deleteOrphan: a row not touched since boot is
deleted, a row updated after $since (reconnected) is preserved, and a missing
row is a no-op.

The test caught that the guard compared $updatedAt to $since as raw strings.
$updatedAt reads back in ISO form (2026-...T...+00:00) while $since is DB
format (space-separated), so 'T' > ' ' made the comparison always true and
deleteOrphan never deleted anything. Compare them as \DateTime instants.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant