feat: add shadow LangGraph orchestrator runtime
This commit is contained in:
@@ -146,6 +146,72 @@ function engineCatalog(config) {
|
||||
];
|
||||
}
|
||||
|
||||
async function probeServiceHealth(config, {
|
||||
fetchImpl = globalThis.fetch,
|
||||
} = {}) {
|
||||
const checkedAt = Date.now();
|
||||
if (!config.serviceUrl) {
|
||||
return {
|
||||
checkedAt,
|
||||
ok: false,
|
||||
status: 'unconfigured',
|
||||
latencyMs: 0,
|
||||
httpStatus: null,
|
||||
details: null,
|
||||
};
|
||||
}
|
||||
if (typeof fetchImpl !== 'function') {
|
||||
return {
|
||||
checkedAt,
|
||||
ok: false,
|
||||
status: 'fetch_unavailable',
|
||||
latencyMs: 0,
|
||||
httpStatus: null,
|
||||
details: null,
|
||||
};
|
||||
}
|
||||
const startedAt = Date.now();
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(
|
||||
() => controller.abort(),
|
||||
Math.max(500, Number(config.requestTimeoutMs) || 5000),
|
||||
);
|
||||
try {
|
||||
const response = await fetchImpl(`${config.serviceUrl}/ready`, {
|
||||
headers: { accept: 'application/json' },
|
||||
signal: controller.signal,
|
||||
});
|
||||
const body = await response.json().catch(() => null);
|
||||
const healthy = response.ok && body?.status === 'ok';
|
||||
return {
|
||||
checkedAt,
|
||||
ok: healthy,
|
||||
status: healthy ? 'healthy' : 'unhealthy',
|
||||
latencyMs: Math.max(0, Date.now() - startedAt),
|
||||
httpStatus: response.status,
|
||||
details: body && typeof body === 'object' ? {
|
||||
service: body.service == null ? null : String(body.service).slice(0, 128),
|
||||
checkpoint: body.checkpoint && typeof body.checkpoint === 'object' ? {
|
||||
kind: body.checkpoint.kind == null ? null : String(body.checkpoint.kind).slice(0, 64),
|
||||
durable: Boolean(body.checkpoint.durable),
|
||||
} : null,
|
||||
execution: body.execution == null ? null : String(body.execution).slice(0, 64),
|
||||
} : null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
checkedAt,
|
||||
ok: false,
|
||||
status: error?.name === 'AbortError' ? 'timeout' : 'unreachable',
|
||||
latencyMs: Math.max(0, Date.now() - startedAt),
|
||||
httpStatus: null,
|
||||
details: null,
|
||||
};
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureConfigTable(pool) {
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS ${CONFIG_TABLE} (
|
||||
@@ -177,7 +243,10 @@ async function loadStoredState(pool) {
|
||||
};
|
||||
}
|
||||
|
||||
export function createOrchestratorAdminConfigService(pool, { env = process.env } = {}) {
|
||||
export function createOrchestratorAdminConfigService(pool, {
|
||||
env = process.env,
|
||||
fetchImpl = globalThis.fetch,
|
||||
} = {}) {
|
||||
async function loadEffectiveState() {
|
||||
const stored = await loadStoredState(pool);
|
||||
if (stored) return { ...stored, source: 'admin-db' };
|
||||
@@ -232,12 +301,13 @@ export function createOrchestratorAdminConfigService(pool, { env = process.env }
|
||||
return this.getAdminConfig();
|
||||
},
|
||||
|
||||
async getRuntimeState() {
|
||||
async getRuntimeState({ probe = false } = {}) {
|
||||
const state = await loadEffectiveState();
|
||||
return {
|
||||
...state,
|
||||
runtime: runtimeState(state.config, env),
|
||||
engines: engineCatalog(state.config),
|
||||
...(probe ? { serviceHealth: await probeServiceHealth(state.config, { fetchImpl }) } : {}),
|
||||
};
|
||||
},
|
||||
|
||||
@@ -292,5 +362,6 @@ export const orchestratorAdminConfigInternals = {
|
||||
CONFIG_SCOPE,
|
||||
CONFIG_TABLE,
|
||||
engineCatalog,
|
||||
probeServiceHealth,
|
||||
runtimeState,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user