fix(admin): bound owner-email enrichment concurrency to avoid self-throttle
A large concurrent burst of getUserById calls during the tunnels-list email enrichment self-inflicts an upstream throttle (truncated/empty bodies) that even the per-call retry can't fully escape, intermittently rendering owner_email as '—'. Add mapWithConcurrency and resolve owner emails at most a few at a time so each lookup stays inside the throttle allowance; retry + null fallback preserved.
This commit is contained in:
+16
-7
@@ -1,6 +1,6 @@
|
||||
import type { User } from '@supabase/supabase-js';
|
||||
import { getSupabaseAdmin } from '@/lib/supabase/admin';
|
||||
import { withAdminRetry } from '@/lib/admin/retry';
|
||||
import { withAdminRetry, mapWithConcurrency } from '@/lib/admin/retry';
|
||||
import {
|
||||
parseOrder,
|
||||
parseSort,
|
||||
@@ -77,6 +77,12 @@ type TunnelRow = TunnelJoinRow & {
|
||||
const USER_SCAN_MAX_PAGES = 50;
|
||||
const USER_SCAN_PER_PAGE = 1000;
|
||||
|
||||
// Resolve tunnel owner emails a few at a time rather than all at once: a large
|
||||
// concurrent burst of getUserById calls self-inflicts an upstream throttle
|
||||
// (truncated/empty bodies) that even per-call retries can't fully escape, which
|
||||
// is what intermittently rendered owner_email as "—" under load.
|
||||
const OWNER_EMAIL_CONCURRENCY = 4;
|
||||
|
||||
function userSortValue(u: User, sort: UserSort): string | number {
|
||||
switch (sort) {
|
||||
case 'email':
|
||||
@@ -291,11 +297,14 @@ export async function getTunnelsList(opts: {
|
||||
|
||||
// Resolve owner emails (per-row getUserById; acceptable for current scale).
|
||||
// The user_id comes from an existing tunnel row, so an empty body here is a
|
||||
// transient burst flake rather than a genuine not-found — retry it. The
|
||||
// try/catch null fallback remains as a last resort so one bad row can never
|
||||
// 500 the whole list (it surfaces as "—" only if every retry still fails).
|
||||
const emails = await Promise.all(
|
||||
rows.map(async (t) => {
|
||||
// transient burst flake rather than a genuine not-found — retry it, and bound
|
||||
// the concurrency so the enrichment doesn't self-throttle. The try/catch null
|
||||
// fallback remains as a last resort so one bad row can never 500 the whole
|
||||
// list (it surfaces as "—" only if every retry still fails).
|
||||
const emails = await mapWithConcurrency(
|
||||
rows,
|
||||
OWNER_EMAIL_CONCURRENCY,
|
||||
async (t) => {
|
||||
try {
|
||||
const { data: u } = await withAdminRetry(() =>
|
||||
admin.auth.admin.getUserById(t.user_id),
|
||||
@@ -304,7 +313,7 @@ export async function getTunnelsList(opts: {
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
const tunnels: TunnelItem[] = rows.map((t, i) => ({
|
||||
|
||||
Reference in New Issue
Block a user