mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(db): add tenant-safe query defaults and reaper host provenance
Add EventQuery::for_community so relay call sites can keep concise struct updates without restoring a tenantless Default. The constructor requires the server-resolved CommunityId and preserves the old optional filter defaults everywhere else. Return the owning community host from the ephemeral-channel reaper by joining communities in the archive UPDATE. Reaper consumers can now build TenantContext per archived row from DB-resolved community+host instead of hoisting or forging a batch-level tenant. Co-authored-by: Mari <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co> Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
This commit is contained in:
@@ -733,10 +733,12 @@ pub struct BotChannelEntry {
|
||||
}
|
||||
|
||||
/// A channel archived by the ephemeral-channel reaper.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ReapedEphemeralChannel {
|
||||
/// Community that owns the archived channel.
|
||||
pub community_id: CommunityId,
|
||||
/// Normalized host mapped to that community.
|
||||
pub host: String,
|
||||
/// Archived channel UUID.
|
||||
pub channel_id: Uuid,
|
||||
}
|
||||
@@ -1325,17 +1327,19 @@ pub async fn bump_ttl_deadline(
|
||||
|
||||
/// Archive ephemeral channels whose TTL deadline has passed.
|
||||
///
|
||||
/// Returns the `(community_id, channel_id)` list that was archived. Idempotent — the
|
||||
/// Returns the `(community_id, host, channel_id)` list that was archived. Idempotent — the
|
||||
/// `archived_at IS NULL` guard prevents double-archiving even if called
|
||||
/// concurrently from multiple relay pods.
|
||||
pub async fn reap_expired_ephemeral_channels(pool: &PgPool) -> Result<Vec<ReapedEphemeralChannel>> {
|
||||
let rows = sqlx::query(
|
||||
"UPDATE channels SET archived_at = NOW() \
|
||||
WHERE ttl_seconds IS NOT NULL \
|
||||
AND ttl_deadline < NOW() \
|
||||
AND archived_at IS NULL \
|
||||
AND deleted_at IS NULL \
|
||||
RETURNING community_id, id",
|
||||
"UPDATE channels AS ch SET archived_at = NOW() \
|
||||
FROM communities AS c \
|
||||
WHERE ch.community_id = c.id \
|
||||
AND ch.ttl_seconds IS NOT NULL \
|
||||
AND ch.ttl_deadline < NOW() \
|
||||
AND ch.archived_at IS NULL \
|
||||
AND ch.deleted_at IS NULL \
|
||||
RETURNING ch.community_id, c.host, ch.id",
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
@@ -1343,9 +1347,11 @@ pub async fn reap_expired_ephemeral_channels(pool: &PgPool) -> Result<Vec<Reaped
|
||||
rows.into_iter()
|
||||
.map(|row| {
|
||||
let community_id: Uuid = row.try_get("community_id")?;
|
||||
let host: String = row.try_get("host")?;
|
||||
let channel_id: Uuid = row.try_get("id")?;
|
||||
Ok(ReapedEphemeralChannel {
|
||||
community_id: CommunityId::from_uuid(community_id),
|
||||
host,
|
||||
channel_id,
|
||||
})
|
||||
})
|
||||
@@ -1640,6 +1646,57 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn reap_expired_ephemeral_channels_returns_row_community_and_host() {
|
||||
let pool = setup_pool().await;
|
||||
let community_id = make_test_community(&pool).await;
|
||||
let community = CommunityId::from_uuid(community_id);
|
||||
let expected_host: String =
|
||||
sqlx::query_scalar("SELECT host FROM communities WHERE id = $1")
|
||||
.bind(community_id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("load community host");
|
||||
let owner_pk = random_pubkey();
|
||||
ensure_user(&pool, community, &owner_pk)
|
||||
.await
|
||||
.expect("ensure owner");
|
||||
let channel = create_test_channel(
|
||||
&pool,
|
||||
community_id,
|
||||
"test-reaper-host-provenance",
|
||||
ChannelType::Stream,
|
||||
ChannelVisibility::Open,
|
||||
None,
|
||||
&owner_pk,
|
||||
Some(60),
|
||||
)
|
||||
.await
|
||||
.expect("create ephemeral channel");
|
||||
|
||||
sqlx::query(
|
||||
"UPDATE channels SET ttl_deadline = NOW() - interval '1 second' WHERE community_id = $1 AND id = $2",
|
||||
)
|
||||
.bind(community_id)
|
||||
.bind(channel.id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("expire channel");
|
||||
|
||||
let reaped = reap_expired_ephemeral_channels(&pool)
|
||||
.await
|
||||
.expect("run reaper");
|
||||
assert!(
|
||||
reaped.iter().any(|row| {
|
||||
row.community_id == community
|
||||
&& row.host == expected_host
|
||||
&& row.channel_id == channel.id
|
||||
}),
|
||||
"reaper should carry the archived row's community id and host"
|
||||
);
|
||||
}
|
||||
|
||||
/// A random non-admin, non-owner user cannot remove someone else's bot.
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
|
||||
@@ -71,6 +71,36 @@ pub struct EventQuery {
|
||||
pub max_limit: Option<i64>,
|
||||
}
|
||||
|
||||
impl EventQuery {
|
||||
/// Construct an unconstrained query inside a server-resolved community.
|
||||
///
|
||||
/// `community_id` has no safe default. This keeps call sites concise while
|
||||
/// making tenant provenance explicit at construction.
|
||||
#[must_use]
|
||||
pub const fn for_community(community_id: CommunityId) -> Self {
|
||||
Self {
|
||||
community_id,
|
||||
channel_id: None,
|
||||
kinds: None,
|
||||
pubkey: None,
|
||||
since: None,
|
||||
until: None,
|
||||
limit: None,
|
||||
offset: None,
|
||||
p_tag_hex: None,
|
||||
d_tag: None,
|
||||
d_tags: None,
|
||||
before_id: None,
|
||||
global_only: false,
|
||||
authors: None,
|
||||
ids: None,
|
||||
e_tags: None,
|
||||
channel_ids: None,
|
||||
max_limit: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Maximum length for a `d_tag` value (bytes). NIP-33 d-tags are short identifiers;
|
||||
/// anything beyond this is either a bug or abuse.
|
||||
pub const D_TAG_MAX_LEN: usize = 1024;
|
||||
|
||||
Reference in New Issue
Block a user