UNPKG

inngest

Version:

Official SDK for Inngest.com. Inngest is the reliability layer for modern applications. Inngest combines durable execution, events, and queues into a zero-infra platform with built-in observability.

1 lines • 19.7 kB
{"version":3,"file":"requestProcessor.cjs","names":["accessor: ConnectionAccessor","wakeSignal: WakeSignal","callbacks: ConnectionCoreCallbacks","logger: Logger","ConnectionState","parseGatewayExecutorRequest","ensureUnsharedArrayBuffer","ConnectMessage","GatewayMessageType","WorkerRequestAckData","extendLeaseInterval: ReturnType<typeof setInterval> | undefined","WorkerRequestExtendLeaseData","WAKE_REASON","parseWorkerReplyAck","WorkerRequestExtendLeaseAckData"],"sources":["../../../../../src/components/connect/strategies/core/requestProcessor.ts"],"sourcesContent":["/**\n * Processes incoming executor requests, manages lease extensions, and handles\n * reply acknowledgements.\n *\n * Extracted from ConnectionCore so the reconcile loop orchestrator only\n * dispatches messages to this module rather than containing the full\n * execution flow inline.\n */\n\nimport type { Logger } from \"../../../../middleware/logger.ts\";\nimport {\n ConnectMessage,\n type ConnectMessage as ConnectMessageType,\n GatewayMessageType,\n WorkerRequestAckData,\n WorkerRequestExtendLeaseAckData,\n WorkerRequestExtendLeaseData,\n} from \"../../../../proto/src/components/connect/protobuf/connect.ts\";\nimport { ensureUnsharedArrayBuffer } from \"../../buffer.ts\";\nimport {\n parseGatewayExecutorRequest,\n parseWorkerReplyAck,\n} from \"../../messages.ts\";\nimport { ConnectionState } from \"../../types.ts\";\nimport type { Connection, ConnectionCoreCallbacks } from \"./connection.ts\";\nimport {\n type ConnectionAccessor,\n WAKE_REASON,\n type WakeSignal,\n} from \"./types.ts\";\n\nfunction toError(value: unknown): Error {\n if (value instanceof Error) {\n return value;\n }\n return new Error(String(value));\n}\n\nexport class RequestProcessor {\n constructor(\n private readonly accessor: ConnectionAccessor,\n private readonly wakeSignal: WakeSignal,\n private readonly callbacks: ConnectionCoreCallbacks,\n private readonly logger: Logger,\n ) {}\n\n /** Handle an incoming executor request. */\n async handleExecutorRequest(\n connectMessage: ConnectMessageType,\n conn: Connection,\n ): Promise<void> {\n const currentState = this.callbacks.getState();\n if (currentState !== ConnectionState.ACTIVE) {\n this.logger.warn(\n { connectionId: conn.id },\n \"Received request while not active, skipping\",\n );\n return;\n }\n\n const gatewayExecutorRequest = parseGatewayExecutorRequest(\n connectMessage.payload,\n );\n\n this.logger.debug(\n {\n requestId: gatewayExecutorRequest.requestId,\n appId: gatewayExecutorRequest.appId,\n appName: gatewayExecutorRequest.appName,\n functionSlug: gatewayExecutorRequest.functionSlug,\n stepId: gatewayExecutorRequest.stepId,\n connectionId: conn.id,\n },\n \"Received gateway executor request\",\n );\n\n if (\n typeof gatewayExecutorRequest.appName !== \"string\" ||\n gatewayExecutorRequest.appName.length === 0\n ) {\n this.logger.warn(\n {\n requestId: gatewayExecutorRequest.requestId,\n appId: gatewayExecutorRequest.appId,\n functionSlug: gatewayExecutorRequest.functionSlug,\n stepId: gatewayExecutorRequest.stepId,\n connectionId: conn.id,\n },\n \"No app name in request, skipping\",\n );\n return;\n }\n\n if (!this.accessor.appIds.includes(gatewayExecutorRequest.appName)) {\n this.logger.warn(\n {\n requestId: gatewayExecutorRequest.requestId,\n appId: gatewayExecutorRequest.appId,\n appName: gatewayExecutorRequest.appName,\n functionSlug: gatewayExecutorRequest.functionSlug,\n stepId: gatewayExecutorRequest.stepId,\n connectionId: conn.id,\n },\n \"No request handler found for app, skipping\",\n );\n return;\n }\n\n // Send ACK\n conn.ws.send(\n ensureUnsharedArrayBuffer(\n ConnectMessage.encode(\n ConnectMessage.create({\n kind: GatewayMessageType.WORKER_REQUEST_ACK,\n payload: WorkerRequestAckData.encode(\n WorkerRequestAckData.create({\n accountId: gatewayExecutorRequest.accountId,\n envId: gatewayExecutorRequest.envId,\n appId: gatewayExecutorRequest.appId,\n functionSlug: gatewayExecutorRequest.functionSlug,\n requestId: gatewayExecutorRequest.requestId,\n stepId: gatewayExecutorRequest.stepId,\n userTraceCtx: gatewayExecutorRequest.userTraceCtx,\n systemTraceCtx: gatewayExecutorRequest.systemTraceCtx,\n runId: gatewayExecutorRequest.runId,\n }),\n ).finish(),\n }),\n ).finish(),\n ),\n );\n\n this.accessor.inProgressRequests.wg.add(1);\n this.accessor.inProgressRequests.requestLeases[\n gatewayExecutorRequest.requestId\n ] = gatewayExecutorRequest.leaseId;\n const leaseAcquiredAt = Date.now();\n this.accessor.inProgressRequests.requestMeta[\n gatewayExecutorRequest.requestId\n ] = {\n requestId: gatewayExecutorRequest.requestId,\n runId: gatewayExecutorRequest.runId,\n stepId: gatewayExecutorRequest.stepId,\n appId: gatewayExecutorRequest.appId,\n envId: gatewayExecutorRequest.envId,\n functionSlug: gatewayExecutorRequest.functionSlug,\n accountId: gatewayExecutorRequest.accountId,\n leaseAcquiredAt,\n leaseLastExtendedAt: leaseAcquiredAt,\n };\n\n const inFlightCount = Object.keys(\n this.accessor.inProgressRequests.requestLeases,\n ).length;\n this.logger.debug(\n {\n requestId: gatewayExecutorRequest.requestId,\n functionSlug: gatewayExecutorRequest.functionSlug,\n inFlightCount,\n },\n \"Request acknowledged\",\n );\n\n const startedAt = Date.now();\n\n // Start lease extension interval\n const originalWs = conn.ws;\n const originalConnectionId = conn.id;\n let extendLeaseInterval: ReturnType<typeof setInterval> | undefined;\n extendLeaseInterval = setInterval(() => {\n const currentLeaseId =\n this.accessor.inProgressRequests.requestLeases[\n gatewayExecutorRequest.requestId\n ];\n if (!currentLeaseId) {\n clearInterval(extendLeaseInterval);\n return;\n }\n\n // Use the current live connection's WebSocket for lease extensions.\n // During a drain, the original WebSocket may be closed by the gateway\n // while the request is still in flight.\n const latestConn = {\n ws: this.accessor.activeConnection?.ws ?? originalWs,\n id: this.accessor.activeConnection?.id ?? originalConnectionId,\n };\n\n this.logger.debug(\n {\n connectionId: latestConn.id,\n leaseId: currentLeaseId,\n requestId: gatewayExecutorRequest.requestId,\n functionSlug: gatewayExecutorRequest.functionSlug,\n runId: gatewayExecutorRequest.runId,\n stepId: gatewayExecutorRequest.stepId,\n },\n \"Extending lease\",\n );\n\n if (latestConn.ws.readyState !== WebSocket.OPEN) {\n this.logger.warn(\n {\n connectionId: latestConn.id,\n requestId: gatewayExecutorRequest.requestId,\n },\n \"Cannot extend lease, no open WebSocket available\",\n );\n return;\n }\n\n try {\n latestConn.ws.send(\n ensureUnsharedArrayBuffer(\n ConnectMessage.encode(\n ConnectMessage.create({\n kind: GatewayMessageType.WORKER_REQUEST_EXTEND_LEASE,\n payload: WorkerRequestExtendLeaseData.encode(\n WorkerRequestExtendLeaseData.create({\n accountId: gatewayExecutorRequest.accountId,\n envId: gatewayExecutorRequest.envId,\n appId: gatewayExecutorRequest.appId,\n functionSlug: gatewayExecutorRequest.functionSlug,\n requestId: gatewayExecutorRequest.requestId,\n stepId: gatewayExecutorRequest.stepId,\n runId: gatewayExecutorRequest.runId,\n userTraceCtx: gatewayExecutorRequest.userTraceCtx,\n systemTraceCtx: gatewayExecutorRequest.systemTraceCtx,\n leaseId: currentLeaseId,\n }),\n ).finish(),\n }),\n ).finish(),\n ),\n );\n const meta =\n this.accessor.inProgressRequests.requestMeta[\n gatewayExecutorRequest.requestId\n ];\n if (meta) meta.leaseLastExtendedAt = Date.now();\n } catch (err) {\n this.logger.warn(\n {\n connectionId: latestConn.id,\n requestId: gatewayExecutorRequest.requestId,\n err: toError(err),\n },\n \"Failed to send lease extension\",\n );\n }\n }, conn.extendLeaseIntervalMs);\n\n try {\n const responseBytes = await this.callbacks.handleExecutionRequest(\n gatewayExecutorRequest,\n );\n\n const durationMs = Date.now() - startedAt;\n this.logger.debug(\n {\n requestId: gatewayExecutorRequest.requestId,\n functionSlug: gatewayExecutorRequest.functionSlug,\n durationMs,\n },\n \"Request execution completed\",\n );\n\n if (!this.accessor.activeConnection) {\n this.logger.warn(\n { requestId: gatewayExecutorRequest.requestId },\n \"No current WebSocket, buffering response\",\n );\n if (this.callbacks.onBufferResponse) {\n this.callbacks.onBufferResponse(\n gatewayExecutorRequest.requestId,\n responseBytes,\n );\n }\n return;\n }\n\n this.logger.debug(\n {\n connectionId: this.accessor.activeConnection.id,\n requestId: gatewayExecutorRequest.requestId,\n },\n \"Sending worker reply\",\n );\n\n this.accessor.activeConnection.ws.send(\n ensureUnsharedArrayBuffer(\n ConnectMessage.encode(\n ConnectMessage.create({\n kind: GatewayMessageType.WORKER_REPLY,\n payload: responseBytes,\n }),\n ).finish(),\n ),\n );\n } catch (err) {\n const durationMs = Date.now() - startedAt;\n this.logger.warn(\n {\n requestId: gatewayExecutorRequest.requestId,\n durationMs,\n err: toError(err),\n },\n \"Execution error\",\n );\n } finally {\n this.accessor.inProgressRequests.wg.done();\n delete this.accessor.inProgressRequests.requestLeases[\n gatewayExecutorRequest.requestId\n ];\n delete this.accessor.inProgressRequests.requestMeta[\n gatewayExecutorRequest.requestId\n ];\n clearInterval(extendLeaseInterval);\n\n const remainingInFlight = Object.keys(\n this.accessor.inProgressRequests.requestLeases,\n ).length;\n this.logger.debug(\n {\n requestId: gatewayExecutorRequest.requestId,\n remainingInFlight,\n },\n \"Request finished\",\n );\n\n // Wake the loop if shutdown is pending and this was the last request\n if (this.accessor.shutdownRequested && !this.hasInFlightRequests()) {\n this.wakeSignal.wake(WAKE_REASON.RequestFinishedOnShutdown);\n }\n }\n }\n\n /** Handle a reply ACK from the gateway. */\n handleReplyAck(\n connectMessage: ConnectMessageType,\n connectionId: string,\n ): void {\n const replyAck = parseWorkerReplyAck(connectMessage.payload);\n\n this.logger.debug(\n { connectionId, requestId: replyAck.requestId },\n \"Acknowledging reply ack\",\n );\n\n this.callbacks.onReplyAck?.(replyAck.requestId);\n }\n\n /** Handle a lease extension ACK from the gateway. */\n handleExtendLeaseAck(\n connectMessage: ConnectMessageType,\n connectionId: string,\n ): void {\n const extendLeaseAck = WorkerRequestExtendLeaseAckData.decode(\n connectMessage.payload,\n );\n\n // Late lease-extend ACKs can arrive after a request has already cleaned up\n // both maps in the completion or lease-lost path. This handler stays\n // synchronous, so once we observe the request missing from either map we\n // can safely ignore the ACK instead of recreating a stale lease entry.\n const hasLease = Object.hasOwn(\n this.accessor.inProgressRequests.requestLeases,\n extendLeaseAck.requestId,\n );\n const meta =\n this.accessor.inProgressRequests.requestMeta[extendLeaseAck.requestId];\n if (!hasLease || !meta) {\n this.logger.debug(\n {\n connectionId,\n requestId: extendLeaseAck.requestId,\n newLeaseId: extendLeaseAck.newLeaseId,\n hadLease: hasLease,\n hadMeta: !!meta,\n },\n \"Ignoring extend lease ack for non-in-flight request\",\n );\n return;\n }\n\n this.logger.debug(\n { connectionId, newLeaseId: extendLeaseAck.newLeaseId },\n \"Received extend lease ack\",\n );\n\n if (extendLeaseAck.newLeaseId) {\n this.accessor.inProgressRequests.requestLeases[extendLeaseAck.requestId] =\n extendLeaseAck.newLeaseId;\n } else {\n this.logger.error(\n {\n connectionId,\n requestId: extendLeaseAck.requestId,\n functionSlug: meta?.functionSlug,\n runId: meta?.runId,\n stepId: meta?.stepId,\n },\n \"Lease lost: the server did not renew the lease for this request. \" +\n \"Another worker may have claimed it. The in-progress execution \" +\n \"will continue but its result may be discarded.\",\n );\n delete this.accessor.inProgressRequests.requestLeases[\n extendLeaseAck.requestId\n ];\n // Also drop meta so the shutdown dump helper and debug-state snapshot\n // don't report a request we've explicitly released the lease for.\n delete this.accessor.inProgressRequests.requestMeta[\n extendLeaseAck.requestId\n ];\n\n // If this was the last in-flight request and a shutdown has been\n // requested, wake the reconcile loop so close() can observe the\n // empty lease map and exit. Without this, the loop stays parked on\n // wakeSignal.promise because the finally-block decrement in\n // handleExecutorRequest never runs (user code is still hanging).\n if (this.accessor.shutdownRequested && !this.hasInFlightRequests()) {\n this.wakeSignal.wake(WAKE_REASON.LeaseLostOnShutdown);\n }\n }\n }\n\n private hasInFlightRequests(): boolean {\n return (\n Object.keys(this.accessor.inProgressRequests.requestLeases).length > 0\n );\n }\n}\n"],"mappings":"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"}