chore: checkpoint before performance tuning

This commit is contained in:
Codex
2026-05-11 07:39:37 +00:00
parent a278de032d
commit 5a198baf77
57 changed files with 14768 additions and 1962 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@unidesk/provider-gateway",
"version": "0.2.9",
"version": "0.2.12",
"private": true,
"type": "module",
"scripts": {
+149 -9
View File
@@ -66,6 +66,12 @@ let stopping = false;
let upgradeSleepUntil = 0;
let upgradeSleepTimer: ReturnType<typeof setTimeout> | null = null;
interface MicroserviceHttpCacheEntry {
createdAt: number;
expiresAt: number;
result: JsonValue;
}
interface HostSshSession {
proc: ReturnType<typeof Bun.spawn>;
openedAt: number;
@@ -78,7 +84,10 @@ interface HostSshStdin {
}
const hostSshSessions = new Map<string, HostSshSession>();
const microserviceHttpCache = new Map<string, MicroserviceHttpCacheEntry>();
const microserviceHttpInFlight = new Map<string, Promise<JsonValue>>();
const gatewayMetadata = readGatewayMetadata();
const microserviceHttpMaxBodyTextLength = 8 * 1024 * 1024;
function readGatewayMetadataFile(path: string): { name: string; version: string } | null {
try {
@@ -201,6 +210,7 @@ function currentLabels(): ProviderLabels {
providerGatewayVersion: gatewayMetadata.version,
providerGatewayStartedAt: startedAt.toISOString(),
providerGatewayUpgradePolicy: "always-enabled",
providerGatewayMicroserviceHttpCache: true,
gatewayUptimeSeconds: Math.floor((Date.now() - startedAt.getTime()) / 1000),
};
}
@@ -211,7 +221,7 @@ function sendJson(value: unknown): void {
}
function sendRegister(): void {
const capabilities = ["heartbeat", "system.status", "docker.status", "docker.ps", "provider.upgrade", "microservice.http", "echo"];
const capabilities = ["heartbeat", "system.status", "docker.status", "docker.ps", "provider.upgrade", "microservice.http", "microservice.http.cache", "echo"];
if (isHostSshConfigured()) capabilities.push("host.ssh");
sendJson({
type: "register",
@@ -1334,6 +1344,13 @@ function payloadNumber(payload: Record<string, JsonValue>, key: string, fallback
return Math.floor(value);
}
function payloadBoundedNumber(payload: Record<string, JsonValue>, key: string, fallback: number, min: number, max: number): number {
const raw = payload[key];
const value = typeof raw === "number" ? raw : typeof raw === "string" ? Number(raw) : fallback;
if (!Number.isFinite(value)) return fallback;
return Math.max(min, Math.min(max, Math.floor(value)));
}
function payloadJsonArrayLimits(payload: Record<string, JsonValue>): Record<string, number> {
const raw = payload.jsonArrayLimits;
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
@@ -1391,6 +1408,92 @@ function applyJsonArrayLimits(bodyText: string, contentType: string, limits: Rec
}
}
function contentTypeIsJson(contentType: string): boolean {
return contentType.toLowerCase().includes("json");
}
function boundedMicroserviceBodyText(
bodyText: string,
contentType: string,
metadata: { serviceId: string; path: string; status: number; upstreamBodyBytes: number },
): { bodyText: string; truncated: boolean } {
if (bodyText.length <= microserviceHttpMaxBodyTextLength) {
return { bodyText, truncated: false };
}
if (contentTypeIsJson(contentType)) {
return {
bodyText: JSON.stringify({
ok: false,
error: "microservice proxy response body is too large",
serviceId: metadata.serviceId,
path: metadata.path,
upstreamStatus: metadata.status,
upstreamBodyBytes: metadata.upstreamBodyBytes,
transformedBodyBytes: bodyText.length,
responseBodyLimitBytes: microserviceHttpMaxBodyTextLength,
hint: "Use a paged endpoint or tighten __unideskArrayLimit so the response stays below the proxy safety limit.",
}),
truncated: true,
};
}
return { bodyText: truncateText(bodyText, microserviceHttpMaxBodyTextLength), truncated: true };
}
function microserviceHttpCacheKey(serviceId: string, targetBaseUrl: string, method: string, path: string, query: string, jsonArrayLimits: Record<string, number>): string {
return JSON.stringify([serviceId, targetBaseUrl, method, path, query, jsonArrayLimits]);
}
function microserviceHttpCachePrefix(serviceId: string, targetBaseUrl: string): string {
return JSON.stringify([serviceId, targetBaseUrl]).slice(0, -1);
}
function cloneJsonObject(value: JsonValue): Record<string, JsonValue> {
return value && typeof value === "object" && !Array.isArray(value) ? { ...value as Record<string, JsonValue> } : { value };
}
function resultWithCacheMetadata(result: JsonValue, metadata: Record<string, JsonValue>): JsonValue {
const record = cloneJsonObject(result);
record.cache = metadata;
return record;
}
function readMicroserviceHttpCache(key: string): JsonValue | null {
const entry = microserviceHttpCache.get(key);
if (entry === undefined) return null;
const now = Date.now();
if (entry.expiresAt <= now) {
microserviceHttpCache.delete(key);
return null;
}
return resultWithCacheMetadata(entry.result, {
hit: true,
ageMs: now - entry.createdAt,
ttlRemainingMs: entry.expiresAt - now,
layer: "provider-gateway",
});
}
function rememberMicroserviceHttpCache(key: string, ttlMs: number, result: JsonValue): void {
if (ttlMs <= 0 || !result || typeof result !== "object" || Array.isArray(result)) return;
const record = result as Record<string, JsonValue>;
if (record.ok !== true || record.upstreamOk !== true) return;
if (record.truncated === true) return;
const now = Date.now();
microserviceHttpCache.set(key, { createdAt: now, expiresAt: now + ttlMs, result: { ...record } });
if (microserviceHttpCache.size > 300) {
for (const [cacheKey, entry] of microserviceHttpCache) {
if (entry.expiresAt <= now || microserviceHttpCache.size > 240) microserviceHttpCache.delete(cacheKey);
}
}
}
function invalidateMicroserviceHttpCache(serviceId: string, targetBaseUrl: string): void {
const prefix = microserviceHttpCachePrefix(serviceId, targetBaseUrl);
for (const key of microserviceHttpCache.keys()) {
if (key.startsWith(prefix)) microserviceHttpCache.delete(key);
}
}
async function runMicroserviceHttp(payload: Record<string, JsonValue>): Promise<JsonValue> {
const rawMethod = String(payload.method || "GET").toUpperCase();
const allowedMethods = new Set(["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"]);
@@ -1407,15 +1510,35 @@ async function runMicroserviceHttp(payload: Record<string, JsonValue>): Promise<
const timeoutMs = Math.max(1000, Math.min(30_000, payloadNumber(payload, "timeoutMs", 10_000)));
const jsonArrayLimits = payloadJsonArrayLimits(payload);
const bodyText = payloadString(payload, "bodyText") ?? "";
const serviceId = payloadString(payload, "serviceId") ?? "unknown";
const cacheTtlMs = payloadBoundedNumber(payload, "cacheTtlMs", 0, 0, 60_000);
const cacheable = cacheTtlMs > 0 && (method === "GET" || method === "HEAD") && bodyText.length === 0;
const cacheKey = cacheable ? microserviceHttpCacheKey(serviceId, targetBaseUrl, method, path, query, jsonArrayLimits) : "";
if (cacheable) {
const cached = readMicroserviceHttpCache(cacheKey);
if (cached !== null) return cached;
const inFlight = microserviceHttpInFlight.get(cacheKey);
if (inFlight !== undefined) {
const result = await inFlight;
return resultWithCacheMetadata(result, {
hit: true,
deduped: true,
layer: "provider-gateway",
});
}
} else if (method !== "GET" && method !== "HEAD") {
invalidateMicroserviceHttpCache(serviceId, targetBaseUrl);
}
const requestHeaders = typeof payload.requestHeaders === "object" && payload.requestHeaders !== null && !Array.isArray(payload.requestHeaders)
? payload.requestHeaders as Record<string, JsonValue>
: {};
const headers = new Headers();
const contentType = typeof requestHeaders["content-type"] === "string" ? requestHeaders["content-type"] : "";
if (contentType.length > 0) headers.set("content-type", contentType);
const requestStartedAt = Date.now();
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const requestPromise = (async (): Promise<JsonValue> => {
const response = await fetch(url, {
method,
headers,
@@ -1425,39 +1548,56 @@ async function runMicroserviceHttp(payload: Record<string, JsonValue>): Promise<
const rawBodyText = await response.text();
const contentType = response.headers.get("content-type") ?? "text/plain; charset=utf-8";
const transformed = applyJsonArrayLimits(rawBodyText, contentType, jsonArrayLimits);
const bounded = boundedMicroserviceBodyText(transformed.bodyText, contentType, {
serviceId,
path,
status: response.status,
upstreamBodyBytes: rawBodyText.length,
});
return {
ok: true,
serviceId: payloadString(payload, "serviceId") ?? "unknown",
serviceId,
method,
url: url.toString(),
status: response.status,
upstreamOk: response.ok,
contentType,
bodyText: truncateText(transformed.bodyText, 1024 * 1024),
bodyText: bounded.bodyText,
upstreamBodyBytes: rawBodyText.length,
returnedBodyBytes: Math.min(transformed.bodyText.length, 1024 * 1024),
truncated: transformed.bodyText.length > 1024 * 1024,
transformedBodyBytes: transformed.bodyText.length,
returnedBodyBytes: bounded.bodyText.length,
responseBodyLimitBytes: microserviceHttpMaxBodyTextLength,
truncated: bounded.truncated,
transform: transformed.transform,
upstreamDurationMs: Date.now() - requestStartedAt,
};
})();
if (cacheable) microserviceHttpInFlight.set(cacheKey, requestPromise);
try {
const result = await requestPromise;
if (cacheable) rememberMicroserviceHttpCache(cacheKey, cacheTtlMs, result);
return result;
} catch (error) {
return {
ok: false,
serviceId: payloadString(payload, "serviceId") ?? "unknown",
serviceId,
method,
url: url.toString(),
timeoutMs,
error: error instanceof Error ? error.message : String(error),
};
} finally {
if (cacheable && microserviceHttpInFlight.get(cacheKey) === requestPromise) microserviceHttpInFlight.delete(cacheKey);
clearTimeout(timer);
}
}
async function handleDispatch(message: CoreDispatchMessage): Promise<void> {
logger("info", "dispatch_received", { taskId: message.taskId, command: message.command, payload: message.payload });
await sendTaskStatus(message.taskId, "accepted", "provider accepted task");
const verboseLifecycle = message.command !== "microservice.http";
if (verboseLifecycle) await sendTaskStatus(message.taskId, "accepted", "provider accepted task");
try {
await sendTaskStatus(message.taskId, "running", "provider running task");
if (verboseLifecycle) await sendTaskStatus(message.taskId, "running", "provider running task");
if (message.command === "docker.ps") {
const result = await runDockerPs();
await sendTaskStatus(message.taskId, "succeeded", "docker ps completed", result);