Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
a84a32f
Add owner deletion admission control plane
TheSentinel454 Sep 22, 2026
acf5612
Fence archived community owner rotation
TheSentinel454 Sep 23, 2026
4432b57
Allow privileged abort at the reversible deletion boundary
TheSentinel454 Sep 25, 2026
3edd594
Reject owner deletion rows that omit provenance
TheSentinel454 Sep 25, 2026
2abeed3
Reach each owner-delete rejection guard separately
TheSentinel454 Sep 25, 2026
6d82d2b
Add typed deletion drain operator CronJob
TheSentinel454 Sep 25, 2026
1cef1ed
Cover operator-bound signing and replay on owner delete
TheSentinel454 Sep 25, 2026
99c26f9
Automate owner deletion preparation
TheSentinel454 Sep 25, 2026
d9c153f
Reject reserved deletion job pod labels
TheSentinel454 Sep 25, 2026
8ebe3b8
Record that owner deletion consent is an operator assertion
TheSentinel454 Sep 25, 2026
98f4e91
Harden owner deletion preparation coverage
TheSentinel454 Sep 25, 2026
494d574
Correct the deletion drain runbook's operational claims
TheSentinel454 Sep 25, 2026
e5b3d46
Serialize owner convergence before deletion abort
TheSentinel454 Sep 25, 2026
b02926e
Test blocked owner deletion preparation guards
Sep 28, 2026
e6714ec
Cover deletion drain chart contracts
TheSentinel454 Sep 28, 2026
1037263
Bound operator CronJob names
TheSentinel454 Sep 28, 2026
7e3e142
Clarify deletion job workload identity
TheSentinel454 Sep 28, 2026
dfd5db4
Scope video menu probes to emitted messages
TheSentinel454 Sep 28, 2026
fcd831e
Renumber owner deletion admission migration
codex Sep 28, 2026
c237fd4
Merge main and reconcile owner deletion migration assertions
Sep 28, 2026
1f1b5f4
docs: clarify community deletion abort boundary
Sep 28, 2026
8956cf8
Merge #7818 dependency refresh into #7827
codex Sep 28, 2026
b689026
Merge refreshed #7827 dependency into #7830
codex Sep 28, 2026
9f6f720
Merge main readiness changes and retain test credential annotation
Sep 28, 2026
464a4e6
Merge refreshed admission base into operator cron
Sep 28, 2026
53c78cd
Merge refreshed operator cron base into owner preparation
Sep 28, 2026
dc0a125
test(deletion): synchronize lease-loss cancellation
codex Sep 28, 2026
e56f4d4
Add typed deletion drain operator CronJob
TheSentinel454 Sep 25, 2026
0731205
Reject reserved deletion job pod labels
TheSentinel454 Sep 25, 2026
5c3af3b
Correct the deletion drain runbook's operational claims
TheSentinel454 Sep 25, 2026
d791f86
Cover deletion drain chart contracts
TheSentinel454 Sep 28, 2026
c0a392a
Bound operator CronJob names
TheSentinel454 Sep 28, 2026
cdef026
Clarify deletion job workload identity
TheSentinel454 Sep 28, 2026
38db86b
Scope video menu probes to emitted messages
TheSentinel454 Sep 28, 2026
9c175de
Merge rebased operator CronJob base without changing owner preparation
Sep 28, 2026
131ef21
test(desktop): await channel head before scroll summary check
codex Sep 28, 2026
c18281b
Merge commit '131ef21a253367db5e092fd6a392398e4b6eae93' into elrond/p…
Sep 28, 2026
5650ae7
test(desktop): record transient snapshot before fallback
codex Sep 28, 2026
d8c920c
Merge sidebar snapshot test repair from operator cron base
Sep 28, 2026
7a26420
Merge main and retain upstream scroll readiness fix
Sep 28, 2026
3c8def0
Merge main-refreshed operator cron base
Sep 28, 2026
e024135
feat: add owner deletion receipt and quota reservation
codex Sep 28, 2026
3a7f9ab
fix: route deletion receipts through writer acquisition
codex Sep 28, 2026
b50d7f8
docs: describe authoritative owner quota projection
codex Sep 28, 2026
0a4717d
test: advance migration lock expectation to 0053
codex Sep 28, 2026
340ca56
test: cover aborted owner quota reservation
codex Sep 28, 2026
546b686
test: abort quota fixture through store contract
codex Sep 28, 2026
f79fa81
Merge main into owner deletion auto-prepare
codex Sep 28, 2026
c5edf47
Merge current main into owner deletion foundation
codex Sep 29, 2026
99fa5ae
chore: reserve migration 0054 for deletion quota
codex Sep 29, 2026
3b94325
Merge owner deletion foundation and current main
codex Sep 29, 2026
bd95c4a
fix(relay): return stable deletion lifecycle conflicts
codex Sep 29, 2026
0d7798c
fix deletion admission and quota lifecycle proof
codex Sep 29, 2026
225a11c
Merge merged owner preparation foundation into relay deletion draft
codex Sep 29, 2026
37e9b68
fix(db): restore owner deletion approval authority fence
codex Sep 29, 2026
5071879
Merge current main into owner deletion relay draft
codex Sep 29, 2026
7400268
refactor(relay): make owner delete the idempotent recovery call; add …
Sep 29, 2026
c35c74f
fix(db): make the lifetime owner cap absolute and rustfmt the quota test
Sep 29, 2026
63623fe
docs(relay): state limit_reached for both owner caps and pin the can_…
Sep 29, 2026
59375c0
test(relay): pin that a changed acknowledgement version replay is uns…
Sep 29, 2026
bb59f0e
docs(relay): note ack-version dependency of resend recovery
codex Sep 29, 2026
e21151f
docs(relay): ack version is a code constant that also gates execution
codex Sep 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion crates/buzz-db/src/runtime/migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -705,12 +705,13 @@ mod postgres_tests {
let mut migrations: Vec<_> = MIGRATOR.iter().collect();
migrations.sort_by_key(|migration| migration.version);

assert_eq!(migrations.len(), 53);
assert_eq!(migrations.len(), 54);
assert_eq!(migrations[48].version, 49);
assert_eq!(migrations[49].version, 50);
assert_eq!(migrations[50].version, 51);
assert_eq!(migrations[51].version, 52);
assert_eq!(migrations[52].version, 53);
assert_eq!(migrations[53].version, 54);
assert!(migrations[48]
.sql
.as_str()
Expand All @@ -724,6 +725,10 @@ mod postgres_tests {
.sql
.as_str()
.contains("community_deletion_requests_owner_preparable"));
assert!(migrations[53]
.sql
.as_str()
.contains("community_deletion_requests_owner_quota_reservations"));
assert_eq!(migrations[0].version, 1);
assert_eq!(&*migrations[0].description, "initial schema");
assert!(migrations[0]
Expand Down Expand Up @@ -1758,6 +1763,32 @@ mod postgres_tests {
assert!(migration.contains("set local lock_timeout = '5s'"));
}

#[test]
fn owner_deletion_quota_reservation_index_matches_desired_schema() {
let migration = MIGRATOR
.iter()
.find(|migration| migration.version == 54)
.expect("embedded migration 0054")
.sql
.as_ref()
.to_ascii_lowercase();
let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(std::path::Path::parent)
.expect("workspace root");
let schema = std::fs::read_to_string(workspace_root.join("schema/schema.sql"))
.expect("read schema/schema.sql")
.to_ascii_lowercase();

for sql in [&migration, &schema] {
assert!(sql.contains("community_deletion_requests_owner_quota_reservations"));
assert!(sql.contains("request_origin = 'owner'"));
assert!(sql.contains("stage <> 'aborted'"));
assert!(sql.contains("include (community_id, completed_at)"));
}
assert!(migration.contains("set local lock_timeout = '5s'"));
}

/// Structural parity between migration 0029's deletion surface and the
/// desired-state bootstrap schema (`schema/schema.sql`).
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ async fn migration_schema_thread_window_prebuild_does_not_queue_behind_writer()
production_result.is_ok(),
"production migrator must preserve ingestion progress: {production_result:?}"
);
assert_eq!(version, 53);
assert_eq!(version, 54);
assert_eq!(final_oid, oid, "prebuild must not be replaced");
assert_eq!(count, 4, "all writer witnesses must persist");
}
218 changes: 200 additions & 18 deletions crates/buzz-db/src/store/community.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ pub enum CreateCommunityWithOwnerResult {
Created(CreatedCommunityRecord),
/// The host already belongs to another owner.
HostExists,
/// The intended owner already owns the maximum number of communities.
/// The intended owner has reached the active or lifetime community limit.
LimitReached,
}

Expand All @@ -61,6 +61,19 @@ pub struct OwnedCommunityRecord {
pub archived_at: Option<DateTime<Utc>>,
}

/// Owner-list rows plus the authoritative quota projection from one snapshot.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OwnedCommunitiesPage {
/// Visible live owner memberships, excluding communities already under deletion.
pub communities: Vec<OwnedCommunityRecord>,
/// De-duplicated live memberships and incomplete owner deletion reservations.
pub quota_used: i64,
/// Configured active limit only; the lifetime cap is reflected in `can_create`.
pub quota_limit: i64,
/// Whether the snapshot leaves room under both the active and lifetime caps.
pub can_create: bool,
}

/// Community row returned by an owner-authorized archive operation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ArchivedCommunityRecord {
Expand Down Expand Up @@ -206,13 +219,17 @@ impl Db {
pub async fn list_communities_owned_by(
&self,
owner_pubkey: &str,
) -> Result<Vec<OwnedCommunityRecord>> {
) -> Result<OwnedCommunitiesPage> {
let owner_pubkey = owner_pubkey.to_ascii_lowercase();
let mut connection = crate::observability::acquire_writer(
let connection = crate::observability::acquire_writer(
&self.pool,
crate::observability::WriterOperation::Authorization,
)
.await?;
let mut tx = sqlx::Transaction::begin(connection, None).await?;
sqlx::query("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ ONLY")
.execute(&mut *tx)
.await?;
let rows = sqlx::query(
r#"
SELECT c.id, c.host, c.created_at, c.archived_at
Expand All @@ -227,11 +244,11 @@ impl Db {
ORDER BY c.created_at ASC, c.host ASC
"#,
)
.bind(owner_pubkey)
.fetch_all(&mut *connection)
.bind(&owner_pubkey)
.fetch_all(&mut *tx)
.await?;

rows.into_iter()
let communities = rows
.into_iter()
.map(|row| {
let id: Uuid = row.try_get("id")?;
let host: String = row.try_get("host")?;
Expand All @@ -244,7 +261,15 @@ impl Db {
archived_at,
})
})
.collect()
.collect::<Result<Vec<_>>>()?;
let quota = relay_members::owner_quota_in_transaction(&mut tx, &owner_pubkey).await?;
tx.commit().await?;
Ok(OwnedCommunitiesPage {
communities,
quota_used: quota.active,
quota_limit: relay_members::max_communities_per_owner(),
can_create: quota.admits(),
})
}

/// Returns the normalized host mapped to a community id, if the community
Expand Down Expand Up @@ -450,14 +475,10 @@ impl Db {
let host: String = row.try_get("host")?;

// Enforce the limit before inserting the new owner row.
let owned_count: i64 = sqlx::query_scalar(
"SELECT count(*) FROM relay_members WHERE pubkey = $1 AND role = 'owner'",
)
.bind(&owner_pubkey)
.fetch_one(&mut *tx)
.await?;

if owned_count >= relay_members::max_communities_per_owner() {
if !relay_members::owner_quota_in_transaction(&mut tx, &owner_pubkey)
.await?
.admits()
{
tx.rollback().await?;
return Ok(CreateCommunityWithOwnerResult::LimitReached);
}
Expand Down Expand Up @@ -793,6 +814,7 @@ mod postgres_tests {
"EnsuredCommunityRecord",
"CreatedCommunityRecord",
"OwnedCommunityRecord",
"OwnedCommunitiesPage",
"ArchivedCommunityRecord",
"UnarchivedCommunityRecord",
];
Expand Down Expand Up @@ -1093,8 +1115,168 @@ mod postgres_tests {
.await
.expect("list owned communities");

assert_eq!(owned.len(), 1);
assert_eq!(owned[0].id, community_a);
assert_eq!(owned.communities.len(), 1);
assert_eq!(owned.communities[0].id, community_a);
assert_eq!(owned.quota_used, 1);
assert_eq!(
owned.quota_limit,
crate::relay_members::max_communities_per_owner()
);
assert!(owned.can_create);
}

#[tokio::test]
#[ignore = "requires Postgres"]
async fn owner_quota_serializes_concurrent_create_and_transfer_with_reservation() {
let db = setup_db().await;
let recipient = format!("{}{}", Uuid::new_v4().simple(), Uuid::new_v4().simple());
let source_owner = format!("{}{}", Uuid::new_v4().simple(), Uuid::new_v4().simple());

for index in 0..3 {
let host = format!("quota-live-{index}-{}.example", Uuid::new_v4().simple());
assert!(matches!(
db.create_community_with_owner(&host, &recipient)
.await
.expect("create live quota fixture"),
CreateCommunityWithOwnerResult::Created(_)
));
}
let reserved_host = format!("quota-held-{}.example", Uuid::new_v4().simple());
let reserved = db
.create_community_with_owner(&reserved_host, &recipient)
.await
.expect("create reservation fixture");
let CreateCommunityWithOwnerResult::Created(reserved) = reserved else {
panic!("expected reservation community")
};
sqlx::query(
"INSERT INTO community_deletion_requests \
(id, community_id, community_host, requested_by, request_origin, owner_pubkey, \
mediating_operator_pubkey, acknowledgement_version) \
VALUES ($1, $2, $3, $4, 'owner', $4, $5, 1)",
)
.bind(Uuid::new_v4())
.bind(reserved.id.as_uuid())
.bind(&reserved_host)
.bind(&recipient)
.bind("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
.execute(&db.pool)
.await
.expect("insert reservation");
sqlx::query("DELETE FROM relay_members WHERE community_id = $1")
.bind(reserved.id.as_uuid())
.execute(&db.pool)
.await
.expect("simulate purged membership");

let transfer_host = format!("quota-transfer-{}.example", Uuid::new_v4().simple());
let transfer_target = db
.create_community_with_owner(&transfer_host, &source_owner)
.await
.expect("create transfer target");
let CreateCommunityWithOwnerResult::Created(transfer_target) = transfer_target else {
panic!("expected transfer target")
};
let create_host = format!("quota-race-{}.example", Uuid::new_v4().simple());

let (create, transfer) = tokio::join!(
db.create_community_with_owner(&create_host, &recipient),
db.transfer_ownership(transfer_target.id, &recipient, &source_owner),
);
let create = create.expect("concurrent create result");
let transfer = transfer.expect("concurrent transfer result");
let create_won = matches!(create, CreateCommunityWithOwnerResult::Created(_));
let transfer_won = matches!(
transfer,
crate::relay_members::TransferResult::Transferred { .. }
);
assert_ne!(create_won, transfer_won, "exactly one owner grant may win");
assert!(
matches!(create, CreateCommunityWithOwnerResult::LimitReached) || create_won,
"create loser must observe the quota"
);
assert!(
matches!(transfer, crate::relay_members::TransferResult::LimitReached) || transfer_won,
"transfer loser must observe the quota"
);
assert_eq!(
db.list_communities_owned_by(&recipient)
.await
.expect("post-race quota")
.quota_used,
crate::relay_members::max_communities_per_owner()
);
}

#[tokio::test]
#[ignore = "requires Postgres"]
async fn completed_owner_deletions_count_toward_lifetime_cap() {
let db = setup_db().await;
let owner = format!("{:064x}", Uuid::new_v4().as_u128());
let other_owner = format!("{:064x}", Uuid::new_v4().as_u128());
let operator = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";

// Create and completely delete up to the lifetime cap: each tombstone
// frees its active slot but keeps its host and its lifetime count.
for i in 0..crate::relay_members::MAX_LIFETIME_COMMUNITIES_PER_OWNER {
let host = format!("lifetime-{i}-{}.example", Uuid::new_v4().simple());
let CreateCommunityWithOwnerResult::Created(record) = db
.create_community_with_owner(&host, &owner)
.await
.expect("create under lifetime cap")
else {
panic!("create {i} must succeed below the lifetime cap")
};
sqlx::query(
"INSERT INTO community_deletion_requests \
(id, community_id, community_host, requested_by, request_origin, owner_pubkey, \
mediating_operator_pubkey, acknowledgement_version, stage, completed_at) \
VALUES ($1, $2, $3, $4, 'owner', $4, $5, 1, 'retention_pending', now())",
)
.bind(Uuid::new_v4())
.bind(record.id.as_uuid())
.bind(&host)
.bind(&owner)
.bind(operator)
.execute(&db.pool)
.await
.expect("insert completed owner deletion");
sqlx::query("DELETE FROM relay_members WHERE community_id = $1")
.bind(record.id.as_uuid())
.execute(&db.pool)
.await
.expect("simulate purged membership");
}

let page = db
.list_communities_owned_by(&owner)
.await
.expect("owner list at lifetime cap");
assert_eq!(page.quota_used, 0, "completed deletions free active slots");
assert!(!page.can_create, "the lifetime cap still blocks creation");

let host = format!("lifetime-overflow-{}.example", Uuid::new_v4().simple());
assert_eq!(
db.create_community_with_owner(&host, &owner)
.await
.expect("create past lifetime cap"),
CreateCommunityWithOwnerResult::LimitReached
);

let transfer_host = format!("lifetime-transfer-{}.example", Uuid::new_v4().simple());
let CreateCommunityWithOwnerResult::Created(target) = db
.create_community_with_owner(&transfer_host, &other_owner)
.await
.expect("create transfer target")
else {
panic!("expected transfer target")
};
assert_eq!(
db.transfer_ownership(target.id, &owner, &other_owner)
.await
.expect("transfer past lifetime cap"),
crate::relay_members::TransferResult::LimitReached
);
}

#[tokio::test]
Expand Down
Loading
Loading