fhirsmith 0.18.1 → 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.
- package/ACCESSIBILITY.md +119 -0
- package/CHANGELOG.md +190 -9
- package/CONFIGURATION.md +305 -0
- package/LICENSE +29 -0
- package/README.md +29 -122
- package/cluster.js +1220 -0
- package/cluster.md +707 -0
- package/config-template.json +77 -8
- package/extension-tracker/extension-tracker-template.html +5 -8
- package/extension-tracker/extension-tracker.js +0 -2
- package/folder/folder.js +7 -6
- package/ips/analysis.js +560 -0
- package/ips/ips-template.html +5 -8
- package/ips/ips.js +331 -33
- package/ips/jurisdictions.js +65 -0
- package/ips/nationality.js +120 -0
- package/ips/pipeline.js +131 -17
- package/ips/presenter.js +1 -1
- package/ips/profile-editor.js +283 -4
- package/ips/readme.md +170 -20
- package/ips/render.js +769 -109
- package/ips/scripts.js +46 -0
- package/ips/store.js +123 -11
- package/ips/structure.js +247 -0
- package/ips/terminology.js +1 -1
- package/library/body-limit.js +22 -0
- package/library/cache-generation.js +15 -0
- package/library/cgroup-memory.js +133 -1
- package/library/cluster-counts.js +121 -0
- package/library/cluster-events.js +153 -0
- package/library/cluster-metrics.js +193 -0
- package/library/cluster-status.js +468 -0
- package/library/config-reader.js +96 -0
- package/library/daily-report.js +100 -2
- package/library/html-server.js +27 -1
- package/library/i18nsupport.js +23 -1
- package/library/keep-alive.js +18 -0
- package/library/languages.js +17 -3
- package/library/logger.js +31 -0
- package/library/memory-guard.js +428 -0
- package/library/native-memory.js +52 -0
- package/library/npm-audit.js +51 -25
- package/library/openapi-doc.js +7 -5
- package/library/package-manager.js +8 -0
- package/library/role.js +197 -0
- package/library/server-credentials.js +169 -0
- package/library/sqlite-utilities.js +59 -0
- package/library/ssrf-guard.js +100 -0
- package/library/static-response-stats.js +16 -0
- package/library/version-utilities.js +50 -0
- package/library/worker-count.js +192 -0
- package/nginx.md +30 -11
- package/npmprojector/indexer.js +18 -10
- package/package.json +11 -8
- package/packages/feed-check-page.js +117 -0
- package/packages/feed-check.js +279 -0
- package/packages/npm-check.js +57 -3
- package/packages/npm-signing.js +219 -0
- package/packages/openapi.yaml +207 -1
- package/packages/package-crawler.js +322 -41
- package/packages/packages-template.html +6 -8
- package/packages/packages.js +604 -57
- package/packages/readme.md +113 -1
- package/publisher/publisher-template.html +2 -5
- package/publisher/publisher.js +121 -110
- package/publisher/readme.md +75 -0
- package/registry/api.js +179 -1
- package/registry/crawler.js +86 -108
- package/registry/documentation/api.md +68 -2
- package/registry/documentation/configuration.md +34 -0
- package/registry/documentation/internals.md +4 -1
- package/registry/documentation/readme.md +3 -2
- package/registry/model.js +8 -0
- package/registry/openapi.yaml +84 -0
- package/registry/registry-template.html +5 -8
- package/registry/registry.js +58 -6
- package/root-bare-template.html +7 -4
- package/root-template.html +5 -8
- package/server.js +398 -198
- package/shl/shl.js +38 -37
- package/static/a11y/a11y-check.js +832 -0
- package/static/a11y/axe.min.js +12 -0
- package/static/assets/css/bootstrap-fhir.css +15 -15
- package/static/assets/images/flags/au.svg +18 -0
- package/static/assets/images/flags/unknown.svg +11 -0
- package/static/assets/images/flags/us.svg +63 -0
- package/static/assets/images/icon_audit.png +0 -0
- package/static/assets/images/icon_clinical.png +0 -0
- package/static/assets/images/icon_document.png +0 -0
- package/static/assets/images/icon_external.png +0 -0
- package/static/assets/images/icon_medication.png +0 -0
- package/static/assets/images/icon_organization.png +0 -0
- package/static/assets/images/icon_other.png +0 -0
- package/static/assets/images/icon_person.png +0 -0
- package/static/assets/images/icon_section.png +0 -0
- package/stats-db.js +45 -0
- package/stats.js +197 -10
- package/testing/testing-template.html +5 -8
- package/token/nginx_passport_setup.md +1 -1
- package/token/security_guide.md +1 -1
- package/token/token-template.html +2 -5
- package/token/token.js +40 -36
- package/translations/Messages.properties +19 -0
- package/tx/README.md +158 -4
- package/tx/closure/closure-store.js +126 -3
- package/tx/cm/cm-database.js +69 -57
- package/tx/cm/cm-package.js +50 -16
- package/tx/cs/cs-api.js +284 -19
- package/tx/cs/cs-api.md +41 -4
- package/tx/cs/cs-areacode.js +1 -1
- package/tx/cs/cs-country.js +17 -5
- package/tx/cs/cs-cpt.js +33 -16
- package/tx/cs/cs-cs.js +199 -24
- package/tx/cs/cs-currency.js +1 -1
- package/tx/cs/cs-icd11.js +21 -13
- package/tx/cs/cs-icf.js +22 -3
- package/tx/cs/cs-lang.js +2 -2
- package/tx/cs/cs-loinc.js +193 -67
- package/tx/cs/cs-mimetypes.js +12 -2
- package/tx/cs/cs-ndc.js +40 -25
- package/tx/cs/cs-omop.js +54 -29
- package/tx/cs/cs-rxnorm.js +300 -196
- package/tx/cs/cs-snomed.js +861 -95
- package/tx/cs/cs-ucum.js +14 -4
- package/tx/cs/cs-unii.js +18 -14
- package/tx/cs/cs-virtual.js +210 -0
- package/tx/ecosystem-resolver.js +413 -0
- package/tx/html/cluster-dash-metrics.liquid +26 -0
- package/tx/html/cluster-home-metrics.liquid +31 -0
- package/tx/html/cluster-metrics-script.liquid +251 -0
- package/tx/html/codesystem-operations.liquid +3 -3
- package/tx/html/ecl-panel.liquid +1 -1
- package/tx/html/external-metrics.liquid +17 -0
- package/tx/html/operations-form.liquid +17 -17
- package/tx/html/search-form.liquid +11 -11
- package/tx/html/status-line.liquid +58 -0
- package/tx/html/status-panel.liquid +140 -0
- package/tx/html/status-tasks.liquid +25 -0
- package/tx/html/tx-template.html +2 -5
- package/tx/importers/atc-to-fhir.js +17 -16
- package/tx/importers/export-icd11-codesystem.js +3 -2
- package/tx/importers/import-icd11.js +4 -3
- package/tx/importers/import-loinc.module.js +118 -56
- package/tx/importers/import-ndc.module.js +56 -44
- package/tx/importers/import-rxnorm.module.js +118 -79
- package/tx/importers/import-sct.module.js +125 -61
- package/tx/importers/import-unii.module.js +37 -36
- package/tx/importers/subset-loinc.module.js +49 -48
- package/tx/importers/subset-rxnorm.module.js +29 -28
- package/tx/importers/terminal.js +18 -0
- package/tx/importers/tx-import-base.js +13 -12
- package/tx/importers/tx-import-settings.js +5 -4
- package/tx/importers/tx-import.js +53 -52
- package/tx/library/canonical-resource.js +49 -0
- package/tx/library/codesystem.js +94 -5
- package/tx/library/designations.js +45 -2
- package/tx/library/extensions.js +13 -7
- package/tx/library/implicit-valuesets.js +66 -0
- package/tx/library/renderer.js +77 -21
- package/tx/library/sqlite-utilities.js +2 -0
- package/tx/library/tho-precedence.js +40 -0
- package/tx/library/ucum-parsers.js +11 -1
- package/tx/library/wait-for-cache.js +43 -0
- package/tx/library.js +321 -22
- package/tx/openapi-operations.js +6 -4
- package/tx/operation-context.js +460 -99
- package/tx/params.js +25 -0
- package/tx/problems.js +3 -5
- package/tx/provider.js +314 -67
- package/tx/resource-cache-sqlite.js +1050 -0
- package/tx/sct/documentation.md +45 -21
- package/tx/sct/ecl.js +81 -17
- package/tx/sct/editions.js +39 -0
- package/tx/sct/expressions.js +10 -1
- package/tx/sct/load-config.js +107 -0
- package/tx/sct/mmap.js +109 -0
- package/tx/sct/structures.js +175 -15
- package/tx/sct/text-index.js +5 -1
- package/tx/syndication/syndication-feed.js +2 -2
- package/tx/tx-html.js +44 -8
- package/tx/tx.fhir.org.yml +8 -8
- package/tx/tx.js +282 -80
- package/tx/usage-tracker.js +39 -11
- package/tx/vs/vs-api.js +19 -0
- package/tx/vs/vs-database.js +386 -62
- package/tx/vs/vs-package.js +44 -17
- package/tx/vs/vs-vsac.js +649 -101
- package/tx/workers/batch-validate.js +37 -10
- package/tx/workers/batch.js +6 -1
- package/tx/workers/cache-control.js +15 -4
- package/tx/workers/closure.js +156 -13
- package/tx/workers/compare.js +8 -5
- package/tx/workers/expand.js +1173 -289
- package/tx/workers/metadata.js +35 -0
- package/tx/workers/search.js +53 -1
- package/tx/workers/supplement-filters.js +145 -0
- package/tx/workers/translate.js +169 -4
- package/tx/workers/validate.js +223 -119
- package/tx/workers/worker.js +123 -31
- package/tx/xversion/xv-valueset.js +7 -4
- package/utilities/submit-test-report.js +3 -1
- package/utilities/tx-stress-test.js +482 -0
- package/vcl/readme.md +37 -0
- package/vcl/vcl-parser.js +15 -0
- package/xig/readme.md +52 -0
- package/xig/xig-template.html +5 -8
- package/xig/xig.js +238 -422
- package/changelog-template.md +0 -22
- package/registry/test-runner.js +0 -237
- package/release.sh +0 -80
- package/test-scripts/repro-re2-wasm-leak.js +0 -34
- package/tsconfig.eslint.json +0 -9
- package/tx/html/dash-metrics.liquid +0 -147
- package/tx/html/home-metrics.liquid +0 -225
package/cluster.md
ADDED
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
# A master and terminology workers on one machine
|
|
2
|
+
|
|
3
|
+
By default, FHIRsmith runs as a simple server, where one Node.js process runs JavaScript on one CPU core.
|
|
4
|
+
A terminology server under load from many clients can use more than one CPU core. To support such usage,
|
|
5
|
+
FHIRsmith can run as one **master** and several **tx-workers** on the same machine and data directory, with
|
|
6
|
+
a front door that sends each request to the right process:
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
node cluster.js # the master, workers by CPU, and a proxy on the configured port
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Note that nginx is the front door in either simple server or cluster mode: the launcher is not built to face clients itself.
|
|
13
|
+
|
|
14
|
+
The cluster runs behind a reverse proxy to manage security etc. Note that the reverse proxy can
|
|
15
|
+
also be used to manage a virtual cluster of servers, but under such an arrangement, the servers
|
|
16
|
+
are not tightly connected the way they are when FHIRsmith is run as a single cluster; we do not
|
|
17
|
+
recommend mixing these two approaches together.
|
|
18
|
+
|
|
19
|
+
Also note that if you run `node server.js` directly, the cluster settings are ignored, and you get
|
|
20
|
+
a normal single process server.
|
|
21
|
+
|
|
22
|
+
* [How the Cluster works](#how-the-cluster-works)
|
|
23
|
+
* [Status](#status)
|
|
24
|
+
* [Graphs](#graphs)
|
|
25
|
+
* [Configuration](#configuration)
|
|
26
|
+
* [Options](#options)
|
|
27
|
+
* [Memory](#memory)
|
|
28
|
+
* [How many workers](#how-many-workers)
|
|
29
|
+
* [Limits of this arrangement](#limits-of-this-arrangement)
|
|
30
|
+
|
|
31
|
+
## How the Cluster works
|
|
32
|
+
|
|
33
|
+
If you are looking to configure a cluster, see [below](#configuration). You don't need to concern yourself with roles etc.;
|
|
34
|
+
this section just explains how the cluster works.
|
|
35
|
+
|
|
36
|
+
`node server.js` with no `--role` is unchanged: one process, every module, as before. One
|
|
37
|
+
difference: `--port` or `--instance-code` without `--role` now stops it with an error, where
|
|
38
|
+
before they were ignored.
|
|
39
|
+
|
|
40
|
+
The settings this uses are under `server.cluster` and `server.memory` in `config.json`;
|
|
41
|
+
[CONFIGURATION.md](CONFIGURATION.md#cluster) lists them with the rest of the server's
|
|
42
|
+
configuration and links back to the tables here.
|
|
43
|
+
|
|
44
|
+
## Roles
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
node --expose-gc server.js --role=master --instance-code=m0 --port=3001
|
|
48
|
+
node --expose-gc server.js --role=tx-worker --instance-code=tx1 --port=3002
|
|
49
|
+
node --expose-gc server.js --role=tx-worker --instance-code=tx2 --port=3003
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
(`--expose-gc` lets the memory guard force a GC; the launcher adds it.)
|
|
53
|
+
|
|
54
|
+
| Flag | Environment | Config | Meaning |
|
|
55
|
+
|---|---|---|---|
|
|
56
|
+
| `--role` | `FHIRSMITH_ROLE` | | `master` or `tx-worker`. None: an ordinary single server. |
|
|
57
|
+
| `--instance-code` | `FHIRSMITH_INSTANCE_CODE` | `modules.tx.instanceCode` (master only) | Prefixed onto every cache-id the process issues (see [nginx.md](nginx.md#instance-codes)). Each process needs its own, 1-16 letters and digits. A worker must be given one, since the config's is shared with the master. Started by hand, a process without one stops with an error: a master with none here nor in `modules.tx.instanceCode`, or any worker. The launcher gives them `m0` (or `modules.tx.instanceCode`), `tx1`, `tx2`, ... |
|
|
58
|
+
| `--port` | `PORT` | `server.port` | The port the process listens on. |
|
|
59
|
+
|
|
60
|
+
All the processes use the same data directory and the same `config.json`. The role decides
|
|
61
|
+
which parts of the configuration each one acts on.
|
|
62
|
+
|
|
63
|
+
With a role, a process listens on `127.0.0.1` only, so only a front door on the same host
|
|
64
|
+
(the launcher, or nginx) reaches it. `server.cluster.bindAddress` changes that: `"0.0.0.0"`
|
|
65
|
+
(or `"::"`) for every interface, or one address, for an nginx on another machine. Each
|
|
66
|
+
process logs the address as it starts. With no role, a server listens on every interface,
|
|
67
|
+
as before.
|
|
68
|
+
|
|
69
|
+
## What runs where
|
|
70
|
+
|
|
71
|
+
| | master | tx-worker |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| tx module (`/r4`, `/r5` and the other endpoints) | yes (may also serve tx requests) | yes |
|
|
74
|
+
| Every other module (packages, xig, registry, publisher, token, shl, ips, ...) | yes | no |
|
|
75
|
+
| `$closure` (the closure tables) | yes | no: routed to the master (yes, with [`sharedClosure`](#options)) |
|
|
76
|
+
| Syndication feed (the store) | yes | no: routed to the master |
|
|
77
|
+
| VSAC sync, and the resync form on `/info/vsac` | yes | no: reads what the master wrote |
|
|
78
|
+
| Package crawler, XIG download, registry crawl, daily report, npm audit | yes | no (those modules don't run) |
|
|
79
|
+
| IANA media types registry (`terminology-cache/media-types.xml`), downloaded at each load | yes | no: reads the master's copy (none: a warning, and no `registered` filter) |
|
|
80
|
+
| Statistics database, uptime history | yes | no: in memory only |
|
|
81
|
+
| `$cache-control` caches | its own, in memory (shared, with [`sharedCache`](#options)) | its own, in memory (shared, with [`sharedCache`](#options)) |
|
|
82
|
+
| Expansion cache, CodeSystem list refresh | its own | its own |
|
|
83
|
+
| Log file | `logs/server-<date>.log` | the same file (lines interleave) |
|
|
84
|
+
|
|
85
|
+
The master loads the terminologies like any tx server: `$closure` needs subsumption.
|
|
86
|
+
|
|
87
|
+
**VSAC.** The master syncs, and when a sync has written its value sets it adds one to a
|
|
88
|
+
generation counter in the VSAC database. A worker remembers the generation its value set list
|
|
89
|
+
was loaded at. When a request uses the VSAC value sets, the worker reads the counter (one read
|
|
90
|
+
by key, at most once a second) and reloads the list in the background if it has moved, so a
|
|
91
|
+
sync reaches every worker about a second after it finishes. The database is in WAL mode with a
|
|
92
|
+
5 second busy timeout, so a worker's reload doesn't make the master's writes fail.
|
|
93
|
+
|
|
94
|
+
**The package caches are the master's.** The value set and concept map caches in each
|
|
95
|
+
package folder (`.valuesets-2.db`, `.conceptmaps-2.db`) are built, rebuilt and migrated only
|
|
96
|
+
by the master; it has finished every one before it answers `/health`. A worker opens them
|
|
97
|
+
read-only, and never creates, rebuilds or migrates one: if one is missing, incomplete or still
|
|
98
|
+
needs a migration (a worker started on its own, before the master), the worker waits for the
|
|
99
|
+
master to finish it, and after 5 minutes (`FHIRSMITH_CACHE_WAIT_MS`) stops with an error that
|
|
100
|
+
says so. Starting the master first is what makes the wait unnecessary, not what keeps the
|
|
101
|
+
workers from writing.
|
|
102
|
+
|
|
103
|
+
The same goes for the caches of a `folder:` or `file:` library source
|
|
104
|
+
(`terminology-cache/folder-source-master-<hash>`, one folder for every process). Each time the
|
|
105
|
+
master loads the library it rebuilds them in a temporary sibling folder and renames it into
|
|
106
|
+
place, so a running worker never finds one half built; a worker reads the new one when it next
|
|
107
|
+
starts. Only the master removes the folders of sources no longer loaded.
|
|
108
|
+
|
|
109
|
+
**The VSAC, LOINC and RxNorm databases are the master's too.** The master sets them up as it
|
|
110
|
+
loads: the VSAC schema and content hashes, LOINC's journal mode, and the LOINC and RxNorm
|
|
111
|
+
indexes. A worker opens them read-only and writes nothing, so they can be on read-only storage
|
|
112
|
+
for the workers. The NDC, UNII, CPT and OMOP databases need no setup, and a worker opens them
|
|
113
|
+
read-only as well. A worker started before the master uses LOINC and RxNorm as they are
|
|
114
|
+
(without an index the master hasn't made yet, lookups are slower), and has no VSAC value sets
|
|
115
|
+
until the master has made and migrated that database.
|
|
116
|
+
|
|
117
|
+
**Nothing else is shared.** The workers run no module but tx, so they never read what the
|
|
118
|
+
package crawler, the XIG download or the registry crawl write.
|
|
119
|
+
|
|
120
|
+
## Routing
|
|
121
|
+
|
|
122
|
+
Whatever the front door, the rules are the same:
|
|
123
|
+
|
|
124
|
+
1. `$closure` (in any endpoint, e.g. `/r4/$closure`), the syndication feed
|
|
125
|
+
(`/r4/syndication...`) and the source info pages (`/r4/info/...`) go to the **master**.
|
|
126
|
+
2. Every path outside the tx endpoints (`/`, `/packages`, `/xig`, `/dashboard`, `/health`, ...)
|
|
127
|
+
goes to the **master**.
|
|
128
|
+
3. A tx request with an `X-Cache-Id` goes to the process whose code prefixes the id
|
|
129
|
+
(`tx2.mhp4x2k9.4f0c...` to `tx2`). If that process is down (restarting), its caches are gone, and the
|
|
130
|
+
launcher answers at once with `404` and `cache-id-unknown`, "lost to a server restart"
|
|
131
|
+
(with `sharedCache`, any tx process takes it instead). If the prefix is unknown, it goes to
|
|
132
|
+
any tx process, which answers `cache-id-unknown`. Either way the client starts a new cache.
|
|
133
|
+
4. Any other tx request goes to the tx process with the fewest requests outstanding: the
|
|
134
|
+
workers, and the master too unless it is set not to serve tx.
|
|
135
|
+
|
|
136
|
+
The [options](#options) change rules 1 and 3: with `sharedClosure`, `$closure` goes by rule 4;
|
|
137
|
+
with `sharedCache`, so does a request with a cache-id (unless `cacheAffinity` is on).
|
|
138
|
+
|
|
139
|
+
## Nginx note about clusters
|
|
140
|
+
|
|
141
|
+
**Always run the launcher behind nginx, on the same host, with request buffering on**
|
|
142
|
+
(`proxy_request_buffering on`, nginx's default), as one upstream:
|
|
143
|
+
|
|
144
|
+
```nginx
|
|
145
|
+
upstream fhirsmith { server 127.0.0.1:3000; keepalive 32; }
|
|
146
|
+
# in the server block, for every location:
|
|
147
|
+
# include fhirsmith-proxy.conf; (nginx.md: the X-Forwarded-* headers)
|
|
148
|
+
# proxy_pass http://fhirsmith;
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
With buffering on, nginx reads each request body whole and sends it on with a
|
|
152
|
+
`Content-Length`. The launcher passes a chunked body on chunked, but it has none of nginx's
|
|
153
|
+
limits on what clients send. The same host, because the launcher believes `X-Forwarded-Proto`
|
|
154
|
+
only from a loopback address: from any other it sets it to `http`, so behind an HTTPS nginx on
|
|
155
|
+
another machine the servers would advertise `http://` URLs, and the publisher's `Secure` login
|
|
156
|
+
cookie would be lost (see [nginx.md](nginx.md#headers-to-pass-to-the-server)).
|
|
157
|
+
|
|
158
|
+
## The launcher (cluster.js)
|
|
159
|
+
|
|
160
|
+
`node cluster.js` reads `config.json` from the data directory and:
|
|
161
|
+
|
|
162
|
+
* starts the master and waits until it is ready: each process the launcher starts says so on
|
|
163
|
+
its IPC channel as it begins to listen, so neither a process still shutting down on the same
|
|
164
|
+
port nor the memory guard (which may refuse `/health`) can mislead it. On a new data
|
|
165
|
+
directory the master builds the package caches and creates the VSAC database
|
|
166
|
+
* listens on the configured port, and starts the workers one after another, each once the one
|
|
167
|
+
before is ready
|
|
168
|
+
* proxies each request by the rules above, over keep-alive connections to each process. It
|
|
169
|
+
adds the client address to `X-Forwarded-For`, so the servers' logs and rate limits see the
|
|
170
|
+
real client (its own address is a loopback one, which `server.trustProxy` trusts by default)
|
|
171
|
+
* sends each process at most `maxOutstanding` tx requests at once (16); the rest wait in the
|
|
172
|
+
launcher, oldest first, and one that has waited `queueSeconds` (1) is refused with 503 and
|
|
173
|
+
`Retry-After`. Without this, a surge queues inside the processes, where everything waits
|
|
174
|
+
behind everything else: a 4 ms `$lookup` waited 16 s in a 3x surge, and the first 503s came
|
|
175
|
+
after 29 s. A request with a cache-id waits for the process that issued it, which no other
|
|
176
|
+
can stand in for, so it waits longer: `ownerQueueSeconds` (30). So does any request routed
|
|
177
|
+
by its cache-id's prefix (rule 3): one whose prefix no process has (any tx process may take
|
|
178
|
+
it, but it waits as long), and, with `sharedCache` and `cacheAffinity`, one whose issuer is
|
|
179
|
+
full. The master's own pages and paths (the dashboard, the syndication feed, `$closure`
|
|
180
|
+
unless `sharedClosure` sends it to the tx processes, where it waits like any tx request,
|
|
181
|
+
...) and CORS preflights (`OPTIONS`) neither wait nor count against `maxOutstanding`, so a long page on the master leaves its tx slots free; while
|
|
182
|
+
stopping nothing waits. These refusals are the launcher's, logged at most once a minute; they
|
|
183
|
+
are not in the graphs' refusals
|
|
184
|
+
* gives cheap requests a fast lane. A process has `maxOutstanding` + `fastLaneExtra` (16 + 4)
|
|
185
|
+
slots. Cheap requests may use any of them, and when a slot frees they get it first. Other
|
|
186
|
+
requests use up to `maxOutstanding`, or more (up to `maxOutstanding` + `fastLaneExtra`)
|
|
187
|
+
while fewer than `normalMinSlots` of them are running on the process. While they wait, the
|
|
188
|
+
lane leaves them enough slots to have `normalMinSlots` (4) running on each process, so heavy work goes on during a flood of
|
|
189
|
+
cheap requests. Without the lane, a 4 ms `$lookup` waits in line behind the surge (3 s, then
|
|
190
|
+
503), and after it, for the line to drain. The launcher
|
|
191
|
+
doesn't read bodies, so the rule is by method and path: a `GET` (or `HEAD`) of `metadata`,
|
|
192
|
+
`$lookup`, `$subsumes` or `$validate-code`, whose parameters are all in the URL, so it carries
|
|
193
|
+
no inline value set. A `POST` of `$lookup`, `$subsumes` or `$validate-code` takes the lane
|
|
194
|
+
only with a `Content-Length` of at most `fastLaneMaxBytes` (1 KB: one coding, no inline value
|
|
195
|
+
set or code system). Reads (`/CodeSystem/{id}`: a large code system is costly to render),
|
|
196
|
+
searches and `$expand` never do. A request in the lane that turns out to be costly (a
|
|
197
|
+
`GET $validate-code` against a value set that must be expanded) costs at most `fastLaneExtra`
|
|
198
|
+
more requests at once on a process
|
|
199
|
+
* sends no new requests to a process that answered 503 (its memory guard is refusing) for
|
|
200
|
+
`cooldownSeconds` (2), unless every process is refusing. Otherwise the refusing process,
|
|
201
|
+
quick to answer and so the least busy, would draw the waiting requests
|
|
202
|
+
* answers `504` for a process that hasn't begun its response `upstreamTimeoutSeconds` (60)
|
|
203
|
+
after the request was sent to it (a stuck event loop, a hung native call), aborts that
|
|
204
|
+
request, which frees its slot, and logs a warning naming the process. After 3 in a row from
|
|
205
|
+
one process, it gets no new requests for `cooldownSeconds`, as after a 503, while another
|
|
206
|
+
can take them. It is not restarted for this. A response that has begun is not timed: a
|
|
207
|
+
long stream goes on
|
|
208
|
+
* frees a tx request's slot when the process's response has ended, not when the client has
|
|
209
|
+
read it: the launcher reads a tx response from the process as fast as it comes (the process
|
|
210
|
+
held it whole in memory already) and holds what the client hasn't taken yet. Any other
|
|
211
|
+
response (a package download can be large) is passed on only as fast as the client takes
|
|
212
|
+
it. Either way, a client that takes none of what is waiting for it for `writeTimeoutSeconds`
|
|
213
|
+
(60) has its connection closed, and the launcher logs it (at most once a minute, with a
|
|
214
|
+
count). Without this, about 20 clients asking for large responses and never reading them
|
|
215
|
+
would hold every slot of a process, and every tx request would get 503
|
|
216
|
+
* logs its limits (`maxOutstanding`, the three waits, `upstreamTimeoutSeconds`, `writeTimeoutSeconds`, the fast lane's two, `memoryHeadroomGB`,
|
|
217
|
+
`normalMinSlots`, `drainSeconds`, `trimAllocator`, `recycleSeconds`) as it starts, in one line
|
|
218
|
+
* starts again a process that exits. One that keeps failing waits longer each time: 1 s,
|
|
219
|
+
2 s, 4 s ... up to a minute. A process that exits before it is ready, or within a minute of
|
|
220
|
+
being ready, counts as failing; one that ran longer is restarted after 1 s. If the master
|
|
221
|
+
exits `maxStartFailures` (6) times in a row without ever having been ready, the launcher
|
|
222
|
+
logs an `ERROR` and exits 1, so the service manager sees the failure
|
|
223
|
+
* restarts a tx-worker whose memory has stayed over the memory guard's refusal threshold for
|
|
224
|
+
`recycleSeconds` (30) with no request at work on it (see [Memory](#memory)). It sends it no
|
|
225
|
+
new requests, lets those it has finish (up to `drainSeconds`), stops it as on shutdown and
|
|
226
|
+
starts it again at once
|
|
227
|
+
* starts each process with its own Node flags (`node --max-old-space-size=... cluster.js`
|
|
228
|
+
passes them on, with `--expose-gc` added), except a debugger's (`--inspect...`) and a
|
|
229
|
+
watcher's (`--watch...`), which are the launcher's own
|
|
230
|
+
* on `SIGTERM` or `SIGINT`: stops taking connections, closes idle keep-alive connections,
|
|
231
|
+
waits for the requests in progress (up to `drainSeconds`), then sends each process
|
|
232
|
+
`SIGTERM` (each shuts down as a single server does; `SIGKILL` after 30 seconds), and exits
|
|
233
|
+
* if the launcher itself dies (even by `SIGKILL`), each process it started sees its IPC channel
|
|
234
|
+
close and shuts down, so none is left holding its port against the next launcher. A
|
|
235
|
+
process started by hand has no channel, and this doesn't apply to it
|
|
236
|
+
|
|
237
|
+
## Status
|
|
238
|
+
|
|
239
|
+
Under the launcher, the master's `/` and `/dashboard` show the whole cluster. Every
|
|
240
|
+
`statusSeconds` (15) each process sends the launcher a few figures of its own over its IPC
|
|
241
|
+
channel: its JS heap, its client caches and their concepts, its expansion cache, the requests
|
|
242
|
+
it has in flight and whether its memory guard is refusing. The launcher adds what it reads
|
|
243
|
+
itself, merges them, and passes the snapshot to the master.
|
|
244
|
+
|
|
245
|
+
The page keeps to the space it had before (it is often on a wall screen). Top to bottom:
|
|
246
|
+
|
|
247
|
+
* **One line**: Requests (the processes' own counts, summed, and the average per minute),
|
|
248
|
+
Uptime (the master's), Heap (every process's JS heap and the launcher's, summed, and how many
|
|
249
|
+
that is), and Problems, only when there are any.
|
|
250
|
+
* **The problems**, a line each with a ⚠ and a sentence: processes refusing requests, or the
|
|
251
|
+
memory guard's measure at 90% or more of its refusal threshold; a process that isn't ready
|
|
252
|
+
(starting, restarting); figures gone stale; restarts since launch; under 10% of the disk
|
|
253
|
+
free; a background task whose last run failed; and a serious npm audit finding.
|
|
254
|
+
* **The graphs** (see [Graphs](#graphs)); their key is under More.
|
|
255
|
+
* **Background tasks**: one line naming those at work, those whose last run failed (each is
|
|
256
|
+
also a problem, with its state) and the idle ones, then a row for each task at work.
|
|
257
|
+
* **One line** for the server's own upkeep: on `/dashboard`, the FHIRsmith release (and whether
|
|
258
|
+
a newer one is out) and npm audit's result; and, in red, low disk space (under 10% free).
|
|
259
|
+
The npm audit advice and findings follow it when there are any.
|
|
260
|
+
* **More** (closed until opened): the graphs' key, the status panel below, Storage, every
|
|
261
|
+
background task, and on `/dashboard` the data folder.
|
|
262
|
+
|
|
263
|
+
The status panel, under More:
|
|
264
|
+
|
|
265
|
+
* **Cluster** · the number of processes · how long ago the snapshot was taken.
|
|
266
|
+
* **Tiles**:
|
|
267
|
+
* Requests: those the processes served, each its own count since it started, summed; the
|
|
268
|
+
sub-label is the average per minute.
|
|
269
|
+
* Client caches: summed, with their concepts; with `sharedCache`, the one store, counted once
|
|
270
|
+
(the master's count).
|
|
271
|
+
* Expansion cache: summed; each process has its own.
|
|
272
|
+
* In flight: summed, and which processes are refusing.
|
|
273
|
+
* Restarts: since the launcher started.
|
|
274
|
+
* Uptime: the master's.
|
|
275
|
+
* **Memory** · cgroup private: the cgroup's `memory.stat` `anon` (cgroup v1 `total_rss`), what
|
|
276
|
+
the memory guard measures (see [Memory](#memory)), as a bar against the limit. It has a
|
|
277
|
+
segment per process (its `RssAnon`), in the graph lines' colours, and ticks at the refuse
|
|
278
|
+
and abort thresholds, each labelled on one line under its tick (the refuse label moves left
|
|
279
|
+
of the abort label when they would meet, and to a second line only when there is no room).
|
|
280
|
+
The host's memory in use is under it.
|
|
281
|
+
* **A row per process and the launcher**: status, uptime, restarts, requests, private and
|
|
282
|
+
mapped memory in MB (`RssAnon` and `RssFile` from `/proc/<pid>/status`), heap in MB (with
|
|
283
|
+
the limit in the header when every process has the same), client caches and concepts,
|
|
284
|
+
requests in flight, and the age of its figures. The launcher's requests are left out: what
|
|
285
|
+
it passes on includes static files and pages, so it would not add up. `—` means a figure
|
|
286
|
+
doesn't apply or isn't known. A process whose figures are more than three intervals old is
|
|
287
|
+
`stale` and left out of the totals; one that is restarting has none until its replacement
|
|
288
|
+
sends some.
|
|
289
|
+
* **Footnotes**: one line on mapped memory, and anything that couldn't be read. A page
|
|
290
|
+
several processes map counts in each process's mapped figure, and the kernel charges a page
|
|
291
|
+
to the cgroup that first read it, so after a restart pages still cached from the last run are
|
|
292
|
+
charged elsewhere: mapped memory is not in the meter.
|
|
293
|
+
* Then **Storage** (closure tables, entries and concepts, the closure database, the logs and
|
|
294
|
+
the free disk, as many columns as there are tiles), then **All background tasks** in the
|
|
295
|
+
table's style.
|
|
296
|
+
|
|
297
|
+
Nothing is made up. Without `/proc` (macOS) there is no per-process memory. Without a cgroup
|
|
298
|
+
with a memory limit of the service's own (no `MemoryMax`, or the limit only on an enclosing
|
|
299
|
+
slice), the meter is the processes' private memory summed, against the limit each process's
|
|
300
|
+
guard measures itself against. A footnote says what is missing and why.
|
|
301
|
+
|
|
302
|
+
The same snapshot is at **`/dashboard/status.json`**: `scope: "cluster"`, `sampledAt`,
|
|
303
|
+
`intervalMs`, `memory` (`cgroup` `{anon, file}` or null, `processAnon`, `limit`, `limitSource`,
|
|
304
|
+
`rejectAt`, `abortAt`, `rejectFraction`, `abortFraction`, `group`), `caches` (`scope`:
|
|
305
|
+
`summed` or `shared`, `clientCaches`, `clientConcepts`, `expansionItems`), `inFlight`,
|
|
306
|
+
`refusing` (instance codes), `processes` (one object per process, with the columns above, in
|
|
307
|
+
bytes and milliseconds; null where not known), `launcher`, `missing`, and `requests`: the
|
|
308
|
+
master's statistics, its own count and the workers' it has merged (see
|
|
309
|
+
[Request counts](#request-counts)). Under `/dashboard`, the memory guard never refuses it. A
|
|
310
|
+
worker, a server with no role, or a master before its first snapshot answers with its own
|
|
311
|
+
figures: `scope: "process"`, `process`, `memory` (its guard's limit and thresholds) and
|
|
312
|
+
`requests`.
|
|
313
|
+
|
|
314
|
+
A worker's own page, a master before the launcher's first snapshot, and a server with no role
|
|
315
|
+
show the same line for the one process (Heap: its own, of its limit), and under More the same
|
|
316
|
+
tiles and meter, headed **Worker tx1** · this process only,
|
|
317
|
+
**Master m0** · waiting for the launcher's first snapshot, or **Server** · one process. Their
|
|
318
|
+
meter is what their memory guard measures: the cgroup's private memory with a role (this
|
|
319
|
+
process's segment, the rest hatched as "other"), or the process's own RSS without.
|
|
320
|
+
|
|
321
|
+
## Graphs
|
|
322
|
+
|
|
323
|
+
With the launcher, `/` and `/dashboard` show the master and every worker as lines on the
|
|
324
|
+
existing metric panels, with one legend above them. Colour (by lightness), line pattern and
|
|
325
|
+
marker each tell the processes apart; up to eight are drawn, and past eight the legend says
|
|
326
|
+
how many are left out. Each process takes a sample every `stats.sampleSeconds`: with a role,
|
|
327
|
+
a minute unless set.
|
|
328
|
+
|
|
329
|
+
The panels sit on the tiles' grid, two tiles' width each. Until a panel has two samples to draw
|
|
330
|
+
a line between, it says it is waiting for them. The memory panel is each process's private
|
|
331
|
+
memory (`RssAnon`), with a thick dark Total: the
|
|
332
|
+
cgroup's private memory, what the memory guard measures, which the launcher reads when the
|
|
333
|
+
master's sample arrives (without a cgroup of the service's own, the processes' summed), and a
|
|
334
|
+
grey dash-dot line, labelled with its value, at the guard's refusal threshold. Where the
|
|
335
|
+
private memory can't be read (not Linux), the panel is the heap change since each process
|
|
336
|
+
started, as before, and its title says so. Requests per Minute and Client Cache add a Total
|
|
337
|
+
too: every process's samples summed per sampling interval. With `sharedCache`, every process
|
|
338
|
+
samples the same shared client-cache counts, so those lines overlap, and the Total takes the
|
|
339
|
+
count once rather than summing it. Turnaround and the blocking panels have no total, since a
|
|
340
|
+
summed turnaround means nothing.
|
|
341
|
+
|
|
342
|
+
The memory guard counts its 503 refusals before body parsing. Grey hatching on all
|
|
343
|
+
charts marks sampling intervals containing those refusals; it does not mean the guard
|
|
344
|
+
was closed continuously throughout an interval. The note above the charts counts
|
|
345
|
+
refusals in the displayed samples, and hovering a process point shows that sample's
|
|
346
|
+
count. Request-rate lines and turnaround averages continue to describe admitted work.
|
|
347
|
+
Client timeouts, unsent load-generator arrivals and proxy errors are not guard refusals.
|
|
348
|
+
|
|
349
|
+
Samples keep their actual timestamps. A restarting process leaves a gap and retains its
|
|
350
|
+
earlier history; a skipped sampling interval also leaves a gap. The legend reports
|
|
351
|
+
starting, restarting, unavailable or no recent sample when applicable. History lives in
|
|
352
|
+
the launcher for up to 24 hours, capped at 1,440 points per process (including gaps), so
|
|
353
|
+
a `sampleSeconds` under a minute covers less than 24 hours. A launcher restart clears
|
|
354
|
+
the graph history. The table above the graphs is described under [Status](#status).
|
|
355
|
+
|
|
356
|
+
### Request counts
|
|
357
|
+
|
|
358
|
+
A tx-worker has no statistics database and sends no daily report. Under the launcher, each
|
|
359
|
+
worker sends the requests it counted to the master every minute, and once more when it stops
|
|
360
|
+
(the launcher stops the workers before the master). The master adds them to its own counts,
|
|
361
|
+
so its statistics database (`databases/stats.db`) and the daily report count every process,
|
|
362
|
+
each request once. The counting has these limits:
|
|
363
|
+
|
|
364
|
+
* **A worker that crashes loses up to a minute of its counts**: those it hadn't sent yet. (A
|
|
365
|
+
worker that is stopped sends them as it stops.)
|
|
366
|
+
* **A master that crashes loses the counts it hasn't written yet**, its own and the workers'
|
|
367
|
+
it has merged, as a single server loses its own today.
|
|
368
|
+
* **Counts can land in the next day's report.** A worker's requests reach the database with
|
|
369
|
+
the master's next write, up to a minute or so after they were served, so a request just
|
|
370
|
+
before midnight can be counted in the next day.
|
|
371
|
+
* **Workers started by hand behind nginx have no IPC channel**, so their requests never reach
|
|
372
|
+
the master's `stats.db` or daily report (see [Limits](#limits-of-this-arrangement)).
|
|
373
|
+
|
|
374
|
+
`/dashboard/status.json` gives this total (`requests`: the master's and the workers' parts),
|
|
375
|
+
the workers' up to a minute behind. The status panel's Requests tile counts differently: the
|
|
376
|
+
sum of what each process reports every `statusSeconds`, since each started (see
|
|
377
|
+
[Status](#status)). A worker's own page shows only its own.
|
|
378
|
+
|
|
379
|
+
A standalone server, or roles started separately behind nginx, shows only its own graph
|
|
380
|
+
data. Combining process histories requires the launcher's IPC connection; no separate
|
|
381
|
+
metrics service or HTTP polling is used. Its page draws the same panels on the same grid, with
|
|
382
|
+
one series ("This process", or the worker's code on a worker's own page): its private memory
|
|
383
|
+
with its memory guard's refusal threshold, and no Total. A server with no role samples every
|
|
384
|
+
`stats.intervalMinutes` (10), as before, unless `stats.sampleSeconds` is set.
|
|
385
|
+
|
|
386
|
+
### The daily report
|
|
387
|
+
|
|
388
|
+
The daily report (see [CONFIGURATION.md](CONFIGURATION.md#email-and-the-daily-report)) is the
|
|
389
|
+
master's. Its stability paragraph is about the master's own runs. Only the launcher knows what
|
|
390
|
+
happened to the workers, and what it answered itself, so it sends the master what it saw, the
|
|
391
|
+
same way it passes on the workers' request counts, and the master keeps it in `stats.db`
|
|
392
|
+
(table `cluster_events`). The report has a **Cluster** paragraph after the stability one:
|
|
393
|
+
|
|
394
|
+
* when the launcher started (a launcher restart restarts every process);
|
|
395
|
+
* for each process that exited or was recycled: how many times and how (the exit code or
|
|
396
|
+
signal, or why it was recycled), how long it was unavailable in all (from its exit, or the
|
|
397
|
+
start of its recycle, until it was ready again), and the requests that were lost to it (504:
|
|
398
|
+
no response in `upstreamTimeoutSeconds`; 502: it didn't answer; a cache-id that came while
|
|
399
|
+
it was restarting);
|
|
400
|
+
* the requests the launcher refused itself (503: no process free in `queueSeconds`, no process
|
|
401
|
+
ready, or stopping), and the connections it closed because the client took none of a
|
|
402
|
+
response in `writeTimeoutSeconds`.
|
|
403
|
+
|
|
404
|
+
Events go to the master as they happen, and what was counted every minute; while the master is
|
|
405
|
+
down they wait in the launcher (up to 5,000 events, the oldest dropped first) and go when it
|
|
406
|
+
is ready again. So:
|
|
407
|
+
|
|
408
|
+
* **a process still unavailable at midnight** has that time in the next day's report, when it
|
|
409
|
+
is ready again;
|
|
410
|
+
* **what the launcher saw is lost if the launcher stops while the master is down**, including
|
|
411
|
+
when it gives up starting the master (`maxStartFailures`). That shows as an outage in the
|
|
412
|
+
stability paragraph;
|
|
413
|
+
* **without the launcher** (roles behind nginx), there is no Cluster paragraph.
|
|
414
|
+
|
|
415
|
+
## Configuration
|
|
416
|
+
|
|
417
|
+
```json
|
|
418
|
+
"server": {
|
|
419
|
+
"port": 3000,
|
|
420
|
+
"cluster": { "workers": 4, "masterServesTx": true, "internalPort": 3001, "drainSeconds": 30 }
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
| Setting | Flag | Default | Meaning |
|
|
425
|
+
|---|---|---|---|
|
|
426
|
+
| `server.port` | `--port`, `PORT` | 3000 | The port clients use. |
|
|
427
|
+
| `server.cluster.workers` | `--workers`, `FHIRSMITH_WORKERS` | from the CPUs, or fewer if memory is short | How many tx-workers. Not given: by the engine's rule from the physical cores and vCPUs (see [How many workers](#how-many-workers)), then lowered to as many as fit under the memory guard's refusal threshold with `memoryHeadroomGB` to spare once loaded (at least 1; see [Memory](#memory)). Given: that many, always, with a warning if they don't fit. A whole number (anything else stops the launcher with an error); 0: the master alone. |
|
|
428
|
+
| `server.cluster.cores` | | detected | The physical cores the rule uses, instead of the detected ones: for a machine whose topology the launcher can't see or reports wrongly. |
|
|
429
|
+
| `server.cluster.workerRule` | | the engine's | The rule by name (`classic`, or one an engine adds). |
|
|
430
|
+
| `server.cluster.memoryHeadroomGB` | | 2 | Memory to leave free under the refusal threshold once every process is loaded and idle. 2: about what one heavy operation uses (a large SNOMED `$expand`, a big `$batch-validate-code`); with less, the first one shuts the gate for everyone. |
|
|
431
|
+
| `server.cluster.masterServesTx` | `--master-serves-tx=false` | true | Whether the master also takes ordinary tx requests. |
|
|
432
|
+
| `server.cluster.internalPort` | `--internal-port` | port + 1 | The master's port; worker *n* gets this + *n*. A range that includes `server.port` stops the launcher with an error. |
|
|
433
|
+
| `server.cluster.bindAddress` | | `127.0.0.1` | The address the master and the workers listen on (see [Roles](#roles)), and the launcher reaches them at (loopback for `"0.0.0.0"` or `"::"`). Not the launcher's own port, which is `server.port` on every interface. |
|
|
434
|
+
| `server.cluster.drainSeconds` | | 30 | How long the requests in progress get to finish when the launcher stops, and on a tx-worker before a memory recycle stops it; a request still running then is cut short. 30: half again the operation limit (`modules.tx.operationTimeLimit`, 20 s), so an operation in progress finishes or reaches its limit. 0: no wait. |
|
|
435
|
+
| `server.cluster.maxOutstanding` | | 16 | Tx requests sent to one process at once; the rest wait in the launcher. The master's own paths and CORS preflights don't count. 16: a process is one event loop whose operations take turns every 25 ms, so a cheap request waits a turn behind each heavy one, and 16 keeps that under half a second. More cores don't change it. 0: no limit (nothing waits). |
|
|
436
|
+
| `server.cluster.queueSeconds` | | 1 | Seconds a tx request may wait for a process with room. 1: a client is better told the server is busy than left to its own timeout, and when a surge ends, the line it left drains within this long (with 3, the line took 3 s to clear and the requests just after the surge waited behind it: a `$lookup` took 1.4 s for 5 s; with 1, under 10 ms after 2 s, at the same goodput). When hit, the client gets 503, `Retry-After: 5` and `Connection: close`. 0: that 503 comes at once. |
|
|
437
|
+
| `server.cluster.ownerQueueSeconds` | | 30 | The same, for a request with a cache-id, which only the process that issued the id can take. 30: the request has nowhere else to go, so it waits about as long as a client's own timeout. 0: 503 at once. |
|
|
438
|
+
| `server.cluster.cooldownSeconds` | | 2 | Seconds a process that answered 503 (its memory guard refusing) gets no new requests while another can take them. 2: the guard re-measures only when a request arrives, so a longer pause holds its recovery back. Clients see nothing; their requests go to another process. 0: no pause. |
|
|
439
|
+
| `server.cluster.upstreamTimeoutSeconds` | | 60 | Seconds from sending a request to a process until its response begins, after which the client gets 504 (an OperationOutcome naming the process) and the request is aborted. 60: an operation stops at its limit (`modules.tx.operationTimeLimit`, 20 s), but that limit counts the operation's own compute, not the turns it waits while the others on its event loop run (up to `maxOutstanding` of them), so a heavy operation in a busy process takes longer than 20 s by the clock. Three times the limit leaves room for that; a process silent for longer is taken to be wedged (a stuck event loop, a hung native call), not working. The time a request waits in the launcher's queue is not counted. Raise it with the operation limit. 0: no timeout. |
|
|
440
|
+
| `server.cluster.writeTimeoutSeconds` | | 60 | Seconds a client may take none of the response waiting for it in the launcher before its connection is closed (the client sees the response cut short). It bounds how long a client that doesn't read holds the launcher's memory (a tx response) or a process's request (any other). 60: a client that reads takes some within milliseconds, so this is about a thousand times what a reading client needs; it is nginx's own `send_timeout`, and behind nginx with buffering on (the default), nginx takes the response at once and this never fires. 0: no timeout. |
|
|
441
|
+
| `server.cluster.maxStartFailures` | | 6 | Times in a row the master may exit without ever having been ready before the launcher gives up and exits 1. Without it, a master that can't start left a launcher that ran but never listened, which systemd counts as up. 6: the waits between starts (1, 2, 4, 8, 16 s) put the sixth start 31 s after the first failure, past the 30 s an old process still shutting down may hold the port for. Once the master has been ready, it is started again however often it exits, as a worker always is. Clients see no difference: nothing listens either way. Logged with the master's first start. 0: never give up. |
|
|
442
|
+
| `server.cluster.recycleSeconds` | | 30 | Seconds a tx-worker may stay over the memory guard's refusal threshold with no request at work before the launcher restarts it. 30: long enough for the guard's own recovery (a forced last-resort GC and `malloc_trim` at once and again 5 s later, `malloc_trim` alone up to once a second) to have its effect, short next to a client's retries; meanwhile every request to the server is refused. The restart costs the worker's caches (see [Limits](#limits-of-this-arrangement)) and the about 15 s it takes to start; the others serve meanwhile, and the gate opens once the old process has exited. 0: never. |
|
|
443
|
+
| `server.cluster.fastLaneExtra` | | 4 | How many more than `maxOutstanding` a process may have when the request is in the fast lane (above). 4: room for the cheap requests of a surge without letting a lane request that turns out to be costly crowd a process. 0: no fast lane. |
|
|
444
|
+
| `server.cluster.normalMinSlots` | | 4 | While other requests wait, the fast lane leaves each process enough slots for this many of them to run, so heavy work keeps going during a flood of cheap requests. 4: in a 3x surge on 4 CPUs (1 master + 2 workers) the heavy requests answered were the same with 4, 8 or no minimum (about 7,100-7,400 in 30 s), but each heavy request running slows the cheap ones on its event loop: the `$lookup` probe took 88 ms (median) with 4, 150-210 ms with 8. With the lane held to 4 slots of its own, the surge was served at 457/s, not about 1,200/s, and the probe took 1.1 s. 0: no minimum. At most `maxOutstanding`. |
|
|
445
|
+
| `server.cluster.fastLaneMaxBytes` | | 1024 | The largest `Content-Length` with which a `POST` `$lookup`, `$subsumes` or `$validate-code` takes the fast lane. 1024: a single coding with its parameters is about 450 bytes; an inline value set or code system is larger. Half of a validator's calls are such `POST CodeSystem/$validate-code`s, and with them in the lane a 3x surge on 4 CPUs was served at twice the rate (1210/s vs 608/s, with the same CPU) and they took 24 ms instead of 1 s; the other requests did no worse. 0: a `POST` never does. |
|
|
446
|
+
| `server.memory.trimAllocator` | | true with a role; false without | Whether a process calls `malloc_trim` (Linux with glibc, and the optional `koffi` dependency): with a role, once after loading the library (it gives back 0.6-0.7 GB a process), and while the memory guard is refusing (see [Memory](#memory)). true: the C allocator keeps freed memory resident, and no GC touches it. false with a role: for diagnosis. Without a role it is off unless set true. Each process's memory line at start says whether it is on. |
|
|
447
|
+
| `modules.tx.instanceCode` | | `m0` under the launcher | The master's instance code. The workers are `tx1`, `tx2`, ... The `m0` default is the launcher's: a master started by hand needs this, `--instance-code` or `FHIRSMITH_INSTANCE_CODE` (see [Roles](#roles)). |
|
|
448
|
+
| `server.cluster.statusSeconds` | | 15 | How often each process sends the launcher its figures, and the launcher sends the master its snapshot of them all, for the status panel and `/dashboard/status.json` (see [Status](#status)). 15: the panel is never more than half a minute behind, and each send is a few hundred bytes. A number above 0. |
|
|
449
|
+
| `stats.sampleSeconds` | | 60, with a role | How often each process takes a graph sample (see [Graphs](#graphs)). Not set: a minute with a role (or `stats.intervalMinutes`, if shorter), which with the launcher's 1,440 points a process keeps 24 hours of history; with no role, `stats.intervalMinutes` (10) as before. Shorter shows a short session in more detail, and keeps less history. The request counters are still written every `intervalMinutes`. |
|
|
450
|
+
|
|
451
|
+
Under systemd, the launcher and its processes are one unit and one cgroup:
|
|
452
|
+
|
|
453
|
+
```ini
|
|
454
|
+
[Service]
|
|
455
|
+
WorkingDirectory=/opt/fhirsmith
|
|
456
|
+
Environment=FHIRSMITH_DATA_DIR=/var/lib/fhirsmith
|
|
457
|
+
ExecStart=/usr/bin/node cluster.js
|
|
458
|
+
KillMode=mixed
|
|
459
|
+
MemoryMax=16G
|
|
460
|
+
TimeoutStopSec=2min
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
A stop can take `drainSeconds` (30), then up to 30 s for the workers to shut down, 5 s for
|
|
464
|
+
their last request counts, and up to 30 s for the master: 95 s at the most, usually a few
|
|
465
|
+
seconds. So give the service manager longer than that before it kills everything. systemd's
|
|
466
|
+
default `TimeoutStopSec` is 90 s. Docker's default stop timeout is 10 s, shorter than the drain
|
|
467
|
+
alone, after which it kills the launcher and every process with it, mid-request; set
|
|
468
|
+
`stop_grace_period: 2m` (Compose) or `docker stop -t 120`, and run the launcher as the
|
|
469
|
+
container's main process (`CMD ["node", "cluster.js"]` in place of the image's `server.js`,
|
|
470
|
+
not under a shell) so it gets the `SIGTERM`.
|
|
471
|
+
|
|
472
|
+
## Options
|
|
473
|
+
|
|
474
|
+
Two options in `server.cluster` change what the processes share. Both are off by default, and
|
|
475
|
+
both only apply with a role: `node server.js` with no role ignores them.
|
|
476
|
+
|
|
477
|
+
```json
|
|
478
|
+
"server": {
|
|
479
|
+
"cluster": { "workers": 4, "sharedCache": true, "sharedClosure": true }
|
|
480
|
+
}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
| Setting | Default | Meaning |
|
|
484
|
+
|---|---|---|
|
|
485
|
+
| `server.cluster.sharedCache` | false | The `$cache-control` caches go in one SQLite file, `databases/resource-cache.db`, that every process reads and writes. |
|
|
486
|
+
| `server.cluster.cacheAffinity` | false | With `sharedCache`: still send a request with a cache-id to the process that issued it, while it is up. |
|
|
487
|
+
| `server.cluster.sharedCacheMaxMB` | 256 | With `sharedCache`: the most client resources (their JSON, in MB) the file holds, every endpoint's caches together. Past it, the caches least recently used are dropped whole, and a request with a dropped cache-id gets `404` `cache-id-unknown` (`CACHE_ID_EVICTED_SIZE`, which names the cap), so the client starts a new cache. 256: the largest IG Publisher cache session in a corpus of 2,817 from builds against tx.fhir.org held 3.5 MB of resources (p99 2.8 MB, median 55 KB), so this is about 70 times the largest; `cacheMaxTotalConcepts` gives one endpoint about 40 times the largest session's concepts, and this is that room for two busy endpoints. The file on disk is larger than what it holds (indexes, and the pages SQLite keeps for reuse). Each process logs the cap as it starts. 0: no cap. |
|
|
488
|
+
| `server.cluster.sharedClosure` | false | Every tx process answers `$closure`, on the one closure database. |
|
|
489
|
+
| `modules.tx.closure.writeRetries` | 10 | With `sharedClosure`: how many times an add is worked out again when another process changed the table first. |
|
|
490
|
+
|
|
491
|
+
### Shared cache
|
|
492
|
+
|
|
493
|
+
Without it, a cache lives in the memory of the process that issued it, and the routing has to
|
|
494
|
+
send every request that uses it there. A restarted process has lost its caches, and its
|
|
495
|
+
clients get `cache-id-unknown` and start new ones. The load a client brings stays on one
|
|
496
|
+
process. Its cache-ids are `<code>.<start>.<uuid>`, where `<start>` is when the process
|
|
497
|
+
started (base 36), so a restarted process tells an id it issued before the restart ("lost to
|
|
498
|
+
a server restart") from one it never issued.
|
|
499
|
+
|
|
500
|
+
With it, any process can answer any cache-id. **Caches are not carried over a server
|
|
501
|
+
restart**: a client can't be sure a cache is intact across operations it tried while the
|
|
502
|
+
server was down, and the server can't either. So **whenever the master starts** (a master
|
|
503
|
+
restarted by the launcher, a new launcher run, or a master started by hand behind nginx),
|
|
504
|
+
every live cache becomes a tombstone, and a request that uses one gets `cache-id-unknown`
|
|
505
|
+
with "lost to a server restart"; the client starts a new cache. A tx-worker that restarts
|
|
506
|
+
while the master carries on keeps every cache. Workers that survive a master restart issue
|
|
507
|
+
new caches in its new generation. An id that was never issued is reported as never issued.
|
|
508
|
+
|
|
509
|
+
The launcher sends a request with a cache-id to the least-loaded tx process,
|
|
510
|
+
like any other. The cost is a read of the file on each use of a cache: a request reads the
|
|
511
|
+
list of what the cache holds, and only loads the resources it uses. Writes are short
|
|
512
|
+
transactions in WAL mode, so readers don't wait for them. `last_used` is written at most every
|
|
513
|
+
30 seconds per cache. The idle sweep (`modules.tx.cacheTimeout`) runs in every process, and
|
|
514
|
+
running it twice does no harm. Cache-ids are `<code>.<generation>.<random>`: two 22-character
|
|
515
|
+
tokens with no `.` in them, 62 characters with a 16-character code, inside a FHIR id's 64.
|
|
516
|
+
Clients should treat them as opaque. Writes check the generation inside their transaction, so
|
|
517
|
+
a request in progress across a master start cannot recreate an old cache, and a new cache is
|
|
518
|
+
given its id inside the transaction that creates it. A lazy read that comes after a master
|
|
519
|
+
start also gets `404` and `cache-id-unknown`.
|
|
520
|
+
|
|
521
|
+
If another request replaces a resource after its index was read but before its content is
|
|
522
|
+
loaded, the request returns `409` (`CACHE_ID_CHANGED_DURING_REQUEST`, tx-issue-type
|
|
523
|
+
`cannot-determine`). If the cache ends meanwhile (closed, expired, dropped), it returns `404`
|
|
524
|
+
`cache-id-unknown` (`CACHE_ID_GONE_DURING_REQUEST`), and the client starts a new cache. After
|
|
525
|
+
a `409`, retry the whole request to read the new contents together. This prevents using new content under an old
|
|
526
|
+
expansion-cache key; unchanged resources and additions of unrelated resources still work.
|
|
527
|
+
|
|
528
|
+
`cacheAffinity: true` keeps the prefix routing on top. The expansion cache is still per
|
|
529
|
+
process, so a client that stays on one process finds its expansions there; without affinity,
|
|
530
|
+
each process does the first expansion again. The file must be on a local disk.
|
|
531
|
+
|
|
532
|
+
### Shared closure
|
|
533
|
+
|
|
534
|
+
Without it, the master answers every `$closure` request, and its per-table lock keeps the
|
|
535
|
+
calls on one table in order.
|
|
536
|
+
|
|
537
|
+
With it, any tx process answers `$closure`, against the same `databases/closure.db`, and the
|
|
538
|
+
launcher sends `$closure` to the least-loaded tx process. Within a process the per-table lock
|
|
539
|
+
still queues the calls on a table. Across processes the writes are **checked, not locked**. A
|
|
540
|
+
call works out its add against the table as it read it (its version *n*). It writes in one
|
|
541
|
+
`BEGIN IMMEDIATE` transaction that first checks the table is still at version *n*, and was
|
|
542
|
+
not reset meanwhile. If it isn't, nothing is written: the call reads the table again and works
|
|
543
|
+
the add out again, with the code lookups and subsumption tests it already did remembered. It
|
|
544
|
+
waits a few milliseconds, with jitter, between tries. After the first try and `writeRetries`
|
|
545
|
+
retries (`writeRetries` + 1 tries in all) it answers
|
|
546
|
+
`429` (`CLOSURE_TABLE_BUSY`), as a full queue does. Creating a table checks for one of the
|
|
547
|
+
same name, and for the table limit, in the same transaction as the insert. A resync reports
|
|
548
|
+
only entries up to the version it read. The master alone drops stale tables
|
|
549
|
+
(`retentionDays`).
|
|
550
|
+
|
|
551
|
+
Shared table IDs are never reused after reset or disposal, including across restarts; an
|
|
552
|
+
existing database is upgraded in place. An add that races a reset retries internally with
|
|
553
|
+
the new configuration and fresh lookup state. A resync interrupted by reset returns `409`
|
|
554
|
+
(`CLOSURE_TABLE_CHANGED`); retry against the new table, whose version starts at zero. If the
|
|
555
|
+
table was disposed without being recreated, the existing unknown-table `404` applies.
|
|
556
|
+
|
|
557
|
+
This spreads the subsumption work of `$closure` over the processes. When many processes add
|
|
558
|
+
to the same table at once, some adds are worked out more than once.
|
|
559
|
+
|
|
560
|
+
### What changes, by combination
|
|
561
|
+
|
|
562
|
+
| `sharedCache` | `sharedClosure` | A request with a cache-id goes to | `$closure` goes to | After a worker restart | After the master starts |
|
|
563
|
+
|---|---|---|---|---|---|
|
|
564
|
+
| off | off | the process that issued it (if it is down, the launcher answers `cache-id-unknown`, lost to a restart) | the master | its caches are lost | its caches are lost |
|
|
565
|
+
| on | off | the least-loaded tx process (the issuer, with `cacheAffinity`) | the master | every cache is kept | every cache is lost |
|
|
566
|
+
| off | on | the process that issued it | the least-loaded tx process | its caches are lost | its caches are lost |
|
|
567
|
+
| on | on | the least-loaded tx process (the issuer, with `cacheAffinity`) | the least-loaded tx process | every cache is kept | every cache is lost |
|
|
568
|
+
|
|
569
|
+
The syndication feed, the `/info` pages and everything outside the tx endpoints go to the
|
|
570
|
+
master in every combination. For nginx, see
|
|
571
|
+
[nginx.md](nginx.md#with-the-shared-cache). Under the launcher, a lost cache is reported as
|
|
572
|
+
"lost to a server restart" in every combination. Behind nginx.md's configuration, without the
|
|
573
|
+
shared cache, that is so only once the restarted process answers: while a worker is down its
|
|
574
|
+
cache-ids go to its `backup` worker, which answers `cache-id-unknown` saying the id is another
|
|
575
|
+
instance's (`CACHE_ID_OTHER_INSTANCE`), and while the master is down nginx answers its
|
|
576
|
+
cache-ids (`m0.`) with a `502`, since `fs_master` has no backup. Either way the client starts a
|
|
577
|
+
new cache. Closure tables are kept across restarts, as on a single server.
|
|
578
|
+
|
|
579
|
+
## Memory
|
|
580
|
+
|
|
581
|
+
With a role, the memory guard measures the whole cgroup, not the process's RSS, since the kernel's limit is on all the
|
|
582
|
+
processes in it together: the cgroup's private memory (`memory.stat` `anon`; cgroup v1
|
|
583
|
+
`total_rss`). The page cache (the terminology and SQLite files) isn't counted: the kernel drops
|
|
584
|
+
it when it needs the room.
|
|
585
|
+
|
|
586
|
+
* Above `server.memory.rejectFraction` (85%) of the limit, **every** process refuses new
|
|
587
|
+
requests with 503 and `Retry-After`.
|
|
588
|
+
* Above `abortFraction` (92%), **every** process aborts the operations it has running (422
|
|
589
|
+
too-costly); a process with none running carries on. Not only the largest: a process's size
|
|
590
|
+
doesn't say whether it is busy, and a large idle master would otherwise leave a worker's
|
|
591
|
+
runaway `$expand` to the kernel's OOM killer, which would likely kill the master.
|
|
592
|
+
|
|
593
|
+
While refusing, each process looks for memory to get back, at once and then every 5 s (backing
|
|
594
|
+
off to a minute while it gets back little): a forced last-resort GC, then, on Linux with glibc
|
|
595
|
+
and the optional `koffi` dependency, `malloc_trim`. A last-resort GC also gives back the pages
|
|
596
|
+
an ordinary GC frees but V8 keeps for reuse: they count as private memory, and no heap figure
|
|
597
|
+
shows them, so it runs whatever the size of the heap. (Where the runtime has only a plain
|
|
598
|
+
`gc()`, before V8 12, the GC is skipped when the JS heap is too small for it to help.)
|
|
599
|
+
Between those looks, before it refuses a request, it calls `malloc_trim` alone, at most once a
|
|
600
|
+
second and without backing off, and measures again: what a burst used is freed when the burst
|
|
601
|
+
ends but stays resident, and without this a quiet server could sit just over the line until
|
|
602
|
+
the next look, a minute away. (With nothing to give back, a trim takes well under a
|
|
603
|
+
millisecond.) The trim matters: an operation that opens thousands of SQLite connections (a 20,000-entry LOINC
|
|
604
|
+
`$batch-validate-code`, which opens one per entry) leaves gigabytes that the C library's
|
|
605
|
+
allocator keeps resident after the connections close, and no GC touches them. After such a
|
|
606
|
+
batch was aborted, a worker held 8.0 GB, 4.7 GB of it free in malloc's arenas; `malloc_trim`
|
|
607
|
+
took it to 3.9 GB in half a second, and without it the server refused everything until it was
|
|
608
|
+
restarted. Setting `MALLOC_ARENA_MAX=2` in the environment was tried instead and did not give
|
|
609
|
+
the memory back (the worker was still refusing 90 s after the abort); `malloc_trim` did (back
|
|
610
|
+
in 6 s), so the trim is a call through `koffi`, not an environment setting.
|
|
611
|
+
This is on with a role (`server.memory.trimAllocator`, true unless set false, for diagnosis).
|
|
612
|
+
With no role it is off unless set true, so a single server only forces the GC, as before: the
|
|
613
|
+
trim is synchronous native code, not yet run on a single server's 20 GB process. Where it is
|
|
614
|
+
not available (macOS, Windows, musl, no `koffi`), only the GC runs. With a role, each process
|
|
615
|
+
also trims once after loading the library, before it takes requests: the load leaves 0.6-0.7
|
|
616
|
+
GB free but resident, and N+1 processes share the one limit.
|
|
617
|
+
|
|
618
|
+
If a tx-worker stays over the refusal threshold anyway, for `server.cluster.recycleSeconds`
|
|
619
|
+
(30) with no request at work on it, the launcher restarts it: whatever it holds, nothing
|
|
620
|
+
running will free it. With the cgroup measured, only a worker that has grown, since it was
|
|
621
|
+
ready, by more than the cgroup is over the threshold is restarted (a smaller one would come
|
|
622
|
+
back at the same size, and the gate would stay shut); the largest first, one at a time. Without,
|
|
623
|
+
a worker over the threshold on its own, and again only if it has grown since it was ready by
|
|
624
|
+
more than it is over: one already over when it became ready would come back over, and be
|
|
625
|
+
restarted every 45 s or so for good. The launcher logs a `WARNING` for such a worker, once,
|
|
626
|
+
and leaves it. The launcher reads each worker's `RssAnon` from `/proc`,
|
|
627
|
+
so this is Linux only. The master is never restarted for this: it holds what only it has. The
|
|
628
|
+
launcher logs each restart and why, and its limits line says whether this is on.
|
|
629
|
+
|
|
630
|
+
This needs the limit on the processes' own cgroup: `MemoryMax` on the unit that runs
|
|
631
|
+
`cluster.js`, or a container limit. If the limit is only on an enclosing slice, or there is
|
|
632
|
+
none, each process measures its own RSS against the limit, as a single server does. If you
|
|
633
|
+
start the processes as separate systemd units, give each its own `MemoryMax`: each then guards
|
|
634
|
+
its own.
|
|
635
|
+
|
|
636
|
+
Each process is a whole terminology server: it loads the library, so N workers and a master
|
|
637
|
+
use roughly N+1 times the memory of one server (about 2.8 to 3.8 GB each with the full library).
|
|
638
|
+
Size the limit for that. The guard counts them all against the one limit, so N+1 idle processes
|
|
639
|
+
sitting at the refusal threshold refuse every request. In a test with a 16 GiB limit and
|
|
640
|
+
`rejectFraction` set to 0.75 (a test setting; the default is 0.85), so a threshold of 12 GB,
|
|
641
|
+
1 master and 3 workers idle at 12.05 GB refused everything for minutes after a burst, while a
|
|
642
|
+
single server on the same data stayed near 3.5 GB and never refused. With the defaults on a
|
|
643
|
+
32 GB limit the threshold is 27.2 GB (refusing) and 29.4 GB (aborting). So the launcher
|
|
644
|
+
checks: once the master is loaded, it takes the master's memory as each worker's, and if
|
|
645
|
+
`server.cluster.workers` is not given, it starts only as many workers as fit under the
|
|
646
|
+
threshold with `memoryHeadroomGB` (2) to spare, at least one. If it is given, they all start, with
|
|
647
|
+
a `WARNING` line. If even one worker doesn't fit, that is a `WARNING` too, with the numbers. Once every worker is ready it logs what they all use idle, with a `WARNING` if
|
|
648
|
+
less than the headroom is left.
|
|
649
|
+
|
|
650
|
+
## How many workers
|
|
651
|
+
|
|
652
|
+
With `server.cluster.workers` not given, the launcher derives it from the CPUs and logs the
|
|
653
|
+
inputs and the decision in one line as the master is loaded:
|
|
654
|
+
|
|
655
|
+
```
|
|
656
|
+
workers 4: 4 physical cores (8 vCPU, SMT 2), quota none, memory fits 11 → classic rule min(4, round(0.6×8))
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
* **vCPU**: `os.availableParallelism()`, the CPUs this process may run on.
|
|
660
|
+
* **physical cores**: the distinct cores among those CPUs, from
|
|
661
|
+
`/sys/devices/system/cpu/cpuN/topology/core_cpus_list` (`thread_siblings_list` on older
|
|
662
|
+
kernels). Where that can't be read (macOS, Windows, some hypervisors), the vCPUs.
|
|
663
|
+
`server.cluster.cores` overrides it.
|
|
664
|
+
* **quota**: a cgroup v2 `cpu.max` on the launcher's cgroup or one enclosing it caps the cores
|
|
665
|
+
at `ceil(quota)`.
|
|
666
|
+
|
|
667
|
+
| Engine | Rule | 4 vCPU, 4 cores | 8 vCPU, 4 cores (SMT) | 8 vCPU, 8 cores | 16 vCPU, 8 cores (SMT) |
|
|
668
|
+
|---|---|---|---|---|---|
|
|
669
|
+
| classic | `min(cores, round(0.6 × vCPU))` | 2 | 4 | 5 | 8 |
|
|
670
|
+
|
|
671
|
+
Then at least 1, then lowered if the processes won't fit in memory (above). The master serves tx
|
|
672
|
+
too, so there is one more tx process than workers. Physical cores, not vCPUs: a second
|
|
673
|
+
hyperthread adds little to a process that is CPU-bound, and more processes than that only take
|
|
674
|
+
turns on the same cores while each holds its own copy of the library in memory.
|
|
675
|
+
|
|
676
|
+
What it rests on (classic engine; surge goodput in req/s, the same load generator and library;
|
|
677
|
+
`w3` = 3 workers):
|
|
678
|
+
|
|
679
|
+
| Machine | Measured | Rule |
|
|
680
|
+
|---|---|---|
|
|
681
|
+
| local, 4 vCPU = 4 cores | w2 1,678; w3 1,539 (cheap probe 918 ms, timeouts) | 2 |
|
|
682
|
+
| local, 8 vCPU = 4 cores SMT | w3 1,592; w4 1,630; w5 1,615 | 4 |
|
|
683
|
+
| GCP n2-standard-8, 8 vCPU = 4 cores SMT | about 1,550, flat from w3 to w6 | 4 |
|
|
684
|
+
| GCP n2-standard-16, 1 thread per core: 8 vCPU = 8 cores | about 2,430, flat from w4 to w6 | 5 |
|
|
685
|
+
|
|
686
|
+
The classic engine is flat past its rule's number: a process uses about 1.4-1.9 cores under load
|
|
687
|
+
(its libuv pool), so more add memory, not throughput. An engine whose processes use the CPU
|
|
688
|
+
differently adds its own rule (`registerWorkerRule` in `library/worker-count.js`). Machines of
|
|
689
|
+
16 cores and more were not measured: the rule there is extrapolation, and `server.cluster.workers`
|
|
690
|
+
is the way to choose.
|
|
691
|
+
|
|
692
|
+
## Limits of this arrangement
|
|
693
|
+
|
|
694
|
+
* **Caches are per process** (unless [`sharedCache`](#options) is on). A `$cache-control`
|
|
695
|
+
cache lives in the process that issued it; a restarted process loses its caches, and
|
|
696
|
+
clients start new ones, as after any restart. That includes a tx-worker the launcher restarts
|
|
697
|
+
because it stayed over the memory guard's refusal threshold (`recycleSeconds`): its
|
|
698
|
+
`$cache-control` caches, its expansion cache and the code systems it had loaded are gone,
|
|
699
|
+
and its first requests after the restart are slower.
|
|
700
|
+
* **Without the launcher, statistics are per process.** The workers' graphs and request
|
|
701
|
+
counts reach the master over the launcher's IPC channels. With processes started by hand
|
|
702
|
+
behind nginx there are none: `/` and `/dashboard` on the master show only the master, and
|
|
703
|
+
the workers' requests are not in its statistics database or daily report. (Running
|
|
704
|
+
`cluster.js` behind nginx, as one upstream, keeps both.)
|
|
705
|
+
* **Everything the master runs is a single point.** While the master restarts, `$closure`
|
|
706
|
+
(unless [`sharedClosure`](#options) is on), the non-tx modules and the dashboard answer 503
|
|
707
|
+
through the launcher; tx requests carry on.
|