fix(notifications): D6 regression — lone space must get full drain throughput
D6's pre-truncation capped every space's bucket at PER_SPACE=5 before the round-robin, so a single-space deployment (the common single- hackerspace case) drained only 5 emails/min instead of 20 — a 4x slowdown introduced by the fairness fix. Fairness must bind only under contention: per-space queues are now bounded solely by the CANDIDATES fetch and the round-robin itself caps any one space's share when multiple compete; a lone space drains the full BATCH again. Caught verifying the D1-D10 batch.
This commit is contained in:
@@ -22,10 +22,8 @@ export const runtime = 'nodejs'
|
|||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
const BATCH = 20
|
const BATCH = 20
|
||||||
// Per-space cap within one drain so one tenant's billing burst cannot
|
// Oldest-first candidate window we fairness-balance across spaces. Larger
|
||||||
// monopolize the shared global FIFO and starve every other space's email.
|
// than BATCH so a burst space cannot crowd smaller spaces out of the window.
|
||||||
const PER_SPACE = 5
|
|
||||||
// Oldest-first candidate window we fairness-balance across spaces.
|
|
||||||
const CANDIDATES = 200
|
const CANDIDATES = 200
|
||||||
// ~4.5 sends/sec, under Resend's 5 req/sec team limit.
|
// ~4.5 sends/sec, under Resend's 5 req/sec team limit.
|
||||||
const SPACING_MS = 220
|
const SPACING_MS = 220
|
||||||
@@ -62,15 +60,18 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 })
|
if (error) return NextResponse.json({ error: error.message }, { status: 500 })
|
||||||
|
|
||||||
// Fair drain: round-robin the oldest-first candidates across spaces with a
|
// Fair drain: round-robin the oldest-first candidates across spaces so one
|
||||||
// per-space cap, so a single tenant's burst can't head-of-line-block every
|
// tenant's burst can't head-of-line-block others. Fairness binds only under
|
||||||
// other space's notifications in the shared dispatcher.
|
// contention: with a single space pending (the common single-hackerspace
|
||||||
|
// deployment) it still drains the full BATCH; the round-robin itself caps
|
||||||
|
// any one space's share when multiple spaces compete. Per-space queues are
|
||||||
|
// bounded only by the CANDIDATES fetch (oldest-first), and Map insertion
|
||||||
|
// order preserves that oldest-first ordering across spaces.
|
||||||
const bySpace = new Map<string, typeof candidates>()
|
const bySpace = new Map<string, typeof candidates>()
|
||||||
for (const c of candidates ?? []) {
|
for (const c of candidates ?? []) {
|
||||||
const k = c.space_id as string
|
const k = c.space_id as string
|
||||||
if (!bySpace.has(k)) bySpace.set(k, [])
|
if (!bySpace.has(k)) bySpace.set(k, [])
|
||||||
const arr = bySpace.get(k)!
|
bySpace.get(k)!.push(c)
|
||||||
if (arr.length < PER_SPACE) arr.push(c)
|
|
||||||
}
|
}
|
||||||
const rows: NonNullable<typeof candidates> = []
|
const rows: NonNullable<typeof candidates> = []
|
||||||
let added = true
|
let added = true
|
||||||
|
|||||||
Reference in New Issue
Block a user