mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
force db sync button (#1167)
This commit is contained in:
parent
b182c1b03d
commit
2072dd4b3d
@ -53,11 +53,6 @@ function getPollerClaimLimit(): number {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function getLocalApiBaseUrl(): string {
|
||||
const prefix = getEnvVariable("NEXT_PUBLIC_STACK_PORT_PREFIX", "81");
|
||||
return `http://localhost:${prefix}02`;
|
||||
}
|
||||
|
||||
export const GET = createSmartRouteHandler({
|
||||
metadata: {
|
||||
summary: "Poll outgoing requests and push to QStash",
|
||||
@ -70,7 +65,7 @@ export const GET = createSmartRouteHandler({
|
||||
auth: yupObject({}).nullable().optional(),
|
||||
method: yupString().oneOf(["GET"]).defined(),
|
||||
headers: yupObject({
|
||||
authorization: yupTuple([yupString().defined()]).defined(),
|
||||
authorization: yupTuple([yupString().defined()]).optional(),
|
||||
}).defined(),
|
||||
query: yupObject({
|
||||
maxDurationMs: yupString().optional(),
|
||||
@ -85,12 +80,14 @@ export const GET = createSmartRouteHandler({
|
||||
requests_processed: yupNumber().defined(),
|
||||
}).defined(),
|
||||
}),
|
||||
handler: async ({ headers, query }) => {
|
||||
const authHeader = headers.authorization[0];
|
||||
if (authHeader !== `Bearer ${getEnvVariable("CRON_SECRET")}`) {
|
||||
handler: async ({ headers, query, auth }) => {
|
||||
const isAdmin = auth?.type === "admin" && auth.project.id === "internal";
|
||||
const authHeader = headers.authorization?.[0];
|
||||
if (!isAdmin && authHeader !== `Bearer ${getEnvVariable("CRON_SECRET")}`) {
|
||||
throw new StatusError(401, "Unauthorized");
|
||||
}
|
||||
|
||||
|
||||
return await traceSpan("external-db-sync.poller", async (span) => {
|
||||
const startTime = performance.now();
|
||||
const maxDurationMs = parseMaxDurationMs(query.maxDurationMs);
|
||||
|
||||
@ -165,7 +165,7 @@ export const GET = createSmartRouteHandler({
|
||||
auth: yupObject({}).nullable().optional(),
|
||||
method: yupString().oneOf(["GET"]).defined(),
|
||||
headers: yupObject({
|
||||
authorization: yupTuple([yupString().defined()]).defined(),
|
||||
authorization: yupTuple([yupString().defined()]).optional(),
|
||||
}).defined(),
|
||||
query: yupObject({
|
||||
maxDurationMs: yupString().optional(),
|
||||
@ -180,9 +180,10 @@ export const GET = createSmartRouteHandler({
|
||||
iterations: yupNumber().defined(),
|
||||
}).defined(),
|
||||
}),
|
||||
handler: async ({ headers, query }) => {
|
||||
const authHeader = headers.authorization[0];
|
||||
if (authHeader !== `Bearer ${getEnvVariable("CRON_SECRET")}`) {
|
||||
handler: async ({ headers, query, auth }) => {
|
||||
const isAdmin = auth?.type === "admin" && auth.project.id === "internal";
|
||||
const authHeader = headers.authorization?.[0];
|
||||
if (!isAdmin && authHeader !== `Bearer ${getEnvVariable("CRON_SECRET")}`) {
|
||||
throw new StatusError(401, "Unauthorized");
|
||||
}
|
||||
|
||||
|
||||
@ -226,6 +226,8 @@ export default function PageClient() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [autoRefresh, setAutoRefresh] = useState(true);
|
||||
const [savingFusebox, setSavingFusebox] = useState(false);
|
||||
const [forceSyncRunning, setForceSyncRunning] = useState(false);
|
||||
const forceSyncAbortRef = useRef<AbortController | null>(null);
|
||||
const inFlightRef = useRef(false);
|
||||
const summarySamplesRef = useRef<Array<{
|
||||
timestampMillis: number,
|
||||
@ -341,6 +343,41 @@ export default function PageClient() {
|
||||
runAsynchronouslyWithAlert(loadStatus);
|
||||
}, [loadStatus]);
|
||||
|
||||
const forceTriggerSync = useCallback(async () => {
|
||||
const abortController = new AbortController();
|
||||
forceSyncAbortRef.current = abortController;
|
||||
setForceSyncRunning(true);
|
||||
try {
|
||||
const endpoints = [
|
||||
"/internal/external-db-sync/sequencer",
|
||||
"/internal/external-db-sync/poller",
|
||||
];
|
||||
await Promise.all(endpoints.map(async (endpoint) => {
|
||||
const response = await adminApp[stackAppInternalsSymbol].sendRequest(
|
||||
endpoint,
|
||||
{ method: "GET", signal: abortController.signal },
|
||||
"admin",
|
||||
);
|
||||
if (!response.ok) {
|
||||
const body = await response.json().catch(() => null);
|
||||
const message = typeof body?.error === "string" ? body.error : `Failed to trigger ${endpoint}: ${response.status}`;
|
||||
throw new Error(message);
|
||||
}
|
||||
}));
|
||||
await loadStatus();
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.name === "AbortError") return;
|
||||
throw err;
|
||||
} finally {
|
||||
forceSyncAbortRef.current = null;
|
||||
setForceSyncRunning(false);
|
||||
}
|
||||
}, [adminApp, loadStatus]);
|
||||
|
||||
const cancelForceSync = useCallback(() => {
|
||||
forceSyncAbortRef.current?.abort();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
runAsynchronously(loadStatus);
|
||||
}, [loadStatus]);
|
||||
@ -682,50 +719,75 @@ export default function PageClient() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Fusebox</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{!fusebox ? (
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="h-6 w-40" />
|
||||
<Skeleton className="h-6 w-40" />
|
||||
<Skeleton className="h-6 w-48" />
|
||||
<Skeleton className="h-9 w-24" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center justify-between gap-6">
|
||||
<div>
|
||||
<Typography type="p" className="text-sm font-medium">Sequencer</Typography>
|
||||
<Typography type="p" className="text-xs text-muted-foreground">Assigns sequence IDs and queues sync work.</Typography>
|
||||
</div>
|
||||
<Switch
|
||||
checked={fusebox.sequencerEnabled}
|
||||
onCheckedChange={(checked) => setFusebox((current) => current ? { ...current, sequencerEnabled: checked } : current)}
|
||||
/>
|
||||
<div className="grid gap-4 lg:grid-cols-3">
|
||||
<Card className="lg:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle>Fusebox</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{!fusebox ? (
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="h-6 w-40" />
|
||||
<Skeleton className="h-6 w-40" />
|
||||
<Skeleton className="h-6 w-48" />
|
||||
<Skeleton className="h-9 w-24" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-6">
|
||||
<div>
|
||||
<Typography type="p" className="text-sm font-medium">Poller</Typography>
|
||||
<Typography type="p" className="text-xs text-muted-foreground">Dispatches queued sync jobs to QStash.</Typography>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center justify-between gap-6">
|
||||
<div>
|
||||
<Typography type="p" className="text-sm font-medium">Sequencer</Typography>
|
||||
<Typography type="p" className="text-xs text-muted-foreground">Assigns sequence IDs and queues sync work.</Typography>
|
||||
</div>
|
||||
<Switch
|
||||
checked={fusebox.sequencerEnabled}
|
||||
onCheckedChange={(checked) => setFusebox((current) => current ? { ...current, sequencerEnabled: checked } : current)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-6">
|
||||
<div>
|
||||
<Typography type="p" className="text-sm font-medium">Poller</Typography>
|
||||
<Typography type="p" className="text-xs text-muted-foreground">Dispatches queued sync jobs to QStash.</Typography>
|
||||
</div>
|
||||
<Switch
|
||||
checked={fusebox.pollerEnabled}
|
||||
onCheckedChange={(checked) => setFusebox((current) => current ? { ...current, pollerEnabled: checked } : current)}
|
||||
/>
|
||||
</div>
|
||||
<Switch
|
||||
checked={fusebox.pollerEnabled}
|
||||
onCheckedChange={(checked) => setFusebox((current) => current ? { ...current, pollerEnabled: checked } : current)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={saveFusebox} disabled={!fuseboxDirty || savingFusebox} loading={savingFusebox}>
|
||||
Save
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={saveFusebox} disabled={!fuseboxDirty || savingFusebox} loading={savingFusebox}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Force Sync</CardTitle>
|
||||
<CardDescription>Manually trigger sequencer and poller.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`h-2.5 w-2.5 rounded-full ${forceSyncRunning ? "bg-yellow-500 animate-pulse" : "bg-green-500"}`} />
|
||||
<Typography type="p" className="text-sm">{forceSyncRunning ? "Running" : "Idle"}</Typography>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button onClick={() => runAsynchronouslyWithAlert(forceTriggerSync)} disabled={forceSyncRunning} loading={forceSyncRunning}>
|
||||
Run Now
|
||||
</Button>
|
||||
{forceSyncRunning && (
|
||||
<Button onClick={cancelForceSync} variant="destructive" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
</PageLayout>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user