mirror of
https://github.com/stablyai/orca.git
synced 2026-09-25 00:02:35 +00:00
A regional rehome whose host went offline right after accepting the move left its migration row open forever: the target had registered it, the host held nothing on the source, and the completion sweep could never finish it. Eight such rows filled REGIONAL_REHOME_CONCURRENT_LIMIT and every later candidate came back deferred, silently, for 21 hours. The only sweep that touched them fires at 24 hours and also sets enabled = 0 on the durable control, so the first leak to age out would have turned rehoming off, repeatedly. Adds a director sweep that rolls such an attempt back to its source after one migration lease, with abort_reason = 'host_not_arrived', reusing the existing rollback (assignment epoch bump back to the source, lease removal, superseded target reservation release) and leaving the switch untouched. The 24-hour sweep keeps its disable as a last-resort latch. The source cell now names why it deferred, on a new optional response field, and the director stops walking its candidate page on a deferral no later candidate can pass. Each poll that dispatched logs one summary line. Claude-Session: https://claude.ai/session/ced32ebb-7155-4413-adad-1eccd14c2010
571 lines
20 KiB
TypeScript
571 lines
20 KiB
TypeScript
import { createAdaptorServer } from '@hono/node-server'
|
|
import {
|
|
hasAdmissionCapacity,
|
|
HostDataAuthSchema,
|
|
parseRelayHostCapabilities,
|
|
RELAY_ADMISSION_BUDGETS,
|
|
RELAY_HOST_CAPABILITIES_HEADER,
|
|
RELAY_CLOSE_CODE,
|
|
RELAY_DEFAULT_REGION,
|
|
RELAY_PROTOCOL_LIMITS,
|
|
RelayAuthSchema
|
|
} from '@orca-cloud/relay-contract'
|
|
import type { IncomingMessage } from 'node:http'
|
|
import { randomUUID } from 'node:crypto'
|
|
import { performance } from 'node:perf_hooks'
|
|
import { WebSocketServer } from 'ws'
|
|
import type WebSocket from 'ws'
|
|
import type { RawData } from 'ws'
|
|
import { createRelayApp } from './app.js'
|
|
import { RelayAssignmentStore } from './assignment-store.js'
|
|
import type { RelayConfig } from './config.js'
|
|
import { RelayCredentialStore } from './credential-store.js'
|
|
import { readRelayDatabasePoolPressure, type RelayDatabase } from './database.js'
|
|
import { HostSessionRegistry } from './host-session-registry.js'
|
|
import { observeRelayDatabase } from './observed-relay-database.js'
|
|
import { RelayObservability } from './relay-observability.js'
|
|
import { combineRegionalRehomeSafety } from './regional-rehome-safety.js'
|
|
import { RelayConnectionLedger, type RelayConnectionUpgrade } from './relay-connection-ledger.js'
|
|
import { createRelayReadiness } from './relay-readiness.js'
|
|
import { createRelayTokenVerifier, readBearer } from './relay-token-verifier.js'
|
|
import { closeRelayWebSocket } from './relay-websocket-close.js'
|
|
import { ProcessQueuedByteBudget } from './splice-forwarder.js'
|
|
|
|
// A malformed percent-escape in the request target must be a client error, never a URIError
|
|
// thrown out of the `upgrade` listener (which is uncaught and kills the process).
|
|
function decodePathSegment(value: string): string | null {
|
|
try {
|
|
return decodeURIComponent(value)
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
function rejectUpgrade(socket: NodeJS.WritableStream, status: number, message: string): void {
|
|
socket.write(`HTTP/1.1 ${status} ${message}\r\nConnection: close\r\nContent-Length: 0\r\n\r\n`)
|
|
if ('destroy' in socket && typeof socket.destroy === 'function') socket.destroy()
|
|
}
|
|
|
|
function noDelay(socket: WebSocket): void {
|
|
const transport = (socket as WebSocket & { _socket?: { setNoDelay: (enabled: boolean) => void } })
|
|
._socket
|
|
transport?.setNoDelay(true)
|
|
}
|
|
|
|
// Why: a ws receiver error (oversize or malformed frame) with no 'error'
|
|
// listener throws process-wide; ws itself already closes the socket after
|
|
// emitting it, so logging is all that is left to do.
|
|
function guardSocketErrors(socket: WebSocket, kind: string): void {
|
|
socket.on('error', (error) => {
|
|
console.warn(`[orca-relay] ${kind} socket error: ${error.message}`)
|
|
})
|
|
}
|
|
|
|
function admissionSource(request: IncomingMessage): string {
|
|
const forwarded = request.headers['x-forwarded-for']
|
|
const chain = (Array.isArray(forwarded) ? forwarded.join(',') : (forwarded ?? ''))
|
|
.split(',')
|
|
.map((entry) => entry.trim())
|
|
.filter(Boolean)
|
|
// Google Front End appends client and load-balancer addresses after any
|
|
// caller-supplied values, so only the penultimate hop is trustworthy.
|
|
return chain.length >= 2 ? chain.at(-2)! : (request.socket.remoteAddress ?? 'unknown')
|
|
}
|
|
|
|
function firstPayload(raw: RawData, expectedType: string): unknown {
|
|
try {
|
|
const parsed = JSON.parse(raw.toString()) as Record<string, unknown>
|
|
if (parsed.type !== expectedType) return null
|
|
const { type: _type, ...rest } = parsed
|
|
return rest
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export function createRelayServer(
|
|
config: RelayConfig,
|
|
database: RelayDatabase,
|
|
options: {
|
|
now?: () => number
|
|
random?: () => number
|
|
connectionLedgerLimits?: { hardCap: number; controlReserve: number }
|
|
cellIncarnation?: string
|
|
} = {}
|
|
) {
|
|
const cellIncarnation = options.cellIncarnation ?? randomUUID()
|
|
const observability = new RelayObservability({
|
|
role: config.role,
|
|
cellId: config.cellId,
|
|
region: config.region ?? RELAY_DEFAULT_REGION
|
|
})
|
|
const observedDatabase = observeRelayDatabase(database, observability)
|
|
const controls = new WebSocketServer({
|
|
noServer: true,
|
|
clientTracking: false,
|
|
perMessageDeflate: false,
|
|
maxPayload: 1024 * 1024
|
|
})
|
|
const verifyRelayToken = createRelayTokenVerifier(config)
|
|
const store = new RelayCredentialStore(observedDatabase, options.now)
|
|
const assignments = new RelayAssignmentStore(observedDatabase, options.now, {
|
|
requireLiveCells: config.role === 'director',
|
|
regionalRehomeCohortPercent: config.regionCorrectionCohortPercent ?? 0,
|
|
recordControlRenewal: (durationMs, outcome) =>
|
|
observability.recordControlRenewal?.(durationMs, outcome)
|
|
})
|
|
const readiness = createRelayReadiness(observedDatabase, config.jwksUrl, {
|
|
jwksGraceMs: config.readinessJwksGraceMs,
|
|
sqlGraceMs: config.readinessSqlGraceMs,
|
|
observe: (observation) => observability.recordReadiness(observation),
|
|
observeGrace: (event) => observability.recordReadinessGrace(event)
|
|
})
|
|
const ready = readiness.check
|
|
const queuedBytes = new ProcessQueuedByteBudget()
|
|
const sessions = new HostSessionRegistry(
|
|
config,
|
|
verifyRelayToken,
|
|
store,
|
|
assignments,
|
|
queuedBytes,
|
|
observability,
|
|
options.now,
|
|
options.random,
|
|
cellIncarnation
|
|
)
|
|
const app = createRelayApp(config, {
|
|
store,
|
|
assignments,
|
|
drain: (graceMs, options) => sessions.drain(graceMs, options ?? {}),
|
|
drainHost: (input) => sessions.drainHost(input),
|
|
idleRehome: (input) => {
|
|
const now = (options.now ?? Date.now)()
|
|
if (input.directorSafety.observedAt > now || now - input.directorSafety.observedAt > 60_000) {
|
|
return Promise.resolve({ outcome: 'deferred', reason: 'director-safety-stale' })
|
|
}
|
|
return sessions.idleRehome(input,
|
|
() => assignments.commitIdleRegionalRehome(input, combineRegionalRehomeSafety(
|
|
input.directorSafety,
|
|
{ ...observability.regionalRehomeRuntimeSafety(), ...readRelayDatabasePoolPressure(database) }
|
|
), input.cohortPercent),
|
|
() => assignments.reconcileIdleRegionalRehome(input)
|
|
)
|
|
},
|
|
regionalRehomeTrustProbeHostExists: (input) => sessions.get(input) !== null,
|
|
cellIncarnation,
|
|
isDraining: () => sessions.isDraining(),
|
|
runtimeCounts: () => runtimeCounts(),
|
|
regionalRehomeSafetySnapshot: () => ({
|
|
...observability.regionalRehomeRuntimeSafety(),
|
|
...readRelayDatabasePoolPressure(database)
|
|
}),
|
|
ready,
|
|
readinessDegradation: () => readiness.degradedDependencies(),
|
|
recordAssignmentAdmission: (outcome) => observability.recordAssignmentAdmission?.(outcome),
|
|
recordAssignmentRejectionReason: (lane, reason) =>
|
|
observability.recordAssignmentRejectionReason?.(lane, reason),
|
|
recordRegionRequest: (region) => observability.recordRegionRequest?.(region),
|
|
recordRegionSelection: (input) => observability.recordRegionSelection?.(input)
|
|
})
|
|
const observedFetch: typeof app.fetch = async (...args) => {
|
|
const startedAt = performance.now()
|
|
try {
|
|
return await app.fetch(...args)
|
|
} finally {
|
|
observability.recordHttp(performance.now() - startedAt)
|
|
}
|
|
}
|
|
const server = createAdaptorServer({ fetch: observedFetch })
|
|
const clients = new WebSocketServer({
|
|
noServer: true,
|
|
clientTracking: false,
|
|
perMessageDeflate: false,
|
|
maxPayload: RELAY_PROTOCOL_LIMITS.maxFrameBytes
|
|
})
|
|
const dataSockets = new WebSocketServer({
|
|
noServer: true,
|
|
clientTracking: false,
|
|
perMessageDeflate: false,
|
|
maxPayload: RELAY_PROTOCOL_LIMITS.maxFrameBytes
|
|
})
|
|
let preAuthConnections = 0
|
|
let totalConnections = 0
|
|
const configuredConnectionLimits =
|
|
config.connectionHardCap === undefined
|
|
? null
|
|
: (options.connectionLedgerLimits ?? {
|
|
hardCap: config.connectionHardCap,
|
|
controlReserve: RELAY_ADMISSION_BUDGETS.reservedHostControls
|
|
})
|
|
const connectionLedger =
|
|
configuredConnectionLimits === null
|
|
? null
|
|
: new RelayConnectionLedger(
|
|
configuredConnectionLimits.hardCap,
|
|
configuredConnectionLimits.controlReserve
|
|
)
|
|
const preAuthBySource = new Map<string, number>()
|
|
const preAuthAttemptsBySource = new Map<string, { windowStartedAt: number; count: number }>()
|
|
|
|
const admit = (source: string): boolean => {
|
|
const now = Date.now()
|
|
const currentWindow = preAuthAttemptsBySource.get(source)
|
|
const attempts =
|
|
!currentWindow || now - currentWindow.windowStartedAt >= 60_000
|
|
? { windowStartedAt: now, count: 0 }
|
|
: currentWindow
|
|
const sourceCount = preAuthBySource.get(source) ?? 0
|
|
if (
|
|
!hasAdmissionCapacity({
|
|
totalRequests:
|
|
connectionLedger?.counts().physicalConnections ?? totalConnections,
|
|
preAuthConnections,
|
|
sourcePreAuthConnections: sourceCount,
|
|
totalRequestCeiling:
|
|
configuredConnectionLimits === null
|
|
? undefined
|
|
: configuredConnectionLimits.hardCap - configuredConnectionLimits.controlReserve
|
|
}) ||
|
|
attempts.count >= RELAY_ADMISSION_BUDGETS.maxPreAuthAttemptsPerSourcePerMinute
|
|
) {
|
|
return false
|
|
}
|
|
attempts.count++
|
|
preAuthAttemptsBySource.delete(source)
|
|
preAuthAttemptsBySource.set(source, attempts)
|
|
// A bounded LRU keeps source churn from becoming its own memory attack.
|
|
if (preAuthAttemptsBySource.size > 4_096) {
|
|
preAuthAttemptsBySource.delete(preAuthAttemptsBySource.keys().next().value!)
|
|
}
|
|
preAuthConnections++
|
|
preAuthBySource.set(source, sourceCount + 1)
|
|
return true
|
|
}
|
|
const authenticated = (source: string): void => {
|
|
preAuthConnections = Math.max(0, preAuthConnections - 1)
|
|
const next = (preAuthBySource.get(source) ?? 1) - 1
|
|
if (next <= 0) preAuthBySource.delete(source)
|
|
else preAuthBySource.set(source, next)
|
|
}
|
|
|
|
const trackConnection = (
|
|
socket: WebSocket,
|
|
upgrade: RelayConnectionUpgrade | null
|
|
): void => {
|
|
if (upgrade) {
|
|
upgrade.promote(socket)
|
|
return
|
|
}
|
|
totalConnections++
|
|
socket.once('close', () => {
|
|
totalConnections = Math.max(0, totalConnections - 1)
|
|
})
|
|
}
|
|
|
|
const awaitFirstFrame = (
|
|
socket: WebSocket,
|
|
source: string,
|
|
callback: (raw: RawData) => Promise<void>
|
|
): void => {
|
|
let finished = false
|
|
const timer = setTimeout(() => {
|
|
if (finished) return
|
|
finished = true
|
|
authenticated(source)
|
|
observability.recordAuth(false)
|
|
socket.close(RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL, 'first frame timeout')
|
|
}, RELAY_PROTOCOL_LIMITS.firstFrameDeadlineMs)
|
|
socket.once('message', (raw, binary) => {
|
|
if (finished) return
|
|
finished = true
|
|
clearTimeout(timer)
|
|
authenticated(source)
|
|
if (binary) {
|
|
observability.recordAuth(false)
|
|
socket.close(RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL, 'first frame must be text')
|
|
return
|
|
}
|
|
void callback(raw).catch((error: unknown) => {
|
|
console.warn(
|
|
'[orca-relay] first frame handler failed',
|
|
error instanceof Error ? error.message : ''
|
|
)
|
|
closeRelayWebSocket(
|
|
socket,
|
|
RELAY_CLOSE_CODE.LIMIT_EXCEEDED,
|
|
'relay temporarily unavailable'
|
|
)
|
|
})
|
|
})
|
|
socket.once('close', () => {
|
|
if (!finished) {
|
|
finished = true
|
|
clearTimeout(timer)
|
|
authenticated(source)
|
|
}
|
|
})
|
|
}
|
|
|
|
server.on('upgrade', (request, socket, head) => {
|
|
const url = new URL(request.url ?? '/', config.publicUrl)
|
|
const source = admissionSource(request)
|
|
if (url.search) {
|
|
rejectUpgrade(socket, 400, 'Bad Request')
|
|
return
|
|
}
|
|
if (url.pathname.startsWith('/v1/connect/')) {
|
|
const hostId = decodePathSegment(url.pathname.slice('/v1/connect/'.length))
|
|
if (hostId === null || !/^[A-Za-z0-9_-]{16}$/.test(hostId)) {
|
|
rejectUpgrade(socket, 429, 'Too Many Requests')
|
|
return
|
|
}
|
|
const phoneAdmission = connectionLedger?.tryReservePhone() ?? null
|
|
if (connectionLedger && !phoneAdmission) {
|
|
rejectUpgrade(socket, 503, 'Service Unavailable')
|
|
return
|
|
}
|
|
if (!admit(source)) {
|
|
phoneAdmission?.upgrade.release()
|
|
phoneAdmission?.hostData.release()
|
|
rejectUpgrade(socket, 429, 'Too Many Requests')
|
|
return
|
|
}
|
|
const releasePhoneUpgrade = (): void => {
|
|
authenticated(source)
|
|
phoneAdmission?.upgrade.release()
|
|
phoneAdmission?.hostData.release()
|
|
}
|
|
socket.once('close', releasePhoneUpgrade)
|
|
try {
|
|
clients.handleUpgrade(request, socket, head, (webSocket) => {
|
|
socket.off('close', releasePhoneUpgrade)
|
|
trackConnection(webSocket, phoneAdmission?.upgrade ?? null)
|
|
guardSocketErrors(webSocket, 'client')
|
|
if (phoneAdmission) {
|
|
webSocket.once('close', () => phoneAdmission.hostData.release())
|
|
}
|
|
noDelay(webSocket)
|
|
awaitFirstFrame(webSocket, source, async (raw) => {
|
|
const auth = RelayAuthSchema.safeParse(firstPayload(raw, 'relay-auth'))
|
|
if (!auth.success) {
|
|
phoneAdmission?.hostData.release()
|
|
observability.recordAuth(false)
|
|
webSocket.send(
|
|
JSON.stringify({ type: 'relay-hello', ok: false, code: RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL })
|
|
)
|
|
webSocket.close(RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL, 'invalid relay auth')
|
|
return
|
|
}
|
|
if (config.role === 'director') {
|
|
const invite = await store.resolveInviteForMove(hostId, auth.data.credential)
|
|
const identity = invite ? { userId: invite.userId, relayHostId: hostId } : null
|
|
// Released combined-service invites gain their first durable cell assignment here.
|
|
const assignment = identity
|
|
? ((await assignments.resolve(identity)) ?? (await assignments.assign(identity)))
|
|
: null
|
|
if (!invite || !assignment) {
|
|
phoneAdmission?.hostData.release()
|
|
observability.recordAuth(false)
|
|
webSocket.send(
|
|
JSON.stringify({
|
|
type: 'relay-hello',
|
|
ok: false,
|
|
code: RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL
|
|
})
|
|
)
|
|
webSocket.close(RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL, 'invalid invite')
|
|
return
|
|
}
|
|
phoneAdmission?.hostData.release()
|
|
observability.recordAuth(true)
|
|
webSocket.send(
|
|
JSON.stringify({
|
|
type: 'relay-moved',
|
|
v: 1,
|
|
cellUrl: assignment.cellUrl,
|
|
assignmentEpoch: assignment.assignmentEpoch
|
|
})
|
|
)
|
|
webSocket.close(RELAY_CLOSE_CODE.DRAINING, 'connect to assigned cell')
|
|
return
|
|
}
|
|
await sessions.acceptClient(
|
|
webSocket,
|
|
hostId,
|
|
auth.data.credential,
|
|
phoneAdmission?.hostData
|
|
)
|
|
})
|
|
})
|
|
} catch {
|
|
socket.off('close', releasePhoneUpgrade)
|
|
releasePhoneUpgrade()
|
|
socket.destroy()
|
|
}
|
|
return
|
|
}
|
|
if (url.pathname.startsWith('/v1/host/data/')) {
|
|
if (config.role === 'director') {
|
|
rejectUpgrade(socket, 404, 'Not Found')
|
|
return
|
|
}
|
|
const connId = decodePathSegment(url.pathname.slice('/v1/host/data/'.length))
|
|
if (!connId || connId.length > 128) {
|
|
rejectUpgrade(socket, 429, 'Too Many Requests')
|
|
return
|
|
}
|
|
const dataUpgrade = connectionLedger?.tryReserveHostData(connId) ?? null
|
|
if (connectionLedger && !dataUpgrade) {
|
|
rejectUpgrade(socket, 503, 'Service Unavailable')
|
|
return
|
|
}
|
|
if (!admit(source)) {
|
|
dataUpgrade?.release()
|
|
rejectUpgrade(socket, 429, 'Too Many Requests')
|
|
return
|
|
}
|
|
const releaseDataUpgrade = (): void => {
|
|
authenticated(source)
|
|
dataUpgrade?.release()
|
|
}
|
|
socket.once('close', releaseDataUpgrade)
|
|
try {
|
|
dataSockets.handleUpgrade(request, socket, head, (webSocket) => {
|
|
socket.off('close', releaseDataUpgrade)
|
|
trackConnection(webSocket, dataUpgrade)
|
|
guardSocketErrors(webSocket, 'host-data')
|
|
noDelay(webSocket)
|
|
awaitFirstFrame(webSocket, source, async (raw) => {
|
|
const auth = HostDataAuthSchema.safeParse(firstPayload(raw, 'host-data-auth'))
|
|
if (!auth.success) {
|
|
observability.recordAuth(false)
|
|
webSocket.close(RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL, 'invalid host data auth')
|
|
return
|
|
}
|
|
const accepted = await sessions.acceptHostData(
|
|
webSocket,
|
|
connId,
|
|
auth.data.connTicket,
|
|
auth.data.generation
|
|
)
|
|
if (accepted) dataUpgrade?.commitHostData()
|
|
})
|
|
})
|
|
} catch {
|
|
socket.off('close', releaseDataUpgrade)
|
|
releaseDataUpgrade()
|
|
socket.destroy()
|
|
}
|
|
return
|
|
}
|
|
if (url.pathname !== '/v1/host/control') {
|
|
rejectUpgrade(socket, 404, 'Not Found')
|
|
return
|
|
}
|
|
if (config.role === 'director') {
|
|
rejectUpgrade(socket, 404, 'Not Found')
|
|
return
|
|
}
|
|
const bearer = readBearer(request.headers.authorization)
|
|
if (!bearer) {
|
|
observability.recordAuth(false)
|
|
rejectUpgrade(socket, 401, 'Unauthorized')
|
|
return
|
|
}
|
|
void verifyRelayToken(bearer).then((identity) => {
|
|
if (socket.destroyed) return
|
|
if (!identity) {
|
|
observability.recordAuth(false)
|
|
rejectUpgrade(socket, 401, 'Unauthorized')
|
|
return
|
|
}
|
|
const isRebind = sessions.hasActiveControl({
|
|
userId: identity.sub,
|
|
relayHostId: identity.relayHostId
|
|
})
|
|
const controlUpgrade = connectionLedger?.tryReserveControl(isRebind) ?? null
|
|
if (
|
|
(connectionLedger && !controlUpgrade) ||
|
|
(!connectionLedger && totalConnections >= RELAY_ADMISSION_BUDGETS.cloudRunConcurrency)
|
|
) {
|
|
rejectUpgrade(
|
|
socket,
|
|
connectionLedger ? 503 : 429,
|
|
connectionLedger ? 'Service Unavailable' : 'Too Many Requests'
|
|
)
|
|
return
|
|
}
|
|
// Register the release before anything else can throw: the outer catch
|
|
// destroys the socket, so a close-registered release cannot leak the
|
|
// reserved connection unit.
|
|
const releaseControlUpgrade = (): void => controlUpgrade?.release()
|
|
socket.once('close', releaseControlUpgrade)
|
|
observability.recordAuth(true)
|
|
try {
|
|
controls.handleUpgrade(request, socket, head, (webSocket) => {
|
|
socket.off('close', releaseControlUpgrade)
|
|
trackConnection(webSocket, controlUpgrade)
|
|
guardSocketErrors(webSocket, 'control')
|
|
noDelay(webSocket)
|
|
sessions.acceptControl(
|
|
webSocket,
|
|
identity,
|
|
controlUpgrade?.inclusionWatermark,
|
|
parseRelayHostCapabilities(request.headers[RELAY_HOST_CAPABILITIES_HEADER])
|
|
)
|
|
})
|
|
} catch {
|
|
socket.off('close', releaseControlUpgrade)
|
|
releaseControlUpgrade()
|
|
socket.destroy()
|
|
}
|
|
}).catch((error: unknown) => {
|
|
// A throw in the upgrade handling above must cost this socket, not the process.
|
|
console.warn(
|
|
`[orca-relay] control upgrade failed: ${error instanceof Error ? error.message : 'unknown'}`
|
|
)
|
|
socket.destroy()
|
|
})
|
|
})
|
|
|
|
server.on('close', () => {
|
|
controls.close()
|
|
clients.close()
|
|
dataSockets.close()
|
|
})
|
|
const runtimeCounts = () => {
|
|
const ledgerCounts = connectionLedger?.counts()
|
|
return {
|
|
totalConnections: ledgerCounts?.physicalConnections ?? totalConnections,
|
|
preAuthConnections,
|
|
...sessions.runtimeCounts(),
|
|
queuedBytes: queuedBytes.current(),
|
|
...(ledgerCounts
|
|
? {
|
|
inFlightConnections: ledgerCounts.inFlightConnections,
|
|
reservedConnectionUnits: ledgerCounts.reservedConnectionUnits,
|
|
enforcedConnectionUnits: ledgerCounts.enforcedConnectionUnits
|
|
}
|
|
: {})
|
|
}
|
|
}
|
|
const connectionSnapshot = () => connectionLedger?.snapshot()
|
|
return {
|
|
server,
|
|
sessions,
|
|
store,
|
|
assignments,
|
|
queuedBytes,
|
|
observability,
|
|
runtimeCounts,
|
|
connectionSnapshot,
|
|
ready,
|
|
cellIncarnation
|
|
}
|
|
}
|
|
|
|
export function closeWithDrain(socket: WebSocket, graceMs: number): void {
|
|
socket.send(JSON.stringify({ type: 'drain', graceMs, recovery: 'resolve-director' }))
|
|
socket.close(RELAY_CLOSE_CODE.DRAINING, 'resolve configured director')
|
|
}
|