mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
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:
co-authored by
npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr
parent
6e419b9f1c
commit
bf139e8d0b
+2
-2
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user