perf(startup): skip inactive background services

This commit is contained in:
hungtrinh 2026-07-16 11:17:46 +07:00
parent 7dfb346667
commit 27b37705b3
8 changed files with 107 additions and 16 deletions

View file

@ -2,7 +2,6 @@ import { NextResponse } from "next/server";
import { getSettings, updateSettings } from "@/lib/localDb"; import { getSettings, updateSettings } from "@/lib/localDb";
import { applyOutboundProxyEnv } from "@/lib/network/outboundProxy"; import { applyOutboundProxyEnv } from "@/lib/network/outboundProxy";
import { resetComboRotation } from "open-sse/services/combo.js"; import { resetComboRotation } from "open-sse/services/combo.js";
import { runQuotaAutoPingTick } from "@/shared/services/quotaAutoPing";
import bcrypt from "bcryptjs"; import bcrypt from "bcryptjs";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@ -101,10 +100,12 @@ export async function PATCH(request) {
Object.prototype.hasOwnProperty.call(body, "claudeAutoPing") || Object.prototype.hasOwnProperty.call(body, "claudeAutoPing") ||
Object.prototype.hasOwnProperty.call(body, "codexAutoPing") Object.prototype.hasOwnProperty.call(body, "codexAutoPing")
) { ) {
// Run once immediately after opt-in changes so users don't wait for the next scheduler tick. // Keep the scheduler absent when no account opted in; load its provider graph only on demand.
runQuotaAutoPingTick().catch((error) => { import("@/shared/services/quotaAutoPing")
console.warn("[AutoPing] settings-triggered tick failed:", error.message); .then(({ configureQuotaAutoPing }) => {
}); configureQuotaAutoPing(settings);
})
.catch((error) => console.warn("[AutoPing] settings update failed:", error.message));
} }
const { password, oidcClientSecret, ...safeSettings } = settings; const { password, oidcClientSecret, ...safeSettings } = settings;

View file

@ -1,9 +1,14 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { disableTunnel } from "@/lib/tunnel"; import { disableTunnel } from "@/lib/tunnel";
import { getSettings } from "@/lib/localDb";
import { configureTunnelMonitoring } from "@/shared/services/initializeApp";
export async function POST() { export async function POST() {
try { try {
const result = await disableTunnel(); const result = await disableTunnel();
getSettings()
.then(configureTunnelMonitoring)
.catch((error) => console.warn("Tunnel monitor update failed:", error.message));
return NextResponse.json(result); return NextResponse.json(result);
} catch (error) { } catch (error) {
console.error("Tunnel disable error:", error); console.error("Tunnel disable error:", error);

View file

@ -1,11 +1,16 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { enableTunnel } from "@/lib/tunnel"; import { enableTunnel } from "@/lib/tunnel";
import { getSettings } from "@/lib/localDb";
import { configureTunnelMonitoring } from "@/shared/services/initializeApp";
const DNS_WARMUP_DELAY_MS = 8000; const DNS_WARMUP_DELAY_MS = 8000;
export async function POST() { export async function POST() {
try { try {
const result = await enableTunnel(); const result = await enableTunnel();
getSettings()
.then(configureTunnelMonitoring)
.catch((error) => console.warn("Tunnel monitor start failed:", error.message));
// Wait for DNS warmup to propagate at Cloudflare edge after tunnel registered // Wait for DNS warmup to propagate at Cloudflare edge after tunnel registered
await new Promise((r) => setTimeout(r, DNS_WARMUP_DELAY_MS)); await new Promise((r) => setTimeout(r, DNS_WARMUP_DELAY_MS));
return NextResponse.json(result); return NextResponse.json(result);

View file

@ -1,9 +1,14 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { disableTailscale } from "@/lib/tunnel"; import { disableTailscale } from "@/lib/tunnel";
import { getSettings } from "@/lib/localDb";
import { configureTunnelMonitoring } from "@/shared/services/initializeApp";
export async function POST() { export async function POST() {
try { try {
const result = await disableTailscale(); const result = await disableTailscale();
getSettings()
.then(configureTunnelMonitoring)
.catch((error) => console.warn("Tailscale monitor update failed:", error.message));
return NextResponse.json(result); return NextResponse.json(result);
} catch (error) { } catch (error) {
console.error("Tailscale disable error:", error); console.error("Tailscale disable error:", error);

View file

@ -1,9 +1,14 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { enableTailscale } from "@/lib/tunnel"; import { enableTailscale } from "@/lib/tunnel";
import { getSettings } from "@/lib/localDb";
import { configureTunnelMonitoring } from "@/shared/services/initializeApp";
export async function POST() { export async function POST() {
try { try {
const result = await enableTailscale(); const result = await enableTailscale();
getSettings()
.then(configureTunnelMonitoring)
.catch((error) => console.warn("Tailscale monitor start failed:", error.message));
return NextResponse.json(result); return NextResponse.json(result);
} catch (error) { } catch (error) {
console.error("Tailscale enable error:", error.message); console.error("Tailscale enable error:", error.message);

View file

@ -14,7 +14,6 @@ import {
WATCHDOG_INTERVAL_MS, NETWORK_CHECK_INTERVAL_MS, VIRTUAL_IFACE_REGEX, WATCHDOG_INTERVAL_MS, NETWORK_CHECK_INTERVAL_MS, VIRTUAL_IFACE_REGEX,
} from "@/lib/tunnel"; } from "@/lib/tunnel";
import { getMitmStatus, startMitm, loadEncryptedPassword, initDbHooks, restoreToolDNS, removeAllDNSEntriesSync } from "@/mitm/manager"; import { getMitmStatus, startMitm, loadEncryptedPassword, initDbHooks, restoreToolDNS, removeAllDNSEntriesSync } from "@/mitm/manager";
import { startQuotaAutoPing } from "@/shared/services/quotaAutoPing";
import { syncToJson as syncMitmAliasCache } from "@/lib/mitmAliasCache"; import { syncToJson as syncMitmAliasCache } from "@/lib/mitmAliasCache";
import { killAllBridges } from "@/lib/mcp/stdioSseBridge"; import { killAllBridges } from "@/lib/mcp/stdioSseBridge";
@ -98,22 +97,32 @@ async function runHeavyStartup() {
safeRestartTailscale("startup").catch((e) => console.log("[InitApp] Tailscale resume failed:", e.message)); safeRestartTailscale("startup").catch((e) => console.log("[InitApp] Tailscale resume failed:", e.message));
} }
ensureCloudflared().catch(() => {}); if (settings.tunnelEnabled) ensureCloudflared().catch(() => {});
// Sync mitmAlias DB → JSON cache so standalone MITM server can read it if (settings.mitmEnabled) {
syncMitmAliasCache().catch(() => {}); // Sync mitmAlias DB → JSON cache so standalone MITM server can read it.
syncMitmAliasCache().catch(() => {});
autoStartMitm(settings);
}
startWatchdog(); configureTunnelMonitoring(settings);
startNetworkMonitor();
autoStartMitm(); if (hasQuotaAutoPingEnabled(settings)) {
startQuotaAutoPing(); import("@/shared/services/quotaAutoPing")
.then(({ startQuotaAutoPing }) => startQuotaAutoPing())
.catch((e) => console.log("[AutoPing] scheduler start failed:", e.message));
}
} }
async function autoStartMitm() { function hasQuotaAutoPingEnabled(settings) {
return [settings?.claudeAutoPing, settings?.codexAutoPing]
.some((config) => Object.values(config?.connections || {}).some(Boolean));
}
async function autoStartMitm(settings) {
if (g.mitmStartInProgress) return; if (g.mitmStartInProgress) return;
g.mitmStartInProgress = true; g.mitmStartInProgress = true;
try { try {
const settings = await getSettings();
if (!settings.mitmEnabled) return; if (!settings.mitmEnabled) return;
const mitmStatus = await getMitmStatus(); const mitmStatus = await getMitmStatus();
if (mitmStatus.running) return; if (mitmStatus.running) return;
@ -232,6 +241,12 @@ function startWatchdog() {
if (g.watchdogInterval.unref) g.watchdogInterval.unref(); if (g.watchdogInterval.unref) g.watchdogInterval.unref();
} }
function stopWatchdog() {
if (!g.watchdogInterval) return;
clearInterval(g.watchdogInterval);
g.watchdogInterval = null;
}
// ─── Network monitor: detect IPv4 fingerprint change + sleep/wake ──────────── // ─── Network monitor: detect IPv4 fingerprint change + sleep/wake ────────────
function getNetworkFingerprint() { function getNetworkFingerprint() {
@ -293,4 +308,23 @@ function startNetworkMonitor() {
if (g.networkMonitorInterval.unref) g.networkMonitorInterval.unref(); if (g.networkMonitorInterval.unref) g.networkMonitorInterval.unref();
} }
function stopNetworkMonitor() {
if (!g.networkMonitorInterval) return;
clearInterval(g.networkMonitorInterval);
g.networkMonitorInterval = null;
g.lastNetworkFingerprint = null;
g.lastOnline = null;
}
export function configureTunnelMonitoring(settings) {
if (settings?.tunnelEnabled || settings?.tailscaleEnabled) {
startWatchdog();
startNetworkMonitor();
return;
}
stopWatchdog();
stopNetworkMonitor();
}
export default initializeApp; export default initializeApp;

View file

@ -296,3 +296,18 @@ export function startQuotaAutoPing() {
g.interval = setInterval(() => { runQuotaAutoPingTick().catch(() => {}); }, C.tickIntervalMs); g.interval = setInterval(() => { runQuotaAutoPingTick().catch(() => {}); }, C.tickIntervalMs);
if (g.interval.unref) g.interval.unref(); if (g.interval.unref) g.interval.unref();
} }
export function stopQuotaAutoPing() {
if (!g.interval) return;
clearInterval(g.interval);
g.interval = null;
console.log("[AutoPing] scheduler stopped");
}
export function configureQuotaAutoPing(settings) {
const enabled = Object.values(C.providers).some((providerConfig) =>
Object.values(settings?.[providerConfig.settingsKey]?.connections || {}).some(Boolean)
);
if (enabled) startQuotaAutoPing();
else stopQuotaAutoPing();
}

View file

@ -72,6 +72,7 @@ vi.mock("open-sse/executors/index.js", () => ({
describe("quota auto-ping", () => { describe("quota auto-ping", () => {
let runQuotaAutoPingTick; let runQuotaAutoPingTick;
let configureQuotaAutoPing;
let deps; let deps;
let state; let state;
let getCodexUsage; let getCodexUsage;
@ -83,11 +84,12 @@ describe("quota auto-ping", () => {
vi.resetModules(); vi.resetModules();
vi.clearAllMocks(); vi.clearAllMocks();
vi.useRealTimers(); vi.useRealTimers();
delete global.__quotaAutoPing;
({ getCodexUsage } = await import("open-sse/services/usage/codex.js")); ({ getCodexUsage } = await import("open-sse/services/usage/codex.js"));
({ getClaudeUsage } = await import("open-sse/services/usage/claude.js")); ({ getClaudeUsage } = await import("open-sse/services/usage/claude.js"));
({ getExecutor } = await import("open-sse/executors/index.js")); ({ getExecutor } = await import("open-sse/executors/index.js"));
({ runQuotaAutoPingTick } = await import("../../src/shared/services/quotaAutoPing.js")); ({ runQuotaAutoPingTick, configureQuotaAutoPing } = await import("../../src/shared/services/quotaAutoPing.js"));
deps = { deps = {
getSettings: vi.fn(), getSettings: vi.fn(),
@ -117,6 +119,25 @@ describe("quota auto-ping", () => {
expect(deps.proxyAwareFetch).not.toHaveBeenCalled(); expect(deps.proxyAwareFetch).not.toHaveBeenCalled();
}); });
it("starts the scheduler only when an account opts in", () => {
vi.useFakeTimers();
configureQuotaAutoPing({ codexAutoPing: { connections: {} } });
expect(vi.getTimerCount()).toBe(0);
configureQuotaAutoPing({ codexAutoPing: { connections: { "codex-1": true } } });
expect(vi.getTimerCount()).toBe(1);
});
it("stops the scheduler when the last account opts out", () => {
vi.useFakeTimers();
configureQuotaAutoPing({ claudeAutoPing: { connections: { "claude-1": true } } });
configureQuotaAutoPing({ claudeAutoPing: { connections: { "claude-1": false } } });
expect(vi.getTimerCount()).toBe(0);
});
it("does not ping Codex on the first resetAt observation", async () => { it("does not ping Codex on the first resetAt observation", async () => {
deps.getSettings.mockResolvedValue({ codexAutoPing: { connections: { "codex-1": true } } }); deps.getSettings.mockResolvedValue({ codexAutoPing: { connections: { "codex-1": true } } });
deps.getProviderConnections.mockImplementation(async ({ provider }) => ( deps.getProviderConnections.mockImplementation(async ({ provider }) => (