@ultimat3/core 25.1.0 → 25.2.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 +10 -0
- package/package.json +2 -2
- package/src/index.ts +9 -0
- package/src/probe-database-sweep.ts +68 -0
- package/src/probe-database.ts +44 -0
package/README.md
CHANGED
|
@@ -496,6 +496,16 @@ match with `sealAll()`; uniqueness cannot be held across keys.
|
|
|
496
496
|
before trusting an empty `checks`.
|
|
497
497
|
- Anything that opens a socket calls `markListening(server.url.origin)` and releases it on close.
|
|
498
498
|
That is what tells the sealed test network a loopback request is this process, not egress.
|
|
499
|
+
- **A live suite's throwaway database is named by `probeDatabaseName(prefix)`** — test support,
|
|
500
|
+
here because tier 0 is the one tier every package with a live suite can import. Prefix, pid,
|
|
501
|
+
creation second (base 36), eight random hex characters, at most `PROBE_DATABASE_NAME_MAX` (63) bytes, valid unquoted: a
|
|
502
|
+
fixed name let two runs against one server drop each other's database.
|
|
503
|
+
`bun run probe-databases` refuses `create database` on any other name (`X_PROBE_DATABASE_FIXED`).
|
|
504
|
+
A killed run never reaches its `afterAll`, so call `sweepProbeDatabases(executor, PROBE_DB)`
|
|
505
|
+
before the `create`: it drops the same prefix's probe databases whose pid is dead on this host
|
|
506
|
+
AND which no backend is connected to AND whose creation second, carried in the name, is older than
|
|
507
|
+
`PROBE_DATABASE_MIN_AGE_MS` (10 minutes) — a concurrent run on any host is left alone, even between
|
|
508
|
+
its `create` and its first connection.
|
|
499
509
|
|
|
500
510
|
## Metrics: same seam as tracing, one signal over
|
|
501
511
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/core",
|
|
3
|
-
"version": "25.
|
|
3
|
+
"version": "25.2.0",
|
|
4
4
|
"description": "Ultimate's foundation: errors, context, env, config, clock, ids, logging, telemetry, lifecycle",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,6 +37,6 @@
|
|
|
37
37
|
"test": "bun test"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@ultimat3/schema": "25.
|
|
40
|
+
"@ultimat3/schema": "25.2.0"
|
|
41
41
|
}
|
|
42
42
|
}
|
package/src/index.ts
CHANGED
|
@@ -676,6 +676,15 @@ export {
|
|
|
676
676
|
} from './page-meta';
|
|
677
677
|
/** The structural Postgres seam http, auth, action and jobs share without a `@ultimat3/db` edge. */
|
|
678
678
|
export type { PgExecutor } from './pg-executor';
|
|
679
|
+
/** Test support: the one name a live suite gives the database it creates (`probe-databases`). */
|
|
680
|
+
export type { ProbeDatabaseEntropy } from './probe-database';
|
|
681
|
+
export { PROBE_DATABASE_NAME_MAX, probeDatabaseName } from './probe-database';
|
|
682
|
+
export type { ProbeDatabaseSweepOptions } from './probe-database-sweep';
|
|
683
|
+
export {
|
|
684
|
+
PROBE_DATABASE_MIN_AGE_MS,
|
|
685
|
+
probeDatabaseAlive,
|
|
686
|
+
sweepProbeDatabases,
|
|
687
|
+
} from './probe-database-sweep';
|
|
679
688
|
export type { ProcessMetricsOptions, ProcessReading } from './process-metrics';
|
|
680
689
|
export { readProcess, resetProcessMetrics, startProcessMetrics } from './process-metrics';
|
|
681
690
|
export { hasPublicCause, registerPublicCause, resetPublicCauses } from './public-cause';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Test support: drop the probe databases a KILLED run of the same suite left behind. A per-run name
|
|
2
|
+
// (`probe-database.ts`) is never reused, so nothing else would ever drop it — the fixed name it
|
|
3
|
+
// replaced was dropped by the next run. Only a database whose run is provably over goes: its pid
|
|
4
|
+
// dead on this host, no backend connected, AND older than `PROBE_DATABASE_MIN_AGE_MS` — a run on
|
|
5
|
+
// another host that has created its database and not yet connected reads dead and idle here.
|
|
6
|
+
|
|
7
|
+
import { stringField } from './error-render';
|
|
8
|
+
import type { PgExecutor } from './pg-executor';
|
|
9
|
+
|
|
10
|
+
/** `<prefix>_<pid>_<seconds, base 36>_<8 hex>` — exactly what `probeDatabaseName` returns. */
|
|
11
|
+
const PROBE_SHAPE = /^([a-z_][a-z0-9_]*)_(\d+)_([0-9a-z]+)_[0-9a-f]{8}$/;
|
|
12
|
+
|
|
13
|
+
/** A leftover younger than this is left alone: no suite takes ten minutes to make its first query. */
|
|
14
|
+
export const PROBE_DATABASE_MIN_AGE_MS = 10 * 60 * 1000;
|
|
15
|
+
|
|
16
|
+
export interface ProbeDatabaseSweepOptions {
|
|
17
|
+
/** Whether a pid is a live process on this host. Defaults to `probeDatabaseAlive`. */
|
|
18
|
+
readonly isAlive?: ((pid: number) => boolean) | undefined;
|
|
19
|
+
/** The clock a leftover's age is read against. Defaults to `Date.now`. */
|
|
20
|
+
readonly now?: (() => number) | undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* `kill(pid, 0)` delivers nothing and answers whether the process exists: `EPERM` is a process
|
|
25
|
+
* someone else owns, which is alive. Another host's pid reads as dead here, which is why the sweep
|
|
26
|
+
* also demands that nothing is connected.
|
|
27
|
+
*/
|
|
28
|
+
export function probeDatabaseAlive(pid: number): boolean {
|
|
29
|
+
try {
|
|
30
|
+
process.kill(pid, 0);
|
|
31
|
+
return true;
|
|
32
|
+
} catch (thrown) {
|
|
33
|
+
return stringField(thrown, 'code') === 'EPERM';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Drop every database sharing `probeName`'s prefix whose run is over, never `probeName` itself.
|
|
39
|
+
* Call it beside the `create database`; it answers the names it dropped. A name not shaped like
|
|
40
|
+
* `probeDatabaseName`'s answer has no siblings to find, and nothing is sent.
|
|
41
|
+
*/
|
|
42
|
+
export async function sweepProbeDatabases(
|
|
43
|
+
executor: PgExecutor,
|
|
44
|
+
probeName: string,
|
|
45
|
+
options: ProbeDatabaseSweepOptions = {},
|
|
46
|
+
): Promise<readonly string[]> {
|
|
47
|
+
const prefix = PROBE_SHAPE.exec(probeName)?.[1];
|
|
48
|
+
if (prefix === undefined) return [];
|
|
49
|
+
const isAlive = options.isAlive ?? probeDatabaseAlive;
|
|
50
|
+
const now = (options.now ?? Date.now)();
|
|
51
|
+
const rows = await executor.query<{ datname: string; backends: number | string }>(
|
|
52
|
+
`select d.datname, (select count(*) from pg_stat_activity a where a.datname = d.datname) as backends
|
|
53
|
+
from pg_database d where d.datname like $1`,
|
|
54
|
+
[`${prefix.replaceAll('_', '\\_')}\\_%`],
|
|
55
|
+
);
|
|
56
|
+
const swept: string[] = [];
|
|
57
|
+
for (const row of rows) {
|
|
58
|
+
const shape = PROBE_SHAPE.exec(row.datname);
|
|
59
|
+
if (row.datname === probeName || shape?.[1] !== prefix) continue;
|
|
60
|
+
if (Number(row.backends) > 0 || isAlive(Number(shape[2]))) continue;
|
|
61
|
+
const createdMs = Number.parseInt(shape[3] ?? '', 36) * 1000;
|
|
62
|
+
if (!Number.isFinite(createdMs) || now - createdMs < PROBE_DATABASE_MIN_AGE_MS) continue;
|
|
63
|
+
// The name matched `PROBE_SHAPE`, so it is `[a-z0-9_]` only — quoting it needs no escape.
|
|
64
|
+
await executor.query(`drop database if exists "${row.datname}" with (force)`, []);
|
|
65
|
+
swept.push(row.datname);
|
|
66
|
+
}
|
|
67
|
+
return swept;
|
|
68
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Test support: the ONE name a live or contract suite gives the database it creates and drops —
|
|
2
|
+
// prefix, pid, creation second (base 36), eight random hex characters. A fixed name let two runs against one server drop each
|
|
3
|
+
// other's database mid-suite (#705). Here, at tier 0, because every package with such a suite can
|
|
4
|
+
// import core and `@ultimat3/testing` (tier 5) is out of reach below it. `bun run probe-databases`.
|
|
5
|
+
|
|
6
|
+
/** Postgres's NAMEDATALEN - 1: a longer identifier is silently truncated, so two names could meet. */
|
|
7
|
+
export const PROBE_DATABASE_NAME_MAX = 63;
|
|
8
|
+
|
|
9
|
+
const RANDOM_HEX = 8;
|
|
10
|
+
|
|
11
|
+
/** Where a probe name's uniqueness comes from — injectable so a test can pin the answer. */
|
|
12
|
+
export interface ProbeDatabaseEntropy {
|
|
13
|
+
/** Defaults to `process.pid`: two concurrent runs are two processes. */
|
|
14
|
+
readonly pid?: number | undefined;
|
|
15
|
+
/** Defaults to a fresh `crypto.randomUUID()`: two suites in one process differ by this. */
|
|
16
|
+
readonly random?: string | undefined;
|
|
17
|
+
/** Defaults to `Date.now()`: the creation second the sweep ages a leftover by. */
|
|
18
|
+
readonly now?: number | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Lowercase `[a-z0-9_]`, starting with a letter or `_` — an identifier no statement must quote. */
|
|
22
|
+
function identifierPrefix(prefix: string): string {
|
|
23
|
+
const folded = prefix.toLowerCase().replace(/[^a-z0-9_]/g, '_');
|
|
24
|
+
if (folded === '') return 'x_probe';
|
|
25
|
+
return /^[a-z_]/.test(folded) ? folded : `x_${folded}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A database name unique to this call, valid unquoted, at most 63 bytes — truncation takes the
|
|
30
|
+
* prefix, never the suffix. The creation second is in the name because `pg_database` records none,
|
|
31
|
+
* and `sweepProbeDatabases` must not drop a run's database in the moment between its `create` and
|
|
32
|
+
* its first connection (`<prefix>_<pid>_<seconds, base 36>_<8 hex>`).
|
|
33
|
+
*/
|
|
34
|
+
export function probeDatabaseName(prefix: string, entropy: ProbeDatabaseEntropy = {}): string {
|
|
35
|
+
const pid = Math.trunc(Math.abs(entropy.pid ?? process.pid));
|
|
36
|
+
const hex = (entropy.random ?? crypto.randomUUID())
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.replace(/[^0-9a-f]/g, '')
|
|
39
|
+
.slice(0, RANDOM_HEX)
|
|
40
|
+
.padEnd(RANDOM_HEX, '0');
|
|
41
|
+
const seconds = Math.floor(Math.max(0, entropy.now ?? Date.now()) / 1000).toString(36);
|
|
42
|
+
const suffix = `_${String(pid)}_${seconds}_${hex}`;
|
|
43
|
+
return `${identifierPrefix(prefix).slice(0, PROBE_DATABASE_NAME_MAX - suffix.length)}${suffix}`;
|
|
44
|
+
}
|