mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(relay): make per-owner community limit configurable via BUZZ_MAX_COMMUNITIES_PER_OWNER (#2599)
Closes #2600 ## Summary Self-hosted multi-tenant deployments (one relay serving many communities via host-based tenancy) routinely need more than three communities owned by the same operator identity. `MAX_COMMUNITIES_PER_OWNER` is currently a hardcoded const, and hitting it surfaces as a `limit_reached` 409 from `POST /operator/communities` — which provisioning UIs tend to mislabel (mine reported it as "subdomain already taken"). This makes the limit configurable per deployment: - New env var `BUZZ_MAX_COMMUNITIES_PER_OWNER` — read once per process, must parse as a positive integer; missing/invalid/non-positive values fall back to the existing default of **3**, so current deployments are unaffected. - Enforcement locations are unchanged and stay in the authoritative relay-layer checks: community provisioning (`create_community_with_owner`) and ownership transfer (inside the advisory-lock transaction). - Parse/fallback rules are extracted into a pure helper (`effective_owner_limit`) with unit tests, keeping the cached getter trivial. ## Test plan - `cargo test -p buzz-db --lib` — new `owner_limit_*` tests cover default, invalid, non-positive, and positive-override cases. (Pre-existing unrelated failure on clean main: `replica_fence::tests::fence_starts_closed_and_opens_on_advance`, tracked in #2369.) - `cargo clippy -p buzz-db --all-targets` and `cargo fmt` clean. - Deployed on my multi-tenant relay (vibecode.casa) with `BUZZ_MAX_COMMUNITIES_PER_OWNER=100`: provisioning a 4th community for the same owner succeeds; without the var the stock limit of 3 still applies. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Lee Salminen <leesalminen@gmail.com>
This commit is contained in:
@@ -898,7 +898,7 @@ impl Db {
|
|||||||
.fetch_one(&mut *tx)
|
.fetch_one(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if owned_count >= relay_members::MAX_COMMUNITIES_PER_OWNER {
|
if owned_count >= relay_members::max_communities_per_owner() {
|
||||||
tx.rollback().await?;
|
tx.rollback().await?;
|
||||||
return Ok(CreateCommunityWithOwnerResult::LimitReached);
|
return Ok(CreateCommunityWithOwnerResult::LimitReached);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -373,11 +373,37 @@ pub enum TransferResult {
|
|||||||
LimitReached,
|
LimitReached,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Maximum number of communities a single pubkey can own. Enforced at the
|
/// Default maximum number of communities a single pubkey can own. Enforced at
|
||||||
/// relay layer — the authoritative layer — so that concurrent transfers or
|
/// the relay layer — the authoritative layer — so that concurrent transfers or
|
||||||
/// transfer-vs-create races cannot both pass a preflight count.
|
/// transfer-vs-create races cannot both pass a preflight count.
|
||||||
pub const MAX_COMMUNITIES_PER_OWNER: i64 = 3;
|
pub const MAX_COMMUNITIES_PER_OWNER: i64 = 3;
|
||||||
|
|
||||||
|
/// Effective per-owner community limit for this deployment.
|
||||||
|
///
|
||||||
|
/// Reads `BUZZ_MAX_COMMUNITIES_PER_OWNER` once (cached for the process
|
||||||
|
/// lifetime); a missing, unparsable, or non-positive value falls back to
|
||||||
|
/// [`MAX_COMMUNITIES_PER_OWNER`]. Lets multi-tenant operators raise the cap
|
||||||
|
/// without a source change while keeping the stock default for everyone else.
|
||||||
|
pub fn max_communities_per_owner() -> i64 {
|
||||||
|
static LIMIT: std::sync::OnceLock<i64> = std::sync::OnceLock::new();
|
||||||
|
*LIMIT.get_or_init(|| {
|
||||||
|
effective_owner_limit(
|
||||||
|
std::env::var("BUZZ_MAX_COMMUNITIES_PER_OWNER")
|
||||||
|
.ok()
|
||||||
|
.as_deref(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pure resolution of the owner limit from a raw env value — extracted from
|
||||||
|
/// [`max_communities_per_owner`] so the parse/fallback rules are testable
|
||||||
|
/// without process-global env state.
|
||||||
|
fn effective_owner_limit(raw: Option<&str>) -> i64 {
|
||||||
|
raw.and_then(|value| value.trim().parse::<i64>().ok())
|
||||||
|
.filter(|limit| *limit > 0)
|
||||||
|
.unwrap_or(MAX_COMMUNITIES_PER_OWNER)
|
||||||
|
}
|
||||||
|
|
||||||
/// Stable advisory-lock key for serializing ownership-granting operations
|
/// Stable advisory-lock key for serializing ownership-granting operations
|
||||||
/// (transfer + create) per recipient pubkey. Uses FNV-1a over the hex pubkey
|
/// (transfer + create) per recipient pubkey. Uses FNV-1a over the hex pubkey
|
||||||
/// so the same recipient always maps to the same lock across processes.
|
/// so the same recipient always maps to the same lock across processes.
|
||||||
@@ -472,7 +498,7 @@ pub async fn transfer_ownership(
|
|||||||
.fetch_one(&mut *tx)
|
.fetch_one(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if owned_count >= MAX_COMMUNITIES_PER_OWNER {
|
if owned_count >= max_communities_per_owner() {
|
||||||
tx.rollback().await?;
|
tx.rollback().await?;
|
||||||
return Ok(TransferResult::LimitReached);
|
return Ok(TransferResult::LimitReached);
|
||||||
}
|
}
|
||||||
@@ -555,6 +581,32 @@ pub async fn backfill_from_allowlist(pool: &PgPool, community: CommunityId) -> R
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn owner_limit_defaults_when_unset_or_invalid() {
|
||||||
|
assert_eq!(
|
||||||
|
super::effective_owner_limit(None),
|
||||||
|
super::MAX_COMMUNITIES_PER_OWNER
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
super::effective_owner_limit(Some("not-a-number")),
|
||||||
|
super::MAX_COMMUNITIES_PER_OWNER
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
super::effective_owner_limit(Some("0")),
|
||||||
|
super::MAX_COMMUNITIES_PER_OWNER
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
super::effective_owner_limit(Some("-5")),
|
||||||
|
super::MAX_COMMUNITIES_PER_OWNER
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn owner_limit_honors_positive_override() {
|
||||||
|
assert_eq!(super::effective_owner_limit(Some("100")), 100);
|
||||||
|
assert_eq!(super::effective_owner_limit(Some(" 12 ")), 12);
|
||||||
|
}
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user