perf: optimize overview/tasks/polling/microservice-cache and improve frontend progressive loading
This commit is contained in:
@@ -117,6 +117,7 @@ type TaskTerminalWaiter = (task: RawTaskRow | null) => void;
|
||||
|
||||
interface MicroserviceProxyCacheEntry {
|
||||
expiresAt: number;
|
||||
staleExpiresAt: number;
|
||||
status: number;
|
||||
contentType: string;
|
||||
bodyText: string;
|
||||
@@ -140,6 +141,7 @@ const operationPerformanceSamples: OperationPerformanceSample[] = [];
|
||||
const maxPerformanceSamples = 3000;
|
||||
const taskTerminalWaiters = new Map<string, Set<TaskTerminalWaiter>>();
|
||||
const microserviceProxyCache = new Map<string, MicroserviceProxyCacheEntry>();
|
||||
const microserviceProxyRefreshes = new Map<string, Promise<void>>();
|
||||
let lastTaskSweepAt = 0;
|
||||
let taskSweepInFlight: Promise<void> | null = null;
|
||||
const microserviceProxyMaxBodyTextLength = 8 * 1024 * 1024;
|
||||
@@ -1062,8 +1064,169 @@ async function getEvents(limit: number): Promise<ApiEvent[]> {
|
||||
}));
|
||||
}
|
||||
|
||||
async function getTasks(limit: number, statusFilter = "all", lite = false): Promise<ApiTask[]> {
|
||||
function rowString(row: Record<string, unknown>, key: string): string {
|
||||
const value = row[key];
|
||||
return typeof value === "string" ? value : value === null || value === undefined ? "" : String(value);
|
||||
}
|
||||
|
||||
function rowNumber(row: Record<string, unknown>, key: string): number | null {
|
||||
const value = row[key];
|
||||
const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function taskJsonSummary(row: Record<string, unknown>, prefix: "payload" | "result"): JsonValue {
|
||||
const type = rowString(row, `${prefix}_type`);
|
||||
if (type.length === 0) return prefix === "result" ? null : {};
|
||||
const summary: Record<string, JsonValue> = { summaryOnly: true, type };
|
||||
const fields = prefix === "payload"
|
||||
? ["source", "serviceId", "method", "path", "mode", "targetBaseUrl", "timeoutMs", "targetProviderGatewayVersion", "providerGatewayVersion"]
|
||||
: ["error", "reason", "message", "status", "exitCode", "code", "signal", "timeoutMs", "previousStatus", "mode", "policy", "targetProviderGatewayVersion", "providerGatewayVersion", "updaterContainerId"];
|
||||
for (const field of fields) {
|
||||
const value = row[`${prefix}_${field}`];
|
||||
if (value !== null && value !== undefined && String(value).length > 0) {
|
||||
summary[field] = typeof value === "number" || typeof value === "boolean" ? value : String(value);
|
||||
}
|
||||
}
|
||||
const bodyChars = rowNumber(row, `${prefix}_body_text_chars`);
|
||||
if (bodyChars !== null) summary.bodyText = `<omitted:${bodyChars} chars>`;
|
||||
return summary;
|
||||
}
|
||||
|
||||
function taskSummaryFromRow(row: Record<string, unknown>): ApiTask {
|
||||
return {
|
||||
id: String(row.id),
|
||||
providerId: String(row.provider_id),
|
||||
command: String(row.command),
|
||||
status: String(row.status),
|
||||
payload: taskJsonSummary(row, "payload"),
|
||||
result: taskJsonSummary(row, "result"),
|
||||
createdAt: row.created_at instanceof Date ? row.created_at.toISOString() : String(row.created_at),
|
||||
updatedAt: row.updated_at instanceof Date ? row.updated_at.toISOString() : String(row.updated_at),
|
||||
_summaryOnly: true,
|
||||
} as ApiTask;
|
||||
}
|
||||
|
||||
async function getTask(taskId: string): Promise<ApiTask | null> {
|
||||
const rows = await sql<Array<Record<string, unknown>>>`
|
||||
SELECT
|
||||
id,
|
||||
provider_id,
|
||||
command,
|
||||
status,
|
||||
CASE
|
||||
WHEN payload ? 'bodyText' THEN jsonb_set(payload - 'bodyText', '{bodyText}', to_jsonb(('<omitted:' || length(payload->>'bodyText')::text || ' chars>')::text))
|
||||
ELSE payload
|
||||
END AS payload,
|
||||
CASE
|
||||
WHEN result IS NOT NULL AND result ? 'bodyText' THEN jsonb_set(result - 'bodyText', '{bodyText}', to_jsonb(('<omitted:' || length(result->>'bodyText')::text || ' chars>')::text))
|
||||
ELSE result
|
||||
END AS result,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM unidesk_tasks
|
||||
WHERE id = ${taskId}
|
||||
LIMIT 1
|
||||
`;
|
||||
const row = rows[0];
|
||||
if (row === undefined) return null;
|
||||
return {
|
||||
id: String(row.id),
|
||||
providerId: String(row.provider_id),
|
||||
command: String(row.command),
|
||||
status: String(row.status),
|
||||
payload: compactJson(row.payload ?? {}),
|
||||
result: compactJson(row.result ?? null),
|
||||
createdAt: row.created_at instanceof Date ? row.created_at.toISOString() : String(row.created_at),
|
||||
updatedAt: row.updated_at instanceof Date ? row.updated_at.toISOString() : String(row.updated_at),
|
||||
};
|
||||
}
|
||||
|
||||
async function getTasks(limit: number, statusFilter = "all", lite = false, summary = false): Promise<ApiTask[]> {
|
||||
await maybeMarkStaleTasksFailed();
|
||||
if (summary && !lite) {
|
||||
const rows = statusFilter === "pending"
|
||||
? await sql<Array<Record<string, unknown>>>`
|
||||
SELECT
|
||||
id,
|
||||
provider_id,
|
||||
command,
|
||||
status,
|
||||
jsonb_typeof(payload) AS payload_type,
|
||||
payload->>'source' AS "payload_source",
|
||||
payload->>'serviceId' AS "payload_serviceId",
|
||||
payload->>'method' AS "payload_method",
|
||||
payload->>'path' AS "payload_path",
|
||||
payload->>'mode' AS "payload_mode",
|
||||
payload->>'targetBaseUrl' AS "payload_targetBaseUrl",
|
||||
payload->>'timeoutMs' AS "payload_timeoutMs",
|
||||
payload->>'targetProviderGatewayVersion' AS "payload_targetProviderGatewayVersion",
|
||||
payload->>'providerGatewayVersion' AS "payload_providerGatewayVersion",
|
||||
CASE WHEN payload ? 'bodyText' THEN length(payload->>'bodyText') ELSE NULL END AS payload_body_text_chars,
|
||||
jsonb_typeof(result) AS result_type,
|
||||
result->>'error' AS "result_error",
|
||||
result->>'reason' AS "result_reason",
|
||||
result->>'message' AS "result_message",
|
||||
result->>'status' AS "result_status",
|
||||
result->>'exitCode' AS "result_exitCode",
|
||||
result->>'code' AS "result_code",
|
||||
result->>'signal' AS "result_signal",
|
||||
result->>'timeoutMs' AS "result_timeoutMs",
|
||||
result->>'previousStatus' AS "result_previousStatus",
|
||||
result->>'mode' AS "result_mode",
|
||||
COALESCE(result->>'policy', result #>> '{plan,policy}') AS "result_policy",
|
||||
COALESCE(result->>'targetProviderGatewayVersion', result #>> '{plan,targetProviderGatewayVersion}') AS "result_targetProviderGatewayVersion",
|
||||
COALESCE(result->>'providerGatewayVersion', result #>> '{plan,providerGatewayVersion}') AS "result_providerGatewayVersion",
|
||||
result->>'updaterContainerId' AS "result_updaterContainerId",
|
||||
CASE WHEN result ? 'bodyText' THEN length(result->>'bodyText') ELSE NULL END AS result_body_text_chars,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM unidesk_tasks
|
||||
WHERE status IN ('queued', 'dispatched', 'running')
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT ${limit}
|
||||
`
|
||||
: await sql<Array<Record<string, unknown>>>`
|
||||
SELECT
|
||||
id,
|
||||
provider_id,
|
||||
command,
|
||||
status,
|
||||
jsonb_typeof(payload) AS payload_type,
|
||||
payload->>'source' AS "payload_source",
|
||||
payload->>'serviceId' AS "payload_serviceId",
|
||||
payload->>'method' AS "payload_method",
|
||||
payload->>'path' AS "payload_path",
|
||||
payload->>'mode' AS "payload_mode",
|
||||
payload->>'targetBaseUrl' AS "payload_targetBaseUrl",
|
||||
payload->>'timeoutMs' AS "payload_timeoutMs",
|
||||
payload->>'targetProviderGatewayVersion' AS "payload_targetProviderGatewayVersion",
|
||||
payload->>'providerGatewayVersion' AS "payload_providerGatewayVersion",
|
||||
CASE WHEN payload ? 'bodyText' THEN length(payload->>'bodyText') ELSE NULL END AS payload_body_text_chars,
|
||||
jsonb_typeof(result) AS result_type,
|
||||
result->>'error' AS "result_error",
|
||||
result->>'reason' AS "result_reason",
|
||||
result->>'message' AS "result_message",
|
||||
result->>'status' AS "result_status",
|
||||
result->>'exitCode' AS "result_exitCode",
|
||||
result->>'code' AS "result_code",
|
||||
result->>'signal' AS "result_signal",
|
||||
result->>'timeoutMs' AS "result_timeoutMs",
|
||||
result->>'previousStatus' AS "result_previousStatus",
|
||||
result->>'mode' AS "result_mode",
|
||||
COALESCE(result->>'policy', result #>> '{plan,policy}') AS "result_policy",
|
||||
COALESCE(result->>'targetProviderGatewayVersion', result #>> '{plan,targetProviderGatewayVersion}') AS "result_targetProviderGatewayVersion",
|
||||
COALESCE(result->>'providerGatewayVersion', result #>> '{plan,providerGatewayVersion}') AS "result_providerGatewayVersion",
|
||||
result->>'updaterContainerId' AS "result_updaterContainerId",
|
||||
CASE WHEN result ? 'bodyText' THEN length(result->>'bodyText') ELSE NULL END AS result_body_text_chars,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM unidesk_tasks
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT ${limit}
|
||||
`;
|
||||
return rows.map(taskSummaryFromRow);
|
||||
}
|
||||
const rows = statusFilter === "pending"
|
||||
? lite
|
||||
? await sql<Array<Record<string, unknown>>>`
|
||||
@@ -1161,22 +1324,39 @@ async function getPgdataUsage(): Promise<JsonValue> {
|
||||
}
|
||||
|
||||
async function getOverview(): Promise<JsonValue> {
|
||||
const nodes = await getNodes();
|
||||
const pendingTasks = await countPendingTasks();
|
||||
const dockerStatuses = await getNodeDockerStatuses();
|
||||
const systemStatuses = await getNodeSystemStatuses(1);
|
||||
const pgdata = await getPgdataUsage();
|
||||
const online = nodes.filter((node) => node.status === "online").length;
|
||||
const [nodeRows, dockerRows, systemRows, pendingTasks, pgdata] = await Promise.all([
|
||||
sql<Array<{ node_count: string | number; online_node_count: string | number }>>`
|
||||
SELECT
|
||||
count(*)::int AS node_count,
|
||||
count(*) FILTER (WHERE status = 'online')::int AS online_node_count
|
||||
FROM unidesk_nodes
|
||||
`,
|
||||
sql<Array<{ docker_status_node_count: string | number }>>`
|
||||
SELECT count(*) FILTER (WHERE d.status IS NOT NULL)::int AS docker_status_node_count
|
||||
FROM unidesk_nodes n
|
||||
LEFT JOIN unidesk_node_docker_status d ON d.provider_id = n.provider_id
|
||||
`,
|
||||
sql<Array<{ system_status_node_count: string | number }>>`
|
||||
SELECT count(*) FILTER (WHERE s.status IS NOT NULL)::int AS system_status_node_count
|
||||
FROM unidesk_nodes n
|
||||
LEFT JOIN unidesk_node_system_status s ON s.provider_id = n.provider_id
|
||||
`,
|
||||
countPendingTasks(),
|
||||
getPgdataUsage(),
|
||||
]);
|
||||
const nodeRow = nodeRows[0];
|
||||
const dockerRow = dockerRows[0];
|
||||
const systemRow = systemRows[0];
|
||||
return {
|
||||
service: "unidesk-core",
|
||||
ok: true,
|
||||
dbReady,
|
||||
pgdata,
|
||||
uptimeSeconds: Math.floor((Date.now() - serviceStartedAt.getTime()) / 1000),
|
||||
nodeCount: nodes.length,
|
||||
onlineNodeCount: online,
|
||||
dockerStatusNodeCount: dockerStatuses.filter((item) => item.dockerStatus !== null).length,
|
||||
systemStatusNodeCount: systemStatuses.filter((item) => item.current !== null).length,
|
||||
nodeCount: Number(nodeRow?.node_count ?? 0),
|
||||
onlineNodeCount: Number(nodeRow?.online_node_count ?? 0),
|
||||
dockerStatusNodeCount: Number(dockerRow?.docker_status_node_count ?? 0),
|
||||
systemStatusNodeCount: Number(systemRow?.system_status_node_count ?? 0),
|
||||
pendingTaskCount: pendingTasks,
|
||||
taskPendingTimeoutMs: config.taskPendingTimeoutMs,
|
||||
activeSocketCount: activeProviders.size,
|
||||
@@ -1432,12 +1612,27 @@ function responseFromMicroserviceResult(task: Awaited<ReturnType<typeof rawTask>
|
||||
}
|
||||
|
||||
function microserviceCacheTtlMs(serviceId: string, targetPath: string): number {
|
||||
if (serviceId === "pipeline" && targetPath === "/api/snapshot") return 6_000;
|
||||
if (serviceId === "pipeline" && targetPath.startsWith("/api/oa-event-flow/")) return 20_000;
|
||||
if (serviceId === "pipeline" && targetPath.startsWith("/api/model-quota/")) return 60_000;
|
||||
if (serviceId === "pipeline" && targetPath.startsWith("/api/node-control/runs/")) return 6_000;
|
||||
if (serviceId === "pipeline" && targetPath.startsWith("/api/runs/")) return 6_000;
|
||||
if (serviceId === "findjob" && (targetPath === "/api/summary" || targetPath === "/api/jobs" || targetPath === "/api/drafts")) return 8_000;
|
||||
if (serviceId === "met-nonlinear" && (targetPath === "/api/images" || targetPath === "/api/projects")) return 15_000;
|
||||
if (serviceId === "met-nonlinear" && (targetPath === "/api/queue" || targetPath === "/api/summary" || targetPath === "/api/history")) return 1_500;
|
||||
if (serviceId === "met-nonlinear" && (targetPath === "/api/queue" || targetPath === "/api/summary" || targetPath === "/api/history")) return 5_000;
|
||||
if (serviceId === "codex-queue" && targetPath.includes("/transcript")) return 1_000;
|
||||
return 750;
|
||||
}
|
||||
|
||||
function microserviceCacheStaleMs(serviceId: string, targetPath: string): number {
|
||||
if (serviceId === "pipeline" && targetPath.startsWith("/api/model-quota/")) return 5 * 60_000;
|
||||
if (serviceId === "pipeline") return 45_000;
|
||||
if (serviceId === "findjob") return 60_000;
|
||||
if (serviceId === "met-nonlinear" && (targetPath === "/api/images" || targetPath === "/api/projects")) return 5 * 60_000;
|
||||
if (serviceId === "met-nonlinear") return 45_000;
|
||||
return 5_000;
|
||||
}
|
||||
|
||||
function providerMicroserviceCacheTtlMs(serviceId: string, targetPath: string): number {
|
||||
if (serviceId === "met-nonlinear" && (targetPath === "/api/images" || targetPath === "/api/projects")) return 60_000;
|
||||
if (serviceId === "met-nonlinear" && targetPath === "/api/history") return 10_000;
|
||||
@@ -1451,22 +1646,38 @@ function microserviceCacheKey(service: MicroserviceConfig, method: string, targe
|
||||
return JSON.stringify([service.id, method, targetPath, proxyOptions.query, proxyOptions.jsonArrayLimits]);
|
||||
}
|
||||
|
||||
function readMicroserviceCache(key: string): Response | null {
|
||||
const entry = microserviceProxyCache.get(key);
|
||||
if (entry === undefined) return null;
|
||||
if (entry.expiresAt <= Date.now()) {
|
||||
microserviceProxyCache.delete(key);
|
||||
return null;
|
||||
}
|
||||
function responseFromMicroserviceCache(entry: MicroserviceProxyCacheEntry, state: "hit" | "stale"): Response {
|
||||
return new Response(entry.bodyText, {
|
||||
status: entry.status,
|
||||
headers: {
|
||||
"content-type": entry.contentType,
|
||||
"x-unidesk-cache": "hit",
|
||||
"x-unidesk-cache": state,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function readMicroserviceCache(key: string): Response | null {
|
||||
const entry = microserviceProxyCache.get(key);
|
||||
if (entry === undefined) return null;
|
||||
if (entry.staleExpiresAt <= Date.now()) {
|
||||
microserviceProxyCache.delete(key);
|
||||
return null;
|
||||
}
|
||||
if (entry.expiresAt <= Date.now()) return null;
|
||||
return responseFromMicroserviceCache(entry, "hit");
|
||||
}
|
||||
|
||||
function readStaleMicroserviceCache(key: string): Response | null {
|
||||
const entry = microserviceProxyCache.get(key);
|
||||
if (entry === undefined) return null;
|
||||
if (entry.staleExpiresAt <= Date.now()) {
|
||||
microserviceProxyCache.delete(key);
|
||||
return null;
|
||||
}
|
||||
if (entry.expiresAt > Date.now()) return null;
|
||||
return responseFromMicroserviceCache(entry, "stale");
|
||||
}
|
||||
|
||||
async function cacheableResponseSnapshot(response: Response): Promise<MicroserviceProxyCacheEntry | null> {
|
||||
if (response.status < 200 || response.status >= 300) return null;
|
||||
if (response.headers.get("x-unidesk-response-truncated") === "true") return null;
|
||||
@@ -1474,6 +1685,7 @@ async function cacheableResponseSnapshot(response: Response): Promise<Microservi
|
||||
if (bodyText.length > 2 * 1024 * 1024) return null;
|
||||
return {
|
||||
expiresAt: 0,
|
||||
staleExpiresAt: 0,
|
||||
status: response.status,
|
||||
contentType: response.headers.get("content-type") ?? "application/octet-stream",
|
||||
bodyText,
|
||||
@@ -1482,7 +1694,9 @@ async function cacheableResponseSnapshot(response: Response): Promise<Microservi
|
||||
|
||||
function rememberMicroserviceCache(key: string, ttlMs: number, entry: MicroserviceProxyCacheEntry | null): void {
|
||||
if (entry === null || ttlMs <= 0) return;
|
||||
const keyParts = JSON.parse(key) as string[];
|
||||
entry.expiresAt = Date.now() + ttlMs;
|
||||
entry.staleExpiresAt = entry.expiresAt + microserviceCacheStaleMs(String(keyParts[0] ?? ""), String(keyParts[2] ?? ""));
|
||||
microserviceProxyCache.set(key, entry);
|
||||
if (microserviceProxyCache.size > 300) {
|
||||
const now = Date.now();
|
||||
@@ -1550,6 +1764,56 @@ async function directMicroserviceResponse(
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchMicroserviceUpstreamResponse(
|
||||
service: MicroserviceConfig,
|
||||
method: string,
|
||||
targetPath: string,
|
||||
proxyOptions: { query: string; jsonArrayLimits: Record<string, JsonValue> },
|
||||
requestHeaders: Record<string, JsonValue>,
|
||||
bodyText: string,
|
||||
): Promise<Response> {
|
||||
if (canDirectProxyMicroservice(service)) {
|
||||
return directMicroserviceResponse(service, method, targetPath, proxyOptions, requestHeaders, bodyText);
|
||||
}
|
||||
if (!(await providerSupports(service.providerId, "microservice.http"))) {
|
||||
return jsonResponse({ ok: false, error: `provider does not declare microservice.http capability: ${service.providerId}` }, 409);
|
||||
}
|
||||
const { taskId, providerOnline } = await createAndSendTask(service.providerId, "microservice.http", {
|
||||
source: "microservice-frontend-proxy",
|
||||
serviceId: service.id,
|
||||
method,
|
||||
targetBaseUrl: service.backend.nodeBaseUrl,
|
||||
path: targetPath,
|
||||
query: proxyOptions.query,
|
||||
requestHeaders,
|
||||
bodyText,
|
||||
jsonArrayLimits: proxyOptions.jsonArrayLimits,
|
||||
timeoutMs: service.backend.timeoutMs,
|
||||
cacheTtlMs: providerMicroserviceCacheTtlMs(service.id, targetPath),
|
||||
});
|
||||
if (!providerOnline) return jsonResponse({ ok: false, error: `provider is offline: ${service.providerId}`, taskId }, 503);
|
||||
const task = await waitForTaskTerminal(taskId, service.backend.timeoutMs + 3000);
|
||||
return responseFromMicroserviceResult(task);
|
||||
}
|
||||
|
||||
function refreshMicroserviceCacheInBackground(
|
||||
cacheKey: string,
|
||||
ttlMs: number,
|
||||
fetchResponse: () => Promise<Response>,
|
||||
): void {
|
||||
if (microserviceProxyRefreshes.has(cacheKey)) return;
|
||||
const refresh = fetchResponse()
|
||||
.then((response) => cacheableResponseSnapshot(response))
|
||||
.then((entry) => rememberMicroserviceCache(cacheKey, ttlMs, entry))
|
||||
.catch((error) => {
|
||||
logger("warn", "microservice_cache_revalidate_failed", { cacheKey, error: errorToJson(error) });
|
||||
})
|
||||
.finally(() => {
|
||||
microserviceProxyRefreshes.delete(cacheKey);
|
||||
});
|
||||
microserviceProxyRefreshes.set(cacheKey, refresh);
|
||||
}
|
||||
|
||||
async function microserviceRoute(req: Request, url: URL): Promise<Response> {
|
||||
const rest = url.pathname.slice("/api/microservices/".length);
|
||||
const slashIndex = rest.indexOf("/");
|
||||
@@ -1580,12 +1844,9 @@ async function microserviceRoute(req: Request, url: URL): Promise<Response> {
|
||||
if (!isMicroservicePathAllowed(service, targetPath)) {
|
||||
return jsonResponse({ ok: false, error: "microservice path is not allowed", serviceId, targetPath }, 403);
|
||||
}
|
||||
const directProxy = canDirectProxyMicroservice(service);
|
||||
if (!directProxy && !(await providerSupports(service.providerId, "microservice.http"))) {
|
||||
return jsonResponse({ ok: false, error: `provider does not declare microservice.http capability: ${service.providerId}` }, 409);
|
||||
}
|
||||
const proxyOptions = readMicroserviceArrayLimits(url);
|
||||
const cacheKey = microserviceCacheKey(service, method, targetPath, proxyOptions);
|
||||
const cacheTtlMs = microserviceCacheTtlMs(service.id, targetPath);
|
||||
if (method === "GET" || method === "HEAD") {
|
||||
const cached = readMicroserviceCache(cacheKey);
|
||||
if (cached !== null) return cached;
|
||||
@@ -1599,32 +1860,15 @@ async function microserviceRoute(req: Request, url: URL): Promise<Response> {
|
||||
const requestHeaders: Record<string, JsonValue> = {};
|
||||
const contentType = req.headers.get("content-type");
|
||||
if (contentType !== null) requestHeaders["content-type"] = contentType.slice(0, 200);
|
||||
if (directProxy) {
|
||||
const response = await directMicroserviceResponse(service, method, targetPath, proxyOptions, requestHeaders, bodyText);
|
||||
if (method === "GET" || method === "HEAD") {
|
||||
rememberMicroserviceCache(cacheKey, microserviceCacheTtlMs(service.id, targetPath), await cacheableResponseSnapshot(response));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
const { taskId, providerOnline } = await createAndSendTask(service.providerId, "microservice.http", {
|
||||
source: "microservice-frontend-proxy",
|
||||
serviceId: service.id,
|
||||
method,
|
||||
targetBaseUrl: service.backend.nodeBaseUrl,
|
||||
path: targetPath,
|
||||
query: proxyOptions.query,
|
||||
requestHeaders,
|
||||
bodyText,
|
||||
jsonArrayLimits: proxyOptions.jsonArrayLimits,
|
||||
timeoutMs: service.backend.timeoutMs,
|
||||
cacheTtlMs: providerMicroserviceCacheTtlMs(service.id, targetPath),
|
||||
});
|
||||
if (!providerOnline) return jsonResponse({ ok: false, error: `provider is offline: ${service.providerId}`, taskId }, 503);
|
||||
const task = await waitForTaskTerminal(taskId, service.backend.timeoutMs + 3000);
|
||||
const response = responseFromMicroserviceResult(task);
|
||||
if (method === "GET" || method === "HEAD") {
|
||||
rememberMicroserviceCache(cacheKey, microserviceCacheTtlMs(service.id, targetPath), await cacheableResponseSnapshot(response));
|
||||
const stale = readStaleMicroserviceCache(cacheKey);
|
||||
if (stale !== null) {
|
||||
refreshMicroserviceCacheInBackground(cacheKey, cacheTtlMs, () => fetchMicroserviceUpstreamResponse(service, method, targetPath, proxyOptions, requestHeaders, bodyText));
|
||||
return stale;
|
||||
}
|
||||
}
|
||||
const response = await fetchMicroserviceUpstreamResponse(service, method, targetPath, proxyOptions, requestHeaders, bodyText);
|
||||
if (method === "GET" || method === "HEAD") rememberMicroserviceCache(cacheKey, cacheTtlMs, await cacheableResponseSnapshot(response));
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -1939,9 +2183,17 @@ async function routeInner(req: Request, server: Server<WsData>): Promise<Respons
|
||||
if (url.pathname === "/api/nodes/system-status") return jsonResponse({ ok: true, systemStatuses: await withPerformanceOperation("core", "node_system_status", url.search, () => getNodeSystemStatuses(readLimit(url, 60))) });
|
||||
if (url.pathname === "/api/nodes/docker-status") return jsonResponse({ ok: true, dockerStatuses: await withPerformanceOperation("core", "node_docker_status", url.pathname, () => getNodeDockerStatuses()) });
|
||||
if (url.pathname === "/api/events") return jsonResponse({ ok: true, events: await withPerformanceOperation("core", "events", url.search, () => getEvents(readLimit(url, 100))) });
|
||||
if (url.pathname.startsWith("/api/tasks/") && req.method === "GET") {
|
||||
const taskId = decodeURIComponent(url.pathname.slice("/api/tasks/".length));
|
||||
if (taskId.length === 0 || taskId.includes("/")) return jsonResponse({ ok: false, error: "invalid task id" }, 400);
|
||||
const task = await withPerformanceOperation("core", "task_detail", taskId, () => getTask(taskId));
|
||||
if (task === null) return jsonResponse({ ok: false, error: `task not found: ${taskId}` }, 404);
|
||||
return jsonResponse({ ok: true, task });
|
||||
}
|
||||
if (url.pathname === "/api/tasks") {
|
||||
const lite = ["1", "true", "yes"].includes((url.searchParams.get("lite") ?? "").toLowerCase());
|
||||
return jsonResponse({ ok: true, tasks: await withPerformanceOperation("core", "tasks", url.search, () => getTasks(readLimit(url, 100), url.searchParams.get("status") ?? "all", lite)) });
|
||||
const summary = ["1", "true", "yes"].includes((url.searchParams.get("summary") ?? "").toLowerCase());
|
||||
return jsonResponse({ ok: true, tasks: await withPerformanceOperation("core", "tasks", url.search, () => getTasks(readLimit(url, 100), url.searchParams.get("status") ?? "all", lite, summary)) });
|
||||
}
|
||||
if (url.pathname === "/api/microservices") return jsonResponse({ ok: true, microservices: await withPerformanceOperation("core", "microservices", url.pathname, () => getMicroservices()) });
|
||||
if (url.pathname === "/api/performance") return jsonResponse(await getPerformance());
|
||||
|
||||
Reference in New Issue
Block a user