@push.rocks/smartdb 2.18.1 → 3.0.1
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/dist_rust/rustdb_linux_amd64 +0 -0
- package/dist_rust/rustdb_linux_arm64 +0 -0
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/index.d.ts +1 -1
- package/dist_ts/index.js +1 -1
- package/dist_ts/ts_migration/classes.authmetadatamigrationrunner.d.ts +2 -1
- package/dist_ts/ts_migration/classes.authmetadatamigrationrunner.js +3 -3
- package/dist_ts/ts_migration/classes.storagemigrator.d.ts +2 -1
- package/dist_ts/ts_migration/classes.storagemigrator.js +36 -20
- package/dist_ts/ts_migration/index.d.ts +1 -0
- package/dist_ts/ts_migration/index.js +2 -1
- package/dist_ts/ts_migration/migrators/v0_to_v1.d.ts +7 -1
- package/dist_ts/ts_migration/migrators/v0_to_v1.js +162 -47
- package/dist_ts/ts_migration/migrators/v1_auth_metadata_permissions.d.ts +3 -2
- package/dist_ts/ts_migration/migrators/v1_auth_metadata_permissions.js +3 -3
- package/dist_ts/ts_smartdb/index.d.ts +1 -1
- package/dist_ts/ts_smartdb/index.js +1 -1
- package/dist_ts/ts_smartdb/resource-fencing.d.ts +12 -1
- package/dist_ts/ts_smartdb/resource-fencing.js +44 -2
- package/dist_ts/ts_smartdb/rust-db-bridge.d.ts +5 -4
- package/dist_ts/ts_smartdb/rust-db-bridge.js +33 -10
- package/dist_ts/ts_smartdb/server/SmartdbServer.d.ts +4 -2
- package/dist_ts/ts_smartdb/server/SmartdbServer.js +84 -9
- package/dist_ts/ts_smartdb/service-types.d.ts +47 -2
- package/dist_ts/ts_smartdb/service-types.js +7 -2
- package/package.json +1 -1
- package/readme.md +19 -4
- package/readme.plan.md +2 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +4 -0
- package/ts/ts_migration/classes.authmetadatamigrationrunner.ts +3 -1
- package/ts/ts_migration/classes.storagemigrator.ts +38 -19
- package/ts/ts_migration/index.ts +1 -0
- package/ts/ts_migration/migrators/v0_to_v1.ts +224 -44
- package/ts/ts_migration/migrators/v1_auth_metadata_permissions.ts +4 -0
- package/ts/ts_smartdb/index.ts +4 -0
- package/ts/ts_smartdb/resource-fencing.ts +59 -2
- package/ts/ts_smartdb/rust-db-bridge.ts +56 -11
- package/ts/ts_smartdb/server/SmartdbServer.ts +105 -9
- package/ts/ts_smartdb/service-types.ts +59 -2
|
@@ -2,7 +2,12 @@ import {
|
|
|
2
2
|
RustDbBridge,
|
|
3
3
|
SmartDbManagementOperationTerminationError,
|
|
4
4
|
} from '../rust-db-bridge.js';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
AuthMetadataMigrationRunner,
|
|
7
|
+
SmartDbStorageMigrationCleanupError,
|
|
8
|
+
StorageMigrator,
|
|
9
|
+
} from '../../ts_migration/index.js';
|
|
10
|
+
import { validateSmartDbManagementOperationOptions } from '../offline-inspection.js';
|
|
6
11
|
import type {
|
|
7
12
|
IOpLogEntry,
|
|
8
13
|
IOpLogResult,
|
|
@@ -17,6 +22,8 @@ import type {
|
|
|
17
22
|
ISmartDbDatabaseTenantInput,
|
|
18
23
|
ISmartDbEnsureDatabaseTenantInput,
|
|
19
24
|
ISmartDbEnsureDatabaseTenantResult,
|
|
25
|
+
ISmartDbAllocateDatabaseTenantInput,
|
|
26
|
+
ISmartDbAllocateDatabaseTenantResult,
|
|
20
27
|
ISmartDbDeleteDatabaseTenantInput,
|
|
21
28
|
ISmartDbRotateDatabaseTenantPasswordInput,
|
|
22
29
|
ISmartDbDatabaseTenantDescriptor,
|
|
@@ -146,11 +153,63 @@ export class SmartdbServer {
|
|
|
146
153
|
/**
|
|
147
154
|
* Start the server
|
|
148
155
|
*/
|
|
149
|
-
async start(): Promise<void> {
|
|
156
|
+
async start(optionsArg?: ISmartDbManagementOperationOptions): Promise<void> {
|
|
157
|
+
validateSmartDbManagementOperationOptions(optionsArg);
|
|
158
|
+
optionsArg?.signal?.throwIfAborted();
|
|
150
159
|
if (this.isRunning || this.startInProgress || this.bridgeOwned) {
|
|
151
160
|
throw new Error('Server is already running or still owns a Rust bridge');
|
|
152
161
|
}
|
|
153
162
|
|
|
163
|
+
const startupStartedAt = Date.now();
|
|
164
|
+
const startupDeadline = optionsArg?.timeoutMs === undefined
|
|
165
|
+
? undefined
|
|
166
|
+
: startupStartedAt + optionsArg.timeoutMs;
|
|
167
|
+
const timeoutError = new DOMException(
|
|
168
|
+
optionsArg?.timeoutMs === undefined
|
|
169
|
+
? 'SmartDB startup timed out'
|
|
170
|
+
: `SmartDB startup timed out after ${optionsArg.timeoutMs}ms`,
|
|
171
|
+
'TimeoutError',
|
|
172
|
+
);
|
|
173
|
+
const startupAbortController = new AbortController();
|
|
174
|
+
let startupTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
175
|
+
let externalAbortHandler: (() => void) | undefined;
|
|
176
|
+
if (optionsArg?.signal) {
|
|
177
|
+
externalAbortHandler = () => {
|
|
178
|
+
startupAbortController.abort(optionsArg.signal!.reason);
|
|
179
|
+
};
|
|
180
|
+
optionsArg.signal.addEventListener('abort', externalAbortHandler, { once: true });
|
|
181
|
+
}
|
|
182
|
+
if (optionsArg?.timeoutMs !== undefined) {
|
|
183
|
+
startupTimeout = setTimeout(() => {
|
|
184
|
+
startupAbortController.abort(timeoutError);
|
|
185
|
+
}, optionsArg.timeoutMs);
|
|
186
|
+
}
|
|
187
|
+
const checkpoint = () => {
|
|
188
|
+
if (
|
|
189
|
+
startupDeadline !== undefined
|
|
190
|
+
&& Date.now() >= startupDeadline
|
|
191
|
+
&& !startupAbortController.signal.aborted
|
|
192
|
+
) {
|
|
193
|
+
startupAbortController.abort(timeoutError);
|
|
194
|
+
}
|
|
195
|
+
startupAbortController.signal.throwIfAborted();
|
|
196
|
+
};
|
|
197
|
+
const getBridgeOperationOptions = (): ISmartDbManagementOperationOptions | undefined => {
|
|
198
|
+
checkpoint();
|
|
199
|
+
const remainingTimeoutMs = startupDeadline === undefined
|
|
200
|
+
? undefined
|
|
201
|
+
: startupDeadline - Date.now();
|
|
202
|
+
if (remainingTimeoutMs !== undefined && remainingTimeoutMs <= 0) {
|
|
203
|
+
startupAbortController.abort(timeoutError);
|
|
204
|
+
checkpoint();
|
|
205
|
+
}
|
|
206
|
+
if (!optionsArg?.signal && remainingTimeoutMs === undefined) return undefined;
|
|
207
|
+
return {
|
|
208
|
+
...(optionsArg?.signal ? { signal: optionsArg.signal } : {}),
|
|
209
|
+
...(remainingTimeoutMs === undefined ? {} : { timeoutMs: remainingTimeoutMs }),
|
|
210
|
+
};
|
|
211
|
+
};
|
|
212
|
+
|
|
154
213
|
this.startInProgress = true;
|
|
155
214
|
let resolveStartCompletion!: () => void;
|
|
156
215
|
const startCompletionPromise = new Promise<void>((resolveArg) => {
|
|
@@ -158,16 +217,19 @@ export class SmartdbServer {
|
|
|
158
217
|
});
|
|
159
218
|
this.startCompletionPromise = startCompletionPromise;
|
|
160
219
|
try {
|
|
220
|
+
checkpoint();
|
|
161
221
|
// Run storage migration for file-based storage before starting Rust engine
|
|
162
222
|
if (this.options.storage === 'file' && this.options.storagePath) {
|
|
163
223
|
const migrator = new StorageMigrator(this.options.storagePath);
|
|
164
|
-
await migrator.run();
|
|
224
|
+
await migrator.run(startupAbortController.signal);
|
|
225
|
+
checkpoint();
|
|
165
226
|
}
|
|
166
227
|
|
|
167
228
|
// Ownership starts before spawn because a rejected spawn can still have
|
|
168
229
|
// created a child whose termination must be confirmed.
|
|
169
230
|
this.bridgeOwned = true;
|
|
170
|
-
const spawned = await this.bridge.spawn();
|
|
231
|
+
const spawned = await this.bridge.spawn(getBridgeOperationOptions());
|
|
232
|
+
checkpoint();
|
|
171
233
|
if (!spawned) {
|
|
172
234
|
throw new Error(
|
|
173
235
|
'smartdb Rust binary not found. Set SMARTDB_RUST_BINARY env var, ' +
|
|
@@ -179,7 +241,11 @@ export class SmartdbServer {
|
|
|
179
241
|
const authMetadataMigrationRunner = new AuthMetadataMigrationRunner(
|
|
180
242
|
this.options.auth.usersPath,
|
|
181
243
|
);
|
|
182
|
-
await authMetadataMigrationRunner.run(
|
|
244
|
+
await authMetadataMigrationRunner.run(
|
|
245
|
+
this.bridge,
|
|
246
|
+
getBridgeOperationOptions(),
|
|
247
|
+
);
|
|
248
|
+
checkpoint();
|
|
183
249
|
}
|
|
184
250
|
|
|
185
251
|
// Send config, get back connectionUri
|
|
@@ -194,7 +260,8 @@ export class SmartdbServer {
|
|
|
194
260
|
auth: this.options.auth,
|
|
195
261
|
tls: this.options.tls,
|
|
196
262
|
oplog: this.options.oplog,
|
|
197
|
-
});
|
|
263
|
+
}, getBridgeOperationOptions());
|
|
264
|
+
checkpoint();
|
|
198
265
|
if (!this.bridgeOwned || !this.bridge.running) {
|
|
199
266
|
throw new Error('smartdb Rust process exited during startup');
|
|
200
267
|
}
|
|
@@ -214,8 +281,19 @@ export class SmartdbServer {
|
|
|
214
281
|
this.resolvedPort = resolvedPort;
|
|
215
282
|
}
|
|
216
283
|
this.resolvedConnectionUri = result.connectionUri;
|
|
284
|
+
checkpoint();
|
|
217
285
|
this.isRunning = true;
|
|
218
286
|
} catch (error) {
|
|
287
|
+
const startupError = error instanceof SmartDbManagementOperationTerminationError
|
|
288
|
+
|| error instanceof SmartDbStorageMigrationCleanupError
|
|
289
|
+
? error
|
|
290
|
+
: optionsArg?.signal?.aborted
|
|
291
|
+
? optionsArg.signal.reason
|
|
292
|
+
: startupAbortController.signal.aborted || (
|
|
293
|
+
startupDeadline !== undefined && Date.now() >= startupDeadline
|
|
294
|
+
)
|
|
295
|
+
? timeoutError
|
|
296
|
+
: error;
|
|
219
297
|
this.isRunning = false;
|
|
220
298
|
this.resolvedConnectionUri = '';
|
|
221
299
|
this.resolvedPort = undefined;
|
|
@@ -225,14 +303,18 @@ export class SmartdbServer {
|
|
|
225
303
|
this.bridgeOwned = false;
|
|
226
304
|
} catch (cleanupError) {
|
|
227
305
|
throw new AggregateError(
|
|
228
|
-
[
|
|
306
|
+
[startupError, cleanupError],
|
|
229
307
|
'SmartDB startup failed and Rust bridge cleanup was incomplete',
|
|
230
|
-
{ cause:
|
|
308
|
+
{ cause: startupError },
|
|
231
309
|
);
|
|
232
310
|
}
|
|
233
311
|
}
|
|
234
|
-
throw
|
|
312
|
+
throw startupError;
|
|
235
313
|
} finally {
|
|
314
|
+
if (startupTimeout) clearTimeout(startupTimeout);
|
|
315
|
+
if (externalAbortHandler && optionsArg?.signal) {
|
|
316
|
+
optionsArg.signal.removeEventListener('abort', externalAbortHandler);
|
|
317
|
+
}
|
|
236
318
|
this.startInProgress = false;
|
|
237
319
|
resolveStartCompletion();
|
|
238
320
|
if (this.startCompletionPromise === startCompletionPromise) {
|
|
@@ -345,6 +427,17 @@ export class SmartdbServer {
|
|
|
345
427
|
return this.withTenantMongoUri(descriptor, params.password);
|
|
346
428
|
}
|
|
347
429
|
|
|
430
|
+
/** Authoritatively allocate an absent, fenced database tenant. */
|
|
431
|
+
async allocateDatabaseTenant(
|
|
432
|
+
params: ISmartDbAllocateDatabaseTenantInput,
|
|
433
|
+
optionsArg?: ISmartDbManagementOperationOptions,
|
|
434
|
+
): Promise<ISmartDbAllocateDatabaseTenantResult> {
|
|
435
|
+
const descriptor = await this.runDatabaseManagementOperation(
|
|
436
|
+
() => this.bridge.allocateDatabaseTenant(params, optionsArg),
|
|
437
|
+
);
|
|
438
|
+
return this.withTenantMongoUri(descriptor, params.password);
|
|
439
|
+
}
|
|
440
|
+
|
|
348
441
|
/**
|
|
349
442
|
* Idempotently provision or rotate one exclusively owned database tenant.
|
|
350
443
|
*/
|
|
@@ -465,6 +558,9 @@ export class SmartdbServer {
|
|
|
465
558
|
resourceFencingVersion: this.options.storage === 'file' ? 1 : undefined,
|
|
466
559
|
resourceFencingSupported: this.options.storage === 'file' ? false : undefined,
|
|
467
560
|
resourceFencingRequiresDrain: this.options.storage === 'file' ? true : undefined,
|
|
561
|
+
allocationFencingVersion: this.options.storage === 'file' ? 1 : undefined,
|
|
562
|
+
allocationFencingSupported: this.options.storage === 'file' ? false : undefined,
|
|
563
|
+
allocationFencingRequiresDrain: this.options.storage === 'file' ? true : undefined,
|
|
468
564
|
publicationHoldVersion: 1,
|
|
469
565
|
publicationHoldSupported: false,
|
|
470
566
|
publicationHoldRequiresExternalDrain: false,
|
|
@@ -5,6 +5,9 @@ export interface ISmartDbHealth {
|
|
|
5
5
|
resourceFencingVersion?: 1;
|
|
6
6
|
resourceFencingSupported?: boolean;
|
|
7
7
|
resourceFencingRequiresDrain?: true;
|
|
8
|
+
allocationFencingVersion?: 1;
|
|
9
|
+
allocationFencingSupported?: boolean;
|
|
10
|
+
allocationFencingRequiresDrain?: true;
|
|
8
11
|
publicationHoldVersion: 1;
|
|
9
12
|
publicationHoldSupported: boolean;
|
|
10
13
|
publicationHoldRequiresExternalDrain: boolean;
|
|
@@ -53,6 +56,11 @@ export const smartDbResourceFenceErrorCodes = [
|
|
|
53
56
|
'EFENCE_PUBLICATION_HELD',
|
|
54
57
|
'EFENCE_CAPABILITY_MISMATCH',
|
|
55
58
|
'EFENCE_NOT_HELD',
|
|
59
|
+
'EALLOCATION_EXPECTED_ABSENT_CONFLICT',
|
|
60
|
+
'EALLOCATION_REQUIRED',
|
|
61
|
+
'EALLOCATION_MISMATCH',
|
|
62
|
+
'EALLOCATION_GENERATION_EXHAUSTED',
|
|
63
|
+
'EALLOCATION_DEPROVISIONED',
|
|
56
64
|
] as const;
|
|
57
65
|
|
|
58
66
|
export type TSmartDbResourceFenceErrorCode =
|
|
@@ -71,7 +79,8 @@ export interface ISmartDbResourceFenceReceipt
|
|
|
71
79
|
kind:
|
|
72
80
|
| 'smartdb.database.replace.v1'
|
|
73
81
|
| 'smartdb.database.delete.v1'
|
|
74
|
-
| 'smartdb.database.ensure.v1'
|
|
82
|
+
| 'smartdb.database.ensure.v1'
|
|
83
|
+
| 'smartdb.database.allocate.v1';
|
|
75
84
|
/** Digest of the canonical payload interpretation derived by SmartDB. */
|
|
76
85
|
effectivePayloadSha256: string;
|
|
77
86
|
provider?: 'smartdb';
|
|
@@ -84,6 +93,35 @@ export interface ISmartDbResourceFenceReceipt
|
|
|
84
93
|
publicationCapabilitySha256?: string;
|
|
85
94
|
/** Present only while publication is held; never returned after release. */
|
|
86
95
|
publicationCapability?: string;
|
|
96
|
+
allocation?: ISmartDbDatabaseAllocationIdentity;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ISmartDbDatabaseAllocationIdentity {
|
|
100
|
+
version: 1;
|
|
101
|
+
provider: 'smartdb';
|
|
102
|
+
state: 'active';
|
|
103
|
+
databaseName: string;
|
|
104
|
+
username: string;
|
|
105
|
+
allocationId: string;
|
|
106
|
+
generation: number;
|
|
107
|
+
principalId: string;
|
|
108
|
+
resourceIdSha256: string;
|
|
109
|
+
bindingSha256: string;
|
|
110
|
+
receiptSha256: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface ISmartDbDatabaseAllocationLifecycle {
|
|
114
|
+
version: 1;
|
|
115
|
+
provider: 'smartdb';
|
|
116
|
+
state: 'allocating' | 'active' | 'deprovisioning' | 'deprovisioned';
|
|
117
|
+
databaseName: string;
|
|
118
|
+
username: string;
|
|
119
|
+
allocationId: string;
|
|
120
|
+
generation: number;
|
|
121
|
+
principalId: string;
|
|
122
|
+
resourceIdSha256: string;
|
|
123
|
+
bindingSha256: string;
|
|
124
|
+
receiptSha256: string;
|
|
87
125
|
}
|
|
88
126
|
|
|
89
127
|
export interface ISmartDbHeldPublicationReceipt
|
|
@@ -121,6 +159,7 @@ export interface ISmartDbDatabaseResourceFenceState {
|
|
|
121
159
|
scopeId: string;
|
|
122
160
|
highestToken: number;
|
|
123
161
|
publicationState: 'requested' | 'publishing' | 'held' | 'released';
|
|
162
|
+
allocation?: ISmartDbDatabaseAllocationLifecycle;
|
|
124
163
|
}
|
|
125
164
|
|
|
126
165
|
export class SmartDbResourceFenceError extends Error {
|
|
@@ -144,7 +183,7 @@ export const normalizeSmartDbResourceFenceError = (
|
|
|
144
183
|
errorArg instanceof Error
|
|
145
184
|
? errorArg
|
|
146
185
|
: new Error(String(errorArg));
|
|
147
|
-
const match = /^(
|
|
186
|
+
const match = /^((?:EFENCE|EALLOCATION)_[A-Z_]+):\s*(.*)$/s.exec(error.message);
|
|
148
187
|
if (
|
|
149
188
|
!match ||
|
|
150
189
|
!smartDbResourceFenceErrorCodes.includes(
|
|
@@ -170,6 +209,13 @@ export interface ISmartDbDatabaseTenantInput {
|
|
|
170
209
|
export interface ISmartDbEnsureDatabaseTenantInput
|
|
171
210
|
extends ISmartDbDatabaseTenantInput {
|
|
172
211
|
fence: ISmartDbResourceFence;
|
|
212
|
+
allocation?: ISmartDbDatabaseAllocationIdentity;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface ISmartDbAllocateDatabaseTenantInput
|
|
216
|
+
extends ISmartDbDatabaseTenantInput {
|
|
217
|
+
expectedAbsent: true;
|
|
218
|
+
fence: ISmartDbResourceFence;
|
|
173
219
|
}
|
|
174
220
|
|
|
175
221
|
export interface ISmartDbDeleteDatabaseTenantInput {
|
|
@@ -184,6 +230,7 @@ export interface ISmartDbDeleteDatabaseTenantInput {
|
|
|
184
230
|
*/
|
|
185
231
|
username?: string;
|
|
186
232
|
fence?: ISmartDbResourceFence;
|
|
233
|
+
allocation?: ISmartDbDatabaseAllocationIdentity;
|
|
187
234
|
/** Keep the database data plane closed until commitDatabasePublication. */
|
|
188
235
|
holdPublication?: boolean;
|
|
189
236
|
}
|
|
@@ -200,6 +247,7 @@ export interface ISmartDbDatabaseTenantDescriptor {
|
|
|
200
247
|
authSource: string;
|
|
201
248
|
mongodbUri?: string;
|
|
202
249
|
resourceFence?: ISmartDbResourceFenceReceipt;
|
|
250
|
+
allocation?: ISmartDbDatabaseAllocationIdentity;
|
|
203
251
|
}
|
|
204
252
|
|
|
205
253
|
export interface ISmartDbEnsureDatabaseTenantResult
|
|
@@ -207,6 +255,12 @@ export interface ISmartDbEnsureDatabaseTenantResult
|
|
|
207
255
|
resourceFence: ISmartDbResourceFenceReceipt;
|
|
208
256
|
}
|
|
209
257
|
|
|
258
|
+
export interface ISmartDbAllocateDatabaseTenantResult
|
|
259
|
+
extends ISmartDbDatabaseTenantDescriptor {
|
|
260
|
+
resourceFence: ISmartDbResourceFenceReceipt;
|
|
261
|
+
allocation: ISmartDbDatabaseAllocationIdentity;
|
|
262
|
+
}
|
|
263
|
+
|
|
210
264
|
export interface ISmartDbDeleteDatabaseTenantResult {
|
|
211
265
|
databaseName: string;
|
|
212
266
|
/**
|
|
@@ -222,6 +276,7 @@ export interface ISmartDbDeleteDatabaseTenantResult {
|
|
|
222
276
|
*/
|
|
223
277
|
usersPreserved?: boolean;
|
|
224
278
|
resourceFence?: ISmartDbResourceFenceReceipt;
|
|
279
|
+
allocation?: ISmartDbDatabaseAllocationIdentity;
|
|
225
280
|
}
|
|
226
281
|
|
|
227
282
|
export interface ISmartDbDatabaseExportCollection {
|
|
@@ -282,6 +337,7 @@ export interface ISmartDbImportDatabaseInput {
|
|
|
282
337
|
*/
|
|
283
338
|
username?: string;
|
|
284
339
|
fence?: ISmartDbResourceFence;
|
|
340
|
+
allocation?: ISmartDbDatabaseAllocationIdentity;
|
|
285
341
|
/** Keep the database data plane closed until commitDatabasePublication. */
|
|
286
342
|
holdPublication?: boolean;
|
|
287
343
|
}
|
|
@@ -291,6 +347,7 @@ export interface ISmartDbImportDatabaseResult {
|
|
|
291
347
|
collections: number;
|
|
292
348
|
documents: number;
|
|
293
349
|
resourceFence?: ISmartDbResourceFenceReceipt;
|
|
350
|
+
allocation?: ISmartDbDatabaseAllocationIdentity;
|
|
294
351
|
}
|
|
295
352
|
|
|
296
353
|
export type TSmartDbCommitDatabasePublicationResult =
|