@mason-data/cli 0.7.0 → 0.9.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/README.md +101 -73
- package/dist/src/api/error.js +3 -0
- package/dist/src/api/postgres/catalog-worker.js +49 -2
- package/dist/src/api/postgres/catalog.js +368 -25
- package/dist/src/api/postgres/encode.js +3 -1
- package/dist/src/api/postgres/explain.js +3 -2
- package/dist/src/api/postgres/listener.js +11 -3
- package/dist/src/api/postgres/protocol.js +39 -23
- package/dist/src/api/postgres/proxy.js +283 -0
- package/dist/src/api/postgres/schema.js +6 -0
- package/dist/src/api/postgres/server.js +1617 -245
- package/dist/src/api/postgres/translate.js +127 -10
- package/dist/src/api/request-log.js +18 -5
- package/dist/src/cdc/poll.js +5 -5
- package/dist/src/cli/background.js +47 -25
- package/dist/src/cli/bridge-stdin.js +101 -0
- package/dist/src/cli/cancel.js +75 -0
- package/dist/src/cli/command.js +170 -11
- package/dist/src/cli/commands.js +804 -209
- package/dist/src/cli/control.js +59 -0
- package/dist/src/cli/credentials.js +182 -0
- package/dist/src/cli/demo.js +20 -11
- package/dist/src/cli/deploy.js +574 -0
- package/dist/src/cli/dev.js +491 -130
- package/dist/src/cli/doctor.js +131 -38
- package/dist/src/cli/generated-files.js +143 -0
- package/dist/src/cli/guides.js +175 -36
- package/dist/src/cli/hosted.js +850 -0
- package/dist/src/cli/init.js +162 -65
- package/dist/src/cli/login.js +289 -0
- package/dist/src/cli/machine.js +13 -12
- package/dist/src/cli/main.js +11 -5
- package/dist/src/cli/mason.cjs +17 -0
- package/dist/src/cli/mcp.js +31 -22
- package/dist/src/cli/metrics.js +79 -0
- package/dist/src/cli/output-gate.js +176 -0
- package/dist/src/cli/page-share.js +96 -0
- package/dist/src/cli/pg-tls.js +14 -4
- package/dist/src/cli/pg-url.js +4 -0
- package/dist/src/cli/pin.js +19 -14
- package/dist/src/cli/pool-size.cjs +85 -0
- package/dist/src/cli/ports.js +2 -0
- package/dist/src/cli/preflight.js +138 -46
- package/dist/src/cli/project.js +142 -4
- package/dist/src/cli/readiness.js +76 -6
- package/dist/src/cli/registry.js +65 -38
- package/dist/src/cli/replica-hosted.js +665 -0
- package/dist/src/cli/replica.js +76 -0
- package/dist/src/cli/replication-checks.js +7 -5
- package/dist/src/cli/replication-mode.js +105 -26
- package/dist/src/cli/request-filter.js +134 -0
- package/dist/src/cli/roles.js +585 -0
- package/dist/src/cli/run-server.js +637 -48
- package/dist/src/cli/runs.js +1 -1
- package/dist/src/cli/security.js +67 -18
- package/dist/src/cli/serve.js +204 -46
- package/dist/src/cli/share-run.js +329 -0
- package/dist/src/cli/share.js +286 -0
- package/dist/src/cli/source-edit.js +75 -17
- package/dist/src/cli/source-role.js +344 -0
- package/dist/src/cli/sources.js +306 -47
- package/dist/src/cli/style.js +2 -1
- package/dist/src/cli/supervise.js +27 -24
- package/dist/src/cli/sync.js +55 -25
- package/dist/src/cli/token-hosted.js +337 -0
- package/dist/src/cli/tunnel.js +200 -0
- package/dist/src/cli/ui-ssh.js +460 -0
- package/dist/src/cli/ui.js +124 -17
- package/dist/src/cli/usage.js +128 -0
- package/dist/src/cli/user-hosted.js +299 -0
- package/dist/src/cli/user.js +573 -65
- package/dist/src/cli/version.js +9 -7
- package/dist/src/demo/seed.js +29 -0
- package/dist/src/duck/analyze.js +36 -20
- package/dist/src/duck/cancel.js +10 -1
- package/dist/src/duck/collation.js +266 -0
- package/dist/src/duck/constant-key.js +94 -0
- package/dist/src/duck/distinct-key.js +283 -26
- package/dist/src/duck/events.js +165 -0
- package/dist/src/duck/executor-worker.js +20 -11
- package/dist/src/duck/executor.js +47 -33
- package/dist/src/duck/explain.js +60 -46
- package/dist/src/duck/extensions.js +160 -0
- package/dist/src/duck/functions.js +461 -63
- package/dist/src/duck/gaps.js +48 -21
- package/dist/src/duck/generated-plan.js +1070 -0
- package/dist/src/duck/generated.js +906 -0
- package/dist/src/duck/grants.js +176 -0
- package/dist/src/duck/held-types.js +64 -0
- package/dist/src/duck/history.js +127 -0
- package/dist/src/duck/jsonb-order.js +9 -12
- package/dist/src/duck/kept-route.js +6 -5
- package/dist/src/duck/kept.js +15 -13
- package/dist/src/duck/late.js +1469 -40
- package/dist/src/duck/libc-bound.js +341 -0
- package/dist/src/duck/libc-order.js +892 -126
- package/dist/src/duck/pg-eval-worker.js +7 -1
- package/dist/src/duck/pg-eval.js +6 -4
- package/dist/src/duck/pgtree.js +774 -23
- package/dist/src/duck/plan.js +369 -47
- package/dist/src/duck/publication.js +20 -6
- package/dist/src/duck/requests.js +269 -93
- package/dist/src/duck/resolved.js +117 -2
- package/dist/src/duck/rewrite.js +4584 -580
- package/dist/src/duck/row-filter.js +655 -0
- package/dist/src/duck/statement.js +19 -0
- package/dist/src/duck/store.js +439 -177
- package/dist/src/duck/stream.js +19 -10
- package/dist/src/duck/sync.js +1411 -368
- package/dist/src/duck/token.js +4 -1
- package/dist/src/duck/types.js +69 -12
- package/dist/src/duck/usage.js +326 -0
- package/dist/src/duck/users.js +69 -15
- package/dist/src/duck/verified.js +161 -1
- package/dist/src/duck/views.js +5 -5
- package/dist/src/lib/database-name.js +62 -0
- package/dist/src/lib/database-url.js +55 -10
- package/dist/src/lib/dotenv.js +17 -4
- package/dist/src/lib/instant.js +50 -0
- package/dist/src/lib/log-buffer.js +77 -0
- package/dist/src/lib/log-read.js +196 -0
- package/dist/src/lib/pg-numeric.js +390 -49
- package/dist/src/lib/project-version.js +23 -23
- package/dist/src/lib/secret-mask.js +142 -0
- package/dist/src/lib/table-sync.js +282 -0
- package/dist/src/web/app.css +706 -66
- package/dist/src/web/app.js +331 -409
- package/dist/src/web/charts.js +229 -0
- package/dist/src/web/index.html +82 -18
- package/dist/src/web/pages/queries.js +192 -0
- package/dist/src/web/pages/status.js +611 -0
- package/dist/src/web/pages/syncs.js +155 -0
- package/dist/src/web/pages/tables.js +439 -0
- package/dist/src/web/pages/usage.js +397 -0
- package/dist/src/web/pages/users.js +219 -0
- package/dist/src/web/shared.js +584 -0
- package/dist/src/web/vendor/README.md +25 -0
- package/dist/src/web/vendor/geist-1.7.2/Geist-Variable.woff2 +0 -0
- package/dist/src/web/vendor/geist-1.7.2/GeistMono-Variable.woff2 +0 -0
- package/dist/src/web/vendor/geist-1.7.2/LICENSE.txt +92 -0
- package/dist/src/web/vendor/lucide-1.56.0/LICENSE.txt +43 -0
- package/dist/src/web/vendor/lucide-1.56.0/chart-column.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/layout-dashboard.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/logs.mjs +34 -0
- package/dist/src/web/vendor/lucide-1.56.0/menu.mjs +28 -0
- package/dist/src/web/vendor/lucide-1.56.0/square-terminal.mjs +28 -0
- package/dist/src/web/vendor/lucide-1.56.0/table.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/users.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/x.mjs +27 -0
- package/dist/src/web/vendor/uplot-1.6.32/LICENSE.txt +21 -0
- package/dist/src/web/vendor/uplot-1.6.32/uPlot.esm.js +6140 -0
- package/dist/src/web/vendor/uplot-1.6.32/uPlot.min.css +1 -0
- package/dist/src/web/vendor/zilla-slab-5.3.0/LICENSE.txt +93 -0
- package/dist/src/web/vendor/zilla-slab-5.3.0/zilla-slab-latin-700-normal.woff2 +0 -0
- package/package.json +12 -4
package/dist/src/cli/dev.js
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* a database on another machine only when the project was made with --remote. A database restored to another server
|
|
10
10
|
* is read again (its rows compared, the unchanged kept); another database is refused (mason destroy, then mason start).
|
|
11
11
|
* 2. THE COPY (src/duck/store.ts, src/duck/sync.ts): DuckDB in this process, file-backed in `.mason/duck.db`; each
|
|
12
|
-
* added table copied whole from the database and kept current
|
|
13
|
-
*
|
|
12
|
+
* added table copied whole from the database and kept current as its sync: in the sources file says: by the
|
|
13
|
+
* xmin poll, by logical replication (src/duck/stream.ts), or only when mason sync asks (off).
|
|
14
14
|
* 3. THE COMMAND SERVER (src/cli/run-server.ts) on 127.0.0.1, which `mason tables`, `mason add`, `mason status` …
|
|
15
15
|
* ask with the run's token from the project's config (src/cli/control.ts, src/cli/security.ts).
|
|
16
16
|
* 4. THE POSTGRES LISTENER (src/api/postgres/server.ts): a BI tool or an app connects as to a read replica, as a
|
|
@@ -29,27 +29,41 @@ import * as path from "node:path";
|
|
|
29
29
|
import { choosePgPort, freePort } from "./ports.js";
|
|
30
30
|
import { failed } from "./command.js";
|
|
31
31
|
import { checkSource, connectSource, maskedUrl, readSourceFacts, remoteConfirmation, remoteWarning } from "./preflight.js";
|
|
32
|
-
import { createRunServer } from "./run-server.js";
|
|
33
|
-
import {
|
|
32
|
+
import { createRunServer, listenOnLoopback, metricsFacts, usageFacts } from "./run-server.js";
|
|
33
|
+
import { readGenerated } from "./generated-files.js";
|
|
34
|
+
import { addedTables, effectiveSync, entryName, replicatedEntries, rereadSources, shownPath, syncWritten } from "./sources.js";
|
|
34
35
|
import { newToken } from "./security.js";
|
|
35
|
-
import { liveRun } from "./runs.js";
|
|
36
|
-
import { DEMO_DIR, DUCK_FILE, projectSource, theProject, writeConfig } from "./project.js";
|
|
36
|
+
import { alive, liveRun } from "./runs.js";
|
|
37
|
+
import { DEMO_DIR, DUCK_FILE, projectReadOnlySetting, projectSource, startSettingRefusal, theProject, writeConfig } from "./project.js";
|
|
37
38
|
import { closedWithin, closingOnce, DEMO_STILL_CLOSING, demoChanges, demoChangesRefusal, demoPort, demoSettings, openDemo } from "./demo.js";
|
|
38
39
|
import { postgresApiUrl, readPgTls } from "../api/postgres/listener.js";
|
|
39
|
-
import {
|
|
40
|
+
import { connectDatabase } from "../lib/database-name.js";
|
|
41
|
+
import { certificateFingerprint, elsewhereTip, ensureSelfSigned, listenerText, runListenerSettings, selfSignedPair, SELF_SIGNED_FILES } from "./pg-tls.js";
|
|
42
|
+
import { RunShares } from "./share-run.js";
|
|
43
|
+
import { PageShares } from "./page-share.js";
|
|
44
|
+
import { sweepTunnelFiles } from "./tunnel.js";
|
|
40
45
|
import { startPostgresApi, timeoutMs } from "../api/postgres/server.js";
|
|
41
|
-
import { RequestLedger, REQUESTS_TABLE
|
|
46
|
+
import { RequestLedger, REQUESTS_TABLE } from "../duck/requests.js";
|
|
47
|
+
import { historyDaysSetting } from "../duck/history.js";
|
|
48
|
+
import { EVENTS_TABLE, SyncEvents } from "../duck/events.js";
|
|
49
|
+
import { USAGE_TABLE, UsageSampler } from "../duck/usage.js";
|
|
42
50
|
import { withPlaceholders } from "./pg-url.js";
|
|
51
|
+
import { UI } from "./ui.js";
|
|
43
52
|
import { cliPinProblem, ensurePin } from "./pin.js";
|
|
44
|
-
import { DEFAULT_MEMORY_LIMIT, DuckStore, DUCKDB_VERSION } from "../duck/store.js";
|
|
45
|
-
import { copyChunkSecondsOf, copyParallelOf, Sync, syncModeOf } from "../duck/sync.js";
|
|
53
|
+
import { DEFAULT_MEMORY_LIMIT, DuckStore, DUCKDB_VERSION, machineMemory, memoryProblems, memoryProblemText, memoryWarning } from "../duck/store.js";
|
|
54
|
+
import { copyChunkSecondsOf, copyParallelOf, humanBytes, Sync, syncModeOf } from "../duck/sync.js";
|
|
46
55
|
import { storeJsonPathsOf } from "../duck/kept.js";
|
|
47
|
-
import { readLibcOrder } from "../duck/libc-order.js";
|
|
48
|
-
import { isLibcCollation } from "../duck/
|
|
49
|
-
import {
|
|
50
|
-
import {
|
|
51
|
-
import {
|
|
56
|
+
import { libcOrderNote, readLibcOrder } from "../duck/libc-order.js";
|
|
57
|
+
import { isLibcCollation } from "../duck/collation.js";
|
|
58
|
+
import { pgIdent } from "../cdc/pg-wire.js";
|
|
59
|
+
import { listenerLogins, LiveUsers, readUsers, USERS_FILE } from "../duck/users.js";
|
|
60
|
+
import { exampleRole, LiveRoles, missingAttributesText, noRoleWayOut, readRoles, resolveRoles, rolesDefaultLine, rolesFile, usersMissingAttributes, usersWithNoRole, usersWithoutRole } from "./roles.js";
|
|
61
|
+
import { READY_LINE, readRestarts } from "./supervise.js";
|
|
62
|
+
import { dropOwnReplication, dropStatements, liveReadinessFailure, madeReplication, namedPublication, projectReplication, recordRefusal, replicationReadiness, staleReplication } from "./replication-mode.js";
|
|
52
63
|
import { textEntry } from "../lib/log.js";
|
|
64
|
+
import { LogBuffer } from "../lib/log-buffer.js";
|
|
65
|
+
import { logPath } from "../lib/log-read.js";
|
|
66
|
+
import { BIG_TABLE_ROWS, syncText } from "../lib/table-sync.js";
|
|
53
67
|
const say = (m) => { process.stdout.write(`${m}\n`); };
|
|
54
68
|
/**
|
|
55
69
|
* THE RUN'S STEPS, as lines (an opening block of prose was too much to read, as `mason init`'s had been): a step begun,
|
|
@@ -70,8 +84,8 @@ export function sourceMove(recorded, now) {
|
|
|
70
84
|
if (!recorded)
|
|
71
85
|
return { action: "record" };
|
|
72
86
|
if (recorded.database !== now.database) {
|
|
73
|
-
return { refuse: `this project's
|
|
74
|
-
"Its
|
|
87
|
+
return { refuse: `this project's synced data is of the database ${recorded.database}, and .mason/config.json now names ${now.database}. ` +
|
|
88
|
+
"Its synced rows are not that database's: mason destroy (removes the synced data; your files stay), then mason start syncs it" };
|
|
75
89
|
}
|
|
76
90
|
if (recorded.systemIdentifier !== null && now.systemIdentifier !== null && recorded.systemIdentifier !== now.systemIdentifier)
|
|
77
91
|
return { action: "reread" };
|
|
@@ -104,7 +118,58 @@ async function closeDemo(hosted, closed) {
|
|
|
104
118
|
else if (closed !== null)
|
|
105
119
|
say(` ${closed}`);
|
|
106
120
|
}
|
|
121
|
+
/** how soon a pass that failed (the database away) is tried again, and replication mode's pass when a table waits */
|
|
122
|
+
const PASS_RETRY_MS = 5000;
|
|
123
|
+
/** how long a libc collation's order may take to read from the source (a few seconds of read-only SELECTs): past it the
|
|
124
|
+
* read is given up, and the statements that need it refused by name */
|
|
125
|
+
const LIBC_READ_MS = 120_000;
|
|
126
|
+
/** the tables by their sync, as the start line says them: ` (2 polled, every 5s and 30s; 1 replicated; 1 off)` */
|
|
127
|
+
function syncCounts(tables) {
|
|
128
|
+
const polled = tables.flatMap((t) => t.sync?.mode === "poll" ? [t.sync.every] : []);
|
|
129
|
+
const replicated = tables.filter((t) => t.sync?.mode === "replication").length;
|
|
130
|
+
const off = tables.length - polled.length - replicated;
|
|
131
|
+
const parts = [...(polled.length ? [`${polled.length} polled, every ${[...new Set(polled)].join(" and ")}`] : []), ...(replicated ? [`${replicated} replicated`] : []), ...(off ? [`${off} off`] : [])];
|
|
132
|
+
return parts.length ? ` (${parts.join("; ")})` : "";
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* THE SOURCES FILE'S DEFAULT SYNC, SAID ONCE AT THE START: what every table without its own `sync:` gets — `off` (the
|
|
136
|
+
* default, said as such when the file writes none), the file's `poll <interval>` or `replication` — and how to change it.
|
|
137
|
+
*
|
|
138
|
+
* AN UPGRADE THAT STOPS THE POLLS IS SAID LOUDLY (issue 33's review): a file an older Mason wrote has no `sync:`
|
|
139
|
+
* anywhere, and its tables were polled every 5 s; from this release they are `off`. When the copy already holds
|
|
140
|
+
* `copiedBefore` of its tables and no `sync:` is written at all, the line says how many are not polled, that an older
|
|
141
|
+
* Mason polled them, and the exact line that keeps them current as before — true too of a hand-written file on a
|
|
142
|
+
* project that never polled.
|
|
143
|
+
*/
|
|
144
|
+
export function syncDefaultLine(file, shown, copiedBefore = 0) {
|
|
145
|
+
const d = effectiveSync(file, undefined);
|
|
146
|
+
if (!syncWritten(file) && copiedBefore > 0) {
|
|
147
|
+
// true of a file an older Mason polled and of one written by hand that never was: "not polled", the old way said.
|
|
148
|
+
// That Mason left a big table out of its polls, and the line that keeps the rest as before does not (BIG_TABLE_ROWS)
|
|
149
|
+
return `sync: off (the default): ${copiedBefore === 1 ? "1 table is" : `${copiedBefore} tables are`} not polled (an older Mason polled every table under ${BIG_TABLE_ROWS.toLocaleString("en-US")} rows every 5 s); ` +
|
|
150
|
+
`\`sync: poll 5s\` at the top of ${shown} keeps them current as before, but polls the bigger ones too, each poll a full scan on your database: ` +
|
|
151
|
+
"give those `sync: off` on their entry (mason tables shows the rows), or use `mason sync <table>` when you need it";
|
|
152
|
+
}
|
|
153
|
+
if (d.from === "default")
|
|
154
|
+
return `sync: off (the default): after its first sync, a table is read again only by mason sync <table> — sync: poll 5m at the top of ${shown} polls every table, or on an entry that one`;
|
|
155
|
+
return d.sync.mode === "off" ? `sync: off (${shown}): after its first sync, a table is read again only by mason sync <table>, unless its entry says sync: poll <interval>`
|
|
156
|
+
: d.sync.mode === "replication" ? `sync: replication (${shown}): every table without a sync: of its own is kept current from the database's change log`
|
|
157
|
+
: `sync: ${syncText(d.sync)} (${shown}): every table without a sync: of its own is polled every ${d.sync.every}`;
|
|
158
|
+
}
|
|
159
|
+
/** WHAT REPLICATION MAKES ON THE DATABASE, recorded in the project's config as it is (at the start that replicates a
|
|
160
|
+
* table, and when the run makes the slot later): the names `mason destroy`, and a start whose file replicates nothing,
|
|
161
|
+
* drop — never computed again (src/cli/replication-mode.ts madeReplication) */
|
|
162
|
+
function recordReplication(cfg, repl, home) {
|
|
163
|
+
const recorded = repl.owned ? null : repl.publication;
|
|
164
|
+
if ((cfg.publication ?? null) === recorded && JSON.stringify(cfg.replication ?? null) === JSON.stringify(repl))
|
|
165
|
+
return;
|
|
166
|
+
cfg.publication = recorded;
|
|
167
|
+
cfg.replication = repl;
|
|
168
|
+
writeConfig(home, cfg);
|
|
169
|
+
}
|
|
107
170
|
async function run(o, hosted) {
|
|
171
|
+
// when this run began: the uptime /api/state says (engine.startedAt, a supervisor's restart is a new run)
|
|
172
|
+
const startedAt = Date.now();
|
|
108
173
|
const t = theProject(process.cwd());
|
|
109
174
|
if ("problem" in t) {
|
|
110
175
|
say(`mason start: ${t.problem}`);
|
|
@@ -121,7 +186,7 @@ async function run(o, hosted) {
|
|
|
121
186
|
return 1;
|
|
122
187
|
}
|
|
123
188
|
// A DEMO PROJECT IS POLLED (src/cli/demo.ts): its database has no change log, so replication is refused before anything starts
|
|
124
|
-
const demoRefused = demoChangesRefusal(cfg
|
|
189
|
+
const demoRefused = demoChangesRefusal(cfg);
|
|
125
190
|
if (demoRefused)
|
|
126
191
|
return failed("start", demoRefused, { json: false });
|
|
127
192
|
let demoChangeMs = null;
|
|
@@ -134,16 +199,49 @@ async function run(o, hosted) {
|
|
|
134
199
|
return 1;
|
|
135
200
|
}
|
|
136
201
|
}
|
|
137
|
-
|
|
202
|
+
// mason serve has held the project to its pin already, before it wrote anything (src/cli/serve.ts servePin)
|
|
203
|
+
const pinned = o.serve ? null : cliPinProblem(folder, o.version);
|
|
138
204
|
if (pinned) {
|
|
139
205
|
say(`mason start: ${pinned}`);
|
|
140
206
|
return 1;
|
|
141
207
|
}
|
|
208
|
+
// a setting a start refuses (the parent of a background start has asked the same list: src/cli/project.ts)
|
|
209
|
+
const setting = startSettingRefusal(project, process.env, { serve: o.serve !== undefined });
|
|
210
|
+
if (setting !== null)
|
|
211
|
+
return failed("start", setting, { json: false });
|
|
212
|
+
// THE PROJECT'S FILES DEPLOYED READ-ONLY (MASON_PROJECT_READONLY=1): recorded in the config at every start, and removed
|
|
213
|
+
// at a start without, so a command beside the run — up or down, whatever its own environment — is refused as the run
|
|
214
|
+
// is (src/cli/project.ts projectWriteRefusal reads the record and the environment; a value not 0 or 1 is refused above)
|
|
215
|
+
if (projectReadOnlySetting(process.env) === true) {
|
|
216
|
+
if (cfg.projectReadOnly !== true) {
|
|
217
|
+
cfg.projectReadOnly = true;
|
|
218
|
+
writeConfig(home, cfg);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else if (cfg.projectReadOnly !== undefined) {
|
|
222
|
+
delete cfg.projectReadOnly;
|
|
223
|
+
writeConfig(home, cfg);
|
|
224
|
+
}
|
|
225
|
+
// the history's days (MASON_HISTORY_DAYS, a number of days; the old MASON_REQUEST_LOG_DAYS refused, named), read
|
|
226
|
+
// before anything opens (src/duck/history.ts); every history below keeps them
|
|
227
|
+
const keptDays = historyDaysSetting(process.env);
|
|
228
|
+
if (typeof keptDays !== "number")
|
|
229
|
+
return failed("start", { code: "refused", ...keptDays }, { json: false });
|
|
230
|
+
// MASON_DUCKDB_MEMORY not an amount (src/duck/store.ts memoryProblems), refused before anything opens (mason serve has
|
|
231
|
+
// said it already, with its other settings)
|
|
232
|
+
const memory = memoryProblems(process.env)[0];
|
|
233
|
+
if (memory !== undefined)
|
|
234
|
+
return failed("start", { code: "refused", ...memory }, { json: false });
|
|
142
235
|
// THE SOURCE (src/cli/sources.ts): the database and the tables to copy, from the project's sources file; a server's
|
|
143
236
|
// URL from its environment first (mason serve is configured by it alone). Read here, its failure said below where it
|
|
144
237
|
// always was (after the listener's checks): a demo project's port is needed first
|
|
145
238
|
const readSources = () => projectSource(project, process.env, { shellFirst: o.serve !== undefined });
|
|
146
239
|
const src = readSources();
|
|
240
|
+
// what the config records and the file does not say (an older Mason's replication): refused before the port below is
|
|
241
|
+
// recorded or the pin written, and long before anything on the database is dropped or made
|
|
242
|
+
const unsaid = "message" in src ? null : recordRefusal(cfg, src, process.env, shownPath(project.root, src.file), o.serve ? "mason serve" : "mason start");
|
|
243
|
+
if (unsaid !== null)
|
|
244
|
+
return failed("start", unsaid, { json: false });
|
|
147
245
|
// a demo project's database listens on the port its URL names (the one place it is kept), which no listener of the
|
|
148
246
|
// run's may take: read BEFORE the Postgres API's port is chosen and recorded, so the choice avoids it (a recorded or
|
|
149
247
|
// named port that is the demo's is refused by choosePgPort, nothing recorded), and the command server is kept off it
|
|
@@ -167,18 +265,26 @@ async function run(o, hosted) {
|
|
|
167
265
|
return failed("start", pgTls, { json: false });
|
|
168
266
|
const chosen = new Set([pg.port, ...demoPorts]);
|
|
169
267
|
// a server's project folder is the team's, committed, and may be mounted read-only: mason serve writes nothing there
|
|
170
|
-
const wrotePin = o.serve ? null : ensurePin(folder, o.version);
|
|
268
|
+
const wrotePin = o.serve ? null : ensurePin(folder, o.version, project.root);
|
|
171
269
|
if (wrotePin)
|
|
172
270
|
say(` wrote ${path.relative(project.root, wrotePin)}: version ${o.version}, the Mason this project is written for — commit it`);
|
|
173
271
|
if ("message" in src)
|
|
174
272
|
return failed("start", src, { json: false });
|
|
175
|
-
|
|
273
|
+
// a demo project's file naming sync: replication (its database has no change log), refused before anything opens
|
|
274
|
+
const demoFile = demoChangesRefusal(cfg, src, shownPath(project.root, src.file));
|
|
275
|
+
if (demoFile)
|
|
276
|
+
return failed("start", demoFile, { json: false });
|
|
277
|
+
const logFile = logPath(home, "run");
|
|
176
278
|
const log = fs.createWriteStream(logFile, { flags: "a" });
|
|
177
279
|
// one entry a line: a multi-line message's further lines indented under it (src/lib/log.ts `textEntry`);
|
|
178
280
|
// mason serve gets the same entry, for the container's stdout (src/cli/serve.ts)
|
|
281
|
+
// the last entries are kept in memory too (src/lib/log-buffer.ts), for the command server's /api/logs: the one writer
|
|
282
|
+
// feeds the file, stdout and the buffer, so a mode with no file (mason serve) has the log as well
|
|
283
|
+
const logs = new LogBuffer();
|
|
179
284
|
const logLine = (l) => {
|
|
180
285
|
const entry = textEntry(`${new Date().toISOString()} `, l);
|
|
181
286
|
log.write(entry);
|
|
287
|
+
logs.push(entry);
|
|
182
288
|
o.serve?.log(entry.trimEnd());
|
|
183
289
|
};
|
|
184
290
|
// THE DEMO DATABASE (src/cli/demo.ts), hosted here: opened before it is connected to, as any database is
|
|
@@ -203,7 +309,7 @@ async function run(o, hosted) {
|
|
|
203
309
|
const db = maskedUrl(src.resolved);
|
|
204
310
|
begin(`Connecting to ${db}`);
|
|
205
311
|
const checkSaid = [];
|
|
206
|
-
const checked = await checkSource(src.resolved, { remote: cfg.remote, cmd: "mason start", say: (m) => checkSaid.push(m),
|
|
312
|
+
const checked = await checkSource(src.resolved, { remote: cfg.remote, cmd: "mason start", say: (m) => checkSaid.push(m), name: src.from,
|
|
207
313
|
// the checklist's line says it is connecting; everything else checkSource says is the answer
|
|
208
314
|
progress: () => undefined });
|
|
209
315
|
if ("needsRemote" in checked) {
|
|
@@ -214,7 +320,7 @@ async function run(o, hosted) {
|
|
|
214
320
|
say(` ! ${w}`);
|
|
215
321
|
const config = path.join(home, "config.json");
|
|
216
322
|
return failed("start", remoteConfirmation(source.host, { cmd: "mason start",
|
|
217
|
-
wayOut: `this project was not made with --remote: if you mean to
|
|
323
|
+
wayOut: `this project was not made with --remote: if you mean to sync from that machine, set "remote": true in ${config} and start again`,
|
|
218
324
|
next: [["mason start", `once ${config} says "remote": true`]] }), { json: false });
|
|
219
325
|
}
|
|
220
326
|
if ("exit" in checked) {
|
|
@@ -226,79 +332,79 @@ async function run(o, hosted) {
|
|
|
226
332
|
return checked.exit === 2 ? 1 : checked.exit;
|
|
227
333
|
}
|
|
228
334
|
const { source, warnings } = checked;
|
|
335
|
+
// THE DATABASE A TOOL CONNECTS WITH (src/lib/database-name.ts): never the project's name, which names the slot below
|
|
336
|
+
const database = connectDatabase(src, cfg, process.env);
|
|
229
337
|
done(`Connected to ${db}: Postgres ${checked.facts.versionText}${checked.facts.standby ? " (a read replica)" : ""}, ${checked.facts.tables.length} table(s) this user can read`);
|
|
230
338
|
for (const w of warnings)
|
|
231
339
|
say(` ! ${w}`);
|
|
232
|
-
// HOW THE CHANGES ARE READ (src/cli/replication-mode.ts):
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
// THE PUBLICATION: one the operator named (the
|
|
241
|
-
//
|
|
242
|
-
// is what
|
|
243
|
-
const named =
|
|
340
|
+
// HOW THE CHANGES ARE READ (src/cli/replication-mode.ts): each table as its sync: in the sources file says — the tables
|
|
341
|
+
// whose sync is replication through the project's ONE slot and publication, the others polled with SELECTs beside the
|
|
342
|
+
// stream. A switch copies nothing again (src/duck/sync.ts setTables: one poll catches a table up)
|
|
343
|
+
const shownFile = shownPath(project.root, src.file);
|
|
344
|
+
const replicating = replicatedEntries(src).length > 0;
|
|
345
|
+
// whether this run has held the database to logical replication's checks: at this start when the file replicates a
|
|
346
|
+
// table, else when an edit first sets one to replication (checkReplication below)
|
|
347
|
+
let replicationChecked = replicating;
|
|
348
|
+
// THE PUBLICATION: one the operator named (the file's publication:, else PG_PUBLICATION) is read as it is, never
|
|
349
|
+
// altered (src/duck/publication.ts); else Mason makes its own, carrying the replicated tables alone. What the run
|
|
350
|
+
// before made, as it recorded it, is what goes when no table is replicated (madeReplication)
|
|
351
|
+
const named = namedPublication(src, process.env);
|
|
244
352
|
const repl = projectReplication(cfg.name, src.name, named?.name ?? null);
|
|
245
353
|
const before = madeReplication(cfg, src.name);
|
|
246
|
-
if (
|
|
354
|
+
if (replicating) {
|
|
247
355
|
begin("Checking logical replication");
|
|
248
356
|
const ready = await replicationReadiness(source, repl);
|
|
249
357
|
if ("failure" in ready || ready.refused.length) {
|
|
250
358
|
fail("Checking logical replication");
|
|
251
359
|
for (const c of "failure" in ready ? [{ name: "database", detail: ready.failure }] : ready.refused)
|
|
252
360
|
say(` ${c.name}: ${c.detail}`);
|
|
253
|
-
say(
|
|
361
|
+
say(` (${replicatedEntries(src).map((e) => entryName(e.table)).join(", ")} ${replicatedEntries(src).length === 1 ? "is" : "are"} sync: replication in ${shownFile}; sync: poll 5m reads a table with SELECTs instead, which needs none of this)`);
|
|
254
362
|
return 1;
|
|
255
363
|
}
|
|
256
|
-
done(repl.owned ? `Logical replication: Mason makes publication ${repl.publication} and slot ${repl.slot} on ${source.database} (mason destroy,
|
|
257
|
-
: `Logical replication: publication ${repl.publication} is read as it is, never altered; Mason makes slot ${repl.slot} on ${source.database} (mason destroy
|
|
258
|
-
if (named?.from === "recorded")
|
|
259
|
-
say(` publication ${repl.publication} was named at an earlier start of this copy, and is read again (mason start --replication --publication <name> names another)`);
|
|
364
|
+
done(repl.owned ? `Logical replication for ${replicatedEntries(src).length} table(s): Mason makes publication ${repl.publication} and slot ${repl.slot} on ${source.database} (mason destroy drops them, and so does a sources file in which no table's sync is replication)`
|
|
365
|
+
: `Logical replication for ${replicatedEntries(src).length} table(s): publication ${repl.publication} is read as it is, never altered; Mason makes slot ${repl.slot} on ${source.database} (mason destroy drops the slot alone, and so does a sources file in which no table's sync is replication)`);
|
|
260
366
|
for (const w of ready.warnings)
|
|
261
367
|
say(` ! ${w.name}: ${w.detail}`);
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
const
|
|
265
|
-
if (
|
|
368
|
+
// WHAT THE RECORD NAMES AND THIS RUN NO LONGER READS — a slot under another name (the sources file renamed since),
|
|
369
|
+
// or Mason's own publication once another is named — goes, verified, before the record names the new one
|
|
370
|
+
const stale = staleReplication(cfg.replication ?? null, repl);
|
|
371
|
+
if (stale !== null) {
|
|
266
372
|
try {
|
|
267
|
-
for (const d of await dropOwnReplication(source,
|
|
268
|
-
say(` dropped ${d} from ${source.database}:
|
|
373
|
+
for (const d of await dropOwnReplication(source, stale.drop, null, stale.what))
|
|
374
|
+
say(` dropped ${d} from ${source.database}: ${stale.why}`);
|
|
269
375
|
}
|
|
270
376
|
catch (e) {
|
|
271
|
-
|
|
377
|
+
const by = stale.what === "both" ? dropStatements(stale.drop) : `DROP PUBLICATION IF EXISTS ${stale.drop.publication};`;
|
|
378
|
+
say(`mason start: ${e.message}. Mason made ${stale.what === "both" ? "them" : "it"} and nothing reads ${stale.what === "both" ? "them" : "it"} now: drop ${stale.what === "both" ? "them" : "it"} by hand — ${by}`);
|
|
272
379
|
return 1;
|
|
273
380
|
}
|
|
274
381
|
}
|
|
275
|
-
// recorded with the run, so mason status and mason stop say it, the next start reads the same one, and
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
if ((cfg.publication ?? null) !== recorded || JSON.stringify(cfg.replication ?? null) !== JSON.stringify(repl)) {
|
|
279
|
-
cfg.publication = recorded;
|
|
280
|
-
cfg.replication = repl;
|
|
281
|
-
writeConfig(home, cfg);
|
|
282
|
-
}
|
|
382
|
+
// recorded with the run, so mason status and mason stop say it, the next start reads the same one, and mason
|
|
383
|
+
// destroy drops what was made by its own name
|
|
384
|
+
recordReplication(cfg, repl, home);
|
|
283
385
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
//
|
|
287
|
-
if (
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
catch (e) {
|
|
293
|
-
say(`mason start: ${e.message}. They hold the database's WAL while nobody reads them: drop them by hand — ${dropStatements(before)}`);
|
|
294
|
-
return 1;
|
|
295
|
-
}
|
|
296
|
-
if (!before.owned)
|
|
297
|
-
say(` publication ${before.publication} is its owner's: left as it is`);
|
|
298
|
-
cfg.publication = null;
|
|
299
|
-
cfg.replication = null;
|
|
386
|
+
else if ((cfg.replication ?? null) !== null || cfg.changes === "replication") {
|
|
387
|
+
// THE SLOT GOES WHEN THE FILE SAYS NO TABLE IS REPLICATED: it holds the database's WAL while nobody reads it. A file
|
|
388
|
+
// naming tables and silent on sync: was refused above (recordRefusal)
|
|
389
|
+
if (cfg.changes === "replication")
|
|
390
|
+
say(` an older Mason streamed every table of this project; ${shownFile} says otherwise now, so what it made goes`);
|
|
391
|
+
try {
|
|
392
|
+
for (const d of await dropOwnReplication(source, before))
|
|
393
|
+
say(` dropped ${d} from ${source.database}: no table's sync is replication`);
|
|
300
394
|
}
|
|
301
|
-
|
|
395
|
+
catch (e) {
|
|
396
|
+
say(`mason start: ${e.message}. They hold the database's WAL while nobody reads them: drop them by hand — ${dropStatements(before)}`);
|
|
397
|
+
return 1;
|
|
398
|
+
}
|
|
399
|
+
if (!before.owned)
|
|
400
|
+
say(` publication ${before.publication} is its owner's: left as it is`);
|
|
401
|
+
cfg.publication = null;
|
|
402
|
+
cfg.replication = null;
|
|
403
|
+
writeConfig(home, cfg);
|
|
404
|
+
}
|
|
405
|
+
// the project's mode, as an older start recorded it: the sources file says it per table now
|
|
406
|
+
if (cfg.changes !== undefined) {
|
|
407
|
+
delete cfg.changes;
|
|
302
408
|
writeConfig(home, cfg);
|
|
303
409
|
}
|
|
304
410
|
let facts = checked.facts;
|
|
@@ -308,11 +414,25 @@ async function run(o, hosted) {
|
|
|
308
414
|
return 1;
|
|
309
415
|
}
|
|
310
416
|
// THE TABLES THE SOURCES FILE NAMES, against the database's catalog: every mistake said at once, before anything opens
|
|
311
|
-
const firstTables = addedTables(src, facts.tables, source.database, project.root
|
|
417
|
+
const firstTables = addedTables(src, facts.tables, source.database, project.root);
|
|
312
418
|
if (!Array.isArray(firstTables))
|
|
313
419
|
return failed("start", firstTables, { json: false });
|
|
314
420
|
// what the run copies: the sources file as last read, and what is wrong with it now (an edit the run cannot take)
|
|
315
421
|
let sources = { file: src, tables: firstTables, problem: null };
|
|
422
|
+
// THE ROLES FILE (mason/roles.yml, src/cli/roles.ts): what each role may read, held against the tables the sources
|
|
423
|
+
// file copies before anything opens — a table it does not copy or a column the copy lacks refuses the start by name;
|
|
424
|
+
// the listener reads the file again whenever it changes (LiveRoles), no restart
|
|
425
|
+
const rolesRead = readRoles(folder, project.root);
|
|
426
|
+
if (rolesRead !== null && "message" in rolesRead)
|
|
427
|
+
return failed("start", rolesRead, { json: false });
|
|
428
|
+
if (rolesRead !== null) {
|
|
429
|
+
// a role may name a generated table, whose columns the catalog knows only once it is built (src/duck/generated.ts)
|
|
430
|
+
const generatedFiles = readGenerated(folder, project.root);
|
|
431
|
+
const generatedNames = [...generatedFiles.definitions, ...generatedFiles.refused].map((g) => ({ source: g.name, columns: [], pending: true }));
|
|
432
|
+
const resolved = resolveRoles(rolesRead, [...firstTables, ...generatedNames], project.root);
|
|
433
|
+
if ("message" in resolved)
|
|
434
|
+
return failed("start", resolved, { json: false });
|
|
435
|
+
}
|
|
316
436
|
// how many of a copy's chunks are read at once, each a backend on the database (src/duck/sync.ts COPY_PARALLEL)
|
|
317
437
|
let copyParallel;
|
|
318
438
|
try {
|
|
@@ -340,16 +460,18 @@ async function run(o, hosted) {
|
|
|
340
460
|
say(`mason start: ${e.message}`);
|
|
341
461
|
return 1;
|
|
342
462
|
}
|
|
343
|
-
begin("Opening the
|
|
463
|
+
begin("Opening the synced data");
|
|
344
464
|
const duckFile = path.join(home, DUCK_FILE);
|
|
345
465
|
let store;
|
|
346
466
|
// its memory bounded (src/duck/store.ts DEFAULT_MEMORY_LIMIT): past it a statement spills, or fails alone
|
|
347
467
|
const memoryLimit = process.env.MASON_DUCKDB_MEMORY || undefined;
|
|
468
|
+
// what statements may spill (src/duck/store.ts spillCeiling): past it a statement fails alone, and the disk is kept
|
|
469
|
+
const spillLimit = process.env.MASON_SPILL_LIMIT || undefined;
|
|
348
470
|
try {
|
|
349
|
-
store = await DuckStore.open(duckFile, (l) => logLine(`[
|
|
471
|
+
store = await DuckStore.open(duckFile, (l) => logLine(`[engine] ${l}`), { ...(memoryLimit === undefined ? {} : { memoryLimit }), ...(spillLimit === undefined ? {} : { spillLimit }) });
|
|
350
472
|
}
|
|
351
473
|
catch (e) {
|
|
352
|
-
fail("Opening the
|
|
474
|
+
fail("Opening the synced data");
|
|
353
475
|
say(` ${e.message}`);
|
|
354
476
|
return 1;
|
|
355
477
|
}
|
|
@@ -357,22 +479,58 @@ async function run(o, hosted) {
|
|
|
357
479
|
// supervisor to start it again: every session's statement in flight was told 57P02, and nothing can be answered after
|
|
358
480
|
let crash = null;
|
|
359
481
|
store.onCrash((reason) => {
|
|
360
|
-
logLine(`[
|
|
482
|
+
logLine(`[engine] the engine's thread stopped: ${reason}`);
|
|
361
483
|
if (crash !== null)
|
|
362
484
|
crash(reason);
|
|
363
485
|
else
|
|
364
|
-
say(` ! the
|
|
486
|
+
say(` ! the engine's thread stopped: ${reason}`);
|
|
365
487
|
});
|
|
366
|
-
done(`the
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const
|
|
370
|
-
|
|
488
|
+
done(`the synced data in ${path.relative(project.root, duckFile)} (mason destroy deletes it; engine ${DUCKDB_VERSION}), at most ${(memoryLimit ?? DEFAULT_MEMORY_LIMIT).trim()} of memory (MASON_DUCKDB_MEMORY), spilling at most ${humanBytes(store.spillLimitBytes)} to ${path.relative(project.root, duckFile)}.tmp (MASON_SPILL_LIMIT)`);
|
|
489
|
+
// UNDER THE MEMORY FLOOR the run goes on and warns (src/duck/store.ts memoryWarning, the one text): here at its start,
|
|
490
|
+
// in its log, and in its warnings, which mason status, --json and the web page read for as long as it runs
|
|
491
|
+
const memoryWarned = memoryWarning(machineMemory(), process.env);
|
|
492
|
+
if (memoryWarned !== null) {
|
|
493
|
+
// the log (mason serve's stdout among its writers), and mason start's own terminal
|
|
494
|
+
logLine(`[engine] warning: ${memoryProblemText(memoryWarned)}`);
|
|
495
|
+
if (o.serve === undefined)
|
|
496
|
+
say(` ! ${memoryProblemText(memoryWarned)}`);
|
|
497
|
+
}
|
|
498
|
+
const syncMode = syncModeOf(process.env);
|
|
499
|
+
// THE SYNC'S EVENTS (src/duck/events.ts): its failures and notable events, kept as the request log is, before the sync
|
|
500
|
+
// that records them starts
|
|
501
|
+
let events;
|
|
502
|
+
try {
|
|
503
|
+
events = await SyncEvents.open(store, logLine, { days: keptDays });
|
|
504
|
+
}
|
|
505
|
+
catch (e) {
|
|
506
|
+
fail("Opening the synced data");
|
|
507
|
+
say(` the sync's events (${EVENTS_TABLE}) could not be opened: ${e.message}`);
|
|
508
|
+
await store.close();
|
|
509
|
+
return 1;
|
|
510
|
+
}
|
|
511
|
+
// each table is kept current as its own sync: says (the sources file's); the sync's interval is how soon a failed pass
|
|
512
|
+
// is tried again (and MASON_DUCK_SYNC=recopy's whole copies). The slot and publication are the run's whenever a table
|
|
513
|
+
// is replicated, now or after a mason add --sync replication (the demo database has no change log: none)
|
|
514
|
+
const sync = new Sync(store, source, database, (l) => { logLine(`[sync] ${l}`); if (!o.serve && /^synced |failed/.test(l))
|
|
515
|
+
say(textEntry(" ", l).trimEnd()); }, PASS_RETRY_MS, syncMode, cfg.demo === true ? null : repl, { copyParallel, copyChunkSeconds, json: { enabled: storeJsonPaths }, onEvent: (e) => events.record(e),
|
|
516
|
+
// what the run made on the database, recorded as it is made and dropped: mason destroy drops it by that name
|
|
517
|
+
onReplication: (made) => {
|
|
518
|
+
if (made)
|
|
519
|
+
recordReplication(cfg, repl, home);
|
|
520
|
+
else if ((cfg.replication ?? null) !== null) {
|
|
521
|
+
cfg.publication = null;
|
|
522
|
+
cfg.replication = null;
|
|
523
|
+
writeConfig(home, cfg);
|
|
524
|
+
}
|
|
525
|
+
} });
|
|
371
526
|
await sync.open();
|
|
527
|
+
// the tables this copy already holds, before this start copies anything: an upgrade from a Mason that polled every
|
|
528
|
+
// table says how many are not polled (`syncDefaultLine`)
|
|
529
|
+
const copiedBefore = sync.state().filter((s) => s.phase === "copied" && sources.tables.some((t) => t.source === s.source)).length;
|
|
372
530
|
// a database restored to another server: its transaction ids are not the watermarks', so every table is read again
|
|
373
531
|
if (move.action === "reread") {
|
|
374
532
|
await sync.rereadAll();
|
|
375
|
-
say(` ${source.database} is on another server than the
|
|
533
|
+
say(` ${source.database} is on another server than the data was synced from: reading its tables again`);
|
|
376
534
|
}
|
|
377
535
|
if (move.action !== "same") {
|
|
378
536
|
cfg.sourceServer = { systemIdentifier: facts.systemIdentifier, database: source.database };
|
|
@@ -381,20 +539,22 @@ async function run(o, hosted) {
|
|
|
381
539
|
// the views first: setting the tables rebuilds the catalog, which reads them. The sources file read again: a mistake in it
|
|
382
540
|
// keeps the tables read before (a typo never drops a copy), said in the log and by mason status
|
|
383
541
|
const added = () => {
|
|
384
|
-
const r = rereadSources(sources, readSources(), src.resolved, facts.tables, source.database, project.root, {
|
|
542
|
+
const r = rereadSources(sources, readSources(), src.resolved, facts.tables, source.database, project.root, { replicationChecked, slot: cfg.replication?.slot ?? null });
|
|
385
543
|
if (r.said !== null)
|
|
386
544
|
logLine(`[sync] ${r.said}`);
|
|
387
545
|
sources = { file: r.file, tables: r.tables, problem: r.problem };
|
|
388
546
|
sync.setViews(facts.views ?? []);
|
|
389
|
-
|
|
547
|
+
sync.setCollations(facts.collations ?? []);
|
|
548
|
+
return sync.setTables(sources.tables);
|
|
390
549
|
};
|
|
391
550
|
if (pg.said)
|
|
392
551
|
say(` ${pg.said}`);
|
|
393
552
|
const pgAddress = { host: listen.host, port: listen.port };
|
|
394
|
-
const postgresUrl = postgresApiUrl(pgAddress,
|
|
553
|
+
const postgresUrl = postgresApiUrl(pgAddress, database, { tls: pgTls.tls !== null });
|
|
395
554
|
// A LIBC COLLATION'S ORDER IS READ FROM THE DATABASE (src/duck/libc-order.ts): once, a few read-only SELECTs, kept in
|
|
396
555
|
// the copy and read again when the database's collation version moves (an OS upgrade's glibc)
|
|
397
556
|
let libc;
|
|
557
|
+
let textOrder = null;
|
|
398
558
|
const coll = facts.collation;
|
|
399
559
|
if (coll !== undefined && isLibcCollation(coll.provider, coll.collate)) {
|
|
400
560
|
const reading = `Reading how ${source.database} orders text (${coll.collate}${coll.version ? `, version ${coll.version}` : ""})`;
|
|
@@ -413,16 +573,64 @@ async function run(o, hosted) {
|
|
|
413
573
|
});
|
|
414
574
|
libc = r.order;
|
|
415
575
|
if (r.read)
|
|
416
|
-
done(`Read how ${source.database} orders text (${coll.collate}): kept
|
|
576
|
+
done(`Read how ${source.database} orders text (${coll.collate}): kept with the synced data, read again only when its collation version moves`);
|
|
577
|
+
// what the read leaves to say is in the log and in mason status's Text order; an order not followed at the start too
|
|
578
|
+
textOrder = libcOrderNote(r.order);
|
|
579
|
+
if (textOrder !== null)
|
|
580
|
+
(r.order.unread ? say : logLine)(` ! ${textOrder}`);
|
|
417
581
|
}
|
|
418
582
|
catch (e) {
|
|
419
583
|
fail(reading);
|
|
420
584
|
say(` ${e.message}`);
|
|
421
585
|
await sync.stop();
|
|
586
|
+
await events.close();
|
|
422
587
|
await store.close();
|
|
423
588
|
return 1;
|
|
424
589
|
}
|
|
425
590
|
}
|
|
591
|
+
// EVERY OTHER LIBC COLLATION'S ORDER (src/duck/collation.ts), read the same way under its own name: a synced column's
|
|
592
|
+
// at the start, one a statement names first on its first use (the plan waits for it, under a bound). A column's that
|
|
593
|
+
// cannot be read is said, and its statements refused by name (libc-locale-unread) until a read succeeds
|
|
594
|
+
const readNamed = async (sc) => {
|
|
595
|
+
const c = await connectSource(source);
|
|
596
|
+
if ("failure" in c)
|
|
597
|
+
throw new Error(`could not connect to read it: ${c.failure.message}`);
|
|
598
|
+
const read = readLibcOrder(async (sql) => (await c.pg.query(sql)).rows, sc.collate ?? "C", sc.version ?? null, `${pgIdent(sc.schema)}.${pgIdent(sc.name)}`);
|
|
599
|
+
// (a read given up at the bound ends when its connection closes: said, as the race has moved on)
|
|
600
|
+
read.catch((e) => logLine(`[engine] the read of the collation ${sc.name} ended: ${e.message}`));
|
|
601
|
+
let timer;
|
|
602
|
+
const bound = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(`the read took longer than ${LIBC_READ_MS / 1000} s`)), LIBC_READ_MS); });
|
|
603
|
+
try {
|
|
604
|
+
return await Promise.race([read, bound]);
|
|
605
|
+
}
|
|
606
|
+
finally {
|
|
607
|
+
clearTimeout(timer);
|
|
608
|
+
c.pg.close();
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
const libcOrders = new Map();
|
|
612
|
+
const byName = new Map((facts.collations ?? []).map((c) => [`${c.schema}\u0000${c.name}`, c]));
|
|
613
|
+
const columnLibc = new Map();
|
|
614
|
+
for (const t of sources.tables)
|
|
615
|
+
for (const col of t.columns) {
|
|
616
|
+
const sc = col.collation === undefined ? undefined : byName.get(`${col.collation.schema}\u0000${col.collation.name}`);
|
|
617
|
+
if (sc !== undefined && sc.provider === "c" && sc.collate !== null && isLibcCollation("c", sc.collate) && !(coll !== undefined && isLibcCollation(coll.provider, coll.collate) && sc.collate === coll.collate))
|
|
618
|
+
columnLibc.set(sc.collate, sc);
|
|
619
|
+
}
|
|
620
|
+
for (const sc of columnLibc.values()) {
|
|
621
|
+
try {
|
|
622
|
+
const r = await store.libcOrder(sc.collate, sc.version ?? null, () => readNamed(sc));
|
|
623
|
+
libcOrders.set(sc.collate, r.order);
|
|
624
|
+
if (r.read)
|
|
625
|
+
logLine(`[engine] read how ${source.database} orders text under the collation ${sc.name} (${sc.collate}): kept with the synced data`);
|
|
626
|
+
const note = libcOrderNote(r.order);
|
|
627
|
+
if (note !== null)
|
|
628
|
+
logLine(`[engine] ! ${note}`);
|
|
629
|
+
}
|
|
630
|
+
catch (e) {
|
|
631
|
+
say(` ! the order of the collation ${sc.name} (${sc.collate}) a synced column has could not be read: ${e.message}; its sorts are refused until it is`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
426
634
|
begin("Starting the Postgres listener");
|
|
427
635
|
// ONE STATEMENT'S LIMIT (server.ts armTimeout): a runaway query must not hold DuckDB for ever; a session's
|
|
428
636
|
// statement_timeout may only shorten it. Postgres's units (5min, 90s, 300000); 0 for none
|
|
@@ -432,52 +640,74 @@ async function run(o, hosted) {
|
|
|
432
640
|
fail("Starting the Postgres listener");
|
|
433
641
|
say(` MASON_STATEMENT_TIMEOUT=${JSON.stringify(timeoutSetting)} is not a duration: write it as Postgres's statement_timeout is written (5min, 90s, 300000 for ms; 0 for none)`);
|
|
434
642
|
await sync.stop();
|
|
643
|
+
await events.close();
|
|
435
644
|
await store.close();
|
|
436
645
|
return 1;
|
|
437
646
|
}
|
|
438
647
|
// THE REQUEST LOG (src/duck/requests.ts): every statement the listener answers or refuses, kept in the copy's own file
|
|
439
|
-
|
|
440
|
-
|
|
648
|
+
// for MASON_HISTORY_DAYS (read above, before anything opened)
|
|
649
|
+
let requests;
|
|
650
|
+
try {
|
|
651
|
+
requests = await RequestLedger.open(store, logLine, { days: keptDays });
|
|
652
|
+
}
|
|
653
|
+
catch (e) {
|
|
441
654
|
fail("Starting the Postgres listener");
|
|
442
|
-
say(` ${
|
|
655
|
+
say(` the request log (${REQUESTS_TABLE}) could not be opened: ${e.message}`);
|
|
443
656
|
await sync.stop();
|
|
657
|
+
await events.close();
|
|
444
658
|
await store.close();
|
|
445
659
|
return 1;
|
|
446
660
|
}
|
|
447
|
-
|
|
661
|
+
// USAGE OVER TIME (src/duck/usage.ts): a sample a minute, kept as the request log is; it counts the listener's requests
|
|
662
|
+
// as they are recorded, and samples the run's state once the command server's context exists (below)
|
|
663
|
+
let usage;
|
|
448
664
|
try {
|
|
449
|
-
|
|
665
|
+
usage = await UsageSampler.open(store, logLine, { days: keptDays });
|
|
450
666
|
}
|
|
451
667
|
catch (e) {
|
|
452
668
|
fail("Starting the Postgres listener");
|
|
453
|
-
say(` the
|
|
669
|
+
say(` the usage samples (${USAGE_TABLE}) could not be opened: ${e.message}`);
|
|
670
|
+
await requests.close();
|
|
454
671
|
await sync.stop();
|
|
672
|
+
await events.close();
|
|
455
673
|
await store.close();
|
|
456
674
|
return 1;
|
|
457
675
|
}
|
|
458
676
|
let duckListener;
|
|
459
677
|
try {
|
|
460
|
-
duckListener = await store.listener(facts.collation, undefined, libc, () => sync.state(), sync.kept.enabled ? sync.kept : undefined)
|
|
678
|
+
duckListener = await store.listener(facts.collation, undefined, libc, () => [...sync.state(), ...sync.generated.copies()], sync.kept.enabled ? sync.kept : undefined, { ...(facts.collations !== undefined ? { collations: facts.collations } : {}), libcOrders, readLibc: async (sc) => {
|
|
679
|
+
logLine(`[engine] reading how ${source.database} orders text under the collation ${sc.name} (${sc.collate}), which a statement names first`);
|
|
680
|
+
return readNamed(sc);
|
|
681
|
+
} });
|
|
461
682
|
}
|
|
462
683
|
catch (e) {
|
|
463
684
|
fail("Starting the Postgres listener");
|
|
464
685
|
say(` ${e.message}`);
|
|
465
686
|
await requests.close();
|
|
466
687
|
await sync.stop();
|
|
688
|
+
await usage.close();
|
|
689
|
+
await events.close();
|
|
467
690
|
await store.close();
|
|
468
691
|
return 1;
|
|
469
692
|
}
|
|
470
|
-
logLine(`[
|
|
693
|
+
logLine(`[engine] text sorts as the source's collation does: ${JSON.stringify(duckListener.collation)}`);
|
|
471
694
|
// THE USERS (src/duck/users.ts): `mason user` edits the file beside this process; the listener re-reads it at every
|
|
472
695
|
// connection and before every statement of a user's session, so a removal takes effect without a restart
|
|
473
696
|
const users = new LiveUsers(path.join(home, USERS_FILE), cfg.apiKey);
|
|
697
|
+
// THE ROLES (src/cli/roles.ts): the file read again when it or the copied tables change; a session with a role is
|
|
698
|
+
// held to it at every statement, and the catalog is rebuilt with the roles (src/api/postgres/server.ts)
|
|
699
|
+
const liveRoles = new LiveRoles(folder, project.root, () => [...sources.tables, ...sync.generated.named()]);
|
|
474
700
|
let api;
|
|
475
701
|
try {
|
|
476
702
|
api = await startPostgresApi({
|
|
477
|
-
port: pgAddress.port, host: pgAddress.host, tls: pgTls.tls, database
|
|
703
|
+
port: pgAddress.port, host: pgAddress.host, tls: pgTls.tls, database, masonVersion: o.version, product: "mason-data", statementTimeoutMs,
|
|
704
|
+
// behind a TCP proxy (MASON_PG_PROXY_FROM, src/api/postgres/proxy.ts): its PROXY header read from its addresses alone
|
|
705
|
+
...(listen.proxyFrom !== undefined ? { proxyFrom: listen.proxyFrom } : {}),
|
|
478
706
|
...listenerLogins(users, cfg.apiKey),
|
|
707
|
+
roles: () => liveRoles.now(),
|
|
479
708
|
schema: () => sync.schema(),
|
|
480
|
-
|
|
709
|
+
// the one place a request is recorded: the request log's row, and the usage sample's count of it
|
|
710
|
+
record: (row) => { requests.record(row); usage.count(row); },
|
|
481
711
|
log: logLine,
|
|
482
712
|
trace: process.env.MASON_PG_TRACE === "1",
|
|
483
713
|
duck: duckListener,
|
|
@@ -488,47 +718,149 @@ async function run(o, hosted) {
|
|
|
488
718
|
say(` ${e.message}`);
|
|
489
719
|
await requests.close();
|
|
490
720
|
await sync.stop();
|
|
721
|
+
await usage.close();
|
|
722
|
+
await events.close();
|
|
491
723
|
await store.close();
|
|
492
724
|
return 1;
|
|
493
725
|
}
|
|
494
|
-
done(`Postgres listener on ${listenerText(pgTls.listener, api.port)}; ${statementTimeoutMs === 0 ? "statements without a time limit" : `each statement at most ${timeoutSetting.trim()}`} (MASON_STATEMENT_TIMEOUT); ` +
|
|
495
|
-
`every request recorded for ${keptDays} day${keptDays === 1 ? "" : "s"} (
|
|
496
|
-
|
|
726
|
+
done(`Postgres listener on ${listenerText(pgTls.listener, api.port)}; ${listen.proxyFrom !== undefined ? `a PROXY protocol v2 header required from ${listen.proxyFrom.entries.join(", ")} (MASON_PG_PROXY_FROM); ` : ""}${statementTimeoutMs === 0 ? "statements without a time limit" : `each statement at most ${timeoutSetting.trim()}`} (MASON_STATEMENT_TIMEOUT); ` +
|
|
727
|
+
`every request recorded for ${keptDays} day${keptDays === 1 ? "" : "s"} (MASON_HISTORY_DAYS; mason logs --requests)`);
|
|
728
|
+
{
|
|
729
|
+
// a user whose role the file no longer names reads nothing until it is added again (fail closed): said here, by name
|
|
730
|
+
// the row filters checked by the catalog Postgres here (on the tables copied already): a where: Postgres refuses is
|
|
731
|
+
// said now, at its key, before any user meets it
|
|
732
|
+
const roles = await api.checkRoles();
|
|
733
|
+
const list = readUsers(path.join(home, USERS_FILE));
|
|
734
|
+
const orphans = "users" in list ? usersWithoutRole(list.users, new Set(roles.grants.keys())) : [];
|
|
735
|
+
// WHAT A USER ADDED WITHOUT --role READS, SAID ONCE (stage 1b: the file decides it, never the code), and each user
|
|
736
|
+
// the file holds to nothing (no default:) by name, with the way out
|
|
737
|
+
const rolesLine = rolesDefaultLine(roles, shownPath(project.root, rolesFile(folder)));
|
|
738
|
+
say(` ${rolesLine}`);
|
|
739
|
+
logLine(`[postgres] ${rolesLine}`);
|
|
740
|
+
for (const name of "users" in list ? usersWithNoRole(list.users, roles) : []) {
|
|
741
|
+
say(` ! the user ${name} has no role, and ${roles.file ?? ""} names no default: its statements are refused — ${noRoleWayOut(name, roles.file ?? "", exampleRole(roles))}`);
|
|
742
|
+
}
|
|
743
|
+
// a user without a value its role's row filters read: refused at every statement until it is made again with it
|
|
744
|
+
for (const u of "users" in list ? usersMissingAttributes(list.users.filter((x) => x.removedAt === undefined), roles) : []) {
|
|
745
|
+
say(` ! ${missingAttributesText(u)}`);
|
|
746
|
+
}
|
|
747
|
+
for (const u of orphans) {
|
|
748
|
+
say(` ! the user ${u.name} has the role ${u.role}, which ${roles.file ?? "no roles file (mason/roles.yml)"} ${roles.file === null ? "exists to name" : "does not name"}: ` +
|
|
749
|
+
`its statements are refused until the role is in the file, or mason user remove ${u.name} and mason user add ${u.name}${roles.grants.size ? ` --role ${[...roles.grants.keys()][0]}` : ""}`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
// WHAT mason share OPENS (src/cli/share-run.ts): a second socket of this listener, on a run on a laptop alone
|
|
753
|
+
// a tunnel's ssh config a run or a mason share killed with -9 left behind (src/cli/tunnel.ts): removed unless its
|
|
754
|
+
// process lives, so a run stopped with the page's share open leaves nothing at the next start
|
|
755
|
+
if (o.serve === undefined) {
|
|
756
|
+
try {
|
|
757
|
+
sweepTunnelFiles(home, alive);
|
|
758
|
+
}
|
|
759
|
+
catch (e) {
|
|
760
|
+
logLine(`share: the tunnels' files left behind could not be swept: ${e.message}`);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
const shares = o.serve !== undefined ? undefined : new RunShares({
|
|
764
|
+
listener: api, database, log: logLine,
|
|
765
|
+
// the project's own certificate, as --self-signed-tls serves it: made once, and recorded as that flag records it
|
|
766
|
+
certificate: () => {
|
|
767
|
+
try {
|
|
768
|
+
const pair = selfSignedPair(home);
|
|
769
|
+
if (pair.said)
|
|
770
|
+
logLine(`[postgres] ${pair.said} (SHA-256 ${pair.fingerprint})`);
|
|
771
|
+
const recorded = { cert: SELF_SIGNED_FILES.cert, key: SELF_SIGNED_FILES.key, fingerprint: pair.fingerprint };
|
|
772
|
+
if (JSON.stringify(cfg.selfSignedTls ?? null) !== JSON.stringify(recorded)) {
|
|
773
|
+
cfg.selfSignedTls = recorded;
|
|
774
|
+
writeConfig(home, cfg);
|
|
775
|
+
}
|
|
776
|
+
return pair;
|
|
777
|
+
}
|
|
778
|
+
catch (e) {
|
|
779
|
+
return { message: `the self-signed certificate could not be written to ${home}: ${e.message}`, code: "failed", fix: "this folder must be writable: mason doctor checks the machine" };
|
|
780
|
+
}
|
|
781
|
+
},
|
|
782
|
+
});
|
|
783
|
+
const setupPort = o.exactPort === true ? o.port : await freePort(o.port, chosen);
|
|
497
784
|
const token = newToken();
|
|
498
|
-
const
|
|
499
|
-
source, facts: () => facts, warnings,
|
|
500
|
-
slot
|
|
501
|
-
|
|
502
|
-
|
|
785
|
+
const runCtx = {
|
|
786
|
+
source, facts: () => facts, warnings: [...warnings, ...(memoryWarned === null ? [] : [memoryProblemText(memoryWarned)])], textOrder,
|
|
787
|
+
// the slot and publication while a table's sync is replication (the run streams), as the sync says it now
|
|
788
|
+
replication: () => sync.streams() ? repl : null, port: setupPort, token, folder: () => folder, root: project.root, home,
|
|
789
|
+
version: o.version, startedAt,
|
|
790
|
+
// the web page (mason ui): on loopback alone, so mason serve's is reached through mason ui --ssh
|
|
791
|
+
page: true, served: o.serve !== undefined,
|
|
503
792
|
demo: cfg.demo === true,
|
|
504
|
-
users: { file: path.join(home, USERS_FILE), apiKey: cfg.apiKey, database
|
|
505
|
-
|
|
793
|
+
users: { file: path.join(home, USERS_FILE), apiKey: cfg.apiKey, database, postgresUrl },
|
|
794
|
+
// the roles as the listener holds them: their row filters checked by its catalog (a filter Postgres refuses is a problem)
|
|
795
|
+
roles: () => api.roles(),
|
|
796
|
+
...(shares !== undefined ? { shares, pageShares: new PageShares({ shares, home, log: logLine }) } : {}),
|
|
506
797
|
// the sources file is the list of what is added: a change to it is read by the sync at once
|
|
507
|
-
sources: () => sources, sourcesChanged: () => { added(); }, readSource: readSources, runUrl: src.resolved,
|
|
798
|
+
sources: () => sources, sourcesChanged: () => { void added(); }, readSource: readSources, runUrl: src.resolved,
|
|
799
|
+
// a table set to sync: replication while the run is up, in a run that never checked the database for it: the
|
|
800
|
+
// start's own checks, now, and the edit refused when they fail (src/cli/run-server.ts /api/pick)
|
|
801
|
+
checkReplication: async (file) => {
|
|
802
|
+
const tables = replicatedEntries(file).map((e) => entryName(e.table));
|
|
803
|
+
if (replicationChecked || tables.length === 0)
|
|
804
|
+
return null;
|
|
805
|
+
const shown = shownPath(project.root, file.file);
|
|
806
|
+
const demo = demoChangesRefusal(cfg, file, shown);
|
|
807
|
+
if (demo !== null)
|
|
808
|
+
return { ...demo, fix: `mason add ${tables[0]} --sync "poll 5s" polls it`, next: [[`mason add ${tables[0]} --sync "poll 5s"`, "polls it"]] };
|
|
809
|
+
const ready = await replicationReadiness(source, repl);
|
|
810
|
+
const refused = liveReadinessFailure(ready, tables, shown, source.database);
|
|
811
|
+
if (refused !== null)
|
|
812
|
+
return refused;
|
|
813
|
+
if (!("failure" in ready))
|
|
814
|
+
for (const w of ready.warnings)
|
|
815
|
+
say(` ! ${w.name}: ${w.detail}`);
|
|
816
|
+
replicationChecked = true;
|
|
817
|
+
return null;
|
|
818
|
+
},
|
|
508
819
|
log: (m) => say(` ${m}`),
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
820
|
+
logs,
|
|
821
|
+
// the statements running now, and a cancel of one: the listener's own (a CancelRequest's path)
|
|
822
|
+
listener: api,
|
|
823
|
+
duck: { copies: () => sync.state(), file: duckFile, poll: (sources) => sync.pollNow(sources), stream: (o) => sync.streamStatus(o), measured: (table) => sync.measured(table), disk: () => sync.diskStatus(), requests, events, usage,
|
|
824
|
+
restarts: () => readRestarts(home), jsonPaths: () => sync.kept.status(), generated: () => sync.generated.status() },
|
|
825
|
+
};
|
|
826
|
+
const server = createRunServer(runCtx);
|
|
827
|
+
// a sample is /api/state's facts at its moment (src/cli/run-server.ts usageFacts): one computation of each
|
|
828
|
+
usage.start(() => usageFacts(runCtx));
|
|
829
|
+
await listenOnLoopback(server, setupPort);
|
|
513
830
|
const refreshFacts = setInterval(() => {
|
|
514
831
|
void (async () => {
|
|
515
832
|
const c = await connectSource(source);
|
|
516
833
|
if ("failure" in c)
|
|
517
834
|
return; // the commands keep the last catalog; the sync says loudly if the source is away
|
|
835
|
+
const before = facts.collations;
|
|
518
836
|
try {
|
|
519
|
-
facts = await readSourceFacts(c.pg);
|
|
837
|
+
facts = await readSourceFacts(c.pg, undefined, { collations: false });
|
|
520
838
|
}
|
|
521
839
|
finally {
|
|
522
840
|
c.pg.close();
|
|
523
841
|
}
|
|
842
|
+
if (before !== undefined)
|
|
843
|
+
facts.collations = before;
|
|
524
844
|
})().catch((e) => logLine(`catalog refresh failed: ${e.message}`));
|
|
525
845
|
}, 10_000);
|
|
526
|
-
added();
|
|
846
|
+
const tablesSet = added();
|
|
527
847
|
cfg.running = { pid: process.pid, controlUrl: `http://127.0.0.1:${setupPort}`, token, postgresUrl, postgresListener: pgTls.listener };
|
|
528
848
|
cfg.lastRun = new Date().toISOString();
|
|
529
849
|
writeConfig(home, cfg);
|
|
530
|
-
done(`Mason is up: ${sync.state().length} table(s) added, kept current ${syncMode === "
|
|
531
|
-
:
|
|
850
|
+
done(`Mason is up: ${sync.state().length} table(s) added, kept current: ${syncMode === "poll" ? `as each table's sync: says${syncCounts(sources.tables)}` +
|
|
851
|
+
`${sync.streams() ? `; replicated tables streamed as they change (logical replication: slot ${repl.slot}, publication ${repl.publication}${repl.owned ? "" : ", read as it is"})` : ""}`
|
|
852
|
+
: `each table read again whole every ${PASS_RETRY_MS / 1000} s (MASON_DUCK_SYNC=recopy)`}`);
|
|
853
|
+
// THE GENERATED TABLES (src/duck/generated.ts): the folder read and each file held to the catalog, what is refused said
|
|
854
|
+
// here too; a refusal stops nothing, and their SQL is checked again as the tables they read are copied
|
|
855
|
+
await tablesSet;
|
|
856
|
+
await sync.generated.start(() => readGenerated(folder, project.root), api.planner());
|
|
857
|
+
for (const g of sync.generated.status())
|
|
858
|
+
for (const r of g.refusals)
|
|
859
|
+
say(textEntry(" ! ", r.text).trimEnd());
|
|
860
|
+
// THE FILE'S DEFAULT, SAID ONCE (issue 33: nothing Mason does to your database is a hidden default)
|
|
861
|
+
const defaultLine = syncDefaultLine(sources.file, shownPath(project.root, sources.file.file), syncMode === "poll" ? copiedBefore : 0);
|
|
862
|
+
say(` ${defaultLine}`);
|
|
863
|
+
logLine(`[sync] ${defaultLine}`);
|
|
532
864
|
// THE DEMO'S WRITER: a round of changes every MASON_DEMO_CHANGE_MS, begun once the run is up, said in the log
|
|
533
865
|
if (demo !== null && demoChangeMs !== null)
|
|
534
866
|
hosted.stopChanges = demoChanges(demo, demoChangeMs, (l) => logLine(`[demo] ${l}`));
|
|
@@ -536,13 +868,13 @@ async function run(o, hosted) {
|
|
|
536
868
|
// no handler would end the run with the default action, never recording `running: null`
|
|
537
869
|
let stopping = false;
|
|
538
870
|
const stopped = new Promise((resolve) => {
|
|
539
|
-
// a crash of the
|
|
871
|
+
// a crash of the engine's thread: the run exits 1 with its reason in V8's fatal form, which the supervisor reads
|
|
540
872
|
// (src/cli/supervise.ts crashReason), and its config still says it runs, so the supervisor starts it again
|
|
541
873
|
crash = (reason) => {
|
|
542
874
|
if (stopping)
|
|
543
875
|
return;
|
|
544
876
|
stopping = true;
|
|
545
|
-
say(`FATAL ERROR: Mason: the
|
|
877
|
+
say(`FATAL ERROR: Mason: the engine's thread stopped (${reason})`);
|
|
546
878
|
clearInterval(refreshFacts);
|
|
547
879
|
hosted.stopChanges?.();
|
|
548
880
|
server.close();
|
|
@@ -567,6 +899,20 @@ async function run(o, hosted) {
|
|
|
567
899
|
hosted.stopChanges?.();
|
|
568
900
|
server.close();
|
|
569
901
|
void (async () => {
|
|
902
|
+
// before the listener: a share's command is told why, and its tunnel client stopped
|
|
903
|
+
try {
|
|
904
|
+
await shares?.closeAll("Mason is stopping");
|
|
905
|
+
}
|
|
906
|
+
catch (e) {
|
|
907
|
+
say(` closing the share: ${e.message}`);
|
|
908
|
+
}
|
|
909
|
+
// before the listener: a generated table's review plans on its catalog, so it ends first
|
|
910
|
+
try {
|
|
911
|
+
await sync.generated.stop();
|
|
912
|
+
}
|
|
913
|
+
catch (e) {
|
|
914
|
+
say(` stopping the generated tables: ${e.message}`);
|
|
915
|
+
}
|
|
570
916
|
try {
|
|
571
917
|
await api.close();
|
|
572
918
|
}
|
|
@@ -587,20 +933,34 @@ async function run(o, hosted) {
|
|
|
587
933
|
catch (e) {
|
|
588
934
|
syncStop = e;
|
|
589
935
|
}
|
|
936
|
+
// after the sync: a sample in flight ends before the file closes (it reads the sync's state)
|
|
937
|
+
try {
|
|
938
|
+
await usage.close();
|
|
939
|
+
}
|
|
940
|
+
catch (e) {
|
|
941
|
+
say(` closing the usage samples: ${e.message}`);
|
|
942
|
+
}
|
|
943
|
+
// after the sync: its last events are written before the file closes
|
|
944
|
+
try {
|
|
945
|
+
await events.close();
|
|
946
|
+
}
|
|
947
|
+
catch (e) {
|
|
948
|
+
say(` closing the sync's events: ${e.message}`);
|
|
949
|
+
}
|
|
590
950
|
// the demo database after the sync (nothing reads it any more) and before the copy closes
|
|
591
951
|
await closeDemo(hosted, `closed the demo database (it stays in ${path.join(home, DEMO_DIR)}; mason destroy removes it)`);
|
|
592
952
|
if (syncStop !== null)
|
|
593
|
-
say(` closing the
|
|
953
|
+
say(` closing the synced data: ${syncStop.message}`);
|
|
594
954
|
else {
|
|
595
955
|
try {
|
|
596
956
|
await store.close();
|
|
597
|
-
say(` closed the
|
|
957
|
+
say(` closed the synced data (it stays in ${duckFile})`);
|
|
598
958
|
}
|
|
599
959
|
catch (e) {
|
|
600
|
-
say(` closing the
|
|
960
|
+
say(` closing the synced data: ${e.message}`);
|
|
601
961
|
}
|
|
602
962
|
}
|
|
603
|
-
if (
|
|
963
|
+
if ((cfg.replication ?? null) !== null)
|
|
604
964
|
say(` replication slot ${repl.slot} stays on ${source.database}, holding its WAL until the next mason start reads it (mason destroy drops it)` +
|
|
605
965
|
(repl.owned ? "" : `; publication ${repl.publication} is its owner's, left as it is`));
|
|
606
966
|
cfg.running = null;
|
|
@@ -617,16 +977,17 @@ async function run(o, hosted) {
|
|
|
617
977
|
say("");
|
|
618
978
|
say(` Postgres ${withPlaceholders(postgresUrl)} (mason user add <name> makes a user)`);
|
|
619
979
|
// the web page by its command, never its URL: the URL carries the run's token, and this goes to dev.log
|
|
980
|
+
// its words are mason ui's summary (mason --help's), so the two never drift
|
|
620
981
|
if (o.serve === undefined)
|
|
621
|
-
say(
|
|
982
|
+
say(` Web UI mason ui (${UI.summary})`);
|
|
622
983
|
if (demoChangeMs !== null)
|
|
623
|
-
say(` Demo a new order lands every ${demoChangeMs / 1000} s: mason status shows the
|
|
984
|
+
say(` Demo a new order lands every ${demoChangeMs / 1000} s: mason status shows the synced data keeping up`);
|
|
624
985
|
const elsewhere = elsewhereTip(pgTls.listener);
|
|
625
986
|
if (elsewhere)
|
|
626
987
|
say(` ${elsewhere}`);
|
|
627
988
|
say(READY);
|
|
628
989
|
say("");
|
|
629
|
-
o.serve?.ready({ tables: () => sync.state() });
|
|
990
|
+
o.serve?.ready({ tables: () => sync.state(), metrics: () => metricsFacts(runCtx) });
|
|
630
991
|
return await stopped;
|
|
631
992
|
}
|
|
632
993
|
/**
|
|
@@ -640,7 +1001,7 @@ function listenerTls(listen, selfSigned, home, cfg) {
|
|
|
640
1001
|
if (selfSigned) {
|
|
641
1002
|
let made;
|
|
642
1003
|
try {
|
|
643
|
-
made = ensureSelfSigned(home
|
|
1004
|
+
made = ensureSelfSigned(home);
|
|
644
1005
|
}
|
|
645
1006
|
catch (e) {
|
|
646
1007
|
return { problem: `the self-signed certificate could not be written to ${home}: ${e.message}`, code: "failed" };
|
|
@@ -665,5 +1026,5 @@ function listenerTls(listen, selfSigned, home, cfg) {
|
|
|
665
1026
|
* and what tells its supervisor a death after it is a crash to restart (src/cli/supervise.ts, which defines it). */
|
|
666
1027
|
export const READY = READY_LINE;
|
|
667
1028
|
/** The one sentence of how the pieces fit, printed first by `mason start` and at the top of its help. */
|
|
668
|
-
export const PROJECT_LINE = "A Mason project is your app's folder: mason/sources/ names the database and the tables to
|
|
669
|
-
"this machine's
|
|
1029
|
+
export const PROJECT_LINE = "A Mason project is your app's folder: mason/sources/ names the database and the tables to sync, yours to commit; " +
|
|
1030
|
+
"this machine's synced data and Mason's state are in .mason/, rebuilt from that file.";
|