fhirsmith 0.18.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/ACCESSIBILITY.md +119 -0
  2. package/CHANGELOG.md +149 -0
  3. package/CONFIGURATION.md +305 -0
  4. package/LICENSE +29 -0
  5. package/README.md +29 -122
  6. package/cluster.js +1220 -0
  7. package/cluster.md +707 -0
  8. package/config-template.json +77 -8
  9. package/extension-tracker/extension-tracker-template.html +5 -8
  10. package/extension-tracker/extension-tracker.js +0 -2
  11. package/folder/folder.js +7 -6
  12. package/ips/analysis.js +560 -0
  13. package/ips/ips-template.html +5 -8
  14. package/ips/ips.js +132 -59
  15. package/ips/nationality.js +120 -0
  16. package/ips/pipeline.js +74 -21
  17. package/ips/presenter.js +1 -1
  18. package/ips/readme.md +132 -25
  19. package/ips/render.js +466 -77
  20. package/ips/store.js +79 -22
  21. package/ips/structure.js +247 -0
  22. package/ips/terminology.js +1 -1
  23. package/library/body-limit.js +22 -0
  24. package/library/cache-generation.js +15 -0
  25. package/library/cgroup-memory.js +133 -1
  26. package/library/cluster-counts.js +121 -0
  27. package/library/cluster-events.js +153 -0
  28. package/library/cluster-metrics.js +193 -0
  29. package/library/cluster-status.js +468 -0
  30. package/library/config-reader.js +96 -0
  31. package/library/daily-report.js +100 -2
  32. package/library/html-server.js +27 -1
  33. package/library/i18nsupport.js +23 -1
  34. package/library/keep-alive.js +18 -0
  35. package/library/languages.js +17 -3
  36. package/library/logger.js +31 -0
  37. package/library/memory-guard.js +428 -0
  38. package/library/native-memory.js +52 -0
  39. package/library/npm-audit.js +51 -25
  40. package/library/openapi-doc.js +7 -5
  41. package/library/package-manager.js +8 -0
  42. package/library/role.js +197 -0
  43. package/library/server-credentials.js +169 -0
  44. package/library/sqlite-utilities.js +59 -0
  45. package/library/ssrf-guard.js +100 -0
  46. package/library/static-response-stats.js +16 -0
  47. package/library/version-utilities.js +50 -0
  48. package/library/worker-count.js +192 -0
  49. package/nginx.md +30 -11
  50. package/npmprojector/indexer.js +18 -10
  51. package/package.json +11 -8
  52. package/packages/feed-check-page.js +117 -0
  53. package/packages/feed-check.js +279 -0
  54. package/packages/npm-check.js +57 -3
  55. package/packages/npm-signing.js +219 -0
  56. package/packages/openapi.yaml +207 -1
  57. package/packages/package-crawler.js +322 -41
  58. package/packages/packages-template.html +6 -8
  59. package/packages/packages.js +604 -57
  60. package/packages/readme.md +113 -1
  61. package/publisher/publisher-template.html +2 -5
  62. package/publisher/publisher.js +121 -110
  63. package/publisher/readme.md +75 -0
  64. package/registry/crawler.js +84 -108
  65. package/registry/documentation/configuration.md +34 -0
  66. package/registry/registry-template.html +5 -8
  67. package/registry/registry.js +5 -4
  68. package/root-bare-template.html +7 -4
  69. package/root-template.html +5 -8
  70. package/server.js +398 -198
  71. package/shl/shl.js +38 -37
  72. package/static/a11y/a11y-check.js +832 -0
  73. package/static/a11y/axe.min.js +12 -0
  74. package/static/assets/css/bootstrap-fhir.css +15 -15
  75. package/static/assets/images/flags/au.svg +18 -0
  76. package/static/assets/images/flags/unknown.svg +11 -0
  77. package/static/assets/images/flags/us.svg +63 -0
  78. package/static/assets/images/icon_audit.png +0 -0
  79. package/static/assets/images/icon_clinical.png +0 -0
  80. package/static/assets/images/icon_document.png +0 -0
  81. package/static/assets/images/icon_external.png +0 -0
  82. package/static/assets/images/icon_medication.png +0 -0
  83. package/static/assets/images/icon_organization.png +0 -0
  84. package/static/assets/images/icon_other.png +0 -0
  85. package/static/assets/images/icon_person.png +0 -0
  86. package/static/assets/images/icon_section.png +0 -0
  87. package/stats-db.js +45 -0
  88. package/stats.js +197 -10
  89. package/testing/testing-template.html +5 -8
  90. package/token/nginx_passport_setup.md +1 -1
  91. package/token/security_guide.md +1 -1
  92. package/token/token-template.html +2 -5
  93. package/token/token.js +40 -36
  94. package/translations/Messages.properties +19 -0
  95. package/tx/README.md +154 -4
  96. package/tx/closure/closure-store.js +126 -3
  97. package/tx/cm/cm-database.js +69 -57
  98. package/tx/cm/cm-package.js +29 -1
  99. package/tx/cs/cs-api.js +284 -19
  100. package/tx/cs/cs-api.md +41 -4
  101. package/tx/cs/cs-areacode.js +1 -1
  102. package/tx/cs/cs-country.js +17 -5
  103. package/tx/cs/cs-cpt.js +33 -16
  104. package/tx/cs/cs-cs.js +148 -14
  105. package/tx/cs/cs-currency.js +1 -1
  106. package/tx/cs/cs-icd11.js +21 -13
  107. package/tx/cs/cs-icf.js +22 -3
  108. package/tx/cs/cs-lang.js +2 -2
  109. package/tx/cs/cs-loinc.js +185 -66
  110. package/tx/cs/cs-mimetypes.js +12 -2
  111. package/tx/cs/cs-ndc.js +40 -25
  112. package/tx/cs/cs-omop.js +54 -29
  113. package/tx/cs/cs-rxnorm.js +300 -196
  114. package/tx/cs/cs-snomed.js +608 -64
  115. package/tx/cs/cs-ucum.js +14 -4
  116. package/tx/cs/cs-unii.js +18 -14
  117. package/tx/cs/cs-virtual.js +210 -0
  118. package/tx/ecosystem-resolver.js +413 -0
  119. package/tx/html/cluster-dash-metrics.liquid +26 -0
  120. package/tx/html/cluster-home-metrics.liquid +31 -0
  121. package/tx/html/cluster-metrics-script.liquid +251 -0
  122. package/tx/html/codesystem-operations.liquid +3 -3
  123. package/tx/html/ecl-panel.liquid +1 -1
  124. package/tx/html/external-metrics.liquid +17 -0
  125. package/tx/html/operations-form.liquid +17 -17
  126. package/tx/html/search-form.liquid +11 -11
  127. package/tx/html/status-line.liquid +58 -0
  128. package/tx/html/status-panel.liquid +140 -0
  129. package/tx/html/status-tasks.liquid +25 -0
  130. package/tx/html/tx-template.html +2 -5
  131. package/tx/importers/atc-to-fhir.js +17 -16
  132. package/tx/importers/export-icd11-codesystem.js +3 -2
  133. package/tx/importers/import-icd11.js +4 -3
  134. package/tx/importers/import-loinc.module.js +118 -56
  135. package/tx/importers/import-ndc.module.js +56 -44
  136. package/tx/importers/import-rxnorm.module.js +118 -79
  137. package/tx/importers/import-sct.module.js +57 -58
  138. package/tx/importers/import-unii.module.js +37 -36
  139. package/tx/importers/subset-loinc.module.js +49 -48
  140. package/tx/importers/subset-rxnorm.module.js +29 -28
  141. package/tx/importers/terminal.js +18 -0
  142. package/tx/importers/tx-import-base.js +13 -12
  143. package/tx/importers/tx-import-settings.js +5 -4
  144. package/tx/importers/tx-import.js +53 -52
  145. package/tx/library/canonical-resource.js +49 -0
  146. package/tx/library/codesystem.js +94 -5
  147. package/tx/library/designations.js +45 -2
  148. package/tx/library/extensions.js +13 -7
  149. package/tx/library/implicit-valuesets.js +66 -0
  150. package/tx/library/renderer.js +77 -21
  151. package/tx/library/sqlite-utilities.js +2 -0
  152. package/tx/library/ucum-parsers.js +11 -1
  153. package/tx/library/wait-for-cache.js +43 -0
  154. package/tx/library.js +321 -22
  155. package/tx/openapi-operations.js +6 -4
  156. package/tx/operation-context.js +460 -99
  157. package/tx/params.js +25 -0
  158. package/tx/problems.js +3 -5
  159. package/tx/provider.js +278 -42
  160. package/tx/resource-cache-sqlite.js +1050 -0
  161. package/tx/sct/ecl.js +47 -6
  162. package/tx/sct/editions.js +39 -0
  163. package/tx/sct/expressions.js +10 -1
  164. package/tx/sct/load-config.js +107 -0
  165. package/tx/sct/mmap.js +109 -0
  166. package/tx/sct/structures.js +166 -13
  167. package/tx/sct/text-index.js +5 -1
  168. package/tx/syndication/syndication-feed.js +2 -2
  169. package/tx/tx-html.js +44 -8
  170. package/tx/tx.fhir.org.yml +8 -8
  171. package/tx/tx.js +282 -80
  172. package/tx/usage-tracker.js +39 -11
  173. package/tx/vs/vs-api.js +19 -0
  174. package/tx/vs/vs-database.js +386 -62
  175. package/tx/vs/vs-package.js +27 -4
  176. package/tx/vs/vs-vsac.js +648 -100
  177. package/tx/workers/batch-validate.js +37 -10
  178. package/tx/workers/batch.js +6 -1
  179. package/tx/workers/cache-control.js +15 -4
  180. package/tx/workers/closure.js +156 -13
  181. package/tx/workers/compare.js +8 -5
  182. package/tx/workers/expand.js +1173 -289
  183. package/tx/workers/metadata.js +35 -0
  184. package/tx/workers/search.js +53 -1
  185. package/tx/workers/supplement-filters.js +145 -0
  186. package/tx/workers/translate.js +169 -4
  187. package/tx/workers/validate.js +223 -119
  188. package/tx/workers/worker.js +123 -31
  189. package/tx/xversion/xv-valueset.js +7 -4
  190. package/utilities/submit-test-report.js +3 -1
  191. package/utilities/tx-stress-test.js +482 -0
  192. package/vcl/readme.md +37 -0
  193. package/vcl/vcl-parser.js +15 -0
  194. package/xig/readme.md +52 -0
  195. package/xig/xig-template.html +5 -8
  196. package/xig/xig.js +238 -422
  197. package/changelog-template.md +0 -22
  198. package/registry/test-runner.js +0 -237
  199. package/release.sh +0 -80
  200. package/test-scripts/repro-re2-wasm-leak.js +0 -34
  201. package/tsconfig.eslint.json +0 -9
  202. package/tx/html/dash-metrics.liquid +0 -147
  203. package/tx/html/home-metrics.liquid +0 -225
package/cluster.js ADDED
@@ -0,0 +1,1220 @@
1
+ #!/usr/bin/env node
2
+
3
+ //
4
+ // Copyright 2025, Health Intersections Pty Ltd (http://www.healthintersections.com.au)
5
+ //
6
+ // Licensed under BSD-3: https://opensource.org/license/bsd-3-clause
7
+ //
8
+
9
+ /**
10
+ * An optional launcher: one master and N terminology workers on one machine, behind one port
11
+ * (see cluster.md). It applies the routing of nginx.md's configuration, as one upstream: always
12
+ * run it behind nginx on the same host, with request buffering on (proxy_request_buffering,
13
+ * nginx's default). It is not built to face clients itself. It:
14
+ *
15
+ * - starts the master (node server.js --role=master), waits until it is ready (it says so
16
+ * on its IPC channel once it listens), then starts the workers (--role=tx-worker) one
17
+ * after another, each once the one before is ready, each with its own instance code and
18
+ * port; exits 1 if the master exits maxStartFailures times without ever being ready
19
+ * - routes: a request with X-Cache-Id to the process that issued it (by the id's prefix);
20
+ * $closure, the syndication feed, the source info pages and everything that isn't a tx
21
+ * endpoint to the master; any other tx request to the tx process with the fewest
22
+ * requests outstanding
23
+ * - sends a process at most maxOutstanding tx requests at once (those routed to a tx process,
24
+ * not the master's own paths or CORS preflights, which neither wait nor count); the rest
25
+ * wait, and one that has waited queueSeconds (ownerQueueSeconds with a cache-id) gets 503
26
+ * + Retry-After
27
+ * - lets cheap requests (by method and path: the fast lane) use fastLaneExtra more slots
28
+ * than maxOutstanding, ahead of the others, leaving them normalMinSlots
29
+ * - sends no new requests to a process that answered 503 for cooldownSeconds, while
30
+ * another can take them
31
+ * - answers 504 for a process that hasn't begun its response in upstreamTimeoutSeconds, and
32
+ * aborts that request; after SUSPECT_TIMEOUTS in a row, the process cools as on a 503
33
+ * - frees a tx request's slot when the process's response ends, however slowly the client
34
+ * reads it, and closes the connection of a client that takes none of a response for
35
+ * writeTimeoutSeconds
36
+ * - restarts a process that exits, waiting longer each time it fails quickly (up to a minute)
37
+ * - restarts a tx-worker held over the memory guard's refusal threshold for recycleSeconds
38
+ * with no request at work
39
+ * - every statusSeconds, passes the master a snapshot of every process's memory, caches and
40
+ * work, for its status panel (library/cluster-status.js)
41
+ * - on SIGTERM or SIGINT, stops taking connections, lets the requests in progress finish,
42
+ * then stops the processes (each shuts down as a single server does)
43
+ *
44
+ * node cluster.js [--workers=N] [--port=P] [--internal-port=P] [--master-serves-tx=false]
45
+ */
46
+
47
+ const http = require('http');
48
+ const path = require('path');
49
+ const { spawn } = require('child_process');
50
+ const { checkInstanceCode, bindAddress, connectAddress, ROLE_TRIM_ALLOCATOR, READY } = require('./library/role');
51
+ const { ClusterMetrics, METRIC, SNAPSHOT } = require('./library/cluster-metrics');
52
+ const { PendingCounts, COUNTS, COUNTS_INTERVAL_MS } = require('./library/cluster-counts');
53
+ const { PendingClusterEvents, CLUSTER_EVENTS, KINDS } = require('./library/cluster-events');
54
+ const { readCgroupAnon, readCgroupStat, readProcessMemory } = require('./library/cgroup-memory');
55
+ const { buildSnapshot, STATUS, CLUSTER_STATUS, STATUS_INTERVAL_MS } = require('./library/cluster-status');
56
+ const { thresholds } = require('./library/memory-guard');
57
+ const { defaultWorkers, describeDecision } = require('./library/worker-count');
58
+ const { englishMessage } = require('./library/i18nsupport');
59
+ const { formatDuration } = require('./tx/operation-context');
60
+
61
+ const TX_ISSUE_TYPE = 'http://hl7.org/fhir/tools/CodeSystem/tx-issue-type';
62
+ const MESSAGE_ID_EXTENSION = 'http://hl7.org/fhir/StructureDefinition/operationoutcome-message-id';
63
+
64
+ // how long the requests in progress get on stop, and on a worker before a memory recycle
65
+ // (drainSeconds): half again the operation limit (operationTimeLimit, 20 s)
66
+ const DRAIN_MS = 30000;
67
+ // how often start() looks whether the master is ready (an in-memory flag)
68
+ const WAIT_POLL_MS = 250;
69
+ const MAX_BACKOFF_MS = 60000;
70
+ // an idle connection to a process is closed after this long: below the process's own
71
+ // keep-alive timeout (5 s at the least), so no request goes down one it is closing (a 502)
72
+ const UPSTREAM_IDLE_MS = 4000;
73
+ // a process that ran this long after it was ready, before exiting, is restarted at once
74
+ const HEALTHY_RUN_MS = 60000;
75
+ // a master that exits this many times in a row without ever having been ready: the launcher
76
+ // exits 1 (cluster.md, maxStartFailures). With the waits between (1, 2, 4, 8, 16 s), the
77
+ // sixth start is 31 s after the first failure, past the 30 s an old process still shutting
78
+ // down may hold the port for (each process's SIGKILL deadline)
79
+ const MAX_START_FAILURES = 6;
80
+ // tx requests sent to one process at once; the rest wait in the launcher (cluster.md)
81
+ const MAX_OUTSTANDING = 16;
82
+ // a waiting tx request gets 503 after this long; one with a cache-id, which only its owner
83
+ // can take, after OWNER_QUEUE_MS
84
+ const QUEUE_MS = 1000;
85
+ const OWNER_QUEUE_MS = 30000;
86
+ // a process that answered 503 gets no new requests for this long while another can take
87
+ // them. Short: its memory guard re-measures only when a request arrives
88
+ const COOLDOWN_MS = 2000;
89
+ // a process that hasn't begun its response this long after the request was sent is taken to be
90
+ // wedged: the client gets 504 and the request is aborted. The operation limit
91
+ // (modules.tx.operationTimeLimit, 20 s) counts an operation's own compute, not the turns it
92
+ // waits for others on its event loop, so a heavy one in a busy process takes longer by the
93
+ // clock: three times it. Time in the launcher's queue is apart
94
+ const UPSTREAM_TIMEOUT_MS = 60000;
95
+ // this many timeouts in a row, and the process cools (as on a 503) while another can take them
96
+ const SUSPECT_TIMEOUTS = 3;
97
+ // a client that takes none of the response waiting for it this long is cut off (cluster.md,
98
+ // writeTimeoutSeconds): one that reads takes some within milliseconds; nginx's own
99
+ // send_timeout is the same 60 s
100
+ const WRITE_TIMEOUT_MS = 60000;
101
+ // a tx-worker over the memory guard's refusal threshold this long, with no request at work, is
102
+ // restarted (cluster.md, "Memory"): past the guard's own GC and trims, short next to a retry
103
+ const RECYCLE_MS = 30000;
104
+ // how often the launcher measures for that
105
+ const RECYCLE_CHECK_MS = 1000;
106
+ // a request in flight this long is work; a refusal (503) is answered at once
107
+ const AT_WORK_MS = 1000;
108
+ // memory to leave under the refusal threshold with every process loaded and idle: about what
109
+ // one heavy operation (a large SNOMED $expand, a big $batch-validate-code) uses
110
+ const HEADROOM_GB = 2;
111
+ // a fast-lane request (isFastLane) may go to a process with this many more than
112
+ // MAX_OUTSTANDING; a POST takes the lane only with a Content-Length up to FAST_LANE_MAX_BYTES
113
+ const FAST_LANE_EXTRA = 4;
114
+ const FAST_LANE_MAX_BYTES = 1024;
115
+ // while other requests wait, the fast lane leaves them this many of a process's slots, so
116
+ // heavy work goes on during a flood of cheap requests
117
+ const NORMAL_MIN_SLOTS = 4;
118
+ const GB = (b) => (b / 2 ** 30).toFixed(1);
119
+ const HOP_BY_HOP = ['connection', 'keep-alive', 'proxy-connection', 'transfer-encoding', 'upgrade', 'te', 'trailer', 'expect'];
120
+
121
+ /**
122
+ * The settings, from the flags, then the environment, then config.json (server.cluster).
123
+ * @param {string[]} argv - process.argv.slice(2)
124
+ * @param {Object} env - process.env
125
+ * @param {Object} config - config.json
126
+ * @param {Object} [cpu] - for tests: the CPU inputs (library/worker-count.js, cpuInputs)
127
+ */
128
+ function loadSettings(argv, env, config, cpu = {}) {
129
+ const flags = {};
130
+ for (const a of argv) {
131
+ const m = /^--([a-z-]+)=(.*)$/.exec(a);
132
+ if (m) {
133
+ flags[m[1]] = m[2];
134
+ }
135
+ }
136
+ const cluster = (config.server && config.server.cluster) || {};
137
+ const tx = (config.modules && config.modules.tx) || {};
138
+ if (!tx.enabled) {
139
+ throw new Error('cluster.js runs terminology workers: the tx module must be enabled in config.json');
140
+ }
141
+ const port = Number(flags.port || env.PORT || (config.server && config.server.port) || 3000);
142
+ // not given: absent, null, or an empty FHIRSMITH_WORKERS; 0: the master alone
143
+ const workersGiven = flags.workers ?? (env.FHIRSMITH_WORKERS || undefined) ?? cluster.workers ?? undefined;
144
+ if (workersGiven !== undefined && !/^\d+$/.test(String(workersGiven))) {
145
+ throw new Error(`the number of workers (--workers, FHIRSMITH_WORKERS or server.cluster.workers) must be a whole number, not '${workersGiven}'`);
146
+ }
147
+ // not given: from the CPUs, by the engine's rule (library/worker-count.js; cluster.md)
148
+ const workerDecision = defaultWorkers({ ...cpu, cores: cluster.cores ?? cpu.cores, rule: cluster.workerRule });
149
+ const workers = Number(workersGiven ?? workerDecision.workers);
150
+ const internalPort = Number(flags['internal-port'] || cluster.internalPort || port + 1);
151
+ // the master is on internalPort, worker n on internalPort + n
152
+ if (!Number.isInteger(internalPort) || internalPort < 1 || internalPort + workers > 65535) {
153
+ throw new Error(`the internal ports (--internal-port or server.cluster.internalPort ${internalPort}, to + ${workers} for the workers) must be port numbers`);
154
+ }
155
+ if (port >= internalPort && port <= internalPort + workers) {
156
+ throw new Error(`the public port ${port} is one of the internal ports ${internalPort}-${internalPort + workers} (the master's, and one per worker): ` +
157
+ 'choose server.cluster.internalPort (--internal-port) outside it');
158
+ }
159
+ const masterCode = checkInstanceCode(tx.instanceCode || 'm0', 'modules.tx.instanceCode');
160
+ const servesTx = flags['master-serves-tx'] !== undefined ? flags['master-serves-tx'] !== 'false' : cluster.masterServesTx !== false;
161
+ // where the processes listen (server.cluster.bindAddress: loopback by default)
162
+ const host = connectAddress(bindAddress(config));
163
+ const procs = [{ name: 'master', role: 'master', code: masterCode, host, port: internalPort, servesTx }];
164
+ for (let i = 1; i <= workers; i++) {
165
+ const code = 'tx' + i;
166
+ if (code === masterCode) {
167
+ throw new Error(`modules.tx.instanceCode '${masterCode}' is also the code of a worker: choose another`);
168
+ }
169
+ procs.push({ name: 'worker ' + code, role: 'tx-worker', code, host, port: internalPort + i, servesTx: true });
170
+ }
171
+ // with the shared cache any process can answer any cache-id (cluster.md, "Options")
172
+ const sharedCache = cluster.sharedCache === true;
173
+ const statusSeconds = Number(cluster.statusSeconds ?? STATUS_INTERVAL_MS / 1000);
174
+ if (!(statusSeconds > 0)) {
175
+ throw new Error(`server.cluster.statusSeconds must be a number of seconds above 0, not '${cluster.statusSeconds}'`);
176
+ }
177
+ return {
178
+ port,
179
+ procs,
180
+ txPaths: (tx.endpoints || []).map(e => String(e.path).replace(/\/+$/, '')),
181
+ drainMs: Number(cluster.drainSeconds ?? DRAIN_MS / 1000) * 1000,
182
+ maxOutstanding: Number(cluster.maxOutstanding ?? MAX_OUTSTANDING),
183
+ queueMs: Number(cluster.queueSeconds ?? QUEUE_MS / 1000) * 1000,
184
+ ownerQueueMs: Number(cluster.ownerQueueSeconds ?? OWNER_QUEUE_MS / 1000) * 1000,
185
+ cooldownMs: Number(cluster.cooldownSeconds ?? COOLDOWN_MS / 1000) * 1000,
186
+ upstreamTimeoutMs: Number(cluster.upstreamTimeoutSeconds ?? UPSTREAM_TIMEOUT_MS / 1000) * 1000,
187
+ writeTimeoutMs: Number(cluster.writeTimeoutSeconds ?? WRITE_TIMEOUT_MS / 1000) * 1000,
188
+ recycleMs: Number(cluster.recycleSeconds ?? RECYCLE_MS / 1000) * 1000,
189
+ maxStartFailures: Number(cluster.maxStartFailures ?? MAX_START_FAILURES),
190
+ // the number of workers was not given: fewer if memory is short (see sizeWorkers)
191
+ workersDefault: workersGiven === undefined,
192
+ workerDecision,
193
+ headroomBytes: Number(cluster.memoryHeadroomGB ?? HEADROOM_GB) * 2 ** 30,
194
+ memory: (config.server && config.server.memory) || {},
195
+ fastLaneExtra: Number(cluster.fastLaneExtra ?? FAST_LANE_EXTRA),
196
+ fastLaneMaxBytes: Number(cluster.fastLaneMaxBytes ?? FAST_LANE_MAX_BYTES),
197
+ normalMinSlots: Number(cluster.normalMinSlots ?? NORMAL_MIN_SLOTS),
198
+ statusMs: statusSeconds * 1000,
199
+ routing: { cacheAffinity: !sharedCache || cluster.cacheAffinity === true, sharedClosure: cluster.sharedClosure === true, sharedCache }
200
+ };
201
+ }
202
+
203
+ // the path after the tx endpoint ('' for the endpoint itself), or null if not a tx path
204
+ function txRest(url, txPaths) {
205
+ let p = String(url).split('?')[0];
206
+ try {
207
+ p = decodeURIComponent(p);
208
+ } catch {
209
+ // leave it as it is
210
+ }
211
+ const ep = txPaths.find(e => p === e || p.startsWith(e + '/'));
212
+ return ep === undefined ? null : p.substring(ep.length);
213
+ }
214
+
215
+ const FAST_OPS = /^(\/[A-Za-z]+(\/[A-Za-z0-9.-]{1,64})?)?\/\$(lookup|subsumes|validate-code)$/;
216
+
217
+ /**
218
+ * Whether a tx request takes the fast lane (cluster.md), by method, path and declared size (the
219
+ * launcher doesn't read bodies): a GET or HEAD of metadata, $lookup, $subsumes or
220
+ * $validate-code, or a POST of those three with a Content-Length of at most maxBytes (0: none).
221
+ * @param {Object} req - method, url, headers
222
+ */
223
+ function isFastLane(req, txPaths, maxBytes) {
224
+ const rest = txRest(req.url, txPaths);
225
+ if (rest === null) {
226
+ return false;
227
+ }
228
+ const r = rest.replace(/\/+$/, '');
229
+ if (req.method === 'GET' || req.method === 'HEAD') {
230
+ return r === '/metadata' || FAST_OPS.test(r);
231
+ }
232
+ const len = req.headers['content-length'];
233
+ return req.method === 'POST' && maxBytes > 0 && FAST_OPS.test(r) && /^\d+$/.test(len || '') && Number(len) <= maxBytes;
234
+ }
235
+
236
+ /**
237
+ * Where a request goes, by the rules in nginx.md.
238
+ * @param {string} url - the request target (path and query)
239
+ * @param {Object} headers - the request headers (lower case names)
240
+ * @param {string[]} txPaths - the tx endpoint paths, e.g. ['/r4', '/r5']
241
+ * @param {Object} [routing] - from loadSettings: cacheAffinity (default true) sends a
242
+ * request with a cache-id to the process that issued it; sharedClosure sends $closure
243
+ * to any tx process rather than the master
244
+ * @returns {{to: 'master'}|{to: 'owner', code: string}|{to: 'tx'}}
245
+ */
246
+ function classify(url, headers, txPaths, routing = {}) {
247
+ const rest = txRest(url, txPaths);
248
+ if (rest === null) {
249
+ return { to: 'master' };
250
+ }
251
+ // the closure tables (unless shared) and the syndication store are the master's; the
252
+ // source info pages carry the VSAC resync form, which writes the VSAC database
253
+ if (/^\/\$closure\/?$/.test(rest)) {
254
+ // closure never uses a cache-id, so the least loaded, whatever the header says
255
+ return routing.sharedClosure ? { to: 'tx' } : { to: 'master' };
256
+ }
257
+ if (/^\/syndication(\/|$)/.test(rest) || rest.startsWith('/info/')) {
258
+ return { to: 'master' };
259
+ }
260
+ if (routing.cacheAffinity === false) {
261
+ return { to: 'tx' };
262
+ }
263
+ const m = /^([A-Za-z0-9]{1,16})\./.exec(headers['x-cache-id'] || '');
264
+ return m ? { to: 'owner', code: m[1] } : { to: 'tx' };
265
+ }
266
+
267
+ /**
268
+ * The process to send a request to, or null if none is ready.
269
+ * @param {Object} route - from classify
270
+ * @param {Object[]} procs - {code, role, servesTx, ready, outstanding, coolUntil}
271
+ * @param {function(Object): boolean} [hasRoom] - whether a process has room for this request:
272
+ * the least loaded of those that do, if any
273
+ */
274
+ let rotation = 0;
275
+ function pickTarget(route, procs, hasRoom = () => true) {
276
+ if (route.to === 'master') {
277
+ return procs.find(p => p.role === 'master' && p.ready) || null;
278
+ }
279
+ if (route.to === 'owner') {
280
+ const owner = procs.find(p => p.code === route.code && p.ready && p.servesTx);
281
+ if (owner) {
282
+ return owner;
283
+ }
284
+ // the owner is down (send answers that itself, without the shared cache) or the code is
285
+ // unknown: any tx process answers. With the shared cache, the cache is there; for an
286
+ // unknown code it is cache-id-unknown, and the client starts a new cache
287
+ }
288
+ const ready = procs.filter(p => p.servesTx && p.ready);
289
+ if (ready.length === 0) {
290
+ return null;
291
+ }
292
+ // not one that is refusing (it answered 503), unless they all are
293
+ const now = Date.now();
294
+ const warm = ready.filter(p => !(p.coolUntil > now));
295
+ const usable = warm.length ? warm : ready;
296
+ const open = usable.filter(hasRoom);
297
+ const candidates = open.length ? open : usable;
298
+ rotation = (rotation + 1) % candidates.length;
299
+ let best = null;
300
+ for (let i = 0; i < candidates.length; i++) {
301
+ const c = candidates[(rotation + i) % candidates.length];
302
+ if (!best || c.outstanding < best.outstanding) {
303
+ best = c;
304
+ }
305
+ }
306
+ return best;
307
+ }
308
+
309
+ /**
310
+ * The launcher's view of the memory guard's refusal threshold: the same limit, fraction and
311
+ * cgroup (thresholds in library/memory-guard.js). sample(procs) gives each proc's RssAnon (0:
312
+ * not known; Linux only) and, with the cgroup measured, how far it is over (`excess`; else null).
313
+ * @param {Object} memory - config.server.memory
314
+ */
315
+ function memorySampler(memory = {}) {
316
+ const guard = thresholds({ ...memory, group: true });
317
+ const { limit, group, rejectFraction, rejectAt } = guard;
318
+ const anonOf = (pid) => {
319
+ const m = readProcessMemory(pid);
320
+ return m ? m.anon : 0;
321
+ };
322
+ return {
323
+ rejectAt,
324
+ // for the status snapshot (library/cluster-status.js): the guard's limit and thresholds,
325
+ // and the cgroup's memory.stat when the guard measures the cgroup (else null)
326
+ guard,
327
+ cgroupStat: () => (group ? readCgroupStat(group) : null),
328
+ describe: `over ${(rejectAt / 2 ** 30).toFixed(1)} GB (${Math.round(rejectFraction * 100)}% of ${(limit / 2 ** 30).toFixed(1)} GB)` +
329
+ (group ? ' for the whole cgroup' : ' for the process'),
330
+ sample(procs) {
331
+ const excess = group ? readCgroupAnon(group) - rejectAt : null;
332
+ return { excess, own: new Map(procs.map(p => [p, p.child && p.child.pid ? anonOf(p.child.pid) : 0])) };
333
+ }
334
+ };
335
+ }
336
+
337
+ // Node flags for the launcher itself, not its children: a debugger port each child would fight
338
+ // over (--inspect...), or a watcher that restarts a child behind the launcher's back (--watch...)
339
+ const OWN_FLAGS = /^--(inspect(-brk|-wait|-port|-publish-uid)?|debug-port|watch(-path|-preserve-output|-kill-signal)?)(=|$)/;
340
+ // those of them that take their value as the next argument when written without '='
341
+ const OWN_FLAGS_WITH_VALUE = ['--inspect-port', '--debug-port', '--inspect-publish-uid', '--watch-path', '--watch-kill-signal'];
342
+
343
+ /**
344
+ * The launcher's Node flags (process.execArgv) that its children get: all but OWN_FLAGS.
345
+ * @param {string[]} execArgv
346
+ */
347
+ function childExecArgv(execArgv) {
348
+ const out = [];
349
+ for (let i = 0; i < execArgv.length; i++) {
350
+ if (OWN_FLAGS.test(execArgv[i])) {
351
+ if (OWN_FLAGS_WITH_VALUE.includes(execArgv[i])) {
352
+ i++;
353
+ }
354
+ continue;
355
+ }
356
+ out.push(execArgv[i]);
357
+ }
358
+ return out;
359
+ }
360
+
361
+ function isLoopback(address) {
362
+ return address === '127.0.0.1' || address === '::1';
363
+ }
364
+
365
+ /**
366
+ * @param {Object} settings - from loadSettings, plus optionally
367
+ * command(proc): [executable, args] to start a process (tests), healthyRunMs (tests), log
368
+ */
369
+ function createLauncher(settings) {
370
+ const log = settings.log || ((msg) => console.log(`[cluster] ${msg}`));
371
+ const healthyRunMs = settings.healthyRunMs ?? HEALTHY_RUN_MS;
372
+ const maxStartFailures = settings.maxStartFailures ?? MAX_START_FAILURES; // 0: never give up
373
+ const maxOutstanding = settings.maxOutstanding ?? MAX_OUTSTANDING; // 0: no limit
374
+ const queueMs = settings.queueMs ?? QUEUE_MS; // 0: refuse at once
375
+ const ownerQueueMs = settings.ownerQueueMs ?? OWNER_QUEUE_MS;
376
+ const cooldownMs = settings.cooldownMs ?? COOLDOWN_MS; // 0: none
377
+ const upstreamTimeoutMs = settings.upstreamTimeoutMs ?? UPSTREAM_TIMEOUT_MS; // 0: none
378
+ const writeTimeoutMs = settings.writeTimeoutMs ?? WRITE_TIMEOUT_MS; // 0: none
379
+ const recycleMs = settings.recycleMs ?? RECYCLE_MS; // 0: never
380
+ // (tests give their own)
381
+ const memory = settings.memorySampler || memorySampler(settings.memory);
382
+ const headroom = settings.headroomBytes ?? HEADROOM_GB * 2 ** 30;
383
+ const fastLaneExtra = settings.fastLaneExtra ?? FAST_LANE_EXTRA; // 0: no fast lane
384
+ const fastLaneMaxBytes = settings.fastLaneMaxBytes ?? FAST_LANE_MAX_BYTES;
385
+ const normalMinSlots = Math.min(settings.normalMinSlots ?? NORMAL_MIN_SLOTS, maxOutstanding);
386
+ // what each process does: every one has a role (library/role.js)
387
+ const trimAllocator = (settings.memory && settings.memory.trimAllocator) ?? ROLE_TRIM_ALLOCATOR;
388
+ const command = settings.command || ((proc) => [process.execPath, [
389
+ ...new Set(['--expose-gc', ...childExecArgv(process.execArgv)]), path.join(__dirname, 'server.js'),
390
+ `--role=${proc.role}`, `--instance-code=${proc.code}`, `--port=${proc.port}`]]);
391
+ const procs = settings.procs.map(p => ({
392
+ // outstanding: the tx requests it has (those that wait for a slot); active: every request
393
+ ...p, child: null, ready: false, outstanding: 0, fastOutstanding: 0, active: 0, coolUntil: 0, timeouts: 0, failures: 0, startedAt: 0, readyAt: 0, everReady: false, exitedAt: 0, restartTimer: null,
394
+ // for checkMemory: when each request in flight was sent, since when it has been over with
395
+ // none at work, whether it is being restarted, its memory when it was ready
396
+ sentAt: new Set(), overSince: 0, recycling: false, readyMemory: null,
397
+ // the figures it last sent for the status snapshot (library/cluster-status.js)
398
+ statusFigures: null,
399
+ // since when it has been unavailable after an exit or recycle (0: it hasn't), for the daily report
400
+ downSince: 0,
401
+ agent: new http.Agent({ keepAlive: true, maxSockets: 256, timeout: settings.upstreamIdleMs || UPSTREAM_IDLE_MS })
402
+ }));
403
+ const metrics = new ClusterMetrics(procs, {
404
+ // the memory graph's Total and threshold lines: the cgroup when the guard measures it
405
+ rejectAt: memory.rejectAt, group: Boolean(memory.guard && memory.guard.group),
406
+ sharedCache: Boolean(settings.routing && settings.routing.sharedCache)
407
+ });
408
+ // the workers' request counts, on their way to the master's statistics
409
+ const pendingCounts = new PendingCounts();
410
+ // what the launcher saw, on its way to the master's daily report (library/cluster-events.js)
411
+ const pendingEvents = new PendingClusterEvents();
412
+ let eventsTimer = null;
413
+ let server = null;
414
+ let stopping = false;
415
+ // why the launcher gave up starting (see maxStartFailures), or null
416
+ let gaveUp = null;
417
+ let onIdle = null;
418
+ // tx requests waiting for a process with room, oldest first; the fast lane's apart
419
+ const waiting = [];
420
+ const fastWaiting = [];
421
+ let queueRefused = 0;
422
+ let lastQueueLog = 0;
423
+ // requests the launcher has not finished answering, for the drain on stop
424
+ let inFlight = 0;
425
+ let writeTimeouts = 0;
426
+ let lastWriteTimeoutLog = 0;
427
+ let memoryTimer = null;
428
+ let statusTimer = null;
429
+ // requests the launcher has passed on, for the status snapshot
430
+ let proxied = 0;
431
+ const launcherStartedAt = Date.now();
432
+ const statusMs = settings.statusMs || STATUS_INTERVAL_MS;
433
+
434
+ // Only the master renders the public home/dashboard pages. Keep at most one
435
+ // snapshot in flight, coalescing changes if its IPC channel is backpressured.
436
+ function publishMetrics() {
437
+ const master = procs.find(p => p.role === 'master');
438
+ const child = master && master.child;
439
+ if (!child || !child.connected) return;
440
+ if (master.metricsSending === child) {
441
+ master.metricsDirty = true;
442
+ return;
443
+ }
444
+ master.metricsSending = child;
445
+ master.metricsDirty = false;
446
+ try {
447
+ child.send({ type: SNAPSHOT, ...metrics.snapshot() }, () => {
448
+ if (master.metricsSending !== child) return;
449
+ master.metricsSending = null;
450
+ if (master.metricsDirty && master.child === child && child.connected) publishMetrics();
451
+ });
452
+ } catch {
453
+ master.metricsSending = null;
454
+ }
455
+ }
456
+
457
+ // The cluster's memory in all, for the graphs' Total line: the cgroup's private memory
458
+ // when the guard measures it, else what the processes hold privately, summed
459
+ function memoryTotal() {
460
+ const cg = memory.cgroupStat ? memory.cgroupStat() : null;
461
+ if (cg) {
462
+ return cg.anon;
463
+ }
464
+ const own = procs.map(p => (p.child && p.child.pid ? readProcessMemory(p.child.pid) : null)).filter(Boolean);
465
+ return own.length ? own.reduce((a, m) => a + m.anon, 0) : null;
466
+ }
467
+
468
+ // The status snapshot (library/cluster-status.js): what each process last sent, and what the
469
+ // launcher reads itself
470
+ function statusSnapshot(now = Date.now()) {
471
+ const self = readProcessMemory('self');
472
+ const heap = process.memoryUsage();
473
+ return buildSnapshot({
474
+ now, intervalMs: statusMs,
475
+ procs: procs.map(p => {
476
+ const series = metrics.series.get(p.code) || {};
477
+ return {
478
+ code: p.code, role: p.role, status: series.status || (p.ready ? 'ready' : 'starting'), restarts: series.restarts || 0,
479
+ startedAt: p.child ? p.startedAt : 0,
480
+ figures: p.child ? p.statusFigures : null,
481
+ memory: p.child && p.child.pid ? readProcessMemory(p.child.pid) : null
482
+ };
483
+ }),
484
+ launcher: { startedAt: launcherStartedAt, requests: proxied, memory: self, heapUsed: heap.heapUsed,
485
+ heapLimit: require('v8').getHeapStatistics().heap_size_limit },
486
+ guard: memory.guard || { limit: 0, rejectAt: memory.rejectAt, abortAt: 0, rejectFraction: 0, abortFraction: 0, group: null },
487
+ cgroup: memory.cgroupStat ? memory.cgroupStat() : null,
488
+ sharedCache: Boolean(settings.routing && settings.routing.sharedCache),
489
+ procAvailable: self !== null
490
+ });
491
+ }
492
+
493
+ // to the master, one at a time; one that can't go is replaced by the next
494
+ let statusSending = false;
495
+ function sendStatus() {
496
+ const master = procs.find(p => p.role === 'master');
497
+ const child = master && master.child;
498
+ if (statusSending || !child || !child.connected || !master.ready) {
499
+ return;
500
+ }
501
+ statusSending = true;
502
+ try {
503
+ child.send({ type: CLUSTER_STATUS, snapshot: statusSnapshot() }, () => { statusSending = false; });
504
+ } catch {
505
+ statusSending = false;
506
+ }
507
+ }
508
+
509
+ // Pass the workers' request counts (library/cluster-counts.js) and what the launcher saw
510
+ // (library/cluster-events.js) on to the master, once it is ready: it is listening by then.
511
+ // One lot at a time; a lot that can't be delivered is kept for the next try (more arriving,
512
+ // or the master becoming ready)
513
+ function delivery(pending, type) {
514
+ let inFlight = null;
515
+ const deliver = () => {
516
+ const master = procs.find(p => p.role === 'master');
517
+ const child = master && master.child;
518
+ if (inFlight || pending.size === 0 || !child || !child.connected || !master.ready) {
519
+ return inFlight || Promise.resolve();
520
+ }
521
+ const rows = pending.take();
522
+ inFlight = new Promise(resolve => {
523
+ const done = error => {
524
+ inFlight = null;
525
+ if (error) {
526
+ pending.add(rows);
527
+ }
528
+ resolve();
529
+ if (!error && pending.size > 0) {
530
+ void deliver();
531
+ }
532
+ };
533
+ try {
534
+ child.send({ type, rows }, done);
535
+ } catch (error) {
536
+ done(error);
537
+ }
538
+ });
539
+ return inFlight;
540
+ };
541
+ return deliver;
542
+ }
543
+ const deliverCounts = delivery(pendingCounts, COUNTS);
544
+ const deliverEvents = delivery(pendingEvents, CLUSTER_EVENTS);
545
+ // an event goes at once; what is counted goes every minute (eventsTimer)
546
+ function clusterEvent(kind, proc, options) {
547
+ pendingEvents.event(kind, proc ? proc.name : null, options);
548
+ void deliverEvents();
549
+ }
550
+ const countEvent = (kind, proc = null) => pendingEvents.count(kind, proc ? proc.name : null);
551
+
552
+ function startProcess(proc) {
553
+ return new Promise((resolve) => {
554
+ if (stopping) {
555
+ return resolve(false);
556
+ }
557
+ const [cmd, args] = command(proc);
558
+ proc.startedAt = Date.now();
559
+ proc.readyMemory = null;
560
+ proc.statusFigures = null;
561
+ metrics.state(proc.code, 'starting');
562
+ const child = spawn(cmd, args, { stdio: ['inherit', 'inherit', 'inherit', 'ipc'], env: process.env });
563
+ proc.child = child;
564
+ child.on('message', message => {
565
+ if (proc.child === child && message && message.type === METRIC && metrics.record(proc.code, message)) {
566
+ if (proc.role === 'master') {
567
+ // the cluster's total, at the master's sample times
568
+ metrics.recordTotal(message.point.time, memoryTotal());
569
+ }
570
+ publishMetrics();
571
+ } else if (proc.child === child && message && message.type === STATUS && message.figures) {
572
+ proc.statusFigures = message.figures;
573
+ } else if (message && message.type === COUNTS && proc.role !== 'master') {
574
+ // real requests, even from a child that is going away
575
+ pendingCounts.add(message.rows);
576
+ void deliverCounts();
577
+ }
578
+ });
579
+ child.on('disconnect', () => {
580
+ if (proc.child === child) {
581
+ metrics.state(proc.code, 'unavailable');
582
+ publishMetrics();
583
+ }
584
+ });
585
+ publishMetrics();
586
+ child.on('exit', (code, signal) => {
587
+ proc.ready = false;
588
+ proc.exitedAt = Date.now();
589
+ proc.child = null;
590
+ proc.statusFigures = null;
591
+ metrics.state(proc.code, stopping ? 'unavailable' : 'restarting');
592
+ publishMetrics();
593
+ resolve(false);
594
+ if (stopping) {
595
+ return;
596
+ }
597
+ proc.downSince = proc.downSince || proc.exitedAt;
598
+ if (!proc.recycling) {
599
+ // (a recycle is its own event)
600
+ clusterEvent(KINDS.exited, proc, { detail: signal || code });
601
+ }
602
+ if (proc.recycling) {
603
+ // the launcher stopped it (see checkMemory): at once, and not a failure
604
+ proc.recycling = false;
605
+ proc.readyAt = 0;
606
+ log(`${proc.name} exited (${signal || code}) to be recycled; restarting`);
607
+ void startProcess(proc);
608
+ return;
609
+ }
610
+ // counted from when it was ready: one that fails late in a long load is still failing
611
+ const ran = proc.readyAt ? Date.now() - proc.readyAt : 0;
612
+ proc.readyAt = 0;
613
+ proc.failures = ran > healthyRunMs ? 1 : proc.failures + 1;
614
+ if (proc.role === 'master' && !proc.everReady && maxStartFailures > 0 && proc.failures >= maxStartFailures) {
615
+ // nothing would be served: exit, so the service manager sees the failure
616
+ gaveUp = `the master exited ${proc.failures} time(s) (the last: ${signal || code}) and was never ready (server.cluster.maxStartFailures ${maxStartFailures})`;
617
+ log(`ERROR ${gaveUp}: giving up`);
618
+ return;
619
+ }
620
+ const delay = Math.min(MAX_BACKOFF_MS, 1000 * 2 ** (proc.failures - 1));
621
+ log(`${proc.name} exited (${signal || code}); restarting in ${delay / 1000} s`);
622
+ proc.restartTimer = setTimeout(() => startProcess(proc), delay);
623
+ });
624
+ // ready: the process says so from its listen callback (server.js), on its own channel,
625
+ // so an old process still shutting down on the same port can't answer for it
626
+ child.on('message', message => {
627
+ if (!message || message.type !== READY || proc.child !== child || proc.ready) {
628
+ return;
629
+ }
630
+ proc.ready = true;
631
+ proc.everReady = true;
632
+ proc.readyAt = Date.now();
633
+ sendWaiting();
634
+ metrics.state(proc.code, 'ready');
635
+ publishMetrics();
636
+ if (proc.downSince) {
637
+ clusterEvent(KINDS.ready, proc, { ms: proc.readyAt - proc.downSince });
638
+ proc.downSince = 0;
639
+ }
640
+ if (proc.role === 'master') {
641
+ void deliverCounts();
642
+ void deliverEvents();
643
+ }
644
+ log(`${proc.name} (${proc.code}) ready on port ${proc.port}`);
645
+ resolve(true);
646
+ });
647
+ });
648
+ }
649
+
650
+ // A process has maxOutstanding + fastLaneExtra slots. The fast lane may use them all, but
651
+ // while others wait it leaves free what they need to have normalMinSlots running; the
652
+ // others use up to maxOutstanding, or more to reach normalMinSlots
653
+ const full = (p, fast) => {
654
+ const slots = maxOutstanding + fastLaneExtra;
655
+ const short = Math.max(0, normalMinSlots - (p.outstanding - p.fastOutstanding));
656
+ if (maxOutstanding <= 0) {
657
+ return false;
658
+ }
659
+ if (fast) {
660
+ return p.outstanding >= slots - (waiting.length ? short : 0);
661
+ }
662
+ return p.outstanding >= maxOutstanding && !(short > 0 && p.outstanding < slots);
663
+ };
664
+
665
+ // true if the request was sent (or refused), false if it must wait. The master's own
666
+ // paths and CORS preflights don't wait
667
+ function send(w) {
668
+ if (lostToRestart(w)) {
669
+ return true;
670
+ }
671
+ const target = pickTarget(w.route, procs, p => !full(p, w.fast));
672
+ if (!target) {
673
+ countEvent(KINDS.notReady);
674
+ fail(w.res, 503, 'No server process is ready to take this request; try again shortly');
675
+ return true;
676
+ }
677
+ if (w.route.to !== 'master' && w.req.method !== 'OPTIONS' && full(target, w.fast)) {
678
+ return false;
679
+ }
680
+ // only the tx requests, which wait for a slot, take one
681
+ forward(w.req, w.res, target, w.fast, w.route.to !== 'master' && w.req.method !== 'OPTIONS');
682
+ return true;
683
+ }
684
+
685
+ // the fast lane first, then oldest first; one waiting for its owner doesn't hold up the rest
686
+ function sendWaiting() {
687
+ for (const queue of [fastWaiting, waiting]) {
688
+ const fast = queue === fastWaiting;
689
+ for (let i = 0; i < queue.length && procs.some(p => p.ready && p.servesTx && !full(p, fast));) {
690
+ const w = queue[i];
691
+ if (send(w)) {
692
+ clearTimeout(w.timer);
693
+ queue.splice(i, 1);
694
+ } else {
695
+ i++;
696
+ }
697
+ }
698
+ }
699
+ }
700
+
701
+ // Without the shared cache, a cache lives only in the process that issued it: while that
702
+ // process is restarting, the cache is gone, so say so at once rather than have another
703
+ // process answer that the id is someone else's (the restarted process says the same)
704
+ function lostToRestart(w) {
705
+ const owner = w.route.to === 'owner' && !(settings.routing && settings.routing.sharedCache) &&
706
+ procs.find(p => p.code === w.route.code && p.servesTx);
707
+ if (!owner || owner.ready || w.req.method === 'OPTIONS') {
708
+ return false;
709
+ }
710
+ countEvent(KINDS.lostToRestart, owner);
711
+ const id = String(w.req.headers['x-cache-id']);
712
+ const messageId = 'CACHE_ID_LOST_TO_RESTART';
713
+ const text = englishMessage(messageId, [id, formatDuration(Date.now() - (owner.exitedAt || Date.now()))]);
714
+ w.res.writeHead(404, { 'Content-Type': 'application/fhir+json', ...(stopping ? { Connection: 'close' } : {}) });
715
+ w.res.end(JSON.stringify({
716
+ resourceType: 'OperationOutcome',
717
+ issue: [{
718
+ severity: 'error', code: 'not-found',
719
+ details: { text, coding: [{ system: TX_ISSUE_TYPE, code: 'cache-id-unknown' }] },
720
+ diagnostics: `instance '${owner.code}', which issued it, is restarting`,
721
+ extension: [{ url: MESSAGE_ID_EXTENSION, valueString: messageId }]
722
+ }]
723
+ }));
724
+ return true;
725
+ }
726
+
727
+ function refuse(w, ms) {
728
+ queueRefused++;
729
+ countEvent(KINDS.busy);
730
+ const now = Date.now();
731
+ if (now - lastQueueLog >= 60000) {
732
+ lastQueueLog = now;
733
+ log(`over capacity: requests waited ${ms / 1000} s for a process and were refused (${queueRefused} so far)`);
734
+ }
735
+ fail(w.res, 503, `The server is busy: no process was free for ${ms / 1000} s; try again shortly`, true);
736
+ }
737
+
738
+ // a client that took none of a response for writeTimeoutMs: its connection is closed
739
+ function writeTimedOut(req) {
740
+ writeTimeouts++;
741
+ countEvent(KINDS.writeTimeout);
742
+ const now = Date.now();
743
+ if (now - lastWriteTimeoutLog >= 60000) {
744
+ lastWriteTimeoutLog = now;
745
+ log(`a client took none of the response to ${req.method} ${req.url} for ${writeTimeoutMs / 1000} s (writeTimeoutSeconds): ` +
746
+ `its connection was closed (${writeTimeouts} so far)`);
747
+ }
748
+ }
749
+
750
+ function proxy(req, res) {
751
+ proxied++;
752
+ const w = { req, res, route: classify(req.url, req.headers, settings.txPaths, settings.routing) };
753
+ w.fast = fastLaneExtra > 0 && w.route.to !== 'master' && isFastLane(req, settings.txPaths, fastLaneMaxBytes);
754
+ if (send(w)) {
755
+ return;
756
+ }
757
+ if (stopping) {
758
+ countEvent(KINDS.stopping);
759
+ return fail(res, 503, 'The server is stopping; try again shortly');
760
+ }
761
+ const ms = w.route.to === 'owner' ? ownerQueueMs : queueMs;
762
+ if (ms <= 0) {
763
+ return refuse(w, 0);
764
+ }
765
+ const queue = w.fast ? fastWaiting : waiting;
766
+ queue.push(w);
767
+ const leave = () => {
768
+ const i = queue.indexOf(w);
769
+ if (i >= 0) {
770
+ queue.splice(i, 1);
771
+ clearTimeout(w.timer);
772
+ }
773
+ };
774
+ res.on('close', leave);
775
+ w.timer = setTimeout(() => {
776
+ leave();
777
+ refuse(w, ms);
778
+ }, ms);
779
+ }
780
+
781
+ function forward(req, res, target, fast = false, counted = true) {
782
+ inFlight++;
783
+ target.active++;
784
+ if (counted) {
785
+ target.outstanding++;
786
+ target.fastOutstanding += fast ? 1 : 0;
787
+ }
788
+ const sent = { at: Date.now() };
789
+ target.sentAt.add(sent);
790
+ let released = false;
791
+ let finished = false;
792
+ let timedOut = false;
793
+ let timer = null;
794
+ let stall = null;
795
+ // the process is done with it: its response has ended (or failed), so its slot is free,
796
+ // though the client may still be taking the response from the launcher
797
+ const release = () => {
798
+ clearTimeout(timer);
799
+ if (!released) {
800
+ released = true;
801
+ target.active--;
802
+ if (counted) {
803
+ target.outstanding--;
804
+ target.fastOutstanding -= fast ? 1 : 0;
805
+ }
806
+ target.sentAt.delete(sent);
807
+ sendWaiting();
808
+ }
809
+ };
810
+ // the launcher is done with it: the client has the whole response, or is gone
811
+ const finish = () => {
812
+ release();
813
+ clearTimeout(stall);
814
+ if (!finished) {
815
+ finished = true;
816
+ inFlight--;
817
+ if (onIdle && inFlight === 0) {
818
+ onIdle();
819
+ }
820
+ }
821
+ };
822
+ const headers = { ...req.headers };
823
+ for (const h of HOP_BY_HOP) {
824
+ delete headers[h];
825
+ }
826
+ // a chunked body goes on chunked. Without this, Node sends a GET, HEAD, DELETE or OPTIONS
827
+ // body raw (no Content-Length, no chunking), and the process reads it as a second request
828
+ // on the kept-alive connection, past the routing (Node's server accepts only chunked as
829
+ // the last coding, so the body is chunked whatever the header listed)
830
+ if (req.headers['transfer-encoding'] !== undefined) {
831
+ headers['transfer-encoding'] = 'chunked';
832
+ }
833
+ const client = (req.socket.remoteAddress || '').replace(/^::ffff:/, '');
834
+ headers['x-forwarded-for'] = headers['x-forwarded-for'] ? `${headers['x-forwarded-for']}, ${client}` : client;
835
+ if (!headers['x-forwarded-proto'] || !isLoopback(client)) {
836
+ headers['x-forwarded-proto'] = 'http';
837
+ }
838
+ const upstream = http.request({
839
+ host: target.host || '127.0.0.1', port: target.port, method: req.method, path: req.url, headers, agent: target.agent
840
+ }, (ures) => {
841
+ clearTimeout(timer);
842
+ target.timeouts = 0;
843
+ if (ures.statusCode === 503 && cooldownMs > 0) {
844
+ target.coolUntil = Date.now() + cooldownMs;
845
+ setTimeout(sendWaiting, cooldownMs).unref();
846
+ }
847
+ const out = { ...ures.headers };
848
+ for (const h of HOP_BY_HOP) {
849
+ delete out[h];
850
+ }
851
+ if (stopping) {
852
+ out.connection = 'close';
853
+ }
854
+ res.writeHead(ures.statusCode, ures.statusMessage, out);
855
+ // A tx request's response is read to its end as fast as the process sends it, so its
856
+ // slot is free once the process is done, however slowly the client reads (the process
857
+ // held the whole response in memory already). Any other is passed on as the client takes
858
+ // it (a package download can be large). Either way, a client that takes none of what is
859
+ // waiting for it for writeTimeoutMs is cut off
860
+ const stalled = () => {
861
+ if (writeTimeoutMs > 0 && !stall) {
862
+ stall = setTimeout(() => {
863
+ writeTimedOut(req);
864
+ upstream.destroy();
865
+ res.destroy();
866
+ }, writeTimeoutMs);
867
+ }
868
+ };
869
+ res.on('drain', () => {
870
+ clearTimeout(stall);
871
+ stall = null;
872
+ ures.resume();
873
+ });
874
+ ures.on('data', (chunk) => {
875
+ if (!res.write(chunk)) {
876
+ stalled();
877
+ if (!counted) {
878
+ ures.pause();
879
+ }
880
+ }
881
+ });
882
+ ures.on('end', () => {
883
+ release();
884
+ res.end();
885
+ });
886
+ ures.on('error', () => res.destroy());
887
+ });
888
+ upstream.on('error', (err) => {
889
+ if (timedOut) {
890
+ return;
891
+ }
892
+ if (!res.headersSent) {
893
+ countEvent(KINDS.failed, target);
894
+ fail(res, 502, `The ${target.name} process did not answer (${err.code || err.message})`);
895
+ } else {
896
+ res.destroy();
897
+ }
898
+ finish();
899
+ });
900
+ res.on('close', () => {
901
+ if (!res.writableFinished && !released) {
902
+ upstream.destroy();
903
+ }
904
+ finish();
905
+ });
906
+ // until the response begins; a response already streaming is not timed
907
+ if (upstreamTimeoutMs > 0) {
908
+ timer = setTimeout(() => {
909
+ timedOut = true;
910
+ target.timeouts++;
911
+ const suspect = target.timeouts >= SUSPECT_TIMEOUTS && cooldownMs > 0;
912
+ log(`WARNING ${target.name} did not begin its response to ${req.method} ${req.url} in ${upstreamTimeoutMs / 1000} s: answered 504 and aborted ` +
913
+ `(${target.timeouts} in a row${suspect ? `; no new requests for ${cooldownMs / 1000} s while another can take them` : ''})`);
914
+ if (suspect) {
915
+ target.coolUntil = Date.now() + cooldownMs;
916
+ setTimeout(sendWaiting, cooldownMs).unref();
917
+ }
918
+ upstream.destroy();
919
+ countEvent(KINDS.timeout, target);
920
+ fail(res, 504, `The ${target.name} process did not begin its response in ${upstreamTimeoutMs / 1000} s (server.cluster.upstreamTimeoutSeconds); the request was abandoned`, false, 'timeout');
921
+ finish();
922
+ }, upstreamTimeoutMs);
923
+ }
924
+ req.pipe(upstream);
925
+ }
926
+
927
+ /**
928
+ * Every RECYCLE_CHECK_MS: restart a tx-worker over the refusal threshold for recycleMs with no
929
+ * request at work, the largest first, one at a time. Only one that has grown since it was
930
+ * ready by more than the excess: a restart takes it back to its ready size, so restarting
931
+ * one that was already too big would not reopen the gate. Never the master.
932
+ */
933
+ function checkMemory() {
934
+ if (stopping || procs.some(p => p.recycling)) {
935
+ return;
936
+ }
937
+ const now = Date.now();
938
+ const workers = procs.filter(p => p.role === 'tx-worker');
939
+ const { excess, own } = memory.sample(workers);
940
+ const over = new Map();
941
+ let largest = null;
942
+ for (const p of workers) {
943
+ const mine = own.get(p) || 0;
944
+ if (p.ready && p.readyMemory === null) {
945
+ p.readyMemory = mine;
946
+ if (excess === null && mine > memory.rejectAt) {
947
+ log(`WARNING memory: ${p.name} uses ${GB(mine)} GB loaded and idle, ${memory.describe}: its memory guard refuses its requests, ` +
948
+ 'and restarting it would not help, so it is not recycled; use a larger limit');
949
+ }
950
+ }
951
+ const atWork = [...p.sentAt].some(s => now - s.at >= AT_WORK_MS);
952
+ const pExcess = excess === null ? mine - memory.rejectAt : excess;
953
+ over.set(p, mine > 0 && pExcess > 0 && mine - p.readyMemory > pExcess ? mine : 0);
954
+ if (!p.ready || !over.get(p) || atWork) {
955
+ p.overSince = 0;
956
+ continue;
957
+ }
958
+ p.overSince = p.overSince || now;
959
+ // and none in flight at all now (a refusal is answered within milliseconds)
960
+ if (now - p.overSince >= recycleMs && p.active === 0 && (!largest || over.get(p) > over.get(largest))) {
961
+ largest = p;
962
+ }
963
+ }
964
+ if (largest) {
965
+ recycle(largest, `its memory (${(over.get(largest) / 2 ** 30).toFixed(1)} GB) has been ${memory.describe}` +
966
+ (excess !== null ? ` (by ${(excess / 2 ** 30).toFixed(1)} GB)` : '') + ` for ${Math.round((now - largest.overSince) / 1000)} s with no request at work`);
967
+ }
968
+ }
969
+
970
+ // no new requests; those it has finish; it stops as on shutdown, and exit starts it again
971
+ function recycle(p, why) {
972
+ log(`recycling ${p.name}: ${why}`);
973
+ clusterEvent(KINDS.recycled, p, { detail: why });
974
+ p.downSince = Date.now();
975
+ p.recycling = true;
976
+ p.ready = false;
977
+ p.exitedAt = 0;
978
+ p.overSince = 0;
979
+ metrics.state(p.code, 'restarting');
980
+ publishMetrics();
981
+ const child = p.child;
982
+ const t0 = Date.now();
983
+ const stopWhenIdle = () => {
984
+ if (p.child !== child) {
985
+ return;
986
+ }
987
+ if (p.active > 0 && Date.now() - t0 < settings.drainMs) {
988
+ return void setTimeout(stopWhenIdle, 100);
989
+ }
990
+ const kill = setTimeout(() => child.kill('SIGKILL'), 30000);
991
+ child.once('exit', () => clearTimeout(kill));
992
+ child.kill('SIGTERM');
993
+ };
994
+ stopWhenIdle();
995
+ }
996
+
997
+ // close: the request body has not been read, and an overloaded server sheds the connection too.
998
+ // The launcher's own refusals (503: busy, stopping, no process ready; 504: no response in
999
+ // time) are tx-issue-type too-costly, as CLOSURE_TABLE_BUSY is: the code system has none for
1000
+ // a server that can't take the request now. A 502 (the process failed) has none that fits
1001
+ function fail(res, status, text, close = false, code = 'transient') {
1002
+ res.writeHead(status, { 'Content-Type': 'application/fhir+json', 'Retry-After': '5', ...(stopping || close ? { Connection: 'close' } : {}) });
1003
+ const details = status === 503 || status === 504 ? { text, coding: [{ system: TX_ISSUE_TYPE, code: 'too-costly' }] } : { text };
1004
+ res.end(JSON.stringify({
1005
+ resourceType: 'OperationOutcome',
1006
+ issue: [{ severity: 'error', code, details, diagnostics: text }]
1007
+ }));
1008
+ }
1009
+
1010
+ async function start() {
1011
+ const master = procs.find(p => p.role === 'master');
1012
+ // the master first, and on its own: on a new data directory it builds the package
1013
+ // caches and creates the VSAC database, which processes starting together would corrupt.
1014
+ // (If it exits before it is ready, it is started again, as any process is.)
1015
+ clusterEvent(KINDS.launcherStarted, null);
1016
+ eventsTimer = setInterval(deliverEvents, settings.eventsIntervalMs || COUNTS_INTERVAL_MS);
1017
+ eventsTimer.unref();
1018
+ log(`starting the master (${master.code}) on port ${master.port}` + (maxStartFailures > 0
1019
+ ? `; if it exits ${maxStartFailures} times without being ready, the launcher exits (maxStartFailures)` : ''));
1020
+ void startProcess(master);
1021
+ await new Promise((resolve, reject) => {
1022
+ const t = setInterval(() => {
1023
+ if (master.ready || stopping) {
1024
+ clearInterval(t);
1025
+ resolve();
1026
+ } else if (gaveUp) {
1027
+ clearInterval(t);
1028
+ reject(new Error(gaveUp));
1029
+ }
1030
+ }, WAIT_POLL_MS);
1031
+ });
1032
+ if (stopping) {
1033
+ return;
1034
+ }
1035
+ server = http.createServer(proxy);
1036
+ server.keepAliveTimeout = 65000;
1037
+ await new Promise((resolve, reject) => {
1038
+ server.once('error', reject);
1039
+ server.listen(settings.port, resolve);
1040
+ });
1041
+ sizeWorkers(master);
1042
+ log(`listening on port ${settings.port}; starting ${procs.length - 1} worker(s)`);
1043
+ log(`limits: maxOutstanding ${maxOutstanding}, queueSeconds ${queueMs / 1000}, ownerQueueSeconds ${ownerQueueMs / 1000}, cooldownSeconds ${cooldownMs / 1000}, upstreamTimeoutSeconds ${upstreamTimeoutMs / 1000}, writeTimeoutSeconds ${writeTimeoutMs / 1000}, ` +
1044
+ `fastLaneExtra ${fastLaneExtra}, fastLaneMaxBytes ${fastLaneMaxBytes}, normalMinSlots ${normalMinSlots}, memoryHeadroomGB ${headroom / 2 ** 30}, drainSeconds ${settings.drainMs / 1000}, ` +
1045
+ `trimAllocator ${trimAllocator}, statusSeconds ${statusMs / 1000}, recycleSeconds ${recycleMs / 1000}` + (recycleMs > 0 ? ` (a tx-worker ${memory.describe} that long, with no request at work, is restarted)` : ' (never)'));
1046
+ if (recycleMs > 0) {
1047
+ memoryTimer = setInterval(checkMemory, settings.recycleCheckMs || RECYCLE_CHECK_MS);
1048
+ memoryTimer.unref();
1049
+ }
1050
+ statusTimer = setInterval(sendStatus, statusMs);
1051
+ statusTimer.unref();
1052
+ // one after another, so their loads (and memory peaks) don't coincide; in the background
1053
+ void (async () => {
1054
+ for (const p of procs) {
1055
+ if (p !== master && !stopping) {
1056
+ await startProcess(p);
1057
+ }
1058
+ }
1059
+ if (!stopping) {
1060
+ checkIdleMemory();
1061
+ }
1062
+ })();
1063
+ }
1064
+
1065
+
1066
+ /**
1067
+ * Once the master is loaded, take its memory as each worker's. Workers not given: start only
1068
+ * as many as fit under the refusal threshold with `headroom` to spare (at least one); given:
1069
+ * warn if they don't fit.
1070
+ */
1071
+ function sizeWorkers(master) {
1072
+ const workers = procs.filter(p => p.role === 'tx-worker');
1073
+ const decided = (fitText) => {
1074
+ if (settings.workerDecision) {
1075
+ log(describeDecision(settings.workerDecision, procs.length - 1, fitText, !settings.workersDefault));
1076
+ }
1077
+ };
1078
+ const perProcess = memory.sample([master]).own.get(master) || 0;
1079
+ if (!perProcess) {
1080
+ decided('memory not measured');
1081
+ return; // not measurable here (not Linux)
1082
+ }
1083
+ const fits = Math.floor((memory.rejectAt - headroom - perProcess) / perProcess);
1084
+ const fit = Math.max(1, fits);
1085
+ const idle = (1 + workers.length) * perProcess;
1086
+ const line = `the master uses ${GB(perProcess)} GB loaded, so ${workers.length + 1} processes would use about ${GB(idle)} GB idle; ` +
1087
+ `the memory guard refuses requests ${memory.describe}, and memoryHeadroomGB is ${GB(headroom)}`;
1088
+ if (idle + headroom <= memory.rejectAt) {
1089
+ decided(`memory fits ${fits}`);
1090
+ log(`memory: ${line}`);
1091
+ return;
1092
+ }
1093
+ if (settings.workersDefault) {
1094
+ for (const p of workers.slice(fit)) {
1095
+ procs.splice(procs.indexOf(p), 1);
1096
+ metrics.series.delete(p.code);
1097
+ }
1098
+ }
1099
+ const starting = procs.length - 1;
1100
+ decided(`memory fits ${Math.max(0, fits)}`);
1101
+ if (fits < 1) {
1102
+ log(`WARNING memory: ${line}: even 1 worker leaves too little room; starting ${starting}: raise the limit, or run a single server`);
1103
+ } else if (starting < workers.length) {
1104
+ log(`memory: ${line}: starting ${fit} worker(s), not ${workers.length} (set server.cluster.workers to choose)`);
1105
+ } else {
1106
+ log(`WARNING memory: ${line}: ${workers.length} worker(s) leave too little room; at most ${fit} would, or raise the limit`);
1107
+ }
1108
+ }
1109
+
1110
+ // once every worker is ready: what they all use idle, against the threshold
1111
+ function checkIdleMemory() {
1112
+ const { excess, own } = memory.sample(procs);
1113
+ const total = excess !== null ? excess + memory.rejectAt : [...own.values()].reduce((a, b) => a + b, 0);
1114
+ if (!total) {
1115
+ return;
1116
+ }
1117
+ const room = memory.rejectAt - total;
1118
+ log(`${room < headroom ? 'WARNING ' : ''}memory: all processes loaded and idle use ${GB(total)} GB, ` +
1119
+ `${GB(room)} GB under the threshold (memoryHeadroomGB ${GB(headroom)})` +
1120
+ (room < headroom ? '; a heavy request will shut the gate for everyone: use fewer workers or a larger limit' : ''));
1121
+ }
1122
+
1123
+ /**
1124
+ * Stop taking connections, wait for the requests in progress (up to drainMs), then stop
1125
+ * the workers, then the master (SIGTERM, then SIGKILL after 30 s), so the workers' last
1126
+ * request counts reach the master's statistics.
1127
+ */
1128
+ async function stop() {
1129
+ if (stopping) {
1130
+ return;
1131
+ }
1132
+ stopping = true;
1133
+ clearInterval(memoryTimer);
1134
+ clearInterval(statusTimer);
1135
+ clearInterval(eventsTimer);
1136
+ for (const p of procs) {
1137
+ clearTimeout(p.restartTimer);
1138
+ }
1139
+ // those still waiting haven't started: refuse them; drain only those in progress
1140
+ for (const w of [...fastWaiting.splice(0), ...waiting.splice(0)]) {
1141
+ clearTimeout(w.timer);
1142
+ countEvent(KINDS.stopping);
1143
+ fail(w.res, 503, 'The server is stopping; try again shortly');
1144
+ }
1145
+ if (server) {
1146
+ server.close();
1147
+ server.closeIdleConnections();
1148
+ const idle = setInterval(() => server.closeIdleConnections(), 100);
1149
+ if (inFlight > 0) {
1150
+ log(`draining: ${inFlight} request(s) in progress`);
1151
+ await new Promise(resolve => {
1152
+ const timer = setTimeout(resolve, settings.drainMs);
1153
+ onIdle = () => {
1154
+ clearTimeout(timer);
1155
+ resolve();
1156
+ };
1157
+ });
1158
+ }
1159
+ clearInterval(idle);
1160
+ server.closeAllConnections();
1161
+ }
1162
+ // the workers first: each sends its last request counts as it stops, and they reach
1163
+ // the master's statistics before the master stops
1164
+ const stopChild = (p) => new Promise(resolve => {
1165
+ const child = p.child;
1166
+ if (!child) {
1167
+ return resolve();
1168
+ }
1169
+ const kill = setTimeout(() => child.kill('SIGKILL'), 30000);
1170
+ // exited, and everything it sent on the channel has been read
1171
+ let waiting = child.connected ? 2 : 1;
1172
+ const one = () => {
1173
+ if (--waiting === 0) {
1174
+ clearTimeout(kill);
1175
+ resolve();
1176
+ }
1177
+ };
1178
+ child.once('exit', one);
1179
+ if (child.connected) {
1180
+ child.once('disconnect', one);
1181
+ }
1182
+ child.kill('SIGTERM');
1183
+ });
1184
+ await Promise.all(procs.filter(p => p.role !== 'master').map(stopChild));
1185
+ const deliverWait = new Promise(resolve => setTimeout(resolve, 5000).unref());
1186
+ await Promise.race([Promise.all([deliverCounts(), deliverEvents()]), deliverWait]);
1187
+ await Promise.all(procs.filter(p => p.role === 'master').map(stopChild));
1188
+ for (const p of procs) {
1189
+ p.agent.destroy();
1190
+ }
1191
+ log('stopped');
1192
+ }
1193
+
1194
+ return { start, stop, procs, metrics, statusSnapshot, get server() { return server; } };
1195
+ }
1196
+
1197
+ async function main() {
1198
+ const folders = require('./library/folder-setup');
1199
+ let launcher;
1200
+ try {
1201
+ const config = require('./library/config-reader').readConfig(folders.filePath('config.json'));
1202
+ launcher = createLauncher(loadSettings(process.argv.slice(2), process.env, config));
1203
+ } catch (error) {
1204
+ console.error(`cluster.js: ${error.message}`);
1205
+ process.exit(1);
1206
+ }
1207
+ const shutdown = () => launcher.stop().then(() => process.exit(0));
1208
+ process.on('SIGTERM', shutdown);
1209
+ process.on('SIGINT', shutdown);
1210
+ await launcher.start();
1211
+ }
1212
+
1213
+ if (require.main === module) {
1214
+ main().catch((error) => {
1215
+ console.error(error);
1216
+ process.exit(1);
1217
+ });
1218
+ }
1219
+
1220
+ module.exports = { loadSettings, classify, isFastLane, pickTarget, createLauncher, memorySampler, childExecArgv };