(options: ServerOptions = {})
| 726 | * Start the producer HTTP server with graceful shutdown. |
| 727 | */ |
| 728 | export function startServer(options: ServerOptions = {}) { |
| 729 | const port = options.port ?? parseInt(process.env.PRODUCER_PORT ?? "9847", 10); |
| 730 | const log = options.logger ?? defaultLogger; |
| 731 | const app = createProducerApp(options); |
| 732 | |
| 733 | const server = serve({ fetch: app.fetch, port }, () => { |
| 734 | log.info(`Listening on http://localhost:${port}`); |
| 735 | }); |
| 736 | |
| 737 | // Disable timeouts for long renders |
| 738 | server.setTimeout(0); |
| 739 | (server as unknown as import("node:http").Server).requestTimeout = 0; |
| 740 | (server as unknown as import("node:http").Server).keepAliveTimeout = 0; |
| 741 | |
| 742 | // Start the worker-thread health endpoint alongside the main listener. |
| 743 | // The main thread keeps serving /health on `port` for backwards |
| 744 | // compatibility; the worker thread additionally serves /health on |
| 745 | // PRODUCER_HEALTH_PORT (default 9848) so k8s liveness/readiness probes can |
| 746 | // migrate to a listener that doesn't share an event loop with renders. |
| 747 | // |
| 748 | // Opt-out: set PRODUCER_DISABLE_HEALTH_WORKER=1 (e.g. for tests that don't |
| 749 | // want a worker spawned, or for environments where the extra port isn't |
| 750 | // wanted). |
| 751 | // |
| 752 | // We store the *promise* (not the resolved handle) so a SIGTERM that |
| 753 | // arrives before the worker has finished booting still has something to |
| 754 | // await. Awaiting a `let healthWorker = null` mutated from inside `.then` |
| 755 | // would race: if SIGTERM lands before the `.then` callback fires, |
| 756 | // `shutdown()` sees `null` and skips worker cleanup. The promise pattern |
| 757 | // closes that window without making startup blocking. |
| 758 | const healthWorkerPromise: Promise<HealthWorkerHandle | null> = |
| 759 | process.env.PRODUCER_DISABLE_HEALTH_WORKER === "1" |
| 760 | ? Promise.resolve(null) |
| 761 | : startHealthWorker({ logger: log }).catch((err: Error) => { |
| 762 | // Don't crash the producer if the worker fails to start — the main |
| 763 | // /health is still up. Log loudly so the operator notices. |
| 764 | log.error(`[server] health worker failed to start: ${err.message}`); |
| 765 | return null; |
| 766 | }); |
| 767 | |
| 768 | async function shutdown(signal: string) { |
| 769 | log.info(`Received ${signal}, shutting down`); |
| 770 | const { drainBrowserPool } = await import("@hyperframes/engine"); |
| 771 | await drainBrowserPool().catch(() => {}); |
| 772 | // Bounded await: if the worker hasn't come online within 1.5s of |
| 773 | // shutdown there's no useful cleanup left to do — `worker.terminate()` |
| 774 | // from process exit will kill the thread regardless, and we'd rather |
| 775 | // not let a hung-startup worker keep the SIGTERM path waiting. |
| 776 | const handle = await Promise.race<HealthWorkerHandle | null>([ |
| 777 | healthWorkerPromise, |
| 778 | new Promise<null>((res) => setTimeout(() => res(null), 1_500).unref()), |
| 779 | ]); |
| 780 | if (handle) { |
| 781 | await handle.shutdown().catch(() => {}); |
| 782 | } |
| 783 | server.close(() => { |
| 784 | log.info("Server closed"); |
| 785 | process.exit(0); |
no test coverage detected