block/buzz · critical · anyhow::Error

Search DB connection failed: {e}

Error message

Search DB connection failed: {e}

What it means

The search service (Postgres FTS) opens its own pool, preferring READ_DATABASE_URL when set and falling back to DATABASE_URL. If that URL cannot be connected at boot, startup fails — even though search queries the same Postgres rows, an unreachable reader endpoint is treated as fatal rather than degrading search.

Source

Thrown at crates/buzz-relay/src/main.rs:415

    // sockets (via the consumer loop below), enforcing live disconnect fan-out.
    let pubsub_for_conn_ctrl = Arc::clone(&pubsub);
    tokio::spawn(async move { pubsub_for_conn_ctrl.run_conn_control_subscriber().await });

    let auth = AuthService::new(config.auth.clone());

    // Postgres FTS: the searchable row IS the persisted event row (its
    // `tsvector` column is populated by the `insert_event` write), so there is
    // no external collection to provision — the search service just queries the
    // same Postgres over its own pool. Search is lag-tolerant, so it prefers
    // the read replica when one is configured.
    let search_db_url = config
        .read_database_url
        .as_deref()
        .unwrap_or(&config.database_url);
    let search_pool = sqlx::postgres::PgPoolOptions::new()
        .connect(search_db_url)
        .await
        .map_err(|e| anyhow::anyhow!("Search DB connection failed: {e}"))?;
    let search = SearchService::new(search_pool);
    info!(
        replica = config.read_database_url.is_some(),
        "Search service ready (Postgres FTS)"
    );

    let workflow_config = buzz_workflow::WorkflowConfig::default();
    let workflow_engine = Arc::new(WorkflowEngine::new(db.clone(), workflow_config));

    let relay_keypair = if let Some(hex) = &config.relay_private_key {
        nostr::Keys::parse(hex)
            .map_err(|e| anyhow::anyhow!("invalid BUZZ_RELAY_PRIVATE_KEY: {e}"))?
    } else if !config.require_auth_token {
        // Dev mode: use a deterministic keypair so addressable events (kind:39000/39001/39002)
        // replace correctly across restarts. Without this, each restart generates a new pubkey
        // and replace_addressable_event inserts duplicates instead of replacing.
        const DEV_RELAY_PRIVKEY: &str =
            "0000000000000000000000000000000000000000000000000000000000000001";

View on GitHub (pinned to f956e6fe06)

Solutions

  1. Test the reader endpoint: `psql "$READ_DATABASE_URL" -c 'select 1'`
  2. Fix or remove READ_DATABASE_URL — unsetting it falls back to the writer
  3. Verify DNS and security groups for the replica

Example fix

# before
READ_DATABASE_URL=postgres://reader.internal:5432/buzz   # replica gone

# after
READ_DATABASE_URL=   # unset: search falls back to the writer
Defensive patterns

Strategy: retry

Validate before calling

# If a reader is configured, it must be connectable — else unset it.
if [ -n "${READ_DATABASE_URL:-}" ]; then
  psql "$READ_DATABASE_URL" -c 'select 1' || { echo 'READ_DATABASE_URL unreachable'; exit 1; }
fi

Try / catch

relay:
  restart: on-failure:5

Prevention

When it happens

Trigger: READ_DATABASE_URL points to a replica that is unreachable, firewalled, has invalid credentials, or was decommissioned; or DATABASE_URL itself is bad (same causes as the main pool failure).

Common situations: Replica removed or renamed but the env not updated; cross-VPC reader with network policy changes; staging copying prod env with a stale reader endpoint.

Related errors


AI-assisted analysis of block/buzz@f956e6fe06 (2026-08-16). Data as JSON: /api/errors/39a52ac8c3f3c0ab. Report an issue: GitHub.