perf(presence): reduce heartbeat frequency (#3783)

## Summary

- send desktop presence heartbeats every 60 seconds instead of every 30
seconds
- extend presence TTL from 90 to 180 seconds to preserve the existing
three-heartbeat expiry window
- add mutation-sensitive tests that pin the one-minute / three-window
timing contract
- update presence documentation to match

This halves steady-state **desktop** presence `SET` + `PUBLISH` traffic
while retaining tolerance for two missed heartbeats. Mobile already uses
a 60-second heartbeat, so the fleet-wide reduction depends on desktop's
share of connected clients.

## Rollout order

Deploy the relay TTL increase before shipping the desktop heartbeat
change. Old desktop + new relay is safe; new desktop + old relay leaves
only a 90-second TTL on a 60-second cadence and can flap after one
missed heartbeat.

## Verification

At initial live-test commit `00816e233b187bc5ba12c667d675ed050a8cc1c9`:

- isolated clean-room relay built from the exact SHA against fresh
Postgres, Redis, and MinIO
- live Redis `MONITOR` observed kind-20001 writes as `SET ... EX 180`,
global `PUBLISH`, and clean-disconnect / explicit-offline `DEL`
- normal workflows passed: channel create/update/archive/unarchive;
message send/get/reply/thread/search; archived-channel write rejection
and resumed write after unarchive

At follow-up commit `bf38a8c5c96f196ff8ee46e48d4141ee7811f186`:

- `pnpm -C desktop test` — 3829 passed
- `pnpm -C desktop typecheck`
- `cargo test -p buzz-pubsub` — 24 passed, 11 Redis-dependent tests
ignored
- mutation probes fail when the server TTL changes to `999999` or the
desktop heartbeat changes back to 30 seconds
- `git diff --check`

The pre-push suite's relevant checks passed, but its unrelated Tauri
clippy step fails on current `origin/main`:
`desktop/src-tauri/src/linux_media.rs` has three dead-code warnings on
macOS. This PR does not modify that file, so the branch was pushed after
independently running the suites above.

## Buzz context

Originating channel: `buzz-redis-cluster-mode`
(`f4e36d32-afdb-447f-8c87-ab003e069d18`)

---------

Signed-off-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
This commit is contained in:
Tyler
2026-07-30 15:26:11 -04:00
committed by GitHub
co-authored by npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr
parent 6e419b9f1c
commit bf139e8d0b
6 changed files with 32 additions and 9 deletions
+2 -2
View File
@@ -447,7 +447,7 @@ The subscriber uses a **dedicated** `redis::aio::PubSub` connection — not from
**Reconnection:** exponential backoff 1s → 30s (`backoff_secs * 2`). Backoff resets to 1s only after a clean stream end, not on each reconnect attempt.
**Presence:** `SET buzz:presence:{pubkey_hex} {status} EX 90`90-second TTL (3× the 30-second heartbeat interval). Single missed heartbeat does not cause presence flap.
**Presence:** `SET buzz:presence:{pubkey_hex} {status} EX 180`180-second TTL (3× the 60-second heartbeat interval). Single missed heartbeat does not cause presence flap.
**Typing indicators:**
```
@@ -797,7 +797,7 @@ Docker Compose provides the full local development stack. All services include h
| Pattern | Type | TTL | Purpose |
|---------|------|-----|---------|
| `buzz:channel:{uuid}` | Pub/Sub channel | — | Event fan-out (single-community form; shared multi-community Redis must use `buzz:{community}:channel:{uuid}` or equivalent) |
| `buzz:presence:{pubkey_hex}` | String | 90s | Online/away status (single-community form; shared multi-community Redis must scope by community) |
| `buzz:presence:{pubkey_hex}` | String | 180s | Online/away status (single-community form; shared multi-community Redis must scope by community) |
| `buzz:typing:{channel_uuid}` | Sorted Set | 60s | Active typers (5s window; shared multi-community Redis must scope by community) |
### Full-Text Search (Postgres FTS)
+1 -1
View File
@@ -328,7 +328,7 @@ impl PubSubManager {
publisher::publish_event(&self.pool, ctx, topic, event).await
}
/// Set presence with 60s TTL. Call on connect and every 30s heartbeat.
/// Set presence with 180s TTL. Call on connect and every 60s heartbeat.
pub async fn set_presence(
&self,
ctx: &TenantContext,
+10 -4
View File
@@ -1,7 +1,7 @@
//! Presence tracking — online/away status with TTL.
//!
//! Stored as `SET buzz:{community}:presence:{pubkey_hex} "online" EX 90`.
//! TTL is 3x the 30s heartbeat interval so a single missed heartbeat doesn't
//! Stored as `SET buzz:{community}:presence:{pubkey_hex} "online" EX 180`.
//! TTL is 3x the 60s heartbeat interval so a single missed heartbeat doesn't
//! cause presence flap. Clean disconnect deletes immediately.
use buzz_core::TenantContext;
@@ -12,8 +12,8 @@ use std::collections::HashMap;
use crate::error::PubSubError;
use crate::topic::BUZZ_PREFIX;
/// 3x the 30s heartbeat — single missed heartbeat won't cause presence flap.
pub const PRESENCE_TTL_SECS: u64 = 90;
/// 3x the 60s heartbeat — single missed heartbeat won't cause presence flap.
pub const PRESENCE_TTL_SECS: u64 = 180;
/// Returns the Redis key for the presence entry of `pubkey` under `ctx`.
pub fn presence_key(ctx: &TenantContext, pubkey: &PublicKey) -> String {
@@ -109,6 +109,12 @@ mod tests {
TenantContext::resolved(CommunityId::from_uuid(Uuid::from_u128(id)), host)
}
#[test]
fn presence_ttl_is_three_one_minute_heartbeat_windows() {
assert_eq!(PRESENCE_TTL_SECS, 180);
assert_eq!(PRESENCE_TTL_SECS, 3 * 60);
}
#[test]
fn test_presence_key_format() {
let pubkey = make_pubkey();
+2 -2
View File
@@ -11,14 +11,14 @@ import {
mergePresenceUpdate,
parseLivePresenceEvent,
presenceQueryWantsPubkey,
PRESENCE_HEARTBEAT_INTERVAL_MS,
PRESENCE_TTL_SECONDS,
resolveAutomaticPresenceStatus,
} from "@/features/presence/lib/presence";
import type { PresenceLookup, PresenceStatus } from "@/shared/api/types";
const PRESENCE_HEARTBEAT_INTERVAL_MS = 30_000;
const PRESENCE_STATUS_TICK_INTERVAL_MS = 30_000;
const PRESENCE_ACTIVITY_THROTTLE_MS = 1_000;
const PRESENCE_TTL_SECONDS = 90;
const PRESENCE_PREFERENCE_STORAGE_KEY = "buzz-presence-preference";
type PresencePreference = "auto" | "away" | "offline" | null;
@@ -5,7 +5,9 @@ import {
mergePresenceUpdate,
parseLivePresenceEvent,
presenceQueryWantsPubkey,
PRESENCE_HEARTBEAT_INTERVAL_MS,
PRESENCE_IDLE_TIMEOUT_MS,
PRESENCE_TTL_SECONDS,
resolveAutomaticPresenceStatus,
} from "./presence.ts";
@@ -13,6 +15,15 @@ const WILL = "8e39cba681211b3782d0e4483e9343719b9b7be66515252da5491f26421896b1";
const OTHER =
"44b8e82baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
test("presence heartbeat is one minute with a three-window TTL", () => {
assert.equal(PRESENCE_HEARTBEAT_INTERVAL_MS, 60_000);
assert.equal(PRESENCE_TTL_SECONDS, 180);
assert.equal(
PRESENCE_TTL_SECONDS,
3 * (PRESENCE_HEARTBEAT_INTERVAL_MS / 1000),
);
});
test("merge adds an absent pubkey going online (the core bug)", () => {
const old = {};
const next = mergePresenceUpdate(old, WILL, "online");
@@ -36,6 +36,12 @@ export function mergePresenceUpdate(
return { ...old, [pubkey]: status };
}
// Keep the local optimistic cache and relay expiry at three heartbeat windows.
// The relay owns the authoritative TTL; deploy its TTL increase before shipping
// a desktop build with a slower heartbeat.
export const PRESENCE_HEARTBEAT_INTERVAL_MS = 60_000;
export const PRESENCE_TTL_SECONDS = 3 * (PRESENCE_HEARTBEAT_INTERVAL_MS / 1000);
// Away means "human not at the machine" (Slack/Discord semantics), never
// "Buzz is not the focused window". OS-wide idle is authoritative when the
// platform exposes it; otherwise fall back to in-app activity.