mirror of
https://github.com/block/buzz.git
synced 2026-11-13 21:01:53 +00:00
Pinky, an AI agent, is updating this PR on Wes's behalf. ## Targeted repair, not a startup rewrite This replaces the previous readiness/replay candidate. Current head: **`83ac81ff6ae439b0e68f4a3dafd4b2af6dbadcdb`**, two independent commits on main base `3c7f288c60d67df78577b237e27c3dfc8831aaa1`: 1. **`0b6681de2` — keep quota backoff on its transport.** HTTP 429s retain native HTTP backoff without arming the renderer WebSocket gate. WebSocket quota/concurrency CLOSED retains its retry deadline without damping HTTP's separate quota. The explicit `rate-limited: shared admission unavailable` signal still damps HTTP too. Reuse existing all-channel live streams for Home mention invalidation instead of a second per-channel mention subscription; retain kind, recipient, self, membership, replay-deduplication, notification and unread boundaries. 2. **`83ac81ff6` — reuse complete membership rosters within each channel fetch.** Discovery already returns full paginated rosters. Retain those results and request only uncovered directory/pending-owner channel IDs. Skip empty fallback filters and preserve covered counts/pubkeys if that tolerant fallback fails. The next fetch reads current membership again; there is no persistent cache or new invalidation state. No send-route, AUTH-readiness, replay-ordering, scheduler, pacing, relay policy, dependency, or authorization changes. Refresh triggers and activity batching stay unchanged. Independent tracing did not establish a safe-to-delete duplicate normal-startup refresh; recovery invalidations at different replay stages are not interchangeable. **Size:** 18 files, +1,066/-350 overall; nine production paths +76/-205 (**129 fewer production lines**). The roster follow-up itself touches only `fetch.rs` (+31/-14) and its new 378-line regression file. Test bulk is not new startup machinery. ## Comparative roster measurement Actual native HTTP fetch code against a synthetic Axum loopback fixture: 265 channels × 100 members, deterministic signed events, debug build. Seven iterations per condition; medians below exclude the first warm-up. Baseline is `0b6681de2` production fetch, candidate is `83ac81ff6` production fetch, with the same temporary benchmark harness. | Measure | Baseline | Candidate | | --- | ---: | ---: | | HTTP reads per fetch | 7 | 6 | | JSON response-body bytes per fetch | 4,803,136 | 2,453,380 | | Median, no artificial request delay | 262.9 ms | 149.3 ms | | Median, 25 ms artificial delay per request | 376.7 ms | 282.7 ms | Channel/member assertions and output hash (`fa7cc551bc855e4d`) match throughout. This removes **one batched roster request**, not 265 requests. The measured ~43%/~25% reductions describe this large-roster synthetic fixture, **not production startup latency**. Activity requests run concurrently with roster enrichment, so real savings vary. Counts now use the discovery snapshot rather than a later reread within that same non-atomic fetch. ## Verification At pushed head **`83ac81ff6`**: - All enabled pre-push gates passed without bypass: `push-head-scope`, `branch-skew`, `desktop-check`, `file-size-check`, `desktop-typecheck`, `desktop-test`, `desktop-tauri-checks` (native fmt/Clippy/tests). - Full Desktop frontend suite: **6,457 passed**, zero failures; separate tooling suite: **10 passed**. - Full native Desktop workspace: **3,276 passed**, zero failures, 20 ignored, across unit/integration/doc bins. The main native lib accounts for 3,175 passed / 19 ignored; this is not 3,276 lib tests. - `pnpm build` passed the protected-feature artifact matrix; `pnpm build:e2e` also passed. Existing chunk-size/dynamic-import warnings remain. No full repository `just ci` run locally. - **130 selected headless mock-bridge smoke cases passed across two runs**: messaging, reconnect, reconnect affordances, inbox live updates, sidebar snapshots, send-channel binding, boot splash. The first invocation hit the shell's 300-second limit after 117 passes and no failures; all remaining 13 passed in a separate invocation. This is not a claim the interrupted invocation completed. - Six new tests invoke production `fetch_channels` through the real native HTTP client and cover 265/501-channel pagination (including same-timestamp continuation), exact requests, complete/deduplicated rosters, directory/pending fallback, hidden DMs, repeated membership/identity/relay changes, empty lists and errors. - Falsification in an isolated source copy: restoring the redundant baseline fetch fails four roster regressions; removing missing-roster fallback fails two. Restored candidate passes all six. Earlier transport mutations fail HTTP→WS coupling, WS→HTTP coupling, omitted shared-outage damping and duplicate-mention regressions. No mutation was applied to the committed worktree. - Brain's independent static reviews found no blockers in the transport repair or immutable roster patch. Source tracing confirms relay discovery serializes complete rosters, not projected matching p-tags. This was source review, not independent test execution or approval. ### Hosted CI [CI run 34185670504](https://github.com/block/buzz/actions/runs/34185670504) **completed successfully** for this head. GitHub checked out merge ref `25bcf09627e129887481d4441a1af3c6cb0af1ff`; its Git tree is identical to `83ac81ff6` (`49a01f3c2ebf4e547bbe3cd31406f984ac6cefe8`), verified through the GitHub Git API. The final PR rollup has 33 successful checks and 28 skipped, with none failing or pending. - Desktop Core passed frontend/native suites and compiled-feature verification. Windows Rust tests, Windows Desktop build, macOS `.app`/`.dmg` build, all four smoke shards and both relay-backed E2E job shards passed, as did Rust lint, DCO, Semgrep and zizmor. - Browser job totals: **1,620 passed, 13 flaky cases passing automatic retry #1, five skipped, zero terminal failures**. Smoke accounts for 1,389 passed / 12 flaky / three skipped; integration jobs report 231 passed / one flaky / two skipped. No manual CI reruns or source changes to obtain green. - The retries cover channel-browser, empty-edit, huddle menu, runtime-install, overscroll, onboarding avatar, video attachment and workflow UI assertions. They are disclosed, not collectively declared unrelated. The video case reaches the changed real TS all-channel hook despite mocking native I/O: independent tracing found no regression mechanism or blocker, but the missing second-video player remains **unexplained**, and an indirect timing effect is not excluded. The self-authored, unmentioned event cannot use either the new mention branch or the removed old mention handler as a delivery path. A passing retry does not diagnose its cause. - The separate “Desktop Release Candidate” check skipped its validation step; it is not additional release validation. Codex security review was skipped, not passed. ## Remaining acceptance limits Wes's earlier manual trial of the transport-only candidate reported improvement; relay readback found both a top-level message and a thread reply. That was not a fingerprinted or timed trial of the new roster commit. The remaining few-second top-level send wait is **unattributed**, and this PR does not claim to resolve every startup delay. No visible app launch, relaunch, laptop input automation, or production test messages during this follow-up. Local native tests use fixtures; browser suites do not run the Tauri backend. CI building an app does not validate bundled macOS alerts or inactive-channel notification delivery. Those bundled real-workflow checks remain outstanding. No visual UI changes or screenshots; no approval or merge performed. Originating conversation: buzz://message?channel=5b130804-d759-40ad-a564-d64cc907fa8e&id=cb432417e290e06a1842639cfe0268f9d43d186defbcb59260930d129a26af83 --------- Signed-off-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz> Co-authored-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
170 lines
5.9 KiB
Rust
170 lines
5.9 KiB
Rust
//! Real persistent-WS receive loop -> real HTTP submit admission regressions.
|
|
use super::*;
|
|
use crate::relay_admission::{reset_rate_limit_gate, TEST_SERIAL};
|
|
use axum::{routing::post, Json, Router};
|
|
|
|
async fn http_relay() -> (
|
|
String,
|
|
mpsc::Receiver<(nostr::Event, std::time::Instant)>,
|
|
tokio::task::JoinHandle<()>,
|
|
) {
|
|
let (sent, received) = mpsc::channel(4);
|
|
let router = Router::new()
|
|
.route(
|
|
"/query",
|
|
post(|| async {
|
|
(
|
|
axum::http::StatusCode::TOO_MANY_REQUESTS,
|
|
Json(serde_json::json!({"error": "rate-limited: quota exceeded; retry in 1s"})),
|
|
)
|
|
}),
|
|
)
|
|
.route(
|
|
"/events",
|
|
post(move |Json(event): Json<nostr::Event>| {
|
|
let sent = sent.clone();
|
|
async move {
|
|
let received_at = std::time::Instant::now();
|
|
let id = event.id.to_hex();
|
|
assert!(event.verify().is_ok());
|
|
sent.send((event, received_at)).await.unwrap();
|
|
Json(serde_json::json!({"event_id": id, "accepted": true, "message": ""}))
|
|
}
|
|
}),
|
|
);
|
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let address = listener.local_addr().unwrap();
|
|
let server = tokio::spawn(async move {
|
|
axum::serve(listener, router).await.unwrap();
|
|
});
|
|
(format!("http://{address}"), received, server)
|
|
}
|
|
|
|
fn reply(keys: &Keys) -> nostr::Event {
|
|
EventBuilder::new(nostr::Kind::Custom(9), "startup reply")
|
|
.tags([
|
|
nostr::Tag::parse(["h", "5b130804-d759-40ad-a564-d64cc907fa8e"]).unwrap(),
|
|
nostr::Tag::parse(["e", &"a".repeat(64), "", "reply"]).unwrap(),
|
|
])
|
|
.sign_with_keys(keys)
|
|
.unwrap()
|
|
}
|
|
|
|
async fn closed_then_http_submit(message: &str, shared_unavailable: bool) {
|
|
let _serial = TEST_SERIAL.lock().await;
|
|
reset_rate_limit_gate();
|
|
let (ws_url, mut frames, commands) = stub_relay().await;
|
|
let keys = Keys::generate();
|
|
let (session, mut events) = start(ws_url, keys.clone(), None).await;
|
|
session
|
|
.set_subscriptions(vec![
|
|
probe_subscription(),
|
|
Subscription {
|
|
id: "barrier".into(),
|
|
filter: serde_json::json!({"kinds": [1], "limit": 0}),
|
|
},
|
|
])
|
|
.await;
|
|
assert_eq!(next_req(&mut frames, "probe REQ").await, PROBE_ID);
|
|
assert_eq!(next_req(&mut frames, "barrier REQ").await, "barrier");
|
|
commands
|
|
.send(StubCommand::Closed(PROBE_ID.into(), message.into()))
|
|
.await
|
|
.unwrap();
|
|
// An ordered frame on an unaffected persistent subscription proves CLOSED
|
|
// went through the receive loop. No test-side gate activation or sleeps.
|
|
let barrier = EventBuilder::text_note("barrier")
|
|
.sign_with_keys(&keys)
|
|
.unwrap();
|
|
commands
|
|
.send(StubCommand::Event(
|
|
"barrier".into(),
|
|
serde_json::to_value(barrier).unwrap(),
|
|
))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
tokio::time::timeout(Duration::from_secs(3), events.recv())
|
|
.await
|
|
.unwrap()
|
|
.unwrap()
|
|
.subscription_id,
|
|
"barrier"
|
|
);
|
|
|
|
let (http_url, mut submitted, server) = http_relay().await;
|
|
let state = crate::app_state::build_app_state();
|
|
let event = reply(&keys);
|
|
let submit = crate::relay::submit_signed_event_at_with_keys(&event, &state, &http_url, &keys);
|
|
let outcome = tokio::time::timeout(Duration::from_secs(1), submit).await;
|
|
session.shutdown();
|
|
server.abort();
|
|
reset_rate_limit_gate();
|
|
if shared_unavailable {
|
|
assert!(
|
|
outcome.is_err(),
|
|
"shared admission outage must still damp HTTP"
|
|
);
|
|
assert!(
|
|
submitted.try_recv().is_err(),
|
|
"HTTP must not dispatch during the shared outage"
|
|
);
|
|
} else {
|
|
let response = outcome
|
|
.expect("WS quota must not withhold HTTP submission")
|
|
.unwrap();
|
|
assert!(response.accepted);
|
|
assert_eq!(response.event_id, event.id.to_hex());
|
|
assert_eq!(submitted.recv().await.unwrap().0, event);
|
|
}
|
|
assert!(
|
|
frames.try_recv().is_err(),
|
|
"the limited WS subscription must not reopen early"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn persistent_ws_quota_does_not_withhold_http_reply() {
|
|
closed_then_http_submit("rate-limited: quota exceeded; retry in 50s", false).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn persistent_ws_concurrency_does_not_withhold_http_reply() {
|
|
closed_then_http_submit("rate-limited: too many concurrent requests", false).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn persistent_ws_shared_unavailable_still_withholds_http_reply() {
|
|
closed_then_http_submit("rate-limited: shared admission unavailable", true).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn http_429_still_withholds_http_reply_then_accepts_it() {
|
|
let _serial = TEST_SERIAL.lock().await;
|
|
reset_rate_limit_gate();
|
|
let (http_url, mut submitted, server) = http_relay().await;
|
|
let state = crate::app_state::build_app_state();
|
|
let keys = Keys::generate();
|
|
let event = reply(&keys);
|
|
let error = crate::relay::query_relay_at(&state, &http_url, &[serde_json::json!({"limit": 1})])
|
|
.await
|
|
.unwrap_err();
|
|
assert_eq!(error, "relay rate-limited: retry in 1s");
|
|
let before = std::time::Instant::now();
|
|
let outcome = tokio::time::timeout(
|
|
Duration::from_secs(3),
|
|
crate::relay::submit_signed_event_at_with_keys(&event, &state, &http_url, &keys),
|
|
)
|
|
.await;
|
|
server.abort();
|
|
reset_rate_limit_gate();
|
|
let response = outcome.unwrap().unwrap();
|
|
let (received, received_at) = submitted.recv().await.unwrap();
|
|
assert!(
|
|
received_at.duration_since(before) >= Duration::from_millis(900),
|
|
"HTTP submit must honour its own cooldown"
|
|
);
|
|
assert!(response.accepted);
|
|
assert_eq!(received, event);
|
|
}
|