mirror of
https://github.com/block/buzz.git
synced 2026-10-29 05:21:31 +00:00
merge relay deletion review corrections
* private/community-delete-relay-followup: fix deletion admission and quota lifecycle proof Signed-off-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -1127,122 +1127,6 @@ mod postgres_tests {
|
||||
assert!(owned.can_create);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn owner_quota_reservation_survives_purge_stages_and_releases_terminally() {
|
||||
let db = setup_db().await;
|
||||
let owner = format!("{}{}", Uuid::new_v4().simple(), Uuid::new_v4().simple());
|
||||
let host = format!("quota-reservation-{}.example", Uuid::new_v4().simple());
|
||||
let created = db
|
||||
.create_community_with_owner(&host, &owner)
|
||||
.await
|
||||
.expect("create owned community");
|
||||
let CreateCommunityWithOwnerResult::Created(created) = created else {
|
||||
panic!("expected created community")
|
||||
};
|
||||
let mut request_id = Uuid::new_v4();
|
||||
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(request_id)
|
||||
.bind(created.id.as_uuid())
|
||||
.bind(&host)
|
||||
.bind(&owner)
|
||||
.bind("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||||
.execute(&db.pool)
|
||||
.await
|
||||
.expect("insert owner deletion request");
|
||||
|
||||
let pre_purge = db
|
||||
.list_communities_owned_by(&owner)
|
||||
.await
|
||||
.expect("pre-purge quota");
|
||||
assert!(
|
||||
pre_purge.communities.is_empty(),
|
||||
"pending row is suppressed"
|
||||
);
|
||||
assert_eq!(
|
||||
pre_purge.quota_used, 1,
|
||||
"membership and request deduplicate"
|
||||
);
|
||||
|
||||
db.deletion_store()
|
||||
.abort(
|
||||
request_id,
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"quota reservation test abort",
|
||||
)
|
||||
.await
|
||||
.expect("abort quota fixture");
|
||||
let aborted = db
|
||||
.list_communities_owned_by(&owner)
|
||||
.await
|
||||
.expect("aborted quota");
|
||||
assert_eq!(aborted.communities.len(), 1, "abort restores the live row");
|
||||
assert_eq!(
|
||||
aborted.quota_used, 1,
|
||||
"abort must fall back to the preserved membership"
|
||||
);
|
||||
request_id = Uuid::new_v4();
|
||||
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(request_id)
|
||||
.bind(created.id.as_uuid())
|
||||
.bind(&host)
|
||||
.bind(&owner)
|
||||
.bind("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||||
.execute(&db.pool)
|
||||
.await
|
||||
.expect("insert replacement pending quota fixture");
|
||||
|
||||
sqlx::query("DELETE FROM relay_members WHERE community_id = $1")
|
||||
.bind(created.id.as_uuid())
|
||||
.execute(&db.pool)
|
||||
.await
|
||||
.expect("simulate membership purge");
|
||||
for stage in ["postgres_purged", "cache_purged", "logically_verified"] {
|
||||
sqlx::query(
|
||||
"UPDATE community_deletion_requests SET stage = $2, completed_at = NULL WHERE id = $1",
|
||||
)
|
||||
.bind(request_id)
|
||||
.bind(stage)
|
||||
.execute(&db.pool)
|
||||
.await
|
||||
.expect("advance quota fixture");
|
||||
assert_eq!(
|
||||
db.list_communities_owned_by(&owner)
|
||||
.await
|
||||
.expect("reserved quota")
|
||||
.quota_used,
|
||||
1,
|
||||
"{stage} must retain the slot"
|
||||
);
|
||||
}
|
||||
|
||||
sqlx::query(
|
||||
"UPDATE community_deletion_requests \
|
||||
SET stage = 'retention_pending', completed_at = now() WHERE id = $1",
|
||||
)
|
||||
.bind(request_id)
|
||||
.execute(&db.pool)
|
||||
.await
|
||||
.expect("complete quota fixture");
|
||||
assert_eq!(
|
||||
db.list_communities_owned_by(&owner)
|
||||
.await
|
||||
.expect("released quota")
|
||||
.quota_used,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn owner_quota_serializes_concurrent_create_and_transfer_with_reservation() {
|
||||
|
||||
@@ -5349,12 +5349,28 @@ mod postgres_tests {
|
||||
async fn owner_preparation_atomically_approves_exact_inventory_and_converges() {
|
||||
let (db, store) = store().await;
|
||||
let (host, owner, community) = archived_owned_community(&db).await;
|
||||
assert_eq!(
|
||||
db.list_communities_owned_by(&owner)
|
||||
.await
|
||||
.expect("initial owner quota")
|
||||
.quota_used,
|
||||
1
|
||||
);
|
||||
let operator = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
|
||||
let request_id = Uuid::new_v4();
|
||||
store
|
||||
.admit_owner_request(&host, &owner, operator, 1, request_id)
|
||||
.await
|
||||
.expect("admit owner request");
|
||||
let pending_quota = db
|
||||
.list_communities_owned_by(&owner)
|
||||
.await
|
||||
.expect("pending owner quota");
|
||||
assert!(pending_quota.communities.is_empty());
|
||||
assert_eq!(
|
||||
pending_quota.quota_used, 1,
|
||||
"membership and incomplete request must deduplicate"
|
||||
);
|
||||
let claim = store
|
||||
.claim_specific_owner_submission(request_id, "preparer", DEFAULT_LEASE_DURATION)
|
||||
.await
|
||||
@@ -5419,6 +5435,73 @@ mod postgres_tests {
|
||||
.complete_owner_preparation(&claim.lease, &changed)
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
store.begin_quiescing(&claim.lease).await.expect("quiesce");
|
||||
let generation = store.fence(&claim.lease).await.expect("fence");
|
||||
let token = LeaseToken {
|
||||
fence_generation: Some(generation),
|
||||
..claim.lease
|
||||
};
|
||||
store
|
||||
.freeze_destructive_storage_manifest(&token, &inventory.storage)
|
||||
.await
|
||||
.expect("freeze destructive storage");
|
||||
store.mark_drained(&token).await.expect("drain");
|
||||
store
|
||||
.mark_bindings_removed(&token, serde_json::json!({"keys": 0}))
|
||||
.await
|
||||
.expect("bindings");
|
||||
store.purge_postgres(&token).await.expect("purge postgres");
|
||||
assert_eq!(
|
||||
db.list_communities_owned_by(&owner)
|
||||
.await
|
||||
.expect("quota after membership purge")
|
||||
.quota_used,
|
||||
1,
|
||||
"the incomplete owner request must reserve the slot after membership purge"
|
||||
);
|
||||
store
|
||||
.mark_cache_purged(&token, serde_json::json!({"keys": 0}))
|
||||
.await
|
||||
.expect("cache");
|
||||
store
|
||||
.verify_postgres_logically_deleted(&token)
|
||||
.await
|
||||
.expect("logical postgres verify");
|
||||
store
|
||||
.mark_logically_verified(&token, serde_json::json!({"all": true}))
|
||||
.await
|
||||
.expect("mark verified");
|
||||
assert_eq!(
|
||||
db.list_communities_owned_by(&owner)
|
||||
.await
|
||||
.expect("quota before logical completion")
|
||||
.quota_used,
|
||||
1
|
||||
);
|
||||
store
|
||||
.mark_retention_pending(&token, serde_json::json!({"shared_cas": "retained"}))
|
||||
.await
|
||||
.expect("production terminal transition");
|
||||
assert_eq!(
|
||||
db.list_communities_owned_by(&owner)
|
||||
.await
|
||||
.expect("quota after logical completion")
|
||||
.quota_used,
|
||||
0,
|
||||
"the production logical-completion transition releases the slot"
|
||||
);
|
||||
assert!(db
|
||||
.lookup_community_by_host_for_management(&host)
|
||||
.await
|
||||
.expect("permanent tombstone lookup")
|
||||
.is_some());
|
||||
assert_eq!(
|
||||
db.create_community_with_owner(&host, &owner)
|
||||
.await
|
||||
.expect("recreate tombstoned host"),
|
||||
CreateCommunityWithOwnerResult::HostExists
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -492,6 +492,13 @@ pub async fn delete_community(
|
||||
})?;
|
||||
let normalized_host = normalize_candidate_host(&request.host)
|
||||
.map_err(|msg| deletion_api_error(StatusCode::BAD_REQUEST, "invalid_request", &msg))?;
|
||||
if normalized_host != request.host {
|
||||
return Err(deletion_api_error(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"invalid_request",
|
||||
"host must use its exact canonical authority spelling",
|
||||
));
|
||||
}
|
||||
let deployment_host = buzz_core::tenant::relay_url_authority(&state.config.relay_url);
|
||||
if normalized_host == deployment_host {
|
||||
return Err(deletion_api_error(
|
||||
@@ -1219,6 +1226,69 @@ mod postgres_tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn owner_delete_admission_requires_exact_canonical_host_without_mutation() {
|
||||
let operator = Keys::generate();
|
||||
let owner = Keys::generate();
|
||||
let Some(state) = operator_test_state(std::slice::from_ref(&operator)).await else {
|
||||
return;
|
||||
};
|
||||
let host = format!("community-{}.example", Uuid::new_v4().simple());
|
||||
assert_eq!(
|
||||
provision_community(Arc::clone(&state), &operator, &host, &owner)
|
||||
.await
|
||||
.status(),
|
||||
StatusCode::OK
|
||||
);
|
||||
archive_for_owner_deletion(&state, &host, &owner).await;
|
||||
|
||||
for (label, repaired) in [
|
||||
("whitespace", format!(" {host}")),
|
||||
("case", host.to_uppercase()),
|
||||
("url", format!("https://{host}")),
|
||||
] {
|
||||
let request_id = Uuid::new_v4();
|
||||
let response = signed_operator_request(
|
||||
Arc::clone(&state),
|
||||
&operator,
|
||||
"POST",
|
||||
"/operator/communities/delete",
|
||||
Some(owner_delete_body(&repaired, &owner, request_id)),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST, "{label}");
|
||||
assert_eq!(read_json(response).await["code"], "invalid_request");
|
||||
assert_no_persisted_request(&state, request_id, label).await;
|
||||
}
|
||||
|
||||
let request_id = Uuid::new_v4();
|
||||
let body = owner_delete_body(&host, &owner, request_id);
|
||||
let accepted = signed_operator_request(
|
||||
Arc::clone(&state),
|
||||
&operator,
|
||||
"POST",
|
||||
"/operator/communities/delete",
|
||||
Some(body.clone()),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(accepted.status(), StatusCode::ACCEPTED);
|
||||
assert_eq!(read_json(accepted).await["host"], host);
|
||||
|
||||
let receipt = signed_operator_request(
|
||||
Arc::clone(&state),
|
||||
&operator,
|
||||
"POST",
|
||||
"/operator/communities/delete/receipt",
|
||||
Some(body),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(receipt.status(), StatusCode::OK);
|
||||
let receipt = read_json(receipt).await;
|
||||
assert_eq!(receipt["host"], host);
|
||||
assert_eq!(receipt["request_id"], request_id.to_string());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn owner_delete_receipt_is_bound_read_only_and_reports_aborted() {
|
||||
|
||||
@@ -150,6 +150,23 @@ eligible for execution. Transient preparation failures use the existing retry
|
||||
schedule; permanent or exhausted failures block durably. Owner-facing
|
||||
admission has no cancellation endpoint.
|
||||
|
||||
## Client compatibility and rollout order
|
||||
|
||||
Owner-list responses now carry the authoritative quota trio: `quota_used`,
|
||||
`quota_limit`, and `can_create`. Quota-dependent clients fail closed when any
|
||||
member of that trio is absent: they disable new Create controls even when the
|
||||
separate community-deletion capability is absent or false. They do not derive
|
||||
capacity from the number of visible rows, because an in-progress deletion can
|
||||
hide a row while still reserving its owner's slot.
|
||||
|
||||
Roll this contract out in dependency order: relay first, then KGoose, then
|
||||
Desktop and any other quota-dependent clients. This ordering is a compatibility
|
||||
requirement, not authorization to enable owner deletion or its drain job.
|
||||
During that rollout, keep the already-published migration 0052 channel-artifact
|
||||
surface and migration 0053 owner auto-approval surface byte-for-byte intact;
|
||||
do not rewrite their migration ledger entries or recreate a persistent
|
||||
database to introduce the quota projection.
|
||||
|
||||
The chart has no existing PrometheusRule or provider-neutral CronJob alert
|
||||
integration. Operators must alert on failed/missed Jobs and long-running active
|
||||
Jobs in their deployment platform. Adding a chart-native alert abstraction is
|
||||
|
||||
Reference in New Issue
Block a user