Mirror of https://github.com/roostorg/coop
github.com/roostorg/coop
1#!/usr/bin/env node
2import http from 'http';
3import { promisify } from 'util';
4import _ from 'lodash';
5
6import getBottle from '../iocContainer/index.js';
7import makeServer from '../server.js';
8import {
9 getIntegrationRegistry,
10 getIntegrationsConfigPath,
11} from '../services/integrationRegistry/index.js';
12import { logErrorJson, logJson } from '../utils/logging.js';
13import { sleep } from '../utils/misc.js';
14
15const { app, shutdown } = await getBottle().then(async (bottle) =>
16 makeServer(bottle.container),
17);
18
19// Eager-load integration registry so config/plugins are read at startup (fail fast, and so logo URLs are set).
20try {
21 const registry = getIntegrationRegistry();
22 const configPath = getIntegrationsConfigPath();
23 const ids = registry.getConfigurableIds();
24 // eslint-disable-next-line no-restricted-syntax
25 logJson(
26 `Integrations: config=${configPath}, loaded=${ids.length} (${ids.join(', ')})`,
27 );
28} catch (err) {
29 // eslint-disable-next-line no-restricted-syntax
30 logErrorJson({
31 message: 'Failed to load integrations registry',
32 error: err instanceof Error ? err : new Error(String(err)),
33 });
34 process.exit(1);
35}
36
37const port = parsePort(process.env.PORT) ?? 8080;
38app.set('port', port);
39
40const server = http
41 .createServer(app)
42 .listen(port, () => {
43 // eslint-disable-next-line no-restricted-syntax
44 logJson(`Server is running at http://localhost:${port}`);
45 })
46 .on('error', function (error: NodeJS.ErrnoException) {
47 // eslint-disable-next-line no-restricted-syntax
48 logErrorJson({ error });
49 if (error.syscall !== 'listen') {
50 throw error;
51 }
52
53 // handle specific listen errors with friendly messages
54 switch (error.code) {
55 case 'EACCES':
56 // eslint-disable-next-line no-restricted-syntax
57 logErrorJson({
58 error,
59 message: `Port ${port} requires elevated privileges`,
60 });
61 process.exit(1);
62 case 'EADDRINUSE':
63 // eslint-disable-next-line no-restricted-syntax
64 logErrorJson({ error, message: `Port ${port} is already in use.` });
65 process.exit(1);
66 default:
67 throw error;
68 }
69 });
70
71// We need the keep-alive timeout to be longer than the ALB's idle timeout to
72// prevent the 502 errors we've been seeing. See this article for more info:
73// https://adamcrowder.net/posts/node-express-api-and-aws-alb-502/
74server.keepAliveTimeout = 65_000;
75
76const shutdownOnce = _.once(async () => {
77 try {
78 const closeServer = promisify(server.close.bind(server));
79
80 // Immediately disallow new connections + close idle ones.
81 const serverClosedPromise = closeServer();
82 server.closeIdleConnections();
83
84 // For the remaining connections (that were still active when we called
85 // `closeIdleConnections()` above), close them one at at time, as they
86 // become idle. To do that, after a response is finished, we close all idle
87 // connections, rather than just closing the socket that that request was
88 // using to send its response. This is probably overkill (and not optimal
89 // perf-wise, as `closeIdleConnections()` loops over all open connections)
90 // but it's defensive against the (rare and likely-inapplicable) possibility
91 // that the socket might still be in use for a request whose response hasn't
92 // been sent yet. That can happen multiple requests were pipelined over the
93 // same socket, which Nodejs supports (see
94 // https://www.yld.io/blog/exploring-how-nodejs-handles-http-connections/),
95 // although our load balancer probably never uses HTTP pipelining, as it's
96 // ill-supported by backends. For this to make a difference, we're also
97 // assuming that Node's definition of "idle" accounts for the possibility of
98 // pipelining.
99 server.prependListener('request', (_req, res) => {
100 if (!res.headersSent) {
101 res.setHeader('connection', 'close');
102 }
103
104 res.prependOnceListener('finish', () => {
105 server.closeIdleConnections();
106 });
107 });
108
109 // After 30s, if the server hasn't finished closing, force close all
110 // connections. Waiting longer doesn't make sense because the load balancer will
111 // time out the request at that point anyway.
112 await Promise.race([sleep(30_000), serverClosedPromise]);
113 server.closeAllConnections();
114
115 // Now that there are no pending requests or open client connections, clean
116 // up the app resources, like db connections, apollo plugins, etc.
117 await shutdown();
118 process.exit(0);
119 } catch (e) {
120 // eslint-disable-next-line no-restricted-syntax
121 logErrorJson({ message: 'error during shutdown', error: e });
122 process.exit(1);
123 }
124});
125
126process.on('uncaughtException', (err, _) => {
127 // eslint-disable-next-line no-restricted-syntax
128 logErrorJson({
129 message: 'UncaughtException',
130 error: err,
131 });
132 process.exit(1);
133});
134
135process.once('SIGTERM', shutdownOnce);
136process.once('SIGINT', shutdownOnce);
137
138function parsePort(val: string | undefined): number | undefined {
139 if (!val) return undefined;
140
141 const parsed = parseInt(val, 10);
142 return isNaN(parsed) ? undefined : parsed;
143}