spindb 0.34.5 → 0.35.3
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 +4 -4
- package/cli/commands/create.ts +22 -1
- package/cli/commands/engines.ts +56 -22
- package/cli/commands/menu/container-handlers.ts +17 -1
- package/cli/commands/menu/engine-handlers.ts +48 -29
- package/cli/ui/theme.ts +5 -2
- package/config/engines-registry.ts +56 -0
- package/config/engines.json +14 -3
- package/config/engines.schema.json +13 -0
- package/core/base-binary-manager.ts +6 -2
- package/core/base-document-binary-manager.ts +5 -2
- package/core/base-embedded-binary-manager.ts +5 -2
- package/core/base-server-binary-manager.ts +5 -2
- package/core/hostdb-client.ts +157 -22
- package/core/hostdb-metadata.ts +67 -43
- package/engines/clickhouse/binary-urls.ts +1 -1
- package/engines/cockroachdb/binary-urls.ts +9 -7
- package/engines/cockroachdb/hostdb-releases.ts +18 -106
- package/engines/cockroachdb/version-maps.ts +1 -1
- package/engines/couchdb/binary-urls.ts +1 -1
- package/engines/duckdb/binary-urls.ts +1 -1
- package/engines/duckdb/index.ts +4 -74
- package/engines/ferretdb/README.md +76 -38
- package/engines/ferretdb/backup.ts +18 -10
- package/engines/ferretdb/binary-manager.ts +233 -35
- package/engines/ferretdb/binary-urls.ts +69 -24
- package/engines/ferretdb/index.ts +424 -213
- package/engines/ferretdb/restore.ts +23 -16
- package/engines/ferretdb/version-maps.ts +36 -8
- package/engines/index.ts +3 -4
- package/engines/influxdb/binary-urls.ts +1 -1
- package/engines/mariadb/binary-urls.ts +2 -2
- package/engines/meilisearch/binary-urls.ts +1 -1
- package/engines/mysql/binary-urls.ts +2 -2
- package/engines/postgresql/binary-urls.ts +1 -1
- package/engines/qdrant/binary-urls.ts +1 -1
- package/engines/questdb/binary-manager.ts +16 -9
- package/engines/questdb/binary-urls.ts +9 -10
- package/engines/questdb/hostdb-releases.ts +19 -97
- package/engines/questdb/version-maps.ts +2 -2
- package/engines/redis/binary-urls.ts +1 -8
- package/engines/sqlite/binary-urls.ts +1 -1
- package/engines/sqlite/index.ts +4 -74
- package/engines/surrealdb/binary-urls.ts +9 -7
- package/engines/surrealdb/hostdb-releases.ts +18 -106
- package/engines/surrealdb/version-maps.ts +1 -1
- package/engines/typedb/binary-urls.ts +10 -8
- package/engines/typedb/hostdb-releases.ts +18 -113
- package/engines/typedb/version-maps.ts +1 -1
- package/engines/valkey/binary-urls.ts +1 -1
- package/package.json +4 -1
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
* - Stop: Stop FerretDB → Stop PostgreSQL
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
spawn,
|
|
16
|
+
exec,
|
|
17
|
+
type ChildProcess,
|
|
18
|
+
type SpawnOptions,
|
|
19
|
+
} from 'child_process'
|
|
15
20
|
import { promisify } from 'util'
|
|
16
21
|
import { existsSync } from 'fs'
|
|
17
22
|
import net from 'net'
|
|
@@ -43,8 +48,10 @@ import {
|
|
|
43
48
|
SUPPORTED_MAJOR_VERSIONS,
|
|
44
49
|
FALLBACK_VERSION_MAP,
|
|
45
50
|
DEFAULT_DOCUMENTDB_VERSION,
|
|
51
|
+
DEFAULT_V1_POSTGRESQL_VERSION,
|
|
46
52
|
normalizeVersion,
|
|
47
53
|
normalizeDocumentDBVersion,
|
|
54
|
+
isV1,
|
|
48
55
|
} from './version-maps'
|
|
49
56
|
import { getBinaryUrls, isPlatformSupported } from './binary-urls'
|
|
50
57
|
import {
|
|
@@ -157,6 +164,76 @@ function waitForPort(port: number, timeoutMs = 30000): Promise<boolean> {
|
|
|
157
164
|
})
|
|
158
165
|
}
|
|
159
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Spawn a process and pipe input to its stdin.
|
|
169
|
+
* Used for `postgres --single` which reads SQL from stdin.
|
|
170
|
+
*/
|
|
171
|
+
function spawnWithInput(
|
|
172
|
+
command: string,
|
|
173
|
+
args: string[],
|
|
174
|
+
input: string,
|
|
175
|
+
options?: { env?: Record<string, string>; timeout?: number },
|
|
176
|
+
): Promise<{ stdout: string; stderr: string }> {
|
|
177
|
+
return new Promise((resolve, reject) => {
|
|
178
|
+
let proc: ChildProcess
|
|
179
|
+
try {
|
|
180
|
+
proc = spawn(command, args, {
|
|
181
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
182
|
+
env: options?.env ? { ...process.env, ...options.env } : undefined,
|
|
183
|
+
})
|
|
184
|
+
} catch (error) {
|
|
185
|
+
reject(error)
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let stdout = ''
|
|
190
|
+
let stderr = ''
|
|
191
|
+
let timedOut = false
|
|
192
|
+
|
|
193
|
+
const timer = options?.timeout
|
|
194
|
+
? setTimeout(() => {
|
|
195
|
+
timedOut = true
|
|
196
|
+
proc.kill('SIGKILL')
|
|
197
|
+
reject(
|
|
198
|
+
new Error(
|
|
199
|
+
`Command "${command}" timed out after ${options.timeout}ms`,
|
|
200
|
+
),
|
|
201
|
+
)
|
|
202
|
+
}, options.timeout)
|
|
203
|
+
: undefined
|
|
204
|
+
|
|
205
|
+
proc.stdout?.on('data', (data: Buffer) => {
|
|
206
|
+
stdout += data.toString()
|
|
207
|
+
})
|
|
208
|
+
proc.stderr?.on('data', (data: Buffer) => {
|
|
209
|
+
stderr += data.toString()
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
proc.on('close', (code) => {
|
|
213
|
+
if (timer) clearTimeout(timer)
|
|
214
|
+
if (timedOut) return
|
|
215
|
+
if (code === 0) {
|
|
216
|
+
resolve({ stdout, stderr })
|
|
217
|
+
} else {
|
|
218
|
+
reject(
|
|
219
|
+
new Error(
|
|
220
|
+
`Command "${command}" failed with code ${code}: ${stderr || stdout}`,
|
|
221
|
+
),
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
proc.on('error', (err) => {
|
|
227
|
+
if (timer) clearTimeout(timer)
|
|
228
|
+
if (timedOut) return
|
|
229
|
+
reject(err)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
proc.stdin?.write(input)
|
|
233
|
+
proc.stdin?.end()
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
160
237
|
export class FerretDBEngine extends BaseEngine {
|
|
161
238
|
name = ENGINE
|
|
162
239
|
displayName = 'FerretDB'
|
|
@@ -170,10 +247,11 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
170
247
|
|
|
171
248
|
/**
|
|
172
249
|
* Check if the current platform supports FerretDB
|
|
250
|
+
* @param version - Optional version to check (v1 supports Windows, v2 does not)
|
|
173
251
|
*/
|
|
174
|
-
isPlatformSupported(): boolean {
|
|
252
|
+
isPlatformSupported(version?: string): boolean {
|
|
175
253
|
const { platform, arch } = this.getPlatformInfo()
|
|
176
|
-
return isPlatformSupported(platform, arch)
|
|
254
|
+
return isPlatformSupported(platform, arch, version)
|
|
177
255
|
}
|
|
178
256
|
|
|
179
257
|
/**
|
|
@@ -193,12 +271,10 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
193
271
|
|
|
194
272
|
// Get binary download URL from hostdb
|
|
195
273
|
getBinaryUrl(version: string, platform: Platform, arch: Arch): string {
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
DEFAULT_DOCUMENTDB_VERSION
|
|
199
|
-
|
|
200
|
-
arch,
|
|
201
|
-
)
|
|
274
|
+
const backendVersion = isV1(version)
|
|
275
|
+
? DEFAULT_V1_POSTGRESQL_VERSION
|
|
276
|
+
: DEFAULT_DOCUMENTDB_VERSION
|
|
277
|
+
const urls = getBinaryUrls(version, backendVersion, platform, arch)
|
|
202
278
|
return urls.ferretdb
|
|
203
279
|
}
|
|
204
280
|
|
|
@@ -224,12 +300,10 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
224
300
|
const { platform: p, arch: a } = this.getPlatformInfo()
|
|
225
301
|
|
|
226
302
|
if (version) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
DEFAULT_DOCUMENTDB_VERSION,
|
|
232
|
-
)
|
|
303
|
+
const backendVersion = isV1(version)
|
|
304
|
+
? DEFAULT_V1_POSTGRESQL_VERSION
|
|
305
|
+
: DEFAULT_DOCUMENTDB_VERSION
|
|
306
|
+
return ferretdbBinaryManager.isInstalled(version, p, a, backendVersion)
|
|
233
307
|
}
|
|
234
308
|
|
|
235
309
|
// Fallback: extract version from directory name
|
|
@@ -237,11 +311,14 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
237
311
|
const match = dirName.match(/^ferretdb-([\d.]+)-/)
|
|
238
312
|
if (match) {
|
|
239
313
|
const extractedVersion = match[1]
|
|
314
|
+
const backendVersion = isV1(extractedVersion)
|
|
315
|
+
? DEFAULT_V1_POSTGRESQL_VERSION
|
|
316
|
+
: DEFAULT_DOCUMENTDB_VERSION
|
|
240
317
|
return ferretdbBinaryManager.isInstalled(
|
|
241
318
|
extractedVersion,
|
|
242
319
|
p,
|
|
243
320
|
a,
|
|
244
|
-
|
|
321
|
+
backendVersion,
|
|
245
322
|
)
|
|
246
323
|
}
|
|
247
324
|
|
|
@@ -254,11 +331,14 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
254
331
|
// Check if a specific FerretDB version is installed
|
|
255
332
|
async isBinaryInstalled(version: string): Promise<boolean> {
|
|
256
333
|
const { platform, arch } = this.getPlatformInfo()
|
|
334
|
+
const backendVersion = isV1(version)
|
|
335
|
+
? DEFAULT_V1_POSTGRESQL_VERSION
|
|
336
|
+
: DEFAULT_DOCUMENTDB_VERSION
|
|
257
337
|
return ferretdbBinaryManager.isInstalled(
|
|
258
338
|
version,
|
|
259
339
|
platform,
|
|
260
340
|
arch,
|
|
261
|
-
|
|
341
|
+
backendVersion,
|
|
262
342
|
)
|
|
263
343
|
}
|
|
264
344
|
|
|
@@ -270,14 +350,17 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
270
350
|
onProgress?: ProgressCallback,
|
|
271
351
|
): Promise<string> {
|
|
272
352
|
const { platform, arch } = this.getPlatformInfo()
|
|
353
|
+
const backendVersion = isV1(version)
|
|
354
|
+
? DEFAULT_V1_POSTGRESQL_VERSION
|
|
355
|
+
: DEFAULT_DOCUMENTDB_VERSION
|
|
273
356
|
|
|
274
|
-
// Download
|
|
357
|
+
// Download binaries (proxy + backend)
|
|
275
358
|
const { ferretdbPath } = await ferretdbBinaryManager.ensureInstalled(
|
|
276
359
|
version,
|
|
277
360
|
platform,
|
|
278
361
|
arch,
|
|
279
362
|
onProgress,
|
|
280
|
-
|
|
363
|
+
backendVersion,
|
|
281
364
|
)
|
|
282
365
|
|
|
283
366
|
// Register ferretdb binary in config
|
|
@@ -290,6 +373,41 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
290
373
|
return ferretdbPath
|
|
291
374
|
}
|
|
292
375
|
|
|
376
|
+
/**
|
|
377
|
+
* Get backend binary paths and spawn environment for a container.
|
|
378
|
+
* Centralizes the v1/v2 branching logic for backend resolution.
|
|
379
|
+
*/
|
|
380
|
+
private getBackendPaths(
|
|
381
|
+
version: string,
|
|
382
|
+
backendVersion: string,
|
|
383
|
+
platform: Platform,
|
|
384
|
+
arch: Arch,
|
|
385
|
+
): { backendPath: string; pgSpawnEnv: Record<string, string> | undefined } {
|
|
386
|
+
const fullVersion = normalizeVersion(version)
|
|
387
|
+
const backendPath = ferretdbBinaryManager.getBackendBinaryPath(
|
|
388
|
+
fullVersion,
|
|
389
|
+
backendVersion,
|
|
390
|
+
platform,
|
|
391
|
+
arch,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
const baseSpawnEnv = ferretdbBinaryManager.getBackendSpawnEnv(
|
|
395
|
+
fullVersion,
|
|
396
|
+
backendVersion,
|
|
397
|
+
platform,
|
|
398
|
+
arch,
|
|
399
|
+
)
|
|
400
|
+
const pgSpawnEnv =
|
|
401
|
+
platform === 'darwin'
|
|
402
|
+
? {
|
|
403
|
+
...baseSpawnEnv,
|
|
404
|
+
DYLD_FALLBACK_LIBRARY_PATH: join(backendPath, 'lib'),
|
|
405
|
+
}
|
|
406
|
+
: baseSpawnEnv
|
|
407
|
+
|
|
408
|
+
return { backendPath, pgSpawnEnv }
|
|
409
|
+
}
|
|
410
|
+
|
|
293
411
|
/**
|
|
294
412
|
* Initialize a new FerretDB container directory
|
|
295
413
|
* Creates both the PostgreSQL data directory and FerretDB config
|
|
@@ -300,17 +418,16 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
300
418
|
options: Record<string, unknown> = {},
|
|
301
419
|
): Promise<string> {
|
|
302
420
|
const { platform, arch } = this.getPlatformInfo()
|
|
421
|
+
const version = normalizeVersion(_version)
|
|
422
|
+
const v1 = isV1(version)
|
|
303
423
|
|
|
304
|
-
// Get binary paths
|
|
305
|
-
const backendVersion =
|
|
306
|
-
(options.backendVersion as string) ||
|
|
307
|
-
|
|
424
|
+
// Get binary paths - resolve backend based on v1/v2
|
|
425
|
+
const backendVersion = v1
|
|
426
|
+
? (options.backendVersion as string) || DEFAULT_V1_POSTGRESQL_VERSION
|
|
427
|
+
: (options.backendVersion as string) || DEFAULT_DOCUMENTDB_VERSION
|
|
308
428
|
|
|
309
|
-
const documentdbPath =
|
|
310
|
-
|
|
311
|
-
platform,
|
|
312
|
-
arch,
|
|
313
|
-
)
|
|
429
|
+
const { backendPath: documentdbPath, pgSpawnEnv: initSpawnEnv } =
|
|
430
|
+
this.getBackendPaths(version, backendVersion, platform, arch)
|
|
314
431
|
|
|
315
432
|
// Container directory structure
|
|
316
433
|
const containerDir = paths.getContainerPath(containerName, {
|
|
@@ -335,13 +452,6 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
335
452
|
throw new Error(`initdb not found at ${initdb}`)
|
|
336
453
|
}
|
|
337
454
|
|
|
338
|
-
// Get spawn env for Linux (LD_LIBRARY_PATH)
|
|
339
|
-
const spawnEnv = ferretdbBinaryManager.getDocumentDBSpawnEnv(
|
|
340
|
-
fullBackendVersion,
|
|
341
|
-
platform,
|
|
342
|
-
arch,
|
|
343
|
-
)
|
|
344
|
-
|
|
345
455
|
// Homebrew-derived x64 binaries have compiled-in absolute paths for
|
|
346
456
|
// sharedir, pkglibdir ($libdir), and libdir that don't exist when running
|
|
347
457
|
// from ~/.spindb/bin/. We fix this by:
|
|
@@ -355,10 +465,9 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
355
465
|
? join(shareDirBase, 'postgresql')
|
|
356
466
|
: shareDirBase
|
|
357
467
|
|
|
358
|
-
// Homebrew-derived binaries
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
if (platform === 'darwin') {
|
|
468
|
+
// v2 only: Homebrew-derived DocumentDB binaries need compiled-in path fixups
|
|
469
|
+
// v1 uses plain PostgreSQL which has correct relative paths
|
|
470
|
+
if (!v1 && platform === 'darwin') {
|
|
362
471
|
const pgConfigBin = join(documentdbPath, 'bin', `pg_config${ext}`)
|
|
363
472
|
if (existsSync(pgConfigBin)) {
|
|
364
473
|
// Query all relevant compiled-in paths and create symlinks where needed
|
|
@@ -420,17 +529,15 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
420
529
|
}
|
|
421
530
|
}
|
|
422
531
|
}
|
|
423
|
-
} else {
|
|
532
|
+
} else if (!v1) {
|
|
424
533
|
logDebug(
|
|
425
534
|
'Skipping pg_config symlink fixups (not required on this platform)',
|
|
426
535
|
)
|
|
427
536
|
}
|
|
428
537
|
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
|
|
432
|
-
// via absolute paths that don't exist on the target machine.
|
|
433
|
-
if (platform === 'darwin') {
|
|
538
|
+
// v2 only: Fix hardcoded Homebrew dylib paths in DocumentDB extension libraries
|
|
539
|
+
// v1 uses plain PostgreSQL which doesn't have DocumentDB extensions
|
|
540
|
+
if (!v1 && platform === 'darwin') {
|
|
434
541
|
const dylibMarker = join(documentdbPath, '.dylib_fix_done')
|
|
435
542
|
if (!existsSync(dylibMarker)) {
|
|
436
543
|
await this.fixDylibDependencies(documentdbPath)
|
|
@@ -442,16 +549,6 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
442
549
|
}
|
|
443
550
|
}
|
|
444
551
|
|
|
445
|
-
// On macOS, set DYLD_FALLBACK_LIBRARY_PATH as additional library search path.
|
|
446
|
-
// Unlike DYLD_LIBRARY_PATH, this is NOT stripped by SIP.
|
|
447
|
-
const initdbEnv =
|
|
448
|
-
platform === 'darwin'
|
|
449
|
-
? {
|
|
450
|
-
...spawnEnv,
|
|
451
|
-
DYLD_FALLBACK_LIBRARY_PATH: join(documentdbPath, 'lib'),
|
|
452
|
-
}
|
|
453
|
-
: spawnEnv
|
|
454
|
-
|
|
455
552
|
try {
|
|
456
553
|
await spawnAsync(
|
|
457
554
|
initdb,
|
|
@@ -465,7 +562,7 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
465
562
|
'-L',
|
|
466
563
|
actualShareDir,
|
|
467
564
|
],
|
|
468
|
-
{ env:
|
|
565
|
+
{ env: initSpawnEnv, timeout: 60000 },
|
|
469
566
|
)
|
|
470
567
|
logDebug(`Initialized PostgreSQL data directory: ${pgDataDir}`)
|
|
471
568
|
} catch (error) {
|
|
@@ -473,37 +570,40 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
473
570
|
throw new Error(`Failed to initialize PostgreSQL: ${err.message}`)
|
|
474
571
|
}
|
|
475
572
|
|
|
476
|
-
// Copy the bundled postgresql.conf.sample to ensure shared_preload_libraries is set
|
|
573
|
+
// v2 only: Copy the bundled postgresql.conf.sample to ensure shared_preload_libraries is set
|
|
477
574
|
// This is critical for DocumentDB extension to load properly
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
try {
|
|
487
|
-
// Read the bundled config
|
|
488
|
-
let confContent = await readFile(bundledConf, 'utf8')
|
|
489
|
-
|
|
490
|
-
// Update cron.database_name to 'ferretdb' (required for pg_cron to work with DocumentDB)
|
|
491
|
-
confContent = confContent.replace(
|
|
492
|
-
/cron\.database_name\s*=\s*'[^']*'/,
|
|
493
|
-
"cron.database_name = 'ferretdb'",
|
|
494
|
-
)
|
|
575
|
+
// v1 uses initdb defaults (no DocumentDB extensions to preload)
|
|
576
|
+
if (!v1) {
|
|
577
|
+
const bundledConf = existsSync(
|
|
578
|
+
join(shareDirBase, 'postgresql.conf.sample'),
|
|
579
|
+
)
|
|
580
|
+
? join(shareDirBase, 'postgresql.conf.sample')
|
|
581
|
+
: join(shareDirBase, 'postgresql', 'postgresql.conf.sample')
|
|
582
|
+
const pgConf = join(pgDataDir, 'postgresql.conf')
|
|
495
583
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
584
|
+
if (existsSync(bundledConf)) {
|
|
585
|
+
try {
|
|
586
|
+
// Read the bundled config
|
|
587
|
+
let confContent = await readFile(bundledConf, 'utf8')
|
|
588
|
+
|
|
589
|
+
// Update cron.database_name to 'ferretdb' (required for pg_cron to work with DocumentDB)
|
|
590
|
+
confContent = confContent.replace(
|
|
591
|
+
/cron\.database_name\s*=\s*'[^']*'/,
|
|
592
|
+
"cron.database_name = 'ferretdb'",
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
// Write the modified config
|
|
596
|
+
await writeFile(pgConf, confContent)
|
|
597
|
+
logDebug(`Copied and configured postgresql.conf to ${pgConf}`)
|
|
598
|
+
} catch (copyError) {
|
|
599
|
+
logDebug(
|
|
600
|
+
`Warning: Could not copy postgresql.conf.sample: ${copyError}`,
|
|
601
|
+
)
|
|
602
|
+
// Continue anyway - initdb creates a default config
|
|
603
|
+
}
|
|
604
|
+
} else {
|
|
605
|
+
logDebug(`Bundled postgresql.conf.sample not found at ${bundledConf}`)
|
|
504
606
|
}
|
|
505
|
-
} else {
|
|
506
|
-
logDebug(`Bundled postgresql.conf.sample not found at ${bundledConf}`)
|
|
507
607
|
}
|
|
508
608
|
}
|
|
509
609
|
|
|
@@ -545,42 +645,32 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
545
645
|
|
|
546
646
|
const { platform, arch } = this.getPlatformInfo()
|
|
547
647
|
const fullVersion = normalizeVersion(version)
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
648
|
+
const v1 = isV1(version)
|
|
649
|
+
const effectiveBackendVersion = v1
|
|
650
|
+
? backendVersion || DEFAULT_V1_POSTGRESQL_VERSION
|
|
651
|
+
: normalizeDocumentDBVersion(backendVersion || DEFAULT_DOCUMENTDB_VERSION)
|
|
551
652
|
|
|
552
|
-
// Get binary paths
|
|
653
|
+
// Get binary paths using version-aware helper
|
|
553
654
|
const ferretdbPath = ferretdbBinaryManager.getFerretDBBinaryPath(
|
|
554
655
|
fullVersion,
|
|
555
656
|
platform,
|
|
556
657
|
arch,
|
|
557
658
|
)
|
|
558
|
-
const documentdbPath =
|
|
559
|
-
|
|
659
|
+
const { backendPath: documentdbPath, pgSpawnEnv } = this.getBackendPaths(
|
|
660
|
+
version,
|
|
661
|
+
effectiveBackendVersion,
|
|
560
662
|
platform,
|
|
561
663
|
arch,
|
|
562
664
|
)
|
|
563
665
|
|
|
564
|
-
// Get spawn env for postgresql-documentdb binaries:
|
|
565
|
-
// - Linux: LD_LIBRARY_PATH for shared libraries
|
|
566
|
-
// - macOS: DYLD_FALLBACK_LIBRARY_PATH (not stripped by SIP)
|
|
567
|
-
const baseSpawnEnv = ferretdbBinaryManager.getDocumentDBSpawnEnv(
|
|
568
|
-
fullBackendVersion,
|
|
569
|
-
platform,
|
|
570
|
-
arch,
|
|
571
|
-
)
|
|
572
|
-
const pgSpawnEnv =
|
|
573
|
-
platform === 'darwin'
|
|
574
|
-
? {
|
|
575
|
-
...baseSpawnEnv,
|
|
576
|
-
DYLD_FALLBACK_LIBRARY_PATH: join(documentdbPath, 'lib'),
|
|
577
|
-
}
|
|
578
|
-
: baseSpawnEnv
|
|
579
|
-
|
|
580
666
|
const ext = platformService.getExecutableExtension()
|
|
581
667
|
const ferretdbBinary = join(ferretdbPath, 'bin', `ferretdb${ext}`)
|
|
582
668
|
const pgCtl = join(documentdbPath, 'bin', `pg_ctl${ext}`)
|
|
583
|
-
|
|
669
|
+
// v1 backend may be a minimal PostgreSQL install (shared with DocumentDB) that
|
|
670
|
+
// lacks client tools. Use postgres --single as fallback for database creation.
|
|
671
|
+
const psqlCandidate = join(documentdbPath, 'bin', `psql${ext}`)
|
|
672
|
+
const psql = existsSync(psqlCandidate) ? psqlCandidate : null
|
|
673
|
+
const postgresBinary = join(documentdbPath, 'bin', `postgres${ext}`)
|
|
584
674
|
|
|
585
675
|
// Verify binaries exist
|
|
586
676
|
if (!existsSync(ferretdbBinary)) {
|
|
@@ -604,9 +694,10 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
604
694
|
// Allocate backend port
|
|
605
695
|
const backendPort = existingBackendPort || (await allocateBackendPort())
|
|
606
696
|
|
|
607
|
-
// Fix hardcoded Homebrew dylib paths (darwin-x64 binaries)
|
|
697
|
+
// v2 only: Fix hardcoded Homebrew dylib paths (darwin-x64 binaries)
|
|
608
698
|
// Skip if already completed (marker written by initDataDir or a previous start)
|
|
609
|
-
|
|
699
|
+
// v1 uses plain PostgreSQL which doesn't have DocumentDB extensions
|
|
700
|
+
if (!v1 && platform === 'darwin') {
|
|
610
701
|
const dylibMarker = join(documentdbPath, '.dylib_fix_done')
|
|
611
702
|
if (!existsSync(dylibMarker)) {
|
|
612
703
|
await this.fixDylibDependencies(documentdbPath)
|
|
@@ -642,13 +733,46 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
642
733
|
// Exit code != 0 means not running — proceed to start
|
|
643
734
|
}
|
|
644
735
|
|
|
736
|
+
// v1 pre-start: Create ferretdb database using postgres --single mode
|
|
737
|
+
// when psql is unavailable (minimal PG install may lack client tools).
|
|
738
|
+
// postgres --single requires exclusive data dir access, so this MUST
|
|
739
|
+
// happen before pg_ctl start.
|
|
740
|
+
if (v1 && !psql && !pgAlreadyRunning) {
|
|
741
|
+
logDebug(
|
|
742
|
+
'psql not found in backend, using postgres --single to pre-create database',
|
|
743
|
+
)
|
|
744
|
+
try {
|
|
745
|
+
await spawnWithInput(
|
|
746
|
+
postgresBinary,
|
|
747
|
+
['--single', '-D', pgDataDir, 'postgres'],
|
|
748
|
+
"CREATE DATABASE ferretdb ENCODING 'UTF8';\n",
|
|
749
|
+
{ env: pgSpawnEnv, timeout: 30000 },
|
|
750
|
+
)
|
|
751
|
+
logDebug('Pre-created ferretdb database via postgres --single')
|
|
752
|
+
} catch {
|
|
753
|
+
// Database may already exist from a previous start — safe to ignore
|
|
754
|
+
logDebug(
|
|
755
|
+
'postgres --single CREATE DATABASE failed (may already exist)',
|
|
756
|
+
)
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
645
760
|
if (!pgAlreadyRunning) {
|
|
646
761
|
// Use pg_ctl to start PostgreSQL
|
|
647
|
-
//
|
|
762
|
+
// On Windows, spawnAsync pipes stdout/stderr which get inherited by the
|
|
763
|
+
// PostgreSQL background process, preventing the 'close' event from firing
|
|
764
|
+
// until PG itself exits (causing a 60s timeout even though PG is ready).
|
|
765
|
+
// Use exec() on Windows (matches process-manager.ts approach) which runs
|
|
766
|
+
// through the shell and doesn't hold pipes open. On Unix, use -w (wait mode).
|
|
648
767
|
try {
|
|
649
|
-
|
|
650
|
-
pgCtl
|
|
651
|
-
|
|
768
|
+
if (isWindows()) {
|
|
769
|
+
const cmd = `"${pgCtl}" start -D "${pgDataDir}" -l "${pgLogFile}" -o "-p ${backendPort} -h 127.0.0.1"`
|
|
770
|
+
await execAsync(cmd, {
|
|
771
|
+
env: { ...process.env, ...pgSpawnEnv },
|
|
772
|
+
timeout: 30000,
|
|
773
|
+
})
|
|
774
|
+
} else {
|
|
775
|
+
const pgCtlArgs = [
|
|
652
776
|
'start',
|
|
653
777
|
'-D',
|
|
654
778
|
pgDataDir,
|
|
@@ -656,10 +780,13 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
656
780
|
pgLogFile,
|
|
657
781
|
'-o',
|
|
658
782
|
`-p ${backendPort} -h 127.0.0.1`,
|
|
659
|
-
'-w',
|
|
660
|
-
]
|
|
661
|
-
|
|
662
|
-
|
|
783
|
+
'-w',
|
|
784
|
+
]
|
|
785
|
+
await spawnAsync(pgCtl, pgCtlArgs, {
|
|
786
|
+
env: pgSpawnEnv,
|
|
787
|
+
timeout: 60000,
|
|
788
|
+
})
|
|
789
|
+
}
|
|
663
790
|
} catch (pgError) {
|
|
664
791
|
// Read PostgreSQL log for debugging
|
|
665
792
|
let pgLog = ''
|
|
@@ -686,56 +813,60 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
686
813
|
}
|
|
687
814
|
|
|
688
815
|
// 3. Create ferretdb database and extension (first start)
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
// Create ferretdb database if it doesn't exist
|
|
695
|
-
// Add timeout to prevent hanging on Windows
|
|
696
|
-
await spawnAsync(
|
|
697
|
-
psql,
|
|
698
|
-
[
|
|
699
|
-
'-h',
|
|
700
|
-
'127.0.0.1',
|
|
701
|
-
'-p',
|
|
702
|
-
String(backendPort),
|
|
703
|
-
'-U',
|
|
704
|
-
'postgres',
|
|
705
|
-
'-c',
|
|
706
|
-
"CREATE DATABASE ferretdb WITH ENCODING 'UTF8';",
|
|
707
|
-
],
|
|
708
|
-
{ env: pgSpawnEnv, timeout: 30000 },
|
|
709
|
-
).catch(() => {
|
|
710
|
-
// Ignore error if database already exists (error code 42P04)
|
|
816
|
+
// For v1 without psql, database was already created pre-start via postgres --single
|
|
817
|
+
if (psql) {
|
|
818
|
+
onProgress?.({
|
|
819
|
+
stage: 'starting',
|
|
820
|
+
message: 'Initializing FerretDB database...',
|
|
711
821
|
})
|
|
822
|
+
try {
|
|
823
|
+
// Create ferretdb database if it doesn't exist
|
|
824
|
+
await spawnAsync(
|
|
825
|
+
psql,
|
|
826
|
+
[
|
|
827
|
+
'-h',
|
|
828
|
+
'127.0.0.1',
|
|
829
|
+
'-p',
|
|
830
|
+
String(backendPort),
|
|
831
|
+
'-U',
|
|
832
|
+
'postgres',
|
|
833
|
+
'-c',
|
|
834
|
+
"CREATE DATABASE ferretdb WITH ENCODING 'UTF8';",
|
|
835
|
+
],
|
|
836
|
+
{ env: pgSpawnEnv, timeout: 30000 },
|
|
837
|
+
).catch(() => {
|
|
838
|
+
// Ignore error if database already exists (error code 42P04)
|
|
839
|
+
})
|
|
712
840
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
841
|
+
// v2 only: Create DocumentDB extension
|
|
842
|
+
// v1 uses plain PostgreSQL without DocumentDB
|
|
843
|
+
if (!v1) {
|
|
844
|
+
await spawnAsync(
|
|
845
|
+
psql,
|
|
846
|
+
[
|
|
847
|
+
'-h',
|
|
848
|
+
'127.0.0.1',
|
|
849
|
+
'-p',
|
|
850
|
+
String(backendPort),
|
|
851
|
+
'-U',
|
|
852
|
+
'postgres',
|
|
853
|
+
'-d',
|
|
854
|
+
'ferretdb',
|
|
855
|
+
'-c',
|
|
856
|
+
'CREATE EXTENSION IF NOT EXISTS documentdb CASCADE;',
|
|
857
|
+
],
|
|
858
|
+
{ env: pgSpawnEnv, timeout: 30000 },
|
|
859
|
+
).catch((error) => {
|
|
860
|
+
logWarning(`Failed to create documentdb extension: ${error}`)
|
|
861
|
+
// Continue anyway - extension might already exist
|
|
862
|
+
})
|
|
863
|
+
}
|
|
734
864
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
865
|
+
logDebug('FerretDB database initialized')
|
|
866
|
+
} catch (error) {
|
|
867
|
+
logDebug(`Database initialization warning: ${error}`)
|
|
868
|
+
// Continue - might already be initialized
|
|
869
|
+
}
|
|
739
870
|
}
|
|
740
871
|
|
|
741
872
|
// 4. Start FerretDB proxy
|
|
@@ -773,14 +904,22 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
773
904
|
|
|
774
905
|
logDebug(`Using debug port ${debugPort} for FerretDB HTTP debug handler`)
|
|
775
906
|
|
|
907
|
+
// v1 uses plain PostgreSQL without TLS configured, so sslmode=disable is required
|
|
908
|
+
// v2 uses postgresql-documentdb which handles SSL negotiation internally
|
|
909
|
+
const pgUrl = isV1(version)
|
|
910
|
+
? `postgres://postgres@127.0.0.1:${backendPort}/ferretdb?sslmode=disable`
|
|
911
|
+
: `postgres://postgres@127.0.0.1:${backendPort}/ferretdb`
|
|
912
|
+
|
|
776
913
|
const ferretArgs = [
|
|
777
914
|
'--listen-addr',
|
|
778
915
|
`127.0.0.1:${port}`,
|
|
779
916
|
'--postgresql-url',
|
|
780
|
-
|
|
917
|
+
pgUrl,
|
|
781
918
|
'--state-dir',
|
|
782
919
|
containerDir,
|
|
783
|
-
|
|
920
|
+
// v2 requires --no-auth to disable SCRAM authentication
|
|
921
|
+
// v1 has auth disabled by default (flag doesn't exist)
|
|
922
|
+
...(isV1(version) ? [] : ['--no-auth']),
|
|
784
923
|
'--debug-addr',
|
|
785
924
|
`127.0.0.1:${debugPort}`,
|
|
786
925
|
]
|
|
@@ -853,32 +992,20 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
853
992
|
* Stop FerretDB (reverse order: FerretDB first, then PostgreSQL)
|
|
854
993
|
*/
|
|
855
994
|
async stop(container: ContainerConfig): Promise<void> {
|
|
856
|
-
const { name, backendVersion } = container
|
|
995
|
+
const { name, version, backendVersion } = container
|
|
857
996
|
const { platform, arch } = this.getPlatformInfo()
|
|
997
|
+
const v1 = isV1(version)
|
|
858
998
|
|
|
859
|
-
const
|
|
860
|
-
backendVersion ||
|
|
861
|
-
|
|
999
|
+
const effectiveBackendVersion = v1
|
|
1000
|
+
? backendVersion || DEFAULT_V1_POSTGRESQL_VERSION
|
|
1001
|
+
: backendVersion || DEFAULT_DOCUMENTDB_VERSION
|
|
862
1002
|
|
|
863
|
-
const documentdbPath =
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
arch,
|
|
867
|
-
)
|
|
868
|
-
|
|
869
|
-
// Get spawn env for postgresql-documentdb binaries
|
|
870
|
-
const baseStopEnv = ferretdbBinaryManager.getDocumentDBSpawnEnv(
|
|
871
|
-
fullBackendVersion,
|
|
1003
|
+
const { backendPath: documentdbPath, pgSpawnEnv } = this.getBackendPaths(
|
|
1004
|
+
version,
|
|
1005
|
+
effectiveBackendVersion,
|
|
872
1006
|
platform,
|
|
873
1007
|
arch,
|
|
874
1008
|
)
|
|
875
|
-
const pgSpawnEnv =
|
|
876
|
-
platform === 'darwin'
|
|
877
|
-
? {
|
|
878
|
-
...baseStopEnv,
|
|
879
|
-
DYLD_FALLBACK_LIBRARY_PATH: join(documentdbPath, 'lib'),
|
|
880
|
-
}
|
|
881
|
-
: baseStopEnv
|
|
882
1009
|
|
|
883
1010
|
const ext = platformService.getExecutableExtension()
|
|
884
1011
|
const pgCtl = join(documentdbPath, 'bin', `pg_ctl${ext}`)
|
|
@@ -1002,13 +1129,32 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
1002
1129
|
const pidFile = join(containerDir, 'ferretdb.pid')
|
|
1003
1130
|
|
|
1004
1131
|
if (existsSync(pidFile)) {
|
|
1132
|
+
let pid = NaN
|
|
1005
1133
|
try {
|
|
1006
1134
|
const pidContent = await readFile(pidFile, 'utf8')
|
|
1007
|
-
|
|
1135
|
+
pid = parseInt(pidContent.trim(), 10)
|
|
1136
|
+
} catch {
|
|
1137
|
+
// PID file unreadable — clean it up below
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
if (!isNaN(pid) && platformService.isProcessRunning(pid)) {
|
|
1141
|
+
logDebug(`Killing FerretDB process ${pid}`)
|
|
1008
1142
|
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1143
|
+
// On Windows, taskkill without /F sends WM_CLOSE which console/server
|
|
1144
|
+
// processes ignore, causing an error. Use force kill directly.
|
|
1145
|
+
if (isWindows()) {
|
|
1146
|
+
try {
|
|
1147
|
+
await platformService.terminateProcess(pid, true)
|
|
1148
|
+
} catch {
|
|
1149
|
+
logDebug(`Force kill of FerretDB process ${pid} failed`)
|
|
1150
|
+
}
|
|
1151
|
+
} else {
|
|
1152
|
+
// Unix: try graceful SIGTERM first, then SIGKILL
|
|
1153
|
+
try {
|
|
1154
|
+
await platformService.terminateProcess(pid, false)
|
|
1155
|
+
} catch {
|
|
1156
|
+
// Graceful termination failed — force kill below
|
|
1157
|
+
}
|
|
1012
1158
|
|
|
1013
1159
|
// Poll until process exits or timeout (10 seconds)
|
|
1014
1160
|
const maxWaitMs = 10000
|
|
@@ -1026,14 +1172,26 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
1026
1172
|
// Force kill if still running after timeout
|
|
1027
1173
|
if (platformService.isProcessRunning(pid)) {
|
|
1028
1174
|
logWarning(`Graceful termination timed out, force killing ${pid}`)
|
|
1029
|
-
|
|
1175
|
+
try {
|
|
1176
|
+
await platformService.terminateProcess(pid, true)
|
|
1177
|
+
} catch {
|
|
1178
|
+
logDebug(`Force kill of FerretDB process ${pid} failed`)
|
|
1179
|
+
}
|
|
1030
1180
|
}
|
|
1031
1181
|
}
|
|
1032
1182
|
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1183
|
+
// Wait briefly for process to fully exit after force kill
|
|
1184
|
+
const exitWaitMs = isWindows() ? 3000 : 1000
|
|
1185
|
+
const pollMs = 100
|
|
1186
|
+
const exitStart = Date.now()
|
|
1187
|
+
while (Date.now() - exitStart < exitWaitMs) {
|
|
1188
|
+
if (!platformService.isProcessRunning(pid)) break
|
|
1189
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs))
|
|
1190
|
+
}
|
|
1036
1191
|
}
|
|
1192
|
+
|
|
1193
|
+
// Always clean up PID file
|
|
1194
|
+
await unlink(pidFile).catch(() => {})
|
|
1037
1195
|
}
|
|
1038
1196
|
}
|
|
1039
1197
|
|
|
@@ -1045,24 +1203,45 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
1045
1203
|
pgDataDir: string,
|
|
1046
1204
|
spawnEnv?: Record<string, string>,
|
|
1047
1205
|
): Promise<void> {
|
|
1048
|
-
|
|
1049
|
-
//
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
timeout: 30000,
|
|
1053
|
-
})
|
|
1054
|
-
logDebug('PostgreSQL stopped')
|
|
1055
|
-
} catch (error) {
|
|
1056
|
-
logDebug(`pg_ctl stop error: ${error}`)
|
|
1057
|
-
// Try immediate mode if fast fails
|
|
1206
|
+
if (isWindows()) {
|
|
1207
|
+
// On Windows, use exec() instead of spawnAsync() to avoid pipe-related
|
|
1208
|
+
// hangs (same issue as pg_ctl start -w). pg_ctl stop -w can block when
|
|
1209
|
+
// stdout/stderr pipes prevent the child process from exiting cleanly.
|
|
1058
1210
|
try {
|
|
1059
|
-
await
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
)
|
|
1064
|
-
} catch {
|
|
1065
|
-
|
|
1211
|
+
await execAsync(`"${pgCtl}" stop -D "${pgDataDir}" -m fast -w`, {
|
|
1212
|
+
timeout: 30000,
|
|
1213
|
+
env: spawnEnv ? { ...process.env, ...spawnEnv } : undefined,
|
|
1214
|
+
})
|
|
1215
|
+
logDebug('PostgreSQL stopped')
|
|
1216
|
+
} catch (error) {
|
|
1217
|
+
logDebug(`pg_ctl stop error: ${error}`)
|
|
1218
|
+
try {
|
|
1219
|
+
await execAsync(`"${pgCtl}" stop -D "${pgDataDir}" -m immediate -w`, {
|
|
1220
|
+
timeout: 15000,
|
|
1221
|
+
env: spawnEnv ? { ...process.env, ...spawnEnv } : undefined,
|
|
1222
|
+
})
|
|
1223
|
+
} catch {
|
|
1224
|
+
logWarning('Failed to stop PostgreSQL gracefully')
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
} else {
|
|
1228
|
+
try {
|
|
1229
|
+
await spawnAsync(pgCtl, ['stop', '-D', pgDataDir, '-m', 'fast', '-w'], {
|
|
1230
|
+
env: spawnEnv,
|
|
1231
|
+
timeout: 30000,
|
|
1232
|
+
})
|
|
1233
|
+
logDebug('PostgreSQL stopped')
|
|
1234
|
+
} catch (error) {
|
|
1235
|
+
logDebug(`pg_ctl stop error: ${error}`)
|
|
1236
|
+
try {
|
|
1237
|
+
await spawnAsync(
|
|
1238
|
+
pgCtl,
|
|
1239
|
+
['stop', '-D', pgDataDir, '-m', 'immediate', '-w'],
|
|
1240
|
+
{ env: spawnEnv, timeout: 15000 },
|
|
1241
|
+
)
|
|
1242
|
+
} catch {
|
|
1243
|
+
logWarning('Failed to stop PostgreSQL gracefully')
|
|
1244
|
+
}
|
|
1066
1245
|
}
|
|
1067
1246
|
}
|
|
1068
1247
|
}
|
|
@@ -1136,10 +1315,41 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
1136
1315
|
// Validate database name before restore (defense-in-depth)
|
|
1137
1316
|
assertValidDatabaseName(database)
|
|
1138
1317
|
|
|
1139
|
-
|
|
1318
|
+
const result = await restoreBackup(container, backupPath, {
|
|
1140
1319
|
database,
|
|
1141
1320
|
drop: options.drop !== false,
|
|
1142
1321
|
})
|
|
1322
|
+
|
|
1323
|
+
// Restart FerretDB proxy so it picks up the restored data.
|
|
1324
|
+
// pg_restore writes directly to PostgreSQL, but FerretDB's proxy
|
|
1325
|
+
// caches schema/collection metadata in memory and won't see
|
|
1326
|
+
// the restored collections until restarted.
|
|
1327
|
+
const containerDir = paths.getContainerPath(container.name, {
|
|
1328
|
+
engine: ENGINE,
|
|
1329
|
+
})
|
|
1330
|
+
try {
|
|
1331
|
+
await this.stopFerretDBProcess(containerDir)
|
|
1332
|
+
// start() detects PG is already running and only launches the proxy
|
|
1333
|
+
await this.start(container)
|
|
1334
|
+
} catch (error) {
|
|
1335
|
+
const err = error as Error
|
|
1336
|
+
logWarning(
|
|
1337
|
+
`Failed to restart FerretDB proxy after restore: ${err.message}`,
|
|
1338
|
+
)
|
|
1339
|
+
// Retry once — transient issues (port race, slow PG) can resolve on second attempt
|
|
1340
|
+
try {
|
|
1341
|
+
await this.stopFerretDBProcess(containerDir).catch(() => {})
|
|
1342
|
+
await this.start(container)
|
|
1343
|
+
} catch {
|
|
1344
|
+
throw new Error(
|
|
1345
|
+
`Restore succeeded but FerretDB proxy failed to restart. ` +
|
|
1346
|
+
`Data is safely in PostgreSQL. Run 'spindb start ${container.name}' to restart manually. ` +
|
|
1347
|
+
`Original error: ${err.message}`,
|
|
1348
|
+
)
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
return result
|
|
1143
1353
|
}
|
|
1144
1354
|
|
|
1145
1355
|
// Get connection string (MongoDB-compatible)
|
|
@@ -1280,7 +1490,8 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
1280
1490
|
const lastBrace = stdout.lastIndexOf('}')
|
|
1281
1491
|
if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
|
|
1282
1492
|
const stats = JSON.parse(stdout.substring(firstBrace, lastBrace + 1))
|
|
1283
|
-
|
|
1493
|
+
const dataSize = Number(stats?.dataSize)
|
|
1494
|
+
return Number.isFinite(dataSize) && dataSize > 0 ? dataSize : null
|
|
1284
1495
|
}
|
|
1285
1496
|
return null
|
|
1286
1497
|
} catch {
|