@endora-commerce/test-kit 0.100.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/LICENSE +21 -0
- package/README.md +108 -0
- package/dist/database/index.d.ts +17 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +17 -0
- package/dist/database/index.js.map +1 -0
- package/dist/database/lease.d.ts +115 -0
- package/dist/database/lease.d.ts.map +1 -0
- package/dist/database/lease.js +204 -0
- package/dist/database/lease.js.map +1 -0
- package/dist/database/provision.d.ts +135 -0
- package/dist/database/provision.d.ts.map +1 -0
- package/dist/database/provision.js +457 -0
- package/dist/database/provision.js.map +1 -0
- package/dist/database/run-isolation.d.ts +356 -0
- package/dist/database/run-isolation.d.ts.map +1 -0
- package/dist/database/run-isolation.js +490 -0
- package/dist/database/run-isolation.js.map +1 -0
- package/dist/server/compose-test-server.d.ts +251 -0
- package/dist/server/compose-test-server.d.ts.map +1 -0
- package/dist/server/compose-test-server.js +359 -0
- package/dist/server/compose-test-server.js.map +1 -0
- package/dist/server/composition.d.ts +112 -0
- package/dist/server/composition.d.ts.map +1 -0
- package/dist/server/composition.js +32 -0
- package/dist/server/composition.js.map +1 -0
- package/dist/server/index.d.ts +13 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +12 -0
- package/dist/server/index.js.map +1 -0
- package/dist/support/entity-index.d.ts +91 -0
- package/dist/support/entity-index.d.ts.map +1 -0
- package/dist/support/entity-index.js +56 -0
- package/dist/support/entity-index.js.map +1 -0
- package/dist/support/index.d.ts +134 -0
- package/dist/support/index.d.ts.map +1 -0
- package/dist/support/index.js +153 -0
- package/dist/support/index.js.map +1 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Endora sp. z o.o. and the Endora Commerce contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# `@endora-commerce/test-kit`
|
|
2
|
+
|
|
3
|
+
Composes an Endora Commerce platform for a test, from a composition its caller supplies.
|
|
4
|
+
|
|
5
|
+
`specs/109-backend-test-kit/` is the feature; `contracts/test-kit-package.md` is normative.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Why it exists
|
|
10
|
+
|
|
11
|
+
`backend/test/helpers/test-server.ts` is 2993 lines and composes a platform by **finding**
|
|
12
|
+
one: the generated module list, the application's ORM configuration, the resolved manifest
|
|
13
|
+
registry, the overlay loader and the package loader. Every one of those is a fact about
|
|
14
|
+
*this repository's tree*, so a package cannot have any of them — which is why a module
|
|
15
|
+
package's server-bound test had nowhere to run but `backend/test`, and why 211 such files
|
|
16
|
+
are still there.
|
|
17
|
+
|
|
18
|
+
68 % of a server-bound test file's wall clock is that composition (116.6 s of a 171 s run,
|
|
19
|
+
median 4700 ms). The kit does not make it cheaper. What it makes is **reachable**: the same
|
|
20
|
+
composition, from a package, for a module that this repository has never heard of.
|
|
21
|
+
|
|
22
|
+
## The inversion
|
|
23
|
+
|
|
24
|
+
`composeTestServer` takes a `PlatformComposition` and never builds one. Its four members
|
|
25
|
+
are the four things only the caller knows:
|
|
26
|
+
|
|
27
|
+
| Member | What the caller supplies |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `modules` | the module entries to compose — this repository's `MODULES` plus overlay plus discovered packages, or a third party's own set |
|
|
30
|
+
| `orm` | an opener and a closer |
|
|
31
|
+
| `manifests` | the loaded manifest registry the lifecycle needs |
|
|
32
|
+
| `testSupport` | the contributions of exactly the modules in `modules` |
|
|
33
|
+
|
|
34
|
+
Everything else it does is host-shaped and true of any platform: the container, the ORM
|
|
35
|
+
registrations, the audit writer, the event and command buses, the two Redis clients, the
|
|
36
|
+
registry cache, the settings and sales-channel kernels, one `composeModules` pass, one
|
|
37
|
+
contribution window, the tenancy request-scope hook, one boot phase, `buildServer`, and
|
|
38
|
+
`teardownTestServer`.
|
|
39
|
+
|
|
40
|
+
It does **not** read `process.env.DEPLOYMENT`, walk `node_modules` or read a generated
|
|
41
|
+
artefact. Each of those is a fact about the caller's process (D-104's predicate), and a kit
|
|
42
|
+
that answered them would answer them differently from the platform that composes for real.
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { composeTestServer, teardownTestServer } from '@endora-commerce/test-kit/server';
|
|
46
|
+
|
|
47
|
+
const handle = await composeTestServer({
|
|
48
|
+
composition: { modules, orm: { open, close }, manifests },
|
|
49
|
+
buildTenantContext: async (request) => resolveTenantContext(actorOf(request)),
|
|
50
|
+
prepareDatabase: async ({ em }) => { /* truncate, seed */ },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const response = await handle.app.inject({ method: 'GET', url: '/api/v1/…' });
|
|
54
|
+
const service = handle.container.cradle.myService;
|
|
55
|
+
|
|
56
|
+
await teardownTestServer(handle);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Nothing on `TestServerHandle` names a module.** The application's own handle carries 29
|
|
60
|
+
module-specific fields today, and every one of them is a container resolution its readers
|
|
61
|
+
can make for themselves: `handle.container.cradle` is how a caller reaches a module's
|
|
62
|
+
service. Draining those fields is Phase 2.
|
|
63
|
+
|
|
64
|
+
## What it refuses
|
|
65
|
+
|
|
66
|
+
A composition whose `modules` lacks a module declaring `activation.nonDeactivatable` is
|
|
67
|
+
refused by `composeModules` **before the first module registers** — with
|
|
68
|
+
`RequiredModuleAbsentError`, naming the module, the sentence its own manifest gives and the
|
|
69
|
+
remedy. The kit adds no second check and swallows nothing on that path, because a paraphrase
|
|
70
|
+
written here would be a second answer to a question the platform already answers.
|
|
71
|
+
|
|
72
|
+
That matters more than it sounds: 23 modules declare `nonDeactivatable`, and the union with
|
|
73
|
+
each module's transitive `dependencies` closure is a median of 23–24 of 70. So "just my
|
|
74
|
+
module" is not a composition anybody can run, and a stranger's first attempt is the one most
|
|
75
|
+
likely to meet this.
|
|
76
|
+
|
|
77
|
+
## The subpaths
|
|
78
|
+
|
|
79
|
+
| Subpath | Contents |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| `./server` | `composeTestServer`, `teardownTestServer`, and the two types they take and return |
|
|
82
|
+
| `./database` | the per-invocation database lease (issue #189), and the naming and selection rules behind it |
|
|
83
|
+
| `./support` | `TestSupportContribution` — what a module publishes at its own `./test-support` |
|
|
84
|
+
|
|
85
|
+
There is **no root export**: a caller names the seam it wants, so a test that needs only the
|
|
86
|
+
lease does not load the server.
|
|
87
|
+
|
|
88
|
+
`./off-state` (`expectModuleAbsent`, `withModuleOff`) is in the contract's table and is not
|
|
89
|
+
here yet. `backend/test/helpers/off-state.ts` is `check:off-state-coverage`'s subject and
|
|
90
|
+
that check's caller walk is `backend/test/**`, so moving the file before the walk follows it
|
|
91
|
+
(T074) would take 46 modules' off-state proofs out of the population that judges them.
|
|
92
|
+
|
|
93
|
+
## Running its tests
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pnpm --filter @endora-commerce/test-kit run test # service-free
|
|
97
|
+
pnpm --filter @endora-commerce/test-kit run test:services # needs PostgreSQL and Redis
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Two configurations rather than one with a condition, because a condition is a green that can
|
|
101
|
+
quietly become "not looking" (issue #113). CI's `test:frontend` job runs every non-backend
|
|
102
|
+
member's `test` script in an image with no service containers; `test:kit` runs the second
|
|
103
|
+
half beside the services.
|
|
104
|
+
|
|
105
|
+
The service-bound tests compose a fixture platform that is **nobody's module** — two
|
|
106
|
+
synthetic modules, one `@OrgScoped` entity, one route — because the kit may name no module
|
|
107
|
+
package (R1.5). That is the point rather than an inconvenience: what those tests have to
|
|
108
|
+
prove is that the seam works for a *stranger's* composition.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `./database` — the per-invocation database lease (feature 109, R1.1).
|
|
3
|
+
*
|
|
4
|
+
* The cheapest of the kit's subpaths and the one that proves the package
|
|
5
|
+
* shape: the mechanism (issue #189) never had a module coupling, only an
|
|
6
|
+
* application one, and that coupling is now two callbacks the caller supplies.
|
|
7
|
+
*
|
|
8
|
+
* What is published here is the seam plus the naming and selection rules, both
|
|
9
|
+
* halves for the same reason: the two things whose correctness protects the dev
|
|
10
|
+
* database and other people's databases are pure, and a consumer that wants to
|
|
11
|
+
* assert them — as `backend/test/unit/harness/run-isolation.test.ts` does —
|
|
12
|
+
* must be able to name them without a service.
|
|
13
|
+
*/
|
|
14
|
+
export { assertTestDatabaseUrl, leaseRunDatabase, type RunDatabaseLease, type RunDatabaseLeaseInput, type TemplateIdentity, } from './lease.js';
|
|
15
|
+
export { BASE_DATABASE_URL_ENV, ISOLATION_ENV, KEEP_DATABASE_ENV, STALE_RUN_DATABASE_MS, STALE_TEMPLATE_MS, SWEEP_LIMIT, TEMPLATE_DATABASE_ENV, TEMPLATE_DIGEST_LENGTH, TEST_DATABASE_NAME_PATTERN, advisoryLockKey, databaseNameOf, formatTemplateProvenance, isolationMode, keepRunDatabase, parseRunDatabaseName, parseTemplateDatabaseName, parseTemplateProvenance, randomRunToken, redisUrlNamesDatabase, redisUrlWithDatabase, runDatabaseName, runIdentity, sharedDatabaseReason, staleTemplates, strandedRunDatabases, templateDatabaseName, templateDigest, templateDrift, templateFamilyName, withDatabase, type IsolationMode, type StaleTemplateSelection, type StrandedSelection, type TemplateDrift, type TemplateInputs, type TemplateProvenance, type TemplateSource, } from './run-isolation.js';
|
|
16
|
+
export { appliedMigrations, cloneTemplateForCaller, dropRunDatabase, leaseRedisDatabase, provisionRunDatabase, sweepStaleTemplates, sweepStrandedRunDatabases, type Log, type ProvisionInput, type RedisLease, type RunDatabase, } from './provision.js';
|
|
17
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/database/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EACL,qBAAqB,EACrB,gBAAgB,EAChB,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,GACtB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,qBAAqB,EACrB,aAAa,EACb,iBAAiB,EACjB,qBAAqB,EACrB,iBAAiB,EACjB,WAAW,EACX,qBAAqB,EACrB,sBAAsB,EACtB,0BAA0B,EAC1B,eAAe,EACf,cAAc,EACd,wBAAwB,EACxB,aAAa,EACb,eAAe,EACf,oBAAoB,EACpB,yBAAyB,EACzB,uBAAuB,EACvB,cAAc,EACd,qBAAqB,EACrB,oBAAoB,EACpB,eAAe,EACf,WAAW,EACX,oBAAoB,EACpB,cAAc,EACd,oBAAoB,EACpB,oBAAoB,EACpB,cAAc,EACd,aAAa,EACb,kBAAkB,EAClB,YAAY,EACZ,KAAK,aAAa,EAClB,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,cAAc,GACpB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,iBAAiB,EACjB,sBAAsB,EACtB,eAAe,EACf,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,EACnB,yBAAyB,EACzB,KAAK,GAAG,EACR,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,WAAW,GACjB,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `./database` — the per-invocation database lease (feature 109, R1.1).
|
|
3
|
+
*
|
|
4
|
+
* The cheapest of the kit's subpaths and the one that proves the package
|
|
5
|
+
* shape: the mechanism (issue #189) never had a module coupling, only an
|
|
6
|
+
* application one, and that coupling is now two callbacks the caller supplies.
|
|
7
|
+
*
|
|
8
|
+
* What is published here is the seam plus the naming and selection rules, both
|
|
9
|
+
* halves for the same reason: the two things whose correctness protects the dev
|
|
10
|
+
* database and other people's databases are pure, and a consumer that wants to
|
|
11
|
+
* assert them — as `backend/test/unit/harness/run-isolation.test.ts` does —
|
|
12
|
+
* must be able to name them without a service.
|
|
13
|
+
*/
|
|
14
|
+
export { assertTestDatabaseUrl, leaseRunDatabase, } from './lease.js';
|
|
15
|
+
export { BASE_DATABASE_URL_ENV, ISOLATION_ENV, KEEP_DATABASE_ENV, STALE_RUN_DATABASE_MS, STALE_TEMPLATE_MS, SWEEP_LIMIT, TEMPLATE_DATABASE_ENV, TEMPLATE_DIGEST_LENGTH, TEST_DATABASE_NAME_PATTERN, advisoryLockKey, databaseNameOf, formatTemplateProvenance, isolationMode, keepRunDatabase, parseRunDatabaseName, parseTemplateDatabaseName, parseTemplateProvenance, randomRunToken, redisUrlNamesDatabase, redisUrlWithDatabase, runDatabaseName, runIdentity, sharedDatabaseReason, staleTemplates, strandedRunDatabases, templateDatabaseName, templateDigest, templateDrift, templateFamilyName, withDatabase, } from './run-isolation.js';
|
|
16
|
+
export { appliedMigrations, cloneTemplateForCaller, dropRunDatabase, leaseRedisDatabase, provisionRunDatabase, sweepStaleTemplates, sweepStrandedRunDatabases, } from './provision.js';
|
|
17
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/database/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EACL,qBAAqB,EACrB,gBAAgB,GAIjB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,qBAAqB,EACrB,aAAa,EACb,iBAAiB,EACjB,qBAAqB,EACrB,iBAAiB,EACjB,WAAW,EACX,qBAAqB,EACrB,sBAAsB,EACtB,0BAA0B,EAC1B,eAAe,EACf,cAAc,EACd,wBAAwB,EACxB,aAAa,EACb,eAAe,EACf,oBAAoB,EACpB,yBAAyB,EACzB,uBAAuB,EACvB,cAAc,EACd,qBAAqB,EACrB,oBAAoB,EACpB,eAAe,EACf,WAAW,EACX,oBAAoB,EACpB,cAAc,EACd,oBAAoB,EACpB,oBAAoB,EACpB,cAAc,EACd,aAAa,EACb,kBAAkB,EAClB,YAAY,GAQb,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,iBAAiB,EACjB,sBAAsB,EACtB,eAAe,EACf,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,EACnB,yBAAyB,GAK1B,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-invocation database lease, as a seam a caller drives (feature 109,
|
|
3
|
+
* T022; the mechanism is issue #189's).
|
|
4
|
+
*
|
|
5
|
+
* ## What it does, unchanged
|
|
6
|
+
*
|
|
7
|
+
* One `vitest run` gets its **own** database — `<base>_r_<stamp>_<rand>`, a
|
|
8
|
+
* `create database … template` clone of the migrated `<base>_tpl_<digest>` —
|
|
9
|
+
* and its **own** Redis logical database, leased in index 0. Both are released
|
|
10
|
+
* when the run ends and swept if it crashed. Before that, isolation used to be
|
|
11
|
+
* per database and per Redis instance and never per invocation, so a second
|
|
12
|
+
* `vitest run` landed its truncate inside the first one's setup, and every
|
|
13
|
+
* symptom of that is indistinguishable from a real failure.
|
|
14
|
+
*
|
|
15
|
+
* ## What moved, and why this is a seam rather than a script
|
|
16
|
+
*
|
|
17
|
+
* It was `backend/test/global-setup.ts`, whose isolated path could not be
|
|
18
|
+
* reached from outside `backend`. The two things in it that are *this
|
|
19
|
+
* repository's* — which migrations exist, and what "migrated" means for its
|
|
20
|
+
* schema — are the two the caller supplies:
|
|
21
|
+
*
|
|
22
|
+
* - **`identity`** is the digest over the caller's whole migration input.
|
|
23
|
+
* Two callers whose input differs get two templates and cannot contaminate
|
|
24
|
+
* each other; two that agree share one and pay one migration pass between
|
|
25
|
+
* them.
|
|
26
|
+
* - **`migrateTemplate`** is applied to the template while `DATABASE_URL`
|
|
27
|
+
* names it. It is a callback rather than a value for a reason that survives
|
|
28
|
+
* the move: an ORM configuration captures `DATABASE_URL` at import, once
|
|
29
|
+
* per process, so the import has to happen after this function has pointed
|
|
30
|
+
* that variable at the template.
|
|
31
|
+
*
|
|
32
|
+
* Nothing here reads a generated artefact, walks `node_modules` or consults
|
|
33
|
+
* `process.env.DEPLOYMENT`. Every one of those is a fact about the caller's
|
|
34
|
+
* process (R2.2), and a kit that answered them would answer them differently
|
|
35
|
+
* from the platform that composes for real.
|
|
36
|
+
*
|
|
37
|
+
* ## The two documented overrides behave exactly as they did
|
|
38
|
+
*
|
|
39
|
+
* `BACKEND_TEST_ISOLATION=shared` restores the pre-#189 behaviour of every
|
|
40
|
+
* invocation sharing one database, and `BACKEND_TEST_KEEP_DATABASE=1` keeps the
|
|
41
|
+
* run's database for a post-mortem instead of dropping it. Both are read here,
|
|
42
|
+
* from the same two constants the naming rules use — see
|
|
43
|
+
* `backend/test/README.md` § *One database per invocation*, which is still the
|
|
44
|
+
* document that describes them.
|
|
45
|
+
*/
|
|
46
|
+
import { type Log, type ProvisionInput } from './provision.js';
|
|
47
|
+
/** The migration set this run's platform is, exactly as `provisionRunDatabase` takes it. */
|
|
48
|
+
export type TemplateIdentity = ProvisionInput['identity'];
|
|
49
|
+
export interface RunDatabaseLeaseInput {
|
|
50
|
+
/**
|
|
51
|
+
* The base test DSN. The run database and the template are **named from it**,
|
|
52
|
+
* and it is never itself what the workers connect to on the isolated path.
|
|
53
|
+
*/
|
|
54
|
+
readonly baseUrl: string;
|
|
55
|
+
/** Which migration set this run has — the caller's, because the migrations are. */
|
|
56
|
+
readonly identity: TemplateIdentity;
|
|
57
|
+
/**
|
|
58
|
+
* Apply the caller's schema to the template database at this URL.
|
|
59
|
+
*
|
|
60
|
+
* Called with `DATABASE_URL` already pointing at the template, so an ORM
|
|
61
|
+
* configuration imported inside it captures the right database.
|
|
62
|
+
*/
|
|
63
|
+
readonly migrateTemplate: (templateUrl: string) => Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* The migration class names this run applies, in order.
|
|
66
|
+
*
|
|
67
|
+
* Used on the **shared** path only, and only to warn: a shared database
|
|
68
|
+
* accumulates a migration from another branch, and a test that drives the
|
|
69
|
+
* migrator then reads that as a failure of the code under test. Nothing here
|
|
70
|
+
* rebuilds a shared database — it is the operator's, named by them.
|
|
71
|
+
*/
|
|
72
|
+
readonly configuredMigrationNames?: () => Promise<readonly string[]>;
|
|
73
|
+
/** Defaults to `REDIS_URL`, or `redis://localhost:6379`. */
|
|
74
|
+
readonly redisUrl?: string;
|
|
75
|
+
/** Defaults to `process.env`, which is also what the lease writes into. */
|
|
76
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
77
|
+
readonly log?: Log;
|
|
78
|
+
}
|
|
79
|
+
export interface RunDatabaseLease {
|
|
80
|
+
/** Which path this lease took. `shared` means nothing was provisioned. */
|
|
81
|
+
readonly mode: 'per-invocation' | 'shared';
|
|
82
|
+
/** The DSN every worker of this invocation will use — also written to `DATABASE_URL`. */
|
|
83
|
+
readonly databaseUrl: string;
|
|
84
|
+
/** The run database's name, or `undefined` on the shared path. */
|
|
85
|
+
readonly databaseName: string | undefined;
|
|
86
|
+
/** The template this run was cloned from, or `undefined` on the shared path. */
|
|
87
|
+
readonly templateName: string | undefined;
|
|
88
|
+
/** The Redis DSN this invocation owns, or `undefined` when the caller's already named one. */
|
|
89
|
+
readonly redisUrl: string | undefined;
|
|
90
|
+
/** Drop the run database and release the Redis index. Idempotent. */
|
|
91
|
+
readonly release: () => Promise<void>;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The `_test` judgement, applied to the base DSN before anything is created.
|
|
95
|
+
*
|
|
96
|
+
* Exported because it is the guard a caller wants **before** it decides to
|
|
97
|
+
* lease at all, and because a caller that resolves its own DSN should not
|
|
98
|
+
* reimplement the refusal. `ALLOW_NON_TEST_DATABASE_URL=1` is the documented
|
|
99
|
+
* override and stands down the isolation with it (see `sharedDatabaseReason`):
|
|
100
|
+
* a name derived from a base that already breaks the convention would break it
|
|
101
|
+
* too, and isolation refuses to widen the judgement.
|
|
102
|
+
*/
|
|
103
|
+
export declare function assertTestDatabaseUrl(url: string, env?: NodeJS.ProcessEnv): string;
|
|
104
|
+
/**
|
|
105
|
+
* Give this invocation its own database and its own Redis logical database.
|
|
106
|
+
*
|
|
107
|
+
* Writes `DATABASE_URL`, `BACKEND_TEST_BASE_URL`, `BACKEND_TEST_TEMPLATE` and
|
|
108
|
+
* `REDIS_URL` into `env` (which defaults to `process.env`), because a vitest
|
|
109
|
+
* worker inherits the parent's environment and that inheritance **is** the
|
|
110
|
+
* mechanism by which a run's actual DSN reaches the files that use it. A caller
|
|
111
|
+
* that spawns a CLI from a test reads `DATABASE_URL`, never `TEST_DATABASE_URL`
|
|
112
|
+
* — the second names the base.
|
|
113
|
+
*/
|
|
114
|
+
export declare function leaseRunDatabase(input: RunDatabaseLeaseInput): Promise<RunDatabaseLease>;
|
|
115
|
+
//# sourceMappingURL=lease.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lease.d.ts","sourceRoot":"","sources":["../../src/database/lease.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAgBH,OAAO,EAML,KAAK,GAAG,EACR,KAAK,cAAc,EAEpB,MAAM,gBAAgB,CAAC;AAExB,4FAA4F;AAC5F,MAAM,MAAM,gBAAgB,GAAG,cAAc,CAAC,UAAU,CAAC,CAAC;AAE1D,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,mFAAmF;IACnF,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE;;;;;;;OAOG;IACH,QAAQ,CAAC,wBAAwB,CAAC,EAAE,MAAM,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;IACrE,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,2EAA2E;IAC3E,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACjC,QAAQ,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,0EAA0E;IAC1E,QAAQ,CAAC,IAAI,EAAE,gBAAgB,GAAG,QAAQ,CAAC;IAC3C,yFAAyF;IACzF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,gFAAgF;IAChF,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,8FAA8F;IAC9F,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,qEAAqE;IACrE,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAID;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAU/F;AA6BD;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA8G9F"}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-invocation database lease, as a seam a caller drives (feature 109,
|
|
3
|
+
* T022; the mechanism is issue #189's).
|
|
4
|
+
*
|
|
5
|
+
* ## What it does, unchanged
|
|
6
|
+
*
|
|
7
|
+
* One `vitest run` gets its **own** database — `<base>_r_<stamp>_<rand>`, a
|
|
8
|
+
* `create database … template` clone of the migrated `<base>_tpl_<digest>` —
|
|
9
|
+
* and its **own** Redis logical database, leased in index 0. Both are released
|
|
10
|
+
* when the run ends and swept if it crashed. Before that, isolation used to be
|
|
11
|
+
* per database and per Redis instance and never per invocation, so a second
|
|
12
|
+
* `vitest run` landed its truncate inside the first one's setup, and every
|
|
13
|
+
* symptom of that is indistinguishable from a real failure.
|
|
14
|
+
*
|
|
15
|
+
* ## What moved, and why this is a seam rather than a script
|
|
16
|
+
*
|
|
17
|
+
* It was `backend/test/global-setup.ts`, whose isolated path could not be
|
|
18
|
+
* reached from outside `backend`. The two things in it that are *this
|
|
19
|
+
* repository's* — which migrations exist, and what "migrated" means for its
|
|
20
|
+
* schema — are the two the caller supplies:
|
|
21
|
+
*
|
|
22
|
+
* - **`identity`** is the digest over the caller's whole migration input.
|
|
23
|
+
* Two callers whose input differs get two templates and cannot contaminate
|
|
24
|
+
* each other; two that agree share one and pay one migration pass between
|
|
25
|
+
* them.
|
|
26
|
+
* - **`migrateTemplate`** is applied to the template while `DATABASE_URL`
|
|
27
|
+
* names it. It is a callback rather than a value for a reason that survives
|
|
28
|
+
* the move: an ORM configuration captures `DATABASE_URL` at import, once
|
|
29
|
+
* per process, so the import has to happen after this function has pointed
|
|
30
|
+
* that variable at the template.
|
|
31
|
+
*
|
|
32
|
+
* Nothing here reads a generated artefact, walks `node_modules` or consults
|
|
33
|
+
* `process.env.DEPLOYMENT`. Every one of those is a fact about the caller's
|
|
34
|
+
* process (R2.2), and a kit that answered them would answer them differently
|
|
35
|
+
* from the platform that composes for real.
|
|
36
|
+
*
|
|
37
|
+
* ## The two documented overrides behave exactly as they did
|
|
38
|
+
*
|
|
39
|
+
* `BACKEND_TEST_ISOLATION=shared` restores the pre-#189 behaviour of every
|
|
40
|
+
* invocation sharing one database, and `BACKEND_TEST_KEEP_DATABASE=1` keeps the
|
|
41
|
+
* run's database for a post-mortem instead of dropping it. Both are read here,
|
|
42
|
+
* from the same two constants the naming rules use — see
|
|
43
|
+
* `backend/test/README.md` § *One database per invocation*, which is still the
|
|
44
|
+
* document that describes them.
|
|
45
|
+
*/
|
|
46
|
+
import { BASE_DATABASE_URL_ENV, ISOLATION_ENV, KEEP_DATABASE_ENV, TEMPLATE_DATABASE_ENV, TEST_DATABASE_NAME_PATTERN, databaseNameOf, keepRunDatabase, redisUrlNamesDatabase, runIdentity, sharedDatabaseReason, templateDrift, withDatabase, } from './run-isolation.js';
|
|
47
|
+
import { appliedMigrations, dropRunDatabase, leaseRedisDatabase, provisionRunDatabase, sweepStrandedRunDatabases, } from './provision.js';
|
|
48
|
+
const DEFAULT_REDIS_URL = 'redis://localhost:6379';
|
|
49
|
+
/**
|
|
50
|
+
* The `_test` judgement, applied to the base DSN before anything is created.
|
|
51
|
+
*
|
|
52
|
+
* Exported because it is the guard a caller wants **before** it decides to
|
|
53
|
+
* lease at all, and because a caller that resolves its own DSN should not
|
|
54
|
+
* reimplement the refusal. `ALLOW_NON_TEST_DATABASE_URL=1` is the documented
|
|
55
|
+
* override and stands down the isolation with it (see `sharedDatabaseReason`):
|
|
56
|
+
* a name derived from a base that already breaks the convention would break it
|
|
57
|
+
* too, and isolation refuses to widen the judgement.
|
|
58
|
+
*/
|
|
59
|
+
export function assertTestDatabaseUrl(url, env = process.env) {
|
|
60
|
+
const dbName = databaseNameOf(url);
|
|
61
|
+
if (!TEST_DATABASE_NAME_PATTERN.test(dbName) && !env['ALLOW_NON_TEST_DATABASE_URL']) {
|
|
62
|
+
throw new Error(`Refusing to run tests against database "${dbName}" — name must contain "_test" ` +
|
|
63
|
+
`(e.g. b2b_test). Set ALLOW_NON_TEST_DATABASE_URL=1 to override. ` +
|
|
64
|
+
`Resolved URL: ${url}`);
|
|
65
|
+
}
|
|
66
|
+
return url;
|
|
67
|
+
}
|
|
68
|
+
/** Create the base database if it is not there, so a first run on a fresh cluster works. */
|
|
69
|
+
async function ensureDatabaseExists(testUrl, log) {
|
|
70
|
+
const dbName = databaseNameOf(testUrl);
|
|
71
|
+
const { Client: PgClient } = await import('pg');
|
|
72
|
+
const admin = new PgClient({ connectionString: withDatabase(testUrl, 'postgres') });
|
|
73
|
+
await admin.connect();
|
|
74
|
+
try {
|
|
75
|
+
const { rowCount } = await admin.query('select 1 from pg_database where datname = $1', [
|
|
76
|
+
dbName,
|
|
77
|
+
]);
|
|
78
|
+
// `pg` types `rowCount` as `number | null`; for this existence probe `0`
|
|
79
|
+
// and `null` both honestly mean "no such database", and the value reaches
|
|
80
|
+
// nothing but this comparison.
|
|
81
|
+
if (rowCount === null || rowCount === 0) {
|
|
82
|
+
// A `pg_database` name cannot be parameterised in `create database`; the
|
|
83
|
+
// `_test` judgement above plus URL parsing keeps this from being
|
|
84
|
+
// injectable, and the re-check refuses anything the two let through.
|
|
85
|
+
const safeName = dbName.replace(/[^a-zA-Z0-9_]/g, '');
|
|
86
|
+
if (safeName !== dbName)
|
|
87
|
+
throw new Error(`Unsafe DB name: "${dbName}"`);
|
|
88
|
+
await admin.query(`create database "${safeName}"`);
|
|
89
|
+
log(`[test-kit] created database ${safeName}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
await admin.end();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Give this invocation its own database and its own Redis logical database.
|
|
98
|
+
*
|
|
99
|
+
* Writes `DATABASE_URL`, `BACKEND_TEST_BASE_URL`, `BACKEND_TEST_TEMPLATE` and
|
|
100
|
+
* `REDIS_URL` into `env` (which defaults to `process.env`), because a vitest
|
|
101
|
+
* worker inherits the parent's environment and that inheritance **is** the
|
|
102
|
+
* mechanism by which a run's actual DSN reaches the files that use it. A caller
|
|
103
|
+
* that spawns a CLI from a test reads `DATABASE_URL`, never `TEST_DATABASE_URL`
|
|
104
|
+
* — the second names the base.
|
|
105
|
+
*/
|
|
106
|
+
export async function leaseRunDatabase(input) {
|
|
107
|
+
const env = input.env ?? process.env;
|
|
108
|
+
const log = input.log ?? ((message) => process.stdout.write(`${message}\n`));
|
|
109
|
+
const baseUrl = assertTestDatabaseUrl(input.baseUrl, env);
|
|
110
|
+
// The pre-#189 behaviour, on either of two grounds. The first is an explicit
|
|
111
|
+
// opt-out, worth having for a post-mortem — the run database is dropped when
|
|
112
|
+
// the run ends, and sometimes what you want is the database a failing run
|
|
113
|
+
// left behind, under a name you already know. The second is
|
|
114
|
+
// ALLOW_NON_TEST_DATABASE_URL: that override is somebody deliberately
|
|
115
|
+
// pointing the suite at a database whose name breaks the convention, and a
|
|
116
|
+
// name derived from it would break it too.
|
|
117
|
+
const shared = sharedDatabaseReason(env, databaseNameOf(baseUrl));
|
|
118
|
+
if (shared !== undefined) {
|
|
119
|
+
env['DATABASE_URL'] = baseUrl;
|
|
120
|
+
await ensureDatabaseExists(baseUrl, log);
|
|
121
|
+
await input.migrateTemplate(baseUrl);
|
|
122
|
+
log(`[test-kit] ${shared === 'explicit' ? `${ISOLATION_ENV}=shared` : 'ALLOW_NON_TEST_DATABASE_URL'}` +
|
|
123
|
+
` — this invocation shares ${databaseNameOf(baseUrl)} with every other one.`);
|
|
124
|
+
// The shared database accumulates the same way a template does — a
|
|
125
|
+
// migration from another branch, or one of ours appended where the order
|
|
126
|
+
// does not put it — and a test that drives the migrator reads that as a
|
|
127
|
+
// failure of the code under test. This path only *says* so: the database is
|
|
128
|
+
// the operator's, named by them or kept by them for a post-mortem, so it is
|
|
129
|
+
// not this lease's to drop.
|
|
130
|
+
if (input.configuredMigrationNames !== undefined) {
|
|
131
|
+
const drift = templateDrift(await appliedMigrations(baseUrl), await input.configuredMigrationNames(), databaseNameOf(baseUrl));
|
|
132
|
+
if (drift) {
|
|
133
|
+
log(`[test-kit] WARNING: ${drift.message.replace(' Rebuilding it from empty.', '')} ` +
|
|
134
|
+
`Nothing here rebuilds it, because you named it. A test that drives the migrator ` +
|
|
135
|
+
`will fail against it for that reason and not for its own — drop it and re-run, ` +
|
|
136
|
+
`or drop ${ISOLATION_ENV}=shared and let the isolated path rebuild its template.`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
mode: 'shared',
|
|
141
|
+
databaseUrl: baseUrl,
|
|
142
|
+
databaseName: undefined,
|
|
143
|
+
templateName: undefined,
|
|
144
|
+
redisUrl: undefined,
|
|
145
|
+
release: async () => {
|
|
146
|
+
/* nothing was provisioned, so there is nothing to give back */
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const run = await provisionRunDatabase({
|
|
151
|
+
baseUrl,
|
|
152
|
+
identity: input.identity,
|
|
153
|
+
migrateTemplate: async (templateUrl) => {
|
|
154
|
+
env['DATABASE_URL'] = templateUrl;
|
|
155
|
+
await input.migrateTemplate(templateUrl);
|
|
156
|
+
},
|
|
157
|
+
log,
|
|
158
|
+
});
|
|
159
|
+
env['DATABASE_URL'] = run.url;
|
|
160
|
+
// A file that drives the real migrator may not do it to the database its
|
|
161
|
+
// neighbours share — the run database is this invocation's only copy, so a
|
|
162
|
+
// migration sequence that dies half-way takes the whole invocation with it.
|
|
163
|
+
// Exporting the base and the template *name* is what lets such a file clone
|
|
164
|
+
// the same template this run was cloned from: by name, because by the time it
|
|
165
|
+
// asks, another invocation may have built a template of its own.
|
|
166
|
+
env[BASE_DATABASE_URL_ENV] = baseUrl;
|
|
167
|
+
env[TEMPLATE_DATABASE_ENV] = run.template;
|
|
168
|
+
// The same defect on the other service: a batch that "passed alone" would
|
|
169
|
+
// still have collided on Redis keys. An explicit index in REDIS_URL is
|
|
170
|
+
// somebody's deliberate choice and is left alone.
|
|
171
|
+
const redisBaseUrl = input.redisUrl ?? env['REDIS_URL'] ?? DEFAULT_REDIS_URL;
|
|
172
|
+
let lease;
|
|
173
|
+
if (redisUrlNamesDatabase(redisBaseUrl)) {
|
|
174
|
+
log(`[test-kit] REDIS_URL already names a logical database — leaving it alone.`);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
lease = await leaseRedisDatabase(redisBaseUrl, runIdentity(), log);
|
|
178
|
+
env['REDIS_URL'] = lease.url;
|
|
179
|
+
}
|
|
180
|
+
// Runs that crashed before their release. Best-effort, capped, and only ever
|
|
181
|
+
// over names this mechanism generated.
|
|
182
|
+
await sweepStrandedRunDatabases(baseUrl, run.name, log);
|
|
183
|
+
let released = false;
|
|
184
|
+
return {
|
|
185
|
+
mode: 'per-invocation',
|
|
186
|
+
databaseUrl: run.url,
|
|
187
|
+
databaseName: run.name,
|
|
188
|
+
templateName: run.template,
|
|
189
|
+
redisUrl: lease?.url,
|
|
190
|
+
release: async () => {
|
|
191
|
+
if (released)
|
|
192
|
+
return;
|
|
193
|
+
released = true;
|
|
194
|
+
await lease?.release();
|
|
195
|
+
if (keepRunDatabase(env)) {
|
|
196
|
+
log(`[test-kit] ${KEEP_DATABASE_ENV} is set — keeping ${run.name}. ` +
|
|
197
|
+
`Drop it yourself, or leave it for the sweep in ~4 h.`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
await dropRunDatabase(baseUrl, run.name, log);
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=lease.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lease.js","sourceRoot":"","sources":["../../src/database/lease.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The half of per-invocation isolation (issue #189) that talks to a server.
|
|
3
|
+
*
|
|
4
|
+
* Split from `run-isolation.ts` so the naming and selection rules — the two
|
|
5
|
+
* things whose correctness protects the dev database and other people's
|
|
6
|
+
* databases — are unit-testable with no service at all, and run in the fast
|
|
7
|
+
* unit job. Nothing here is imported by a test file: `leaseRunDatabase` in
|
|
8
|
+
* `./lease.ts` is the only caller, and a vitest `globalSetup` calls that once
|
|
9
|
+
* per invocation in the parent process.
|
|
10
|
+
*
|
|
11
|
+
* `pg` and `ioredis` are imported dynamically for the same reason: a run that
|
|
12
|
+
* declared `BACKEND_TEST_SERVICES=none` never reaches this module, and should
|
|
13
|
+
* not pay to load a driver it will not dial.
|
|
14
|
+
*/
|
|
15
|
+
import type { Client } from 'pg';
|
|
16
|
+
export type Log = (message: string) => void;
|
|
17
|
+
/**
|
|
18
|
+
* The migration names applied to a database, in the order they were applied.
|
|
19
|
+
*
|
|
20
|
+
* `mikro_orm_migrations` is missing on a template that has just been created
|
|
21
|
+
* and on one that has never been migrated; both mean "nothing applied", which
|
|
22
|
+
* is a prefix of every order and so needs no rebuild.
|
|
23
|
+
*/
|
|
24
|
+
export declare function appliedMigrations(databaseUrl: string): Promise<string[]>;
|
|
25
|
+
export interface RunDatabase {
|
|
26
|
+
/** The DSN every worker of this invocation will use. */
|
|
27
|
+
readonly url: string;
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly template: string;
|
|
30
|
+
}
|
|
31
|
+
export interface ProvisionInput {
|
|
32
|
+
/** The DSN naming the base database, from TEST_DATABASE_URL or the default. */
|
|
33
|
+
readonly baseUrl: string;
|
|
34
|
+
/**
|
|
35
|
+
* Which migration set this run has — the caller's, because the migrations are.
|
|
36
|
+
*
|
|
37
|
+
* A **value**, unlike the callback this used to be, because a caller reads its
|
|
38
|
+
* own migration order out of whatever module assigns it and hands the answer
|
|
39
|
+
* over. That reading is what the callback existed to delay: **an ORM
|
|
40
|
+
* configuration captures `DATABASE_URL` at import**, once per process, and the
|
|
41
|
+
* parent used to import it while that variable was unset, whose fallback is
|
|
42
|
+
* the caller's *dev* database. `migrateTemplate` is still a callback for
|
|
43
|
+
* exactly that reason.
|
|
44
|
+
*/
|
|
45
|
+
readonly identity: {
|
|
46
|
+
readonly digest: string;
|
|
47
|
+
readonly migrations: readonly string[];
|
|
48
|
+
readonly migrationFiles: number;
|
|
49
|
+
/**
|
|
50
|
+
* What each producer contributed and what the walk found of it. Printed
|
|
51
|
+
* rather than merely floored: a shortfall stops the run by name, but only
|
|
52
|
+
* this line says an origin was read at all, and "the walk saw 7% of itself"
|
|
53
|
+
* is the failure a number with no population behind it cannot report.
|
|
54
|
+
*/
|
|
55
|
+
readonly origins: readonly {
|
|
56
|
+
readonly origin: string;
|
|
57
|
+
readonly registered: number;
|
|
58
|
+
readonly migrationFiles: number;
|
|
59
|
+
}[];
|
|
60
|
+
};
|
|
61
|
+
/** Applies pending migrations to the template and establishes the platform invariants. */
|
|
62
|
+
readonly migrateTemplate: (templateUrl: string) => Promise<void>;
|
|
63
|
+
readonly log?: Log;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Give this invocation its own database.
|
|
67
|
+
*
|
|
68
|
+
* Everything between taking the advisory lock and releasing it is serialized
|
|
69
|
+
* across invocations: identify the template, build it if it is not there or not
|
|
70
|
+
* what it claims, migrate it, disconnect from it, clone, collect the templates
|
|
71
|
+
* nobody uses any more. Holding the lock across the clone as well is deliberate
|
|
72
|
+
* — the clone is a fraction of a second, and it is what guarantees the template
|
|
73
|
+
* is idle at the moment the next invocation's migration opens a connection to
|
|
74
|
+
* it.
|
|
75
|
+
*
|
|
76
|
+
* The lock is taken on the `postgres` maintenance database: PostgreSQL advisory
|
|
77
|
+
* locks are scoped to a database, so every waiter has to agree on which, and it
|
|
78
|
+
* must not be the template — a session holding the lock there is exactly the
|
|
79
|
+
* session that would make the clone illegal. It is one key for the whole
|
|
80
|
+
* `<base>_tpl*` family rather than one per digest, because the sweep walks the
|
|
81
|
+
* templates of other digests (see `templateFamilyName`).
|
|
82
|
+
*
|
|
83
|
+
* **Which template, is the question this function exists to answer** (issue
|
|
84
|
+
* #289). It is `<base>_tpl_<digest>`, the digest being over this run's whole
|
|
85
|
+
* migration input, so a template is shared only by runs whose platform it
|
|
86
|
+
* actually is. Two things then have to agree before it is cloned: the
|
|
87
|
+
* provenance comment the build wrote on it, and the migration names it actually
|
|
88
|
+
* holds. Either disagreeing rebuilds it from empty — including "it says
|
|
89
|
+
* nothing", because a template that cannot say what it holds is the defect.
|
|
90
|
+
*/
|
|
91
|
+
export declare function provisionRunDatabase(input: ProvisionInput): Promise<RunDatabase>;
|
|
92
|
+
/**
|
|
93
|
+
* A database of this run's template, for a caller that is not the invocation.
|
|
94
|
+
*
|
|
95
|
+
* The template is named by the caller, not re-derived here, and that is the
|
|
96
|
+
* point (issue #289): the lease resolved which template this run's
|
|
97
|
+
* platform is and exported the name, so a file cloning one mid-run gets the
|
|
98
|
+
* database the run itself was cloned from — not whatever `<base>_tpl` has
|
|
99
|
+
* become while the run has been going, which with several branches on one
|
|
100
|
+
* cluster is routinely another branch's schema.
|
|
101
|
+
*
|
|
102
|
+
* The advisory lock is the same one provisioning takes, and for the same
|
|
103
|
+
* reason: it is what keeps the template idle at the moment a clone starts.
|
|
104
|
+
* `setupMigratorTestDb` is the only caller — a test file that drives the real
|
|
105
|
+
* migrator and must not do it to the database its neighbours share.
|
|
106
|
+
*/
|
|
107
|
+
export declare function cloneTemplateForCaller(baseUrl: string, template: string): Promise<RunDatabase>;
|
|
108
|
+
/**
|
|
109
|
+
* Drop the templates of migration sets nobody runs any more.
|
|
110
|
+
*
|
|
111
|
+
* Keying a template by its migration set is what stops two branches sharing
|
|
112
|
+
* one; this is the other half of that bargain, because a branch leaves its
|
|
113
|
+
* set's template behind the moment it gains a migration or is rebased. Called
|
|
114
|
+
* under the provisioning lock, so no template can be half-built while it runs.
|
|
115
|
+
*
|
|
116
|
+
* Best-effort and never fatal: a sweep that fails is a disk-space problem, not
|
|
117
|
+
* a reason to fail somebody's test run.
|
|
118
|
+
*/
|
|
119
|
+
export declare function sweepStaleTemplates(admin: Client, base: string, keep: string, log?: Log): Promise<string[]>;
|
|
120
|
+
export declare function dropRunDatabase(baseUrl: string, name: string, log?: Log): Promise<void>;
|
|
121
|
+
/**
|
|
122
|
+
* Drop the run databases of invocations that crashed before their teardown.
|
|
123
|
+
*
|
|
124
|
+
* Best-effort and never fatal: a sweep that fails is a disk-space problem, not
|
|
125
|
+
* a reason to fail somebody's test run.
|
|
126
|
+
*/
|
|
127
|
+
export declare function sweepStrandedRunDatabases(baseUrl: string, keep: string, log?: Log): Promise<string[]>;
|
|
128
|
+
export interface RedisLease {
|
|
129
|
+
readonly url: string;
|
|
130
|
+
readonly index: number;
|
|
131
|
+
readonly runId: string;
|
|
132
|
+
readonly release: () => Promise<void>;
|
|
133
|
+
}
|
|
134
|
+
export declare function leaseRedisDatabase(redisUrl: string, runId: string, log?: Log): Promise<RedisLease>;
|
|
135
|
+
//# sourceMappingURL=provision.d.ts.map
|