drizzle-multitenant 1.0.7 → 1.0.9
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 +1 -1
- package/README.md +36 -372
- package/dist/cli/index.js +686 -6
- package/dist/cli/index.js.map +1 -1
- package/dist/{context-DBerWr50.d.ts → context-DoHx79MS.d.ts} +1 -1
- package/dist/cross-schema/index.d.ts +152 -1
- package/dist/cross-schema/index.js +208 -1
- package/dist/cross-schema/index.js.map +1 -1
- package/dist/index.d.ts +62 -5
- package/dist/index.js +1181 -50
- package/dist/index.js.map +1 -1
- package/dist/integrations/express.d.ts +3 -3
- package/dist/integrations/fastify.d.ts +3 -3
- package/dist/integrations/nestjs/index.d.ts +1 -1
- package/dist/integrations/nestjs/index.js +484 -3
- package/dist/integrations/nestjs/index.js.map +1 -1
- package/dist/migrator/index.d.ts +116 -1
- package/dist/migrator/index.js +418 -0
- package/dist/migrator/index.js.map +1 -1
- package/dist/types-B5eSRLFW.d.ts +235 -0
- package/package.json +9 -3
- package/.claude/settings.local.json +0 -20
- package/dist/types-DKVaTaIb.d.ts +0 -130
- package/proposals/improvements.md +0 -383
- package/roadmap.md +0 -921
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Request, Response, NextFunction, RequestHandler } from 'express';
|
|
2
|
-
import { T as TenantManager } from '../types-
|
|
3
|
-
import { T as TenantContextData, a as TenantContext } from '../context-
|
|
4
|
-
export { c as createTenantContext } from '../context-
|
|
2
|
+
import { T as TenantManager } from '../types-B5eSRLFW.js';
|
|
3
|
+
import { T as TenantContextData, a as TenantContext } from '../context-DoHx79MS.js';
|
|
4
|
+
export { c as createTenantContext } from '../context-DoHx79MS.js';
|
|
5
5
|
import 'pg';
|
|
6
6
|
import 'drizzle-orm/node-postgres';
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FastifyRequest, FastifyReply, FastifyPluginAsync } from 'fastify';
|
|
2
|
-
import { T as TenantManager } from '../types-
|
|
3
|
-
import { T as TenantContextData, a as TenantContext } from '../context-
|
|
4
|
-
export { c as createTenantContext } from '../context-
|
|
2
|
+
import { T as TenantManager } from '../types-B5eSRLFW.js';
|
|
3
|
+
import { T as TenantContextData, a as TenantContext } from '../context-DoHx79MS.js';
|
|
4
|
+
export { c as createTenantContext } from '../context-DoHx79MS.js';
|
|
5
5
|
import 'pg';
|
|
6
6
|
import 'drizzle-orm/node-postgres';
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as _nestjs_common from '@nestjs/common';
|
|
2
2
|
import { ModuleMetadata, Type, InjectionToken, DynamicModule, CanActivate, ExecutionContext, NestInterceptor, CallHandler, Provider } from '@nestjs/common';
|
|
3
3
|
import { Request } from 'express';
|
|
4
|
-
import { C as Config, a as TenantDb, T as TenantManager, S as SharedDb } from '../../types-
|
|
4
|
+
import { C as Config, a as TenantDb, T as TenantManager, S as SharedDb } from '../../types-B5eSRLFW.js';
|
|
5
5
|
import { Reflector } from '@nestjs/core';
|
|
6
6
|
import { Observable } from 'rxjs';
|
|
7
7
|
import 'pg';
|
|
@@ -9225,14 +9225,265 @@ var DEFAULT_CONFIG = {
|
|
|
9225
9225
|
max: 10,
|
|
9226
9226
|
idleTimeoutMillis: 3e4,
|
|
9227
9227
|
connectionTimeoutMillis: 5e3
|
|
9228
|
+
},
|
|
9229
|
+
retry: {
|
|
9230
|
+
maxAttempts: 3,
|
|
9231
|
+
initialDelayMs: 100,
|
|
9232
|
+
maxDelayMs: 5e3,
|
|
9233
|
+
backoffMultiplier: 2,
|
|
9234
|
+
jitter: true
|
|
9228
9235
|
}
|
|
9229
9236
|
};
|
|
9230
9237
|
|
|
9238
|
+
// src/debug.ts
|
|
9239
|
+
var PREFIX = "[drizzle-multitenant]";
|
|
9240
|
+
var DEFAULT_SLOW_QUERY_THRESHOLD = 1e3;
|
|
9241
|
+
var DebugLogger = class {
|
|
9242
|
+
enabled;
|
|
9243
|
+
logQueries;
|
|
9244
|
+
logPoolEvents;
|
|
9245
|
+
slowQueryThreshold;
|
|
9246
|
+
logger;
|
|
9247
|
+
constructor(config) {
|
|
9248
|
+
this.enabled = config?.enabled ?? false;
|
|
9249
|
+
this.logQueries = config?.logQueries ?? true;
|
|
9250
|
+
this.logPoolEvents = config?.logPoolEvents ?? true;
|
|
9251
|
+
this.slowQueryThreshold = config?.slowQueryThreshold ?? DEFAULT_SLOW_QUERY_THRESHOLD;
|
|
9252
|
+
this.logger = config?.logger ?? this.defaultLogger;
|
|
9253
|
+
}
|
|
9254
|
+
/**
|
|
9255
|
+
* Check if debug mode is enabled
|
|
9256
|
+
*/
|
|
9257
|
+
isEnabled() {
|
|
9258
|
+
return this.enabled;
|
|
9259
|
+
}
|
|
9260
|
+
/**
|
|
9261
|
+
* Log a query execution
|
|
9262
|
+
*/
|
|
9263
|
+
logQuery(tenantId, query, durationMs) {
|
|
9264
|
+
if (!this.enabled || !this.logQueries) return;
|
|
9265
|
+
const isSlowQuery = durationMs >= this.slowQueryThreshold;
|
|
9266
|
+
const type = isSlowQuery ? "slow_query" : "query";
|
|
9267
|
+
const context = {
|
|
9268
|
+
type,
|
|
9269
|
+
tenantId,
|
|
9270
|
+
query: this.truncateQuery(query),
|
|
9271
|
+
durationMs
|
|
9272
|
+
};
|
|
9273
|
+
if (isSlowQuery) {
|
|
9274
|
+
this.logger(
|
|
9275
|
+
`${PREFIX} tenant=${tenantId} SLOW_QUERY duration=${durationMs}ms query="${this.truncateQuery(query)}"`,
|
|
9276
|
+
context
|
|
9277
|
+
);
|
|
9278
|
+
} else {
|
|
9279
|
+
this.logger(
|
|
9280
|
+
`${PREFIX} tenant=${tenantId} query="${this.truncateQuery(query)}" duration=${durationMs}ms`,
|
|
9281
|
+
context
|
|
9282
|
+
);
|
|
9283
|
+
}
|
|
9284
|
+
}
|
|
9285
|
+
/**
|
|
9286
|
+
* Log pool creation
|
|
9287
|
+
*/
|
|
9288
|
+
logPoolCreated(tenantId, schemaName) {
|
|
9289
|
+
if (!this.enabled || !this.logPoolEvents) return;
|
|
9290
|
+
const context = {
|
|
9291
|
+
type: "pool_created",
|
|
9292
|
+
tenantId,
|
|
9293
|
+
schemaName
|
|
9294
|
+
};
|
|
9295
|
+
this.logger(
|
|
9296
|
+
`${PREFIX} tenant=${tenantId} POOL_CREATED schema=${schemaName}`,
|
|
9297
|
+
context
|
|
9298
|
+
);
|
|
9299
|
+
}
|
|
9300
|
+
/**
|
|
9301
|
+
* Log pool eviction
|
|
9302
|
+
*/
|
|
9303
|
+
logPoolEvicted(tenantId, schemaName, reason) {
|
|
9304
|
+
if (!this.enabled || !this.logPoolEvents) return;
|
|
9305
|
+
const context = {
|
|
9306
|
+
type: "pool_evicted",
|
|
9307
|
+
tenantId,
|
|
9308
|
+
schemaName,
|
|
9309
|
+
metadata: reason ? { reason } : void 0
|
|
9310
|
+
};
|
|
9311
|
+
const reasonStr = reason ? ` reason=${reason}` : "";
|
|
9312
|
+
this.logger(
|
|
9313
|
+
`${PREFIX} tenant=${tenantId} POOL_EVICTED schema=${schemaName}${reasonStr}`,
|
|
9314
|
+
context
|
|
9315
|
+
);
|
|
9316
|
+
}
|
|
9317
|
+
/**
|
|
9318
|
+
* Log pool error
|
|
9319
|
+
*/
|
|
9320
|
+
logPoolError(tenantId, error) {
|
|
9321
|
+
if (!this.enabled || !this.logPoolEvents) return;
|
|
9322
|
+
const context = {
|
|
9323
|
+
type: "pool_error",
|
|
9324
|
+
tenantId,
|
|
9325
|
+
error: error.message
|
|
9326
|
+
};
|
|
9327
|
+
this.logger(
|
|
9328
|
+
`${PREFIX} tenant=${tenantId} POOL_ERROR error="${error.message}"`,
|
|
9329
|
+
context
|
|
9330
|
+
);
|
|
9331
|
+
}
|
|
9332
|
+
/**
|
|
9333
|
+
* Log warmup event
|
|
9334
|
+
*/
|
|
9335
|
+
logWarmup(tenantId, success, durationMs, alreadyWarm) {
|
|
9336
|
+
if (!this.enabled || !this.logPoolEvents) return;
|
|
9337
|
+
const context = {
|
|
9338
|
+
type: "warmup",
|
|
9339
|
+
tenantId,
|
|
9340
|
+
durationMs,
|
|
9341
|
+
metadata: { success, alreadyWarm }
|
|
9342
|
+
};
|
|
9343
|
+
const status = alreadyWarm ? "already_warm" : success ? "success" : "failed";
|
|
9344
|
+
this.logger(
|
|
9345
|
+
`${PREFIX} tenant=${tenantId} WARMUP status=${status} duration=${durationMs}ms`,
|
|
9346
|
+
context
|
|
9347
|
+
);
|
|
9348
|
+
}
|
|
9349
|
+
/**
|
|
9350
|
+
* Log connection retry event
|
|
9351
|
+
*/
|
|
9352
|
+
logConnectionRetry(identifier, attempt, maxAttempts, error, delayMs) {
|
|
9353
|
+
if (!this.enabled || !this.logPoolEvents) return;
|
|
9354
|
+
const context = {
|
|
9355
|
+
type: "connection_retry",
|
|
9356
|
+
tenantId: identifier,
|
|
9357
|
+
error: error.message,
|
|
9358
|
+
metadata: { attempt, maxAttempts, delayMs }
|
|
9359
|
+
};
|
|
9360
|
+
this.logger(
|
|
9361
|
+
`${PREFIX} tenant=${identifier} CONNECTION_RETRY attempt=${attempt}/${maxAttempts} delay=${delayMs}ms error="${error.message}"`,
|
|
9362
|
+
context
|
|
9363
|
+
);
|
|
9364
|
+
}
|
|
9365
|
+
/**
|
|
9366
|
+
* Log connection success after retries
|
|
9367
|
+
*/
|
|
9368
|
+
logConnectionSuccess(identifier, attempts, totalTimeMs) {
|
|
9369
|
+
if (!this.enabled || !this.logPoolEvents) return;
|
|
9370
|
+
const context = {
|
|
9371
|
+
type: "pool_created",
|
|
9372
|
+
tenantId: identifier,
|
|
9373
|
+
durationMs: totalTimeMs,
|
|
9374
|
+
metadata: { attempts }
|
|
9375
|
+
};
|
|
9376
|
+
if (attempts > 1) {
|
|
9377
|
+
this.logger(
|
|
9378
|
+
`${PREFIX} tenant=${identifier} CONNECTION_SUCCESS attempts=${attempts} totalTime=${totalTimeMs}ms`,
|
|
9379
|
+
context
|
|
9380
|
+
);
|
|
9381
|
+
}
|
|
9382
|
+
}
|
|
9383
|
+
/**
|
|
9384
|
+
* Log a custom debug message
|
|
9385
|
+
*/
|
|
9386
|
+
log(message, context) {
|
|
9387
|
+
if (!this.enabled) return;
|
|
9388
|
+
this.logger(`${PREFIX} ${message}`, context);
|
|
9389
|
+
}
|
|
9390
|
+
/**
|
|
9391
|
+
* Default logger implementation using console
|
|
9392
|
+
*/
|
|
9393
|
+
defaultLogger(message, _context) {
|
|
9394
|
+
console.log(message);
|
|
9395
|
+
}
|
|
9396
|
+
/**
|
|
9397
|
+
* Truncate long queries for readability
|
|
9398
|
+
*/
|
|
9399
|
+
truncateQuery(query, maxLength = 100) {
|
|
9400
|
+
const normalized = query.replace(/\s+/g, " ").trim();
|
|
9401
|
+
if (normalized.length <= maxLength) {
|
|
9402
|
+
return normalized;
|
|
9403
|
+
}
|
|
9404
|
+
return normalized.substring(0, maxLength - 3) + "...";
|
|
9405
|
+
}
|
|
9406
|
+
};
|
|
9407
|
+
function createDebugLogger(config) {
|
|
9408
|
+
return new DebugLogger(config);
|
|
9409
|
+
}
|
|
9410
|
+
|
|
9411
|
+
// src/retry.ts
|
|
9412
|
+
function isRetryableError(error) {
|
|
9413
|
+
const message = error.message.toLowerCase();
|
|
9414
|
+
if (message.includes("econnrefused") || message.includes("econnreset") || message.includes("etimedout") || message.includes("enotfound") || message.includes("connection refused") || message.includes("connection reset") || message.includes("connection terminated") || message.includes("connection timed out") || message.includes("timeout expired") || message.includes("socket hang up")) {
|
|
9415
|
+
return true;
|
|
9416
|
+
}
|
|
9417
|
+
if (message.includes("too many connections") || message.includes("sorry, too many clients") || message.includes("the database system is starting up") || message.includes("the database system is shutting down") || message.includes("server closed the connection unexpectedly") || message.includes("could not connect to server")) {
|
|
9418
|
+
return true;
|
|
9419
|
+
}
|
|
9420
|
+
if (message.includes("ssl connection") || message.includes("ssl handshake")) {
|
|
9421
|
+
return true;
|
|
9422
|
+
}
|
|
9423
|
+
return false;
|
|
9424
|
+
}
|
|
9425
|
+
function calculateDelay(attempt, config) {
|
|
9426
|
+
const exponentialDelay = config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt);
|
|
9427
|
+
const cappedDelay = Math.min(exponentialDelay, config.maxDelayMs);
|
|
9428
|
+
if (config.jitter) {
|
|
9429
|
+
const jitterFactor = 1 + Math.random() * 0.25;
|
|
9430
|
+
return Math.floor(cappedDelay * jitterFactor);
|
|
9431
|
+
}
|
|
9432
|
+
return Math.floor(cappedDelay);
|
|
9433
|
+
}
|
|
9434
|
+
function sleep(ms) {
|
|
9435
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
9436
|
+
}
|
|
9437
|
+
async function withRetry(operation, config) {
|
|
9438
|
+
const retryConfig = {
|
|
9439
|
+
maxAttempts: config?.maxAttempts ?? DEFAULT_CONFIG.retry.maxAttempts,
|
|
9440
|
+
initialDelayMs: config?.initialDelayMs ?? DEFAULT_CONFIG.retry.initialDelayMs,
|
|
9441
|
+
maxDelayMs: config?.maxDelayMs ?? DEFAULT_CONFIG.retry.maxDelayMs,
|
|
9442
|
+
backoffMultiplier: config?.backoffMultiplier ?? DEFAULT_CONFIG.retry.backoffMultiplier,
|
|
9443
|
+
jitter: config?.jitter ?? DEFAULT_CONFIG.retry.jitter,
|
|
9444
|
+
isRetryable: config?.isRetryable ?? isRetryableError,
|
|
9445
|
+
onRetry: config?.onRetry
|
|
9446
|
+
};
|
|
9447
|
+
const startTime = Date.now();
|
|
9448
|
+
let lastError = null;
|
|
9449
|
+
for (let attempt = 0; attempt < retryConfig.maxAttempts; attempt++) {
|
|
9450
|
+
try {
|
|
9451
|
+
const result = await operation();
|
|
9452
|
+
return {
|
|
9453
|
+
result,
|
|
9454
|
+
attempts: attempt + 1,
|
|
9455
|
+
totalTimeMs: Date.now() - startTime
|
|
9456
|
+
};
|
|
9457
|
+
} catch (error) {
|
|
9458
|
+
lastError = error;
|
|
9459
|
+
const isLastAttempt = attempt >= retryConfig.maxAttempts - 1;
|
|
9460
|
+
if (isLastAttempt || !retryConfig.isRetryable(lastError)) {
|
|
9461
|
+
throw lastError;
|
|
9462
|
+
}
|
|
9463
|
+
const delay = calculateDelay(attempt, retryConfig);
|
|
9464
|
+
retryConfig.onRetry?.(attempt + 1, lastError, delay);
|
|
9465
|
+
await sleep(delay);
|
|
9466
|
+
}
|
|
9467
|
+
}
|
|
9468
|
+
throw lastError ?? new Error("Retry failed with no error");
|
|
9469
|
+
}
|
|
9470
|
+
|
|
9231
9471
|
// src/pool.ts
|
|
9232
9472
|
var PoolManager = class {
|
|
9233
9473
|
constructor(config) {
|
|
9234
9474
|
this.config = config;
|
|
9235
9475
|
const maxPools = config.isolation.maxPools ?? DEFAULT_CONFIG.maxPools;
|
|
9476
|
+
this.debugLogger = createDebugLogger(config.debug);
|
|
9477
|
+
const userRetry = config.connection.retry ?? {};
|
|
9478
|
+
this.retryConfig = {
|
|
9479
|
+
maxAttempts: userRetry.maxAttempts ?? DEFAULT_CONFIG.retry.maxAttempts,
|
|
9480
|
+
initialDelayMs: userRetry.initialDelayMs ?? DEFAULT_CONFIG.retry.initialDelayMs,
|
|
9481
|
+
maxDelayMs: userRetry.maxDelayMs ?? DEFAULT_CONFIG.retry.maxDelayMs,
|
|
9482
|
+
backoffMultiplier: userRetry.backoffMultiplier ?? DEFAULT_CONFIG.retry.backoffMultiplier,
|
|
9483
|
+
jitter: userRetry.jitter ?? DEFAULT_CONFIG.retry.jitter,
|
|
9484
|
+
isRetryable: userRetry.isRetryable ?? isRetryableError,
|
|
9485
|
+
onRetry: userRetry.onRetry
|
|
9486
|
+
};
|
|
9236
9487
|
this.pools = new LRUCache({
|
|
9237
9488
|
max: maxPools,
|
|
9238
9489
|
dispose: (entry, key) => {
|
|
@@ -9243,10 +9494,14 @@ var PoolManager = class {
|
|
|
9243
9494
|
}
|
|
9244
9495
|
pools;
|
|
9245
9496
|
tenantIdBySchema = /* @__PURE__ */ new Map();
|
|
9497
|
+
pendingConnections = /* @__PURE__ */ new Map();
|
|
9246
9498
|
sharedPool = null;
|
|
9247
9499
|
sharedDb = null;
|
|
9500
|
+
sharedDbPending = null;
|
|
9248
9501
|
cleanupInterval = null;
|
|
9249
9502
|
disposed = false;
|
|
9503
|
+
debugLogger;
|
|
9504
|
+
retryConfig;
|
|
9250
9505
|
/**
|
|
9251
9506
|
* Get or create a database connection for a tenant
|
|
9252
9507
|
*/
|
|
@@ -9258,11 +9513,85 @@ var PoolManager = class {
|
|
|
9258
9513
|
entry = this.createPoolEntry(tenantId, schemaName);
|
|
9259
9514
|
this.pools.set(schemaName, entry);
|
|
9260
9515
|
this.tenantIdBySchema.set(schemaName, tenantId);
|
|
9516
|
+
this.debugLogger.logPoolCreated(tenantId, schemaName);
|
|
9261
9517
|
void this.config.hooks?.onPoolCreated?.(tenantId);
|
|
9262
9518
|
}
|
|
9263
9519
|
entry.lastAccess = Date.now();
|
|
9264
9520
|
return entry.db;
|
|
9265
9521
|
}
|
|
9522
|
+
/**
|
|
9523
|
+
* Get or create a database connection for a tenant with retry and validation
|
|
9524
|
+
*
|
|
9525
|
+
* This async version validates the connection by executing a ping query
|
|
9526
|
+
* and retries on transient failures with exponential backoff.
|
|
9527
|
+
*
|
|
9528
|
+
* @example
|
|
9529
|
+
* ```typescript
|
|
9530
|
+
* // Get tenant database with automatic retry
|
|
9531
|
+
* const db = await manager.getDbAsync('tenant-123');
|
|
9532
|
+
*
|
|
9533
|
+
* // Queries will use the validated connection
|
|
9534
|
+
* const users = await db.select().from(users);
|
|
9535
|
+
* ```
|
|
9536
|
+
*/
|
|
9537
|
+
async getDbAsync(tenantId) {
|
|
9538
|
+
this.ensureNotDisposed();
|
|
9539
|
+
const schemaName = this.config.isolation.schemaNameTemplate(tenantId);
|
|
9540
|
+
let entry = this.pools.get(schemaName);
|
|
9541
|
+
if (entry) {
|
|
9542
|
+
entry.lastAccess = Date.now();
|
|
9543
|
+
return entry.db;
|
|
9544
|
+
}
|
|
9545
|
+
const pending = this.pendingConnections.get(schemaName);
|
|
9546
|
+
if (pending) {
|
|
9547
|
+
entry = await pending;
|
|
9548
|
+
entry.lastAccess = Date.now();
|
|
9549
|
+
return entry.db;
|
|
9550
|
+
}
|
|
9551
|
+
const connectionPromise = this.connectWithRetry(tenantId, schemaName);
|
|
9552
|
+
this.pendingConnections.set(schemaName, connectionPromise);
|
|
9553
|
+
try {
|
|
9554
|
+
entry = await connectionPromise;
|
|
9555
|
+
this.pools.set(schemaName, entry);
|
|
9556
|
+
this.tenantIdBySchema.set(schemaName, tenantId);
|
|
9557
|
+
this.debugLogger.logPoolCreated(tenantId, schemaName);
|
|
9558
|
+
void this.config.hooks?.onPoolCreated?.(tenantId);
|
|
9559
|
+
entry.lastAccess = Date.now();
|
|
9560
|
+
return entry.db;
|
|
9561
|
+
} finally {
|
|
9562
|
+
this.pendingConnections.delete(schemaName);
|
|
9563
|
+
}
|
|
9564
|
+
}
|
|
9565
|
+
/**
|
|
9566
|
+
* Connect to a tenant database with retry logic
|
|
9567
|
+
*/
|
|
9568
|
+
async connectWithRetry(tenantId, schemaName) {
|
|
9569
|
+
const maxAttempts = this.retryConfig.maxAttempts;
|
|
9570
|
+
const result = await withRetry(
|
|
9571
|
+
async () => {
|
|
9572
|
+
const entry = this.createPoolEntry(tenantId, schemaName);
|
|
9573
|
+
try {
|
|
9574
|
+
await entry.pool.query("SELECT 1");
|
|
9575
|
+
return entry;
|
|
9576
|
+
} catch (error) {
|
|
9577
|
+
try {
|
|
9578
|
+
await entry.pool.end();
|
|
9579
|
+
} catch {
|
|
9580
|
+
}
|
|
9581
|
+
throw error;
|
|
9582
|
+
}
|
|
9583
|
+
},
|
|
9584
|
+
{
|
|
9585
|
+
...this.retryConfig,
|
|
9586
|
+
onRetry: (attempt, error, delayMs) => {
|
|
9587
|
+
this.debugLogger.logConnectionRetry(tenantId, attempt, maxAttempts, error, delayMs);
|
|
9588
|
+
this.retryConfig.onRetry?.(attempt, error, delayMs);
|
|
9589
|
+
}
|
|
9590
|
+
}
|
|
9591
|
+
);
|
|
9592
|
+
this.debugLogger.logConnectionSuccess(tenantId, result.attempts, result.totalTimeMs);
|
|
9593
|
+
return result.result;
|
|
9594
|
+
}
|
|
9266
9595
|
/**
|
|
9267
9596
|
* Get or create the shared database connection
|
|
9268
9597
|
*/
|
|
@@ -9283,6 +9612,78 @@ var PoolManager = class {
|
|
|
9283
9612
|
}
|
|
9284
9613
|
return this.sharedDb;
|
|
9285
9614
|
}
|
|
9615
|
+
/**
|
|
9616
|
+
* Get or create the shared database connection with retry and validation
|
|
9617
|
+
*
|
|
9618
|
+
* This async version validates the connection by executing a ping query
|
|
9619
|
+
* and retries on transient failures with exponential backoff.
|
|
9620
|
+
*
|
|
9621
|
+
* @example
|
|
9622
|
+
* ```typescript
|
|
9623
|
+
* // Get shared database with automatic retry
|
|
9624
|
+
* const sharedDb = await manager.getSharedDbAsync();
|
|
9625
|
+
*
|
|
9626
|
+
* // Queries will use the validated connection
|
|
9627
|
+
* const plans = await sharedDb.select().from(plans);
|
|
9628
|
+
* ```
|
|
9629
|
+
*/
|
|
9630
|
+
async getSharedDbAsync() {
|
|
9631
|
+
this.ensureNotDisposed();
|
|
9632
|
+
if (this.sharedDb) {
|
|
9633
|
+
return this.sharedDb;
|
|
9634
|
+
}
|
|
9635
|
+
if (this.sharedDbPending) {
|
|
9636
|
+
return this.sharedDbPending;
|
|
9637
|
+
}
|
|
9638
|
+
this.sharedDbPending = this.connectSharedWithRetry();
|
|
9639
|
+
try {
|
|
9640
|
+
const db = await this.sharedDbPending;
|
|
9641
|
+
return db;
|
|
9642
|
+
} finally {
|
|
9643
|
+
this.sharedDbPending = null;
|
|
9644
|
+
}
|
|
9645
|
+
}
|
|
9646
|
+
/**
|
|
9647
|
+
* Connect to shared database with retry logic
|
|
9648
|
+
*/
|
|
9649
|
+
async connectSharedWithRetry() {
|
|
9650
|
+
const maxAttempts = this.retryConfig.maxAttempts;
|
|
9651
|
+
const result = await withRetry(
|
|
9652
|
+
async () => {
|
|
9653
|
+
const pool = new Pool({
|
|
9654
|
+
connectionString: this.config.connection.url,
|
|
9655
|
+
...DEFAULT_CONFIG.poolConfig,
|
|
9656
|
+
...this.config.connection.poolConfig
|
|
9657
|
+
});
|
|
9658
|
+
try {
|
|
9659
|
+
await pool.query("SELECT 1");
|
|
9660
|
+
pool.on("error", (err) => {
|
|
9661
|
+
void this.config.hooks?.onError?.("shared", err);
|
|
9662
|
+
});
|
|
9663
|
+
this.sharedPool = pool;
|
|
9664
|
+
this.sharedDb = drizzle(pool, {
|
|
9665
|
+
schema: this.config.schemas.shared
|
|
9666
|
+
});
|
|
9667
|
+
return this.sharedDb;
|
|
9668
|
+
} catch (error) {
|
|
9669
|
+
try {
|
|
9670
|
+
await pool.end();
|
|
9671
|
+
} catch {
|
|
9672
|
+
}
|
|
9673
|
+
throw error;
|
|
9674
|
+
}
|
|
9675
|
+
},
|
|
9676
|
+
{
|
|
9677
|
+
...this.retryConfig,
|
|
9678
|
+
onRetry: (attempt, error, delayMs) => {
|
|
9679
|
+
this.debugLogger.logConnectionRetry("shared", attempt, maxAttempts, error, delayMs);
|
|
9680
|
+
this.retryConfig.onRetry?.(attempt, error, delayMs);
|
|
9681
|
+
}
|
|
9682
|
+
}
|
|
9683
|
+
);
|
|
9684
|
+
this.debugLogger.logConnectionSuccess("shared", result.attempts, result.totalTimeMs);
|
|
9685
|
+
return result.result;
|
|
9686
|
+
}
|
|
9286
9687
|
/**
|
|
9287
9688
|
* Get schema name for a tenant
|
|
9288
9689
|
*/
|
|
@@ -9308,13 +9709,77 @@ var PoolManager = class {
|
|
|
9308
9709
|
getActiveTenantIds() {
|
|
9309
9710
|
return Array.from(this.tenantIdBySchema.values());
|
|
9310
9711
|
}
|
|
9712
|
+
/**
|
|
9713
|
+
* Get the retry configuration
|
|
9714
|
+
*/
|
|
9715
|
+
getRetryConfig() {
|
|
9716
|
+
return { ...this.retryConfig };
|
|
9717
|
+
}
|
|
9718
|
+
/**
|
|
9719
|
+
* Pre-warm pools for specified tenants to reduce cold start latency
|
|
9720
|
+
*
|
|
9721
|
+
* Uses automatic retry with exponential backoff for connection failures.
|
|
9722
|
+
*/
|
|
9723
|
+
async warmup(tenantIds, options = {}) {
|
|
9724
|
+
this.ensureNotDisposed();
|
|
9725
|
+
const startTime = Date.now();
|
|
9726
|
+
const { concurrency = 10, ping = true, onProgress } = options;
|
|
9727
|
+
const results = [];
|
|
9728
|
+
for (let i = 0; i < tenantIds.length; i += concurrency) {
|
|
9729
|
+
const batch = tenantIds.slice(i, i + concurrency);
|
|
9730
|
+
const batchResults = await Promise.all(
|
|
9731
|
+
batch.map(async (tenantId) => {
|
|
9732
|
+
const tenantStart = Date.now();
|
|
9733
|
+
onProgress?.(tenantId, "starting");
|
|
9734
|
+
try {
|
|
9735
|
+
const alreadyWarm = this.hasPool(tenantId);
|
|
9736
|
+
if (ping) {
|
|
9737
|
+
await this.getDbAsync(tenantId);
|
|
9738
|
+
} else {
|
|
9739
|
+
this.getDb(tenantId);
|
|
9740
|
+
}
|
|
9741
|
+
const durationMs = Date.now() - tenantStart;
|
|
9742
|
+
onProgress?.(tenantId, "completed");
|
|
9743
|
+
this.debugLogger.logWarmup(tenantId, true, durationMs, alreadyWarm);
|
|
9744
|
+
return {
|
|
9745
|
+
tenantId,
|
|
9746
|
+
success: true,
|
|
9747
|
+
alreadyWarm,
|
|
9748
|
+
durationMs
|
|
9749
|
+
};
|
|
9750
|
+
} catch (error) {
|
|
9751
|
+
const durationMs = Date.now() - tenantStart;
|
|
9752
|
+
onProgress?.(tenantId, "failed");
|
|
9753
|
+
this.debugLogger.logWarmup(tenantId, false, durationMs, false);
|
|
9754
|
+
return {
|
|
9755
|
+
tenantId,
|
|
9756
|
+
success: false,
|
|
9757
|
+
alreadyWarm: false,
|
|
9758
|
+
durationMs,
|
|
9759
|
+
error: error.message
|
|
9760
|
+
};
|
|
9761
|
+
}
|
|
9762
|
+
})
|
|
9763
|
+
);
|
|
9764
|
+
results.push(...batchResults);
|
|
9765
|
+
}
|
|
9766
|
+
return {
|
|
9767
|
+
total: results.length,
|
|
9768
|
+
succeeded: results.filter((r) => r.success).length,
|
|
9769
|
+
failed: results.filter((r) => !r.success).length,
|
|
9770
|
+
alreadyWarm: results.filter((r) => r.alreadyWarm).length,
|
|
9771
|
+
durationMs: Date.now() - startTime,
|
|
9772
|
+
details: results
|
|
9773
|
+
};
|
|
9774
|
+
}
|
|
9311
9775
|
/**
|
|
9312
9776
|
* Manually evict a tenant pool
|
|
9313
9777
|
*/
|
|
9314
|
-
async evictPool(tenantId) {
|
|
9778
|
+
async evictPool(tenantId, reason = "manual") {
|
|
9315
9779
|
const schemaName = this.config.isolation.schemaNameTemplate(tenantId);
|
|
9316
9780
|
const entry = this.pools.get(schemaName);
|
|
9317
9781
|
if (entry) {
|
|
9782
|
+
this.debugLogger.logPoolEvicted(tenantId, schemaName, reason);
|
|
9318
9783
|
this.pools.delete(schemaName);
|
|
9319
9784
|
this.tenantIdBySchema.delete(schemaName);
|
|
9320
9785
|
await this.closePool(entry.pool, tenantId);
|
|
@@ -9373,8 +9838,9 @@ var PoolManager = class {
|
|
|
9373
9838
|
options: `-c search_path=${schemaName},public`
|
|
9374
9839
|
});
|
|
9375
9840
|
pool.on("error", async (err) => {
|
|
9841
|
+
this.debugLogger.logPoolError(tenantId, err);
|
|
9376
9842
|
void this.config.hooks?.onError?.(tenantId, err);
|
|
9377
|
-
await this.evictPool(tenantId);
|
|
9843
|
+
await this.evictPool(tenantId, "error");
|
|
9378
9844
|
});
|
|
9379
9845
|
const db = drizzle(pool, {
|
|
9380
9846
|
schema: this.config.schemas.tenant
|
|
@@ -9392,6 +9858,9 @@ var PoolManager = class {
|
|
|
9392
9858
|
disposePoolEntry(entry, schemaName) {
|
|
9393
9859
|
const tenantId = this.tenantIdBySchema.get(schemaName);
|
|
9394
9860
|
this.tenantIdBySchema.delete(schemaName);
|
|
9861
|
+
if (tenantId) {
|
|
9862
|
+
this.debugLogger.logPoolEvicted(tenantId, schemaName, "lru_eviction");
|
|
9863
|
+
}
|
|
9395
9864
|
void this.closePool(entry.pool, tenantId ?? schemaName).then(() => {
|
|
9396
9865
|
if (tenantId) {
|
|
9397
9866
|
void this.config.hooks?.onPoolEvicted?.(tenantId);
|
|
@@ -9422,7 +9891,7 @@ var PoolManager = class {
|
|
|
9422
9891
|
for (const schemaName of toEvict) {
|
|
9423
9892
|
const tenantId = this.tenantIdBySchema.get(schemaName);
|
|
9424
9893
|
if (tenantId) {
|
|
9425
|
-
await this.evictPool(tenantId);
|
|
9894
|
+
await this.evictPool(tenantId, "ttl_expired");
|
|
9426
9895
|
}
|
|
9427
9896
|
}
|
|
9428
9897
|
}
|
|
@@ -9444,9 +9913,15 @@ function createTenantManager(config) {
|
|
|
9444
9913
|
getDb(tenantId) {
|
|
9445
9914
|
return poolManager.getDb(tenantId);
|
|
9446
9915
|
},
|
|
9916
|
+
async getDbAsync(tenantId) {
|
|
9917
|
+
return poolManager.getDbAsync(tenantId);
|
|
9918
|
+
},
|
|
9447
9919
|
getSharedDb() {
|
|
9448
9920
|
return poolManager.getSharedDb();
|
|
9449
9921
|
},
|
|
9922
|
+
async getSharedDbAsync() {
|
|
9923
|
+
return poolManager.getSharedDbAsync();
|
|
9924
|
+
},
|
|
9450
9925
|
getSchemaName(tenantId) {
|
|
9451
9926
|
return poolManager.getSchemaName(tenantId);
|
|
9452
9927
|
},
|
|
@@ -9459,9 +9934,15 @@ function createTenantManager(config) {
|
|
|
9459
9934
|
getActiveTenantIds() {
|
|
9460
9935
|
return poolManager.getActiveTenantIds();
|
|
9461
9936
|
},
|
|
9937
|
+
getRetryConfig() {
|
|
9938
|
+
return poolManager.getRetryConfig();
|
|
9939
|
+
},
|
|
9462
9940
|
async evictPool(tenantId) {
|
|
9463
9941
|
await poolManager.evictPool(tenantId);
|
|
9464
9942
|
},
|
|
9943
|
+
async warmup(tenantIds, options) {
|
|
9944
|
+
return poolManager.warmup(tenantIds, options);
|
|
9945
|
+
},
|
|
9465
9946
|
async dispose() {
|
|
9466
9947
|
await poolManager.dispose();
|
|
9467
9948
|
}
|