unrip/src/core/runtime-health.mjs
philipp ddd3dfb9e2
All checks were successful
deploy / deploy (push) Successful in 58s
Remove verifier salt refresh from executor hot path
Proof: quote-response execution now uses verifierSaltCache.getCachedFreshSalt() only, rejects verifier_salt_unavailable before signing or relay when no fresh cached salt exists, preserves stale command expiry before salt lookup, exposes salt cache and executor queue-delay state in health/sentinel/dashboard surfaces, and is covered by targeted regression tests plus full npm test and dashboard build.

Assumptions: the existing verifier salt freshness policy remains safe for cached signing use; fixed in-code queue-delay warning thresholds are sufficient for operator alerts without new latency env vars; no strategy selection, pricing, edge, notional, inventory, arming, signer, or pair enablement behavior changed.

Still fake: venue-native terminal fill ids and fee-complete realized PnL remain unavailable; executor concurrency remains single-consumer; live post-deploy evidence still needs collection after repo-workflow deployment.
2026-06-12 18:00:03 +02:00

576 lines
19 KiB
JavaScript

export const SERVICE_HEALTH_LEVELS = ['healthy', 'warning', 'critical', 'offline', 'paused'];
export const DEFAULT_EXECUTOR_QUEUE_DELAY_WARNING_MS = 1_000;
export const DEFAULT_EXECUTOR_QUEUE_DELAY_CRITICAL_MS = 5_000;
const HEALTH_RANK = {
healthy: 0,
warning: 1,
critical: 2,
offline: 3,
paused: 1,
};
const ALERT_RANK = {
info: 0,
warning: 1,
critical: 2,
};
export function createRuntimeHealthThresholds(config = {}) {
return {
ingestMessageStaleMs: Number(config.opsSentinelIngestMessageStaleMs || 30_000),
ingestQuoteStaleMs: Number(config.opsSentinelIngestQuoteStaleMs || 30_000),
ingestPublishStaleMs: Number(config.opsSentinelIngestPublishStaleMs || 30_000),
executorRelayStaleMs: Number(config.opsSentinelExecutorRelayStaleMs || 30_000),
historyWriterStaleMs: Number(config.opsSentinelHistoryWriterStaleMs || 45_000),
dashboardSourceDegradedMs: Number(config.opsSentinelDashboardSourceDegradedMs || 30_000),
sentinelStaleMs: Number(config.opsSentinelSelfStaleMs || 20_000),
anomalyWindowSize: Number(config.opsSentinelAnomalyWindowSize || 6),
anomalyQuoteRateCollapseRatio: Number(config.opsSentinelAnomalyQuoteRateCollapseRatio || 0.25),
anomalyReconnectSpikeMultiplier: Number(config.opsSentinelAnomalyReconnectSpikeMultiplier || 2),
containmentCooldownMs: Number(config.opsSentinelContainmentCooldownMs || 60_000),
executorQueueDelayWarningMs: Number(
config.executorQueueDelayWarningMs || DEFAULT_EXECUTOR_QUEUE_DELAY_WARNING_MS,
),
executorQueueDelayCriticalMs: Number(
config.executorQueueDelayCriticalMs || DEFAULT_EXECUTOR_QUEUE_DELAY_CRITICAL_MS,
),
};
}
export function evaluateRuntimeHealth({
servicesByName,
activePair,
activePairs = activePair ? [activePair] : [],
activeAlerts = [],
now = new Date().toISOString(),
} = {}) {
const serviceHealth = new Map();
const alertIndex = indexAlertsByService(activeAlerts);
for (const [service, snapshot] of Object.entries(servicesByName || {})) {
const alerts = alertIndex.get(service) || [];
serviceHealth.set(service, deriveServiceHealth({
service,
snapshot,
activePair,
activePairs,
activeAlerts: alerts,
now,
}));
}
return serviceHealth;
}
export function deriveServiceHealth({
service,
snapshot,
activePair = null,
activePairs = activePair ? [activePair] : [],
activeAlerts = [],
now = new Date().toISOString(),
} = {}) {
const state = snapshot?.state || {};
const health = snapshot?.health || {};
const reachable = snapshot?.reachable !== false;
const paused = state.paused ?? health.paused ?? false;
const highestAlertSeverity = highestAlertSeverityForService(activeAlerts);
const freshnessAt = inferServiceFreshnessTimestamp(service, state, health);
const freshnessAgeMs = ageMs(freshnessAt, now);
const reasons = [];
let status = paused ? 'paused' : reachable ? 'healthy' : 'offline';
let label = status;
if (!reachable) {
reasons.push('service unreachable');
label = 'offline';
}
if (service === 'near-intents-ingest') {
const ingestClassification = classifyNearIntentsIngestHealth({
state,
health,
activeAlerts,
reachable,
now,
});
status = escalateHealth(status, ingestClassification.status);
label = ingestClassification.label;
reasons.push(...ingestClassification.reasons);
} else {
if (health.ok === false && reachable) {
status = escalateHealth(status, 'critical');
reasons.push(health.reason || 'service health check failed');
}
if (highestAlertSeverity === 'critical') {
status = escalateHealth(status, 'critical');
reasons.push(`critical alert active (${activeAlerts[0]?.alert_code || 'runtime'})`);
} else if (highestAlertSeverity === 'warning') {
status = escalateHealth(status, 'warning');
reasons.push(`warning alert active (${activeAlerts[0]?.alert_code || 'runtime'})`);
}
}
if (service !== 'near-intents-ingest' && status === 'healthy') {
label = 'healthy';
}
if (service !== 'near-intents-ingest' && status === 'paused') {
label = 'paused';
}
if (service !== 'near-intents-ingest' && status === 'critical' && label === 'critical') {
label = 'critical';
}
if (service !== 'near-intents-ingest' && status === 'warning' && label === 'warning') {
label = 'warning';
}
if (service !== 'near-intents-ingest' && status === 'offline') {
label = 'offline';
}
if (service === 'trade-executor' && state.relay?.connected === false) {
status = escalateHealth(status, 'critical');
label = 'relay disconnected';
if (!reasons.includes('solver relay disconnected')) {
reasons.push('solver relay disconnected');
}
}
if (service === 'trade-executor') {
const saltState = state.verifier_salt_cache || health.verifier_salt_cache || null;
const queueDelay = state.executor_queue_delay || health.executor_queue_delay || null;
if (saltState?.configured && saltState.salt_fresh !== true) {
status = escalateHealth(status, 'warning');
if (label === 'healthy') label = 'salt unavailable';
reasons.push('verifier salt cache is stale or unavailable');
}
if (saltState?.last_refresh_error) {
status = escalateHealth(status, 'warning');
if (label === 'healthy') label = 'salt refresh failed';
reasons.push('verifier salt refresh failed');
}
if (queueDelay?.status === 'critical') {
status = escalateHealth(status, 'critical');
label = 'queue delayed';
reasons.push(queueDelay.warning_reason || 'executor queue delay is critical');
} else if (queueDelay?.status === 'warning') {
status = escalateHealth(status, 'warning');
if (label === 'healthy') label = 'queue delayed';
reasons.push(queueDelay.warning_reason || 'executor queue delay is high');
}
}
if (service === 'history-writer') {
if (state.database_connectivity === false) {
status = escalateHealth(status, 'critical');
label = 'database disconnected';
reasons.push('database connectivity failed');
} else if (freshnessAgeMs != null && freshnessAgeMs > 45_000) {
status = escalateHealth(status, 'warning');
label = 'writer lagging';
reasons.push('writer freshness degraded');
}
if (state.retention_mode === 'blocked_invalid_policy') {
status = escalateHealth(status, 'warning');
label = 'retention blocked';
reasons.push('quote lifecycle retention policy is invalid or missing');
} else if (state.retention_mode === 'blocked_rollup_stale') {
status = escalateHealth(status, 'warning');
label = 'rollup stale';
reasons.push('quote lifecycle rollup is stale, pruning is fail-closed');
} else if (state.retention_mode === 'pressure') {
status = escalateHealth(status, 'warning');
label = 'storage pressure';
reasons.push('quote lifecycle storage pressure retention mode is active');
}
}
if (service === 'operator-dashboard') {
if ((state.source_error_count || 0) > 0 || (health.source_error_count || 0) > 0) {
status = escalateHealth(status, 'warning');
label = 'sources degraded';
reasons.push('dashboard source degraded');
}
}
if (
['strategy-engine', 'trade-executor'].includes(service)
&& (state.armed ?? false)
&& hasCriticalTruthAlert(activeAlerts, activePairs.length ? activePairs : activePair)
) {
status = escalateHealth(status, 'critical');
if (label === 'healthy' || label === 'warning') {
label = 'armed on stale truth';
}
reasons.push('armed while critical upstream truth is stale');
}
return {
service,
status,
label,
reachable,
paused,
armed: state.armed ?? null,
health_ok: status === 'healthy' || status === 'paused',
highest_alert_severity: highestAlertSeverity,
reasons: dedupeReasons(reasons),
freshness_at: freshnessAt,
freshness_age_ms: freshnessAgeMs,
};
}
function classifyNearIntentsIngestHealth({
state,
health,
activeAlerts,
reachable,
now,
}) {
const reasons = [];
const alertCodes = new Set((activeAlerts || []).map((alert) => alert.alert_code));
const connected = state.ingest?.connected ?? health.connected ?? null;
const matchingAgeMs = ageMs(state.ingest?.last_matching_quote_at, now);
const publishedAgeMs = ageMs(state.ingest?.last_published_at, now);
if (!reachable) {
return {
status: 'offline',
label: 'offline',
reasons: ['service unreachable'],
};
}
if (connected === false || alertCodes.has('near_intents_ingest_disconnected')) {
return {
status: 'critical',
label: 'disconnected',
reasons: ['websocket disconnected', 'critical alert active (near_intents_ingest_disconnected)'],
};
}
if (
alertCodes.has('near_intents_publish_stale')
|| (
state.ingest?.last_matching_quote_at
&& state.ingest?.last_published_at
&& matchingAgeMs != null
&& publishedAgeMs != null
&& publishedAgeMs > matchingAgeMs + 5_000
)
) {
return {
status: 'critical',
label: 'publish stalled',
reasons: ['quote publish path stalled', 'critical alert active (near_intents_publish_stale)'],
};
}
if (alertCodes.has('near_intents_quotes_stale') || health.reason === 'quote truth stale') {
reasons.push('connected, no recent quotes for active pair');
if (matchingAgeMs != null) {
reasons.push(`last matching quote ${matchingAgeMs}ms ago`);
}
return {
status: 'warning',
label: 'no recent quotes',
reasons,
};
}
return {
status: health.ok === false ? 'warning' : 'healthy',
label: connected === true ? 'healthy' : 'unknown',
reasons: health.ok === false ? [health.reason || 'service health degraded'] : [],
};
}
function dedupeReasons(reasons) {
return [...new Set((reasons || []).filter(Boolean))];
}
export function inferServiceFreshnessTimestamp(service, state = {}, health = {}) {
switch (service) {
case 'near-intents-ingest':
return (
state.ingest?.last_published_at
|| state.ingest?.last_matching_quote_at
|| state.ingest?.last_message_at
|| null
);
case 'market-reference-ingest':
return state.last_published_at || null;
case 'inventory-sync':
return state.last_sync_at || null;
case 'liquidity-manager':
return state.last_refresh_at || null;
case 'history-writer':
return state.last_write_at || state.last_metrics_at || null;
case 'ops-sentinel':
return state.last_runtime_eval_at || state.last_evaluated_at || health.last_event_at || null;
case 'strategy-engine':
return state.latest_decision?.decision_at || state.latest_inventory_event?.ingested_at || null;
case 'trade-executor':
return state.relay?.last_message_at || state.last_quote_status?.created_at || null;
case 'operator-dashboard':
return state.last_bootstrap_at || state.last_source_error_at || null;
default:
return null;
}
}
export function buildRuntimeAlert({
alert_code,
severity,
reason,
service_scope,
pair = null,
asset_id = null,
tx_hash = null,
details = {},
}) {
return {
alert_code,
severity,
reason,
service_scope,
pair,
asset_id,
tx_hash,
details,
};
}
export function buildMakerCompetitivenessRuntimeAlerts({
makerCompetitiveness,
minPairSamples = 5,
minQuoteNotFoundOrFinishedRate = 0.4,
} = {}) {
const groups = Array.isArray(makerCompetitiveness?.groups) ? makerCompetitiveness.groups : [];
const pairStats = new Map();
for (const group of groups) {
const pair = group.pair || null;
if (!pair) continue;
const current = pairStats.get(pair) || {
pair,
sample_count: 0,
quote_not_found_or_finished_count: 0,
accepted_count: 0,
policy_skip_count: 0,
worst_group: null,
};
const count = Number(group.count || 0);
const staleFinishedCount = group.failure_category === 'quote_not_found_or_finished'
? count
: 0;
current.sample_count += count;
current.quote_not_found_or_finished_count += staleFinishedCount;
current.accepted_count += Number(group.accepted_count || 0);
current.policy_skip_count += Number(group.policy_skip_count || 0);
if (
staleFinishedCount > 0
&& (!current.worst_group || staleFinishedCount > Number(current.worst_group.count || 0))
) {
current.worst_group = group;
}
pairStats.set(pair, current);
}
return [...pairStats.values()]
.filter((stats) => {
if (stats.sample_count < minPairSamples) return false;
if (stats.quote_not_found_or_finished_count <= 0) return false;
return stats.quote_not_found_or_finished_count / stats.sample_count >= minQuoteNotFoundOrFinishedRate;
})
.map((stats) => {
const rate = stats.quote_not_found_or_finished_count / stats.sample_count;
return buildRuntimeAlert({
alert_code: 'maker_quote_not_found_or_finished_rate_high',
severity: 'warning',
reason: `maker responses for ${stats.pair} are frequently reaching relay after the quote is finished`,
service_scope: 'strategy-engine',
pair: stats.pair,
details: {
generated_at: makerCompetitiveness?.generated_at || null,
sample_count: stats.sample_count,
quote_not_found_or_finished_count: stats.quote_not_found_or_finished_count,
quote_not_found_or_finished_rate: Number(rate.toFixed(4)),
accepted_count: stats.accepted_count,
policy_skip_count: stats.policy_skip_count,
min_pair_samples: minPairSamples,
min_quote_not_found_or_finished_rate: minQuoteNotFoundOrFinishedRate,
worst_direction: stats.worst_group?.direction || null,
worst_request_kind: stats.worst_group?.request_kind || null,
worst_quote_age_bucket: stats.worst_group?.quote_age_bucket || null,
worst_notional_bucket: stats.worst_group?.notional_bucket || null,
},
});
});
}
export function buildExecutorRuntimeAlerts({
executor,
thresholds = createRuntimeHealthThresholds(),
now = new Date().toISOString(),
pair = null,
} = {}) {
void now;
const alerts = [];
const state = executor?.state || {};
const health = executor?.health || {};
const saltState = state.verifier_salt_cache || health.verifier_salt_cache || null;
const queueDelay = state.executor_queue_delay || health.executor_queue_delay || null;
if (saltState?.configured && saltState.salt_fresh !== true) {
alerts.push(buildRuntimeAlert({
alert_code: 'trade_executor_verifier_salt_unavailable',
severity: 'warning',
reason: saltState.has_cached_salt
? 'trade-executor verifier salt cache is stale'
: 'trade-executor verifier salt cache is unavailable',
service_scope: 'trade-executor',
pair,
details: {
has_cached_salt: saltState.has_cached_salt ?? null,
salt_fresh: saltState.salt_fresh ?? saltState.fresh ?? null,
salt_age_ms: saltState.salt_age_ms ?? saltState.cached_age_ms ?? null,
max_age_ms: saltState.max_age_ms ?? null,
refresh_in_flight: saltState.refresh_in_flight ?? null,
last_refresh_started_at: saltState.last_refresh_started_at || null,
last_refresh_completed_at: saltState.last_refresh_completed_at || null,
last_refresh_error: saltState.last_refresh_error || null,
},
}));
}
if (saltState?.last_refresh_error) {
alerts.push(buildRuntimeAlert({
alert_code: 'trade_executor_verifier_salt_refresh_failed',
severity: 'warning',
reason: 'trade-executor verifier salt refresh failed',
service_scope: 'trade-executor',
pair,
details: {
last_refresh_error: saltState.last_refresh_error,
last_refresh_started_at: saltState.last_refresh_started_at || null,
last_refresh_completed_at: saltState.last_refresh_completed_at || null,
last_refresh_duration_ms: saltState.last_refresh_duration_ms ?? null,
refresh_in_flight: saltState.refresh_in_flight ?? null,
has_cached_salt: saltState.has_cached_salt ?? null,
salt_fresh: saltState.salt_fresh ?? saltState.fresh ?? null,
},
}));
}
const queueMaxMs = Number(queueDelay?.max_ms);
if (Number.isFinite(queueMaxMs) && queueMaxMs >= thresholds.executorQueueDelayWarningMs) {
const critical = queueMaxMs >= thresholds.executorQueueDelayCriticalMs;
alerts.push(buildRuntimeAlert({
alert_code: 'trade_executor_command_queue_delay_high',
severity: critical ? 'critical' : 'warning',
reason: `trade-executor command queue delay ${queueMaxMs}ms exceeds ${
critical ? thresholds.executorQueueDelayCriticalMs : thresholds.executorQueueDelayWarningMs
}ms`,
service_scope: 'trade-executor',
pair,
details: {
status: queueDelay.status || (critical ? 'critical' : 'warning'),
latest_ms: queueDelay.latest_ms ?? null,
max_ms: queueDelay.max_ms ?? null,
p50_ms: queueDelay.p50_ms ?? null,
p90_ms: queueDelay.p90_ms ?? null,
p99_ms: queueDelay.p99_ms ?? null,
sample_count: queueDelay.sample_count ?? null,
warning_after_ms: thresholds.executorQueueDelayWarningMs,
critical_after_ms: thresholds.executorQueueDelayCriticalMs,
latest_command_id: queueDelay.latest_command_id || null,
latest_quote_id: queueDelay.latest_quote_id || null,
},
}));
}
return alerts;
}
export function shouldRaiseIngestPublishStale({
lastMatchingQuoteAt = null,
lastPublishedAt = null,
matchingQuoteAgeMs = null,
publishedAgeMs = null,
publishStaleMs,
} = {}) {
if (!lastMatchingQuoteAt) return false;
if (!lastPublishedAt) return true;
if (publishedAgeMs == null || publishedAgeMs > publishStaleMs) return true;
if (
matchingQuoteAgeMs != null
&& matchingQuoteAgeMs <= publishStaleMs
&& publishedAgeMs > matchingQuoteAgeMs + 5_000
) {
return true;
}
return false;
}
export function shouldContainExecutorForAlerts(alerts = []) {
void alerts;
return false;
}
export function ageMs(value, now = new Date().toISOString()) {
if (!value) return null;
const left = new Date(value).getTime();
const right = new Date(now).getTime();
if (!Number.isFinite(left) || !Number.isFinite(right)) return null;
return Math.max(0, right - left);
}
function highestAlertSeverityForService(alerts) {
let highest = null;
let highestRank = -1;
for (const alert of alerts || []) {
const rank = ALERT_RANK[alert.severity] ?? -1;
if (rank > highestRank) {
highest = alert.severity;
highestRank = rank;
}
}
return highest;
}
function indexAlertsByService(activeAlerts) {
const index = new Map();
for (const alert of activeAlerts || []) {
const list = index.get(alert.service_scope) || [];
list.push(alert);
index.set(alert.service_scope, list);
}
return index;
}
function hasCriticalTruthAlert(alerts, activePair) {
const activePairSet = new Set(Array.isArray(activePair) ? activePair : [activePair].filter(Boolean));
return (alerts || []).some((alert) => (
alert.severity === 'critical'
&& (
alert.pair == null
|| activePairSet.has(alert.pair)
|| alert.alert_code.includes('stale')
|| alert.alert_code.includes('disconnected')
)
));
}
function escalateHealth(current, next) {
const currentRank = HEALTH_RANK[current] ?? 0;
const nextRank = HEALTH_RANK[next] ?? 0;
return nextRank > currentRank ? next : current;
}