drizzle-multitenant 1.0.10 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +4441 -742
- package/dist/cli/index.js.map +1 -1
- package/dist/{context-DoHx79MS.d.ts → context-Vki959ri.d.ts} +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3392 -742
- 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 +565 -91
- package/dist/integrations/nestjs/index.js.map +1 -1
- package/dist/migrator/index.d.ts +986 -281
- package/dist/migrator/index.js +2799 -664
- package/dist/migrator/index.js.map +1 -1
- package/dist/migrator-BDgFzSh8.d.ts +824 -0
- package/dist/{types-B5eSRLFW.d.ts → types-BhK96FPC.d.ts} +115 -1
- package/package.json +1 -1
|
@@ -9407,8 +9407,161 @@ var DebugLogger = class {
|
|
|
9407
9407
|
function createDebugLogger(config) {
|
|
9408
9408
|
return new DebugLogger(config);
|
|
9409
9409
|
}
|
|
9410
|
+
var PoolCache = class {
|
|
9411
|
+
cache;
|
|
9412
|
+
poolTtlMs;
|
|
9413
|
+
onDispose;
|
|
9414
|
+
constructor(options) {
|
|
9415
|
+
this.poolTtlMs = options.poolTtlMs;
|
|
9416
|
+
this.onDispose = options.onDispose;
|
|
9417
|
+
this.cache = new LRUCache({
|
|
9418
|
+
max: options.maxPools,
|
|
9419
|
+
dispose: (entry, key) => {
|
|
9420
|
+
void this.handleDispose(key, entry);
|
|
9421
|
+
},
|
|
9422
|
+
noDisposeOnSet: true
|
|
9423
|
+
});
|
|
9424
|
+
}
|
|
9425
|
+
/**
|
|
9426
|
+
* Get a pool entry from cache
|
|
9427
|
+
*
|
|
9428
|
+
* This does NOT update the last access time automatically.
|
|
9429
|
+
* Use `touch()` to update access time when needed.
|
|
9430
|
+
*/
|
|
9431
|
+
get(schemaName) {
|
|
9432
|
+
return this.cache.get(schemaName);
|
|
9433
|
+
}
|
|
9434
|
+
/**
|
|
9435
|
+
* Set a pool entry in cache
|
|
9436
|
+
*
|
|
9437
|
+
* If the cache is full, the least recently used entry will be evicted.
|
|
9438
|
+
*/
|
|
9439
|
+
set(schemaName, entry) {
|
|
9440
|
+
this.cache.set(schemaName, entry);
|
|
9441
|
+
}
|
|
9442
|
+
/**
|
|
9443
|
+
* Check if a pool exists in cache
|
|
9444
|
+
*/
|
|
9445
|
+
has(schemaName) {
|
|
9446
|
+
return this.cache.has(schemaName);
|
|
9447
|
+
}
|
|
9448
|
+
/**
|
|
9449
|
+
* Delete a pool from cache
|
|
9450
|
+
*
|
|
9451
|
+
* Note: This triggers the dispose callback if configured.
|
|
9452
|
+
*/
|
|
9453
|
+
delete(schemaName) {
|
|
9454
|
+
return this.cache.delete(schemaName);
|
|
9455
|
+
}
|
|
9456
|
+
/**
|
|
9457
|
+
* Get the number of pools in cache
|
|
9458
|
+
*/
|
|
9459
|
+
size() {
|
|
9460
|
+
return this.cache.size;
|
|
9461
|
+
}
|
|
9462
|
+
/**
|
|
9463
|
+
* Get all schema names in cache
|
|
9464
|
+
*/
|
|
9465
|
+
keys() {
|
|
9466
|
+
return Array.from(this.cache.keys());
|
|
9467
|
+
}
|
|
9468
|
+
/**
|
|
9469
|
+
* Iterate over all entries in cache
|
|
9470
|
+
*
|
|
9471
|
+
* @yields [schemaName, entry] pairs
|
|
9472
|
+
*/
|
|
9473
|
+
*entries() {
|
|
9474
|
+
for (const [key, value] of this.cache.entries()) {
|
|
9475
|
+
yield [key, value];
|
|
9476
|
+
}
|
|
9477
|
+
}
|
|
9478
|
+
/**
|
|
9479
|
+
* Clear all pools from cache
|
|
9480
|
+
*
|
|
9481
|
+
* Each pool's dispose callback will be triggered by the LRU cache.
|
|
9482
|
+
*/
|
|
9483
|
+
async clear() {
|
|
9484
|
+
this.cache.clear();
|
|
9485
|
+
await Promise.resolve();
|
|
9486
|
+
}
|
|
9487
|
+
/**
|
|
9488
|
+
* Evict the least recently used pool
|
|
9489
|
+
*
|
|
9490
|
+
* @returns The schema name of the evicted pool, or undefined if cache is empty
|
|
9491
|
+
*/
|
|
9492
|
+
evictLRU() {
|
|
9493
|
+
const keys = Array.from(this.cache.keys());
|
|
9494
|
+
if (keys.length === 0) {
|
|
9495
|
+
return void 0;
|
|
9496
|
+
}
|
|
9497
|
+
const lruKey = keys[keys.length - 1];
|
|
9498
|
+
this.cache.delete(lruKey);
|
|
9499
|
+
return lruKey;
|
|
9500
|
+
}
|
|
9501
|
+
/**
|
|
9502
|
+
* Evict pools that have exceeded TTL
|
|
9503
|
+
*
|
|
9504
|
+
* @returns Array of schema names that were evicted
|
|
9505
|
+
*/
|
|
9506
|
+
async evictExpired() {
|
|
9507
|
+
if (!this.poolTtlMs) {
|
|
9508
|
+
return [];
|
|
9509
|
+
}
|
|
9510
|
+
const now = Date.now();
|
|
9511
|
+
const toEvict = [];
|
|
9512
|
+
for (const [schemaName, entry] of this.cache.entries()) {
|
|
9513
|
+
if (now - entry.lastAccess > this.poolTtlMs) {
|
|
9514
|
+
toEvict.push(schemaName);
|
|
9515
|
+
}
|
|
9516
|
+
}
|
|
9517
|
+
for (const schemaName of toEvict) {
|
|
9518
|
+
this.cache.delete(schemaName);
|
|
9519
|
+
}
|
|
9520
|
+
return toEvict;
|
|
9521
|
+
}
|
|
9522
|
+
/**
|
|
9523
|
+
* Update last access time for a pool
|
|
9524
|
+
*
|
|
9525
|
+
* This moves the pool to the front of the LRU list.
|
|
9526
|
+
*/
|
|
9527
|
+
touch(schemaName) {
|
|
9528
|
+
const entry = this.cache.get(schemaName);
|
|
9529
|
+
if (entry) {
|
|
9530
|
+
entry.lastAccess = Date.now();
|
|
9531
|
+
}
|
|
9532
|
+
}
|
|
9533
|
+
/**
|
|
9534
|
+
* Get the maximum number of pools allowed in cache
|
|
9535
|
+
*/
|
|
9536
|
+
getMaxPools() {
|
|
9537
|
+
return this.cache.max;
|
|
9538
|
+
}
|
|
9539
|
+
/**
|
|
9540
|
+
* Get the configured TTL in milliseconds
|
|
9541
|
+
*/
|
|
9542
|
+
getTtlMs() {
|
|
9543
|
+
return this.poolTtlMs;
|
|
9544
|
+
}
|
|
9545
|
+
/**
|
|
9546
|
+
* Check if an entry has expired based on TTL
|
|
9547
|
+
*/
|
|
9548
|
+
isExpired(entry) {
|
|
9549
|
+
if (!this.poolTtlMs) {
|
|
9550
|
+
return false;
|
|
9551
|
+
}
|
|
9552
|
+
return Date.now() - entry.lastAccess > this.poolTtlMs;
|
|
9553
|
+
}
|
|
9554
|
+
/**
|
|
9555
|
+
* Handle disposal of a cache entry
|
|
9556
|
+
*/
|
|
9557
|
+
async handleDispose(schemaName, entry) {
|
|
9558
|
+
if (this.onDispose) {
|
|
9559
|
+
await this.onDispose(schemaName, entry);
|
|
9560
|
+
}
|
|
9561
|
+
}
|
|
9562
|
+
};
|
|
9410
9563
|
|
|
9411
|
-
// src/retry.ts
|
|
9564
|
+
// src/pool/retry/retry-handler.ts
|
|
9412
9565
|
function isRetryableError(error) {
|
|
9413
9566
|
const message = error.message.toLowerCase();
|
|
9414
9567
|
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")) {
|
|
@@ -9422,77 +9575,315 @@ function isRetryableError(error) {
|
|
|
9422
9575
|
}
|
|
9423
9576
|
return false;
|
|
9424
9577
|
}
|
|
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
9578
|
function sleep(ms) {
|
|
9435
9579
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
9436
9580
|
}
|
|
9437
|
-
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9581
|
+
var RetryHandler = class {
|
|
9582
|
+
config;
|
|
9583
|
+
constructor(config) {
|
|
9584
|
+
this.config = {
|
|
9585
|
+
maxAttempts: config?.maxAttempts ?? DEFAULT_CONFIG.retry.maxAttempts,
|
|
9586
|
+
initialDelayMs: config?.initialDelayMs ?? DEFAULT_CONFIG.retry.initialDelayMs,
|
|
9587
|
+
maxDelayMs: config?.maxDelayMs ?? DEFAULT_CONFIG.retry.maxDelayMs,
|
|
9588
|
+
backoffMultiplier: config?.backoffMultiplier ?? DEFAULT_CONFIG.retry.backoffMultiplier,
|
|
9589
|
+
jitter: config?.jitter ?? DEFAULT_CONFIG.retry.jitter,
|
|
9590
|
+
isRetryable: config?.isRetryable ?? isRetryableError,
|
|
9591
|
+
onRetry: config?.onRetry
|
|
9592
|
+
};
|
|
9593
|
+
}
|
|
9594
|
+
/**
|
|
9595
|
+
* Execute an operation with retry logic
|
|
9596
|
+
*
|
|
9597
|
+
* @param operation - The async operation to execute
|
|
9598
|
+
* @param overrideConfig - Optional config to override defaults for this call
|
|
9599
|
+
* @returns Result with metadata about attempts and timing
|
|
9600
|
+
*/
|
|
9601
|
+
async withRetry(operation, overrideConfig) {
|
|
9602
|
+
const config = overrideConfig ? { ...this.config, ...overrideConfig } : this.config;
|
|
9603
|
+
const startTime = Date.now();
|
|
9604
|
+
let lastError = null;
|
|
9605
|
+
for (let attempt = 0; attempt < config.maxAttempts; attempt++) {
|
|
9606
|
+
try {
|
|
9607
|
+
const result = await operation();
|
|
9608
|
+
return {
|
|
9609
|
+
result,
|
|
9610
|
+
attempts: attempt + 1,
|
|
9611
|
+
totalTimeMs: Date.now() - startTime
|
|
9612
|
+
};
|
|
9613
|
+
} catch (error) {
|
|
9614
|
+
lastError = error;
|
|
9615
|
+
const isLastAttempt = attempt >= config.maxAttempts - 1;
|
|
9616
|
+
const checkRetryable = config.isRetryable ?? this.isRetryable;
|
|
9617
|
+
if (isLastAttempt || !checkRetryable(lastError)) {
|
|
9618
|
+
throw lastError;
|
|
9619
|
+
}
|
|
9620
|
+
const delay = this.calculateDelay(attempt, config);
|
|
9621
|
+
config.onRetry?.(attempt + 1, lastError, delay);
|
|
9622
|
+
await sleep(delay);
|
|
9623
|
+
}
|
|
9624
|
+
}
|
|
9625
|
+
throw lastError ?? new Error("Retry failed with no error");
|
|
9626
|
+
}
|
|
9627
|
+
/**
|
|
9628
|
+
* Calculate delay with exponential backoff and optional jitter
|
|
9629
|
+
*
|
|
9630
|
+
* @param attempt - Current attempt number (0-indexed)
|
|
9631
|
+
* @param config - Retry configuration
|
|
9632
|
+
* @returns Delay in milliseconds
|
|
9633
|
+
*/
|
|
9634
|
+
calculateDelay(attempt, config) {
|
|
9635
|
+
const cfg = config ? { ...this.config, ...config } : this.config;
|
|
9636
|
+
const exponentialDelay = cfg.initialDelayMs * Math.pow(cfg.backoffMultiplier, attempt);
|
|
9637
|
+
const cappedDelay = Math.min(exponentialDelay, cfg.maxDelayMs);
|
|
9638
|
+
if (cfg.jitter) {
|
|
9639
|
+
const jitterFactor = 1 + Math.random() * 0.25;
|
|
9640
|
+
return Math.floor(cappedDelay * jitterFactor);
|
|
9641
|
+
}
|
|
9642
|
+
return Math.floor(cappedDelay);
|
|
9643
|
+
}
|
|
9644
|
+
/**
|
|
9645
|
+
* Check if an error is retryable
|
|
9646
|
+
*
|
|
9647
|
+
* Uses the configured isRetryable function or the default implementation.
|
|
9648
|
+
*/
|
|
9649
|
+
isRetryable(error) {
|
|
9650
|
+
return (this.config.isRetryable ?? isRetryableError)(error);
|
|
9651
|
+
}
|
|
9652
|
+
/**
|
|
9653
|
+
* Get the current configuration
|
|
9654
|
+
*/
|
|
9655
|
+
getConfig() {
|
|
9656
|
+
return { ...this.config };
|
|
9657
|
+
}
|
|
9658
|
+
/**
|
|
9659
|
+
* Get the maximum number of attempts
|
|
9660
|
+
*/
|
|
9661
|
+
getMaxAttempts() {
|
|
9662
|
+
return this.config.maxAttempts;
|
|
9663
|
+
}
|
|
9664
|
+
};
|
|
9665
|
+
|
|
9666
|
+
// src/pool/health/health-checker.ts
|
|
9667
|
+
var HealthChecker = class {
|
|
9668
|
+
constructor(deps) {
|
|
9669
|
+
this.deps = deps;
|
|
9670
|
+
}
|
|
9671
|
+
/**
|
|
9672
|
+
* Check health of all pools and connections
|
|
9673
|
+
*
|
|
9674
|
+
* Verifies the health of tenant pools and optionally the shared database.
|
|
9675
|
+
* Returns detailed status information for monitoring and load balancer integration.
|
|
9676
|
+
*
|
|
9677
|
+
* @example
|
|
9678
|
+
* ```typescript
|
|
9679
|
+
* // Basic health check
|
|
9680
|
+
* const health = await healthChecker.checkHealth();
|
|
9681
|
+
* console.log(health.healthy); // true/false
|
|
9682
|
+
*
|
|
9683
|
+
* // Check specific tenants only
|
|
9684
|
+
* const health = await healthChecker.checkHealth({
|
|
9685
|
+
* tenantIds: ['tenant-1', 'tenant-2'],
|
|
9686
|
+
* ping: true,
|
|
9687
|
+
* pingTimeoutMs: 3000,
|
|
9688
|
+
* });
|
|
9689
|
+
* ```
|
|
9690
|
+
*/
|
|
9691
|
+
async checkHealth(options = {}) {
|
|
9692
|
+
const startTime = Date.now();
|
|
9693
|
+
const {
|
|
9694
|
+
ping = true,
|
|
9695
|
+
pingTimeoutMs = 5e3,
|
|
9696
|
+
includeShared = true,
|
|
9697
|
+
tenantIds
|
|
9698
|
+
} = options;
|
|
9699
|
+
const poolHealthResults = [];
|
|
9700
|
+
let sharedDbStatus = "ok";
|
|
9701
|
+
let sharedDbResponseTimeMs;
|
|
9702
|
+
let sharedDbError;
|
|
9703
|
+
const poolsToCheck = this.getPoolsToCheck(tenantIds);
|
|
9704
|
+
const poolChecks = poolsToCheck.map(async ({ schemaName, tenantId, entry }) => {
|
|
9705
|
+
return this.checkPoolHealth(tenantId, schemaName, entry, ping, pingTimeoutMs);
|
|
9706
|
+
});
|
|
9707
|
+
poolHealthResults.push(...await Promise.all(poolChecks));
|
|
9708
|
+
const sharedPool = this.deps.getSharedPool();
|
|
9709
|
+
if (includeShared && sharedPool) {
|
|
9710
|
+
const sharedResult = await this.checkSharedDbHealth(sharedPool, ping, pingTimeoutMs);
|
|
9711
|
+
sharedDbStatus = sharedResult.status;
|
|
9712
|
+
sharedDbResponseTimeMs = sharedResult.responseTimeMs;
|
|
9713
|
+
sharedDbError = sharedResult.error;
|
|
9714
|
+
}
|
|
9715
|
+
const degradedPools = poolHealthResults.filter((p) => p.status === "degraded").length;
|
|
9716
|
+
const unhealthyPools = poolHealthResults.filter((p) => p.status === "unhealthy").length;
|
|
9717
|
+
const healthy = unhealthyPools === 0 && sharedDbStatus !== "unhealthy";
|
|
9718
|
+
const result = {
|
|
9719
|
+
healthy,
|
|
9720
|
+
pools: poolHealthResults,
|
|
9721
|
+
sharedDb: sharedDbStatus,
|
|
9722
|
+
totalPools: poolHealthResults.length,
|
|
9723
|
+
degradedPools,
|
|
9724
|
+
unhealthyPools,
|
|
9725
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9726
|
+
durationMs: Date.now() - startTime
|
|
9727
|
+
};
|
|
9728
|
+
if (sharedDbResponseTimeMs !== void 0) {
|
|
9729
|
+
result.sharedDbResponseTimeMs = sharedDbResponseTimeMs;
|
|
9730
|
+
}
|
|
9731
|
+
if (sharedDbError !== void 0) {
|
|
9732
|
+
result.sharedDbError = sharedDbError;
|
|
9733
|
+
}
|
|
9734
|
+
return result;
|
|
9735
|
+
}
|
|
9736
|
+
/**
|
|
9737
|
+
* Get pools to check based on options
|
|
9738
|
+
*/
|
|
9739
|
+
getPoolsToCheck(tenantIds) {
|
|
9740
|
+
const poolsToCheck = [];
|
|
9741
|
+
if (tenantIds && tenantIds.length > 0) {
|
|
9742
|
+
for (const tenantId of tenantIds) {
|
|
9743
|
+
const schemaName = this.deps.getSchemaName(tenantId);
|
|
9744
|
+
const entry = this.deps.getPoolEntry(schemaName);
|
|
9745
|
+
if (entry) {
|
|
9746
|
+
poolsToCheck.push({ schemaName, tenantId, entry });
|
|
9747
|
+
}
|
|
9748
|
+
}
|
|
9749
|
+
} else {
|
|
9750
|
+
for (const [schemaName, entry] of this.deps.getPoolEntries()) {
|
|
9751
|
+
const tenantId = this.deps.getTenantIdBySchema(schemaName) ?? schemaName;
|
|
9752
|
+
poolsToCheck.push({ schemaName, tenantId, entry });
|
|
9753
|
+
}
|
|
9754
|
+
}
|
|
9755
|
+
return poolsToCheck;
|
|
9756
|
+
}
|
|
9757
|
+
/**
|
|
9758
|
+
* Check health of a single tenant pool
|
|
9759
|
+
*/
|
|
9760
|
+
async checkPoolHealth(tenantId, schemaName, entry, ping, pingTimeoutMs) {
|
|
9761
|
+
const pool = entry.pool;
|
|
9762
|
+
const totalConnections = pool.totalCount;
|
|
9763
|
+
const idleConnections = pool.idleCount;
|
|
9764
|
+
const waitingRequests = pool.waitingCount;
|
|
9765
|
+
let status = "ok";
|
|
9766
|
+
let responseTimeMs;
|
|
9767
|
+
let error;
|
|
9768
|
+
if (waitingRequests > 0) {
|
|
9769
|
+
status = "degraded";
|
|
9770
|
+
}
|
|
9771
|
+
if (ping) {
|
|
9772
|
+
const pingResult = await this.executePingQuery(pool, pingTimeoutMs);
|
|
9773
|
+
responseTimeMs = pingResult.responseTimeMs;
|
|
9774
|
+
if (!pingResult.success) {
|
|
9775
|
+
status = "unhealthy";
|
|
9776
|
+
error = pingResult.error;
|
|
9777
|
+
} else if (pingResult.responseTimeMs && pingResult.responseTimeMs > pingTimeoutMs / 2) {
|
|
9778
|
+
if (status === "ok") {
|
|
9779
|
+
status = "degraded";
|
|
9780
|
+
}
|
|
9781
|
+
}
|
|
9782
|
+
}
|
|
9783
|
+
const result = {
|
|
9784
|
+
tenantId,
|
|
9785
|
+
schemaName,
|
|
9786
|
+
status,
|
|
9787
|
+
totalConnections,
|
|
9788
|
+
idleConnections,
|
|
9789
|
+
waitingRequests
|
|
9790
|
+
};
|
|
9791
|
+
if (responseTimeMs !== void 0) {
|
|
9792
|
+
result.responseTimeMs = responseTimeMs;
|
|
9793
|
+
}
|
|
9794
|
+
if (error !== void 0) {
|
|
9795
|
+
result.error = error;
|
|
9796
|
+
}
|
|
9797
|
+
return result;
|
|
9798
|
+
}
|
|
9799
|
+
/**
|
|
9800
|
+
* Check health of shared database
|
|
9801
|
+
*/
|
|
9802
|
+
async checkSharedDbHealth(sharedPool, ping, pingTimeoutMs) {
|
|
9803
|
+
let status = "ok";
|
|
9804
|
+
let responseTimeMs;
|
|
9805
|
+
let error;
|
|
9806
|
+
const waitingRequests = sharedPool.waitingCount;
|
|
9807
|
+
if (waitingRequests > 0) {
|
|
9808
|
+
status = "degraded";
|
|
9809
|
+
}
|
|
9810
|
+
if (ping) {
|
|
9811
|
+
const pingResult = await this.executePingQuery(sharedPool, pingTimeoutMs);
|
|
9812
|
+
responseTimeMs = pingResult.responseTimeMs;
|
|
9813
|
+
if (!pingResult.success) {
|
|
9814
|
+
status = "unhealthy";
|
|
9815
|
+
error = pingResult.error;
|
|
9816
|
+
} else if (pingResult.responseTimeMs && pingResult.responseTimeMs > pingTimeoutMs / 2) {
|
|
9817
|
+
if (status === "ok") {
|
|
9818
|
+
status = "degraded";
|
|
9819
|
+
}
|
|
9820
|
+
}
|
|
9821
|
+
}
|
|
9822
|
+
const result = { status };
|
|
9823
|
+
if (responseTimeMs !== void 0) {
|
|
9824
|
+
result.responseTimeMs = responseTimeMs;
|
|
9825
|
+
}
|
|
9826
|
+
if (error !== void 0) {
|
|
9827
|
+
result.error = error;
|
|
9828
|
+
}
|
|
9829
|
+
return result;
|
|
9830
|
+
}
|
|
9831
|
+
/**
|
|
9832
|
+
* Execute a ping query with timeout
|
|
9833
|
+
*/
|
|
9834
|
+
async executePingQuery(pool, timeoutMs) {
|
|
9835
|
+
const startTime = Date.now();
|
|
9450
9836
|
try {
|
|
9451
|
-
const
|
|
9837
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
9838
|
+
setTimeout(() => reject(new Error("Health check ping timeout")), timeoutMs);
|
|
9839
|
+
});
|
|
9840
|
+
const queryPromise = pool.query("SELECT 1");
|
|
9841
|
+
await Promise.race([queryPromise, timeoutPromise]);
|
|
9452
9842
|
return {
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9843
|
+
success: true,
|
|
9844
|
+
responseTimeMs: Date.now() - startTime
|
|
9845
|
+
};
|
|
9846
|
+
} catch (err) {
|
|
9847
|
+
return {
|
|
9848
|
+
success: false,
|
|
9849
|
+
responseTimeMs: Date.now() - startTime,
|
|
9850
|
+
error: err.message
|
|
9456
9851
|
};
|
|
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
9852
|
}
|
|
9467
9853
|
}
|
|
9468
|
-
|
|
9469
|
-
|
|
9854
|
+
/**
|
|
9855
|
+
* Determine overall health status from pool health results
|
|
9856
|
+
*/
|
|
9857
|
+
determineOverallHealth(pools, sharedDbStatus = "ok") {
|
|
9858
|
+
const unhealthyPools = pools.filter((p) => p.status === "unhealthy").length;
|
|
9859
|
+
return unhealthyPools === 0 && sharedDbStatus !== "unhealthy";
|
|
9860
|
+
}
|
|
9861
|
+
};
|
|
9470
9862
|
|
|
9471
9863
|
// src/pool.ts
|
|
9472
9864
|
var PoolManager = class {
|
|
9473
9865
|
constructor(config) {
|
|
9474
9866
|
this.config = config;
|
|
9475
9867
|
const maxPools = config.isolation.maxPools ?? DEFAULT_CONFIG.maxPools;
|
|
9868
|
+
const poolTtlMs = config.isolation.poolTtlMs ?? DEFAULT_CONFIG.poolTtlMs;
|
|
9476
9869
|
this.debugLogger = createDebugLogger(config.debug);
|
|
9477
|
-
|
|
9478
|
-
this.
|
|
9479
|
-
|
|
9480
|
-
|
|
9481
|
-
|
|
9482
|
-
|
|
9483
|
-
|
|
9484
|
-
|
|
9485
|
-
|
|
9486
|
-
|
|
9487
|
-
|
|
9488
|
-
|
|
9489
|
-
|
|
9490
|
-
|
|
9491
|
-
},
|
|
9492
|
-
noDisposeOnSet: true
|
|
9870
|
+
this.retryHandler = new RetryHandler(config.connection.retry);
|
|
9871
|
+
this.poolCache = new PoolCache({
|
|
9872
|
+
maxPools,
|
|
9873
|
+
poolTtlMs,
|
|
9874
|
+
onDispose: (schemaName, entry) => {
|
|
9875
|
+
this.disposePoolEntry(entry, schemaName);
|
|
9876
|
+
}
|
|
9877
|
+
});
|
|
9878
|
+
this.healthChecker = new HealthChecker({
|
|
9879
|
+
getPoolEntries: () => this.poolCache.entries(),
|
|
9880
|
+
getTenantIdBySchema: (schemaName) => this.tenantIdBySchema.get(schemaName),
|
|
9881
|
+
getPoolEntry: (schemaName) => this.poolCache.get(schemaName),
|
|
9882
|
+
getSchemaName: (tenantId) => this.config.isolation.schemaNameTemplate(tenantId),
|
|
9883
|
+
getSharedPool: () => this.sharedPool
|
|
9493
9884
|
});
|
|
9494
9885
|
}
|
|
9495
|
-
|
|
9886
|
+
poolCache;
|
|
9496
9887
|
tenantIdBySchema = /* @__PURE__ */ new Map();
|
|
9497
9888
|
pendingConnections = /* @__PURE__ */ new Map();
|
|
9498
9889
|
sharedPool = null;
|
|
@@ -9501,22 +9892,23 @@ var PoolManager = class {
|
|
|
9501
9892
|
cleanupInterval = null;
|
|
9502
9893
|
disposed = false;
|
|
9503
9894
|
debugLogger;
|
|
9504
|
-
|
|
9895
|
+
retryHandler;
|
|
9896
|
+
healthChecker;
|
|
9505
9897
|
/**
|
|
9506
9898
|
* Get or create a database connection for a tenant
|
|
9507
9899
|
*/
|
|
9508
9900
|
getDb(tenantId) {
|
|
9509
9901
|
this.ensureNotDisposed();
|
|
9510
9902
|
const schemaName = this.config.isolation.schemaNameTemplate(tenantId);
|
|
9511
|
-
let entry = this.
|
|
9903
|
+
let entry = this.poolCache.get(schemaName);
|
|
9512
9904
|
if (!entry) {
|
|
9513
9905
|
entry = this.createPoolEntry(tenantId, schemaName);
|
|
9514
|
-
this.
|
|
9906
|
+
this.poolCache.set(schemaName, entry);
|
|
9515
9907
|
this.tenantIdBySchema.set(schemaName, tenantId);
|
|
9516
9908
|
this.debugLogger.logPoolCreated(tenantId, schemaName);
|
|
9517
9909
|
void this.config.hooks?.onPoolCreated?.(tenantId);
|
|
9518
9910
|
}
|
|
9519
|
-
|
|
9911
|
+
this.poolCache.touch(schemaName);
|
|
9520
9912
|
return entry.db;
|
|
9521
9913
|
}
|
|
9522
9914
|
/**
|
|
@@ -9537,26 +9929,26 @@ var PoolManager = class {
|
|
|
9537
9929
|
async getDbAsync(tenantId) {
|
|
9538
9930
|
this.ensureNotDisposed();
|
|
9539
9931
|
const schemaName = this.config.isolation.schemaNameTemplate(tenantId);
|
|
9540
|
-
let entry = this.
|
|
9932
|
+
let entry = this.poolCache.get(schemaName);
|
|
9541
9933
|
if (entry) {
|
|
9542
|
-
|
|
9934
|
+
this.poolCache.touch(schemaName);
|
|
9543
9935
|
return entry.db;
|
|
9544
9936
|
}
|
|
9545
9937
|
const pending = this.pendingConnections.get(schemaName);
|
|
9546
9938
|
if (pending) {
|
|
9547
9939
|
entry = await pending;
|
|
9548
|
-
|
|
9940
|
+
this.poolCache.touch(schemaName);
|
|
9549
9941
|
return entry.db;
|
|
9550
9942
|
}
|
|
9551
9943
|
const connectionPromise = this.connectWithRetry(tenantId, schemaName);
|
|
9552
9944
|
this.pendingConnections.set(schemaName, connectionPromise);
|
|
9553
9945
|
try {
|
|
9554
9946
|
entry = await connectionPromise;
|
|
9555
|
-
this.
|
|
9947
|
+
this.poolCache.set(schemaName, entry);
|
|
9556
9948
|
this.tenantIdBySchema.set(schemaName, tenantId);
|
|
9557
9949
|
this.debugLogger.logPoolCreated(tenantId, schemaName);
|
|
9558
9950
|
void this.config.hooks?.onPoolCreated?.(tenantId);
|
|
9559
|
-
|
|
9951
|
+
this.poolCache.touch(schemaName);
|
|
9560
9952
|
return entry.db;
|
|
9561
9953
|
} finally {
|
|
9562
9954
|
this.pendingConnections.delete(schemaName);
|
|
@@ -9566,8 +9958,9 @@ var PoolManager = class {
|
|
|
9566
9958
|
* Connect to a tenant database with retry logic
|
|
9567
9959
|
*/
|
|
9568
9960
|
async connectWithRetry(tenantId, schemaName) {
|
|
9569
|
-
const
|
|
9570
|
-
const
|
|
9961
|
+
const retryConfig = this.retryHandler.getConfig();
|
|
9962
|
+
const maxAttempts = retryConfig.maxAttempts;
|
|
9963
|
+
const result = await this.retryHandler.withRetry(
|
|
9571
9964
|
async () => {
|
|
9572
9965
|
const entry = this.createPoolEntry(tenantId, schemaName);
|
|
9573
9966
|
try {
|
|
@@ -9582,10 +9975,9 @@ var PoolManager = class {
|
|
|
9582
9975
|
}
|
|
9583
9976
|
},
|
|
9584
9977
|
{
|
|
9585
|
-
...this.retryConfig,
|
|
9586
9978
|
onRetry: (attempt, error, delayMs) => {
|
|
9587
9979
|
this.debugLogger.logConnectionRetry(tenantId, attempt, maxAttempts, error, delayMs);
|
|
9588
|
-
|
|
9980
|
+
retryConfig.onRetry?.(attempt, error, delayMs);
|
|
9589
9981
|
}
|
|
9590
9982
|
}
|
|
9591
9983
|
);
|
|
@@ -9647,8 +10039,9 @@ var PoolManager = class {
|
|
|
9647
10039
|
* Connect to shared database with retry logic
|
|
9648
10040
|
*/
|
|
9649
10041
|
async connectSharedWithRetry() {
|
|
9650
|
-
const
|
|
9651
|
-
const
|
|
10042
|
+
const retryConfig = this.retryHandler.getConfig();
|
|
10043
|
+
const maxAttempts = retryConfig.maxAttempts;
|
|
10044
|
+
const result = await this.retryHandler.withRetry(
|
|
9652
10045
|
async () => {
|
|
9653
10046
|
const pool = new Pool({
|
|
9654
10047
|
connectionString: this.config.connection.url,
|
|
@@ -9674,10 +10067,9 @@ var PoolManager = class {
|
|
|
9674
10067
|
}
|
|
9675
10068
|
},
|
|
9676
10069
|
{
|
|
9677
|
-
...this.retryConfig,
|
|
9678
10070
|
onRetry: (attempt, error, delayMs) => {
|
|
9679
10071
|
this.debugLogger.logConnectionRetry("shared", attempt, maxAttempts, error, delayMs);
|
|
9680
|
-
|
|
10072
|
+
retryConfig.onRetry?.(attempt, error, delayMs);
|
|
9681
10073
|
}
|
|
9682
10074
|
}
|
|
9683
10075
|
);
|
|
@@ -9695,13 +10087,13 @@ var PoolManager = class {
|
|
|
9695
10087
|
*/
|
|
9696
10088
|
hasPool(tenantId) {
|
|
9697
10089
|
const schemaName = this.config.isolation.schemaNameTemplate(tenantId);
|
|
9698
|
-
return this.
|
|
10090
|
+
return this.poolCache.has(schemaName);
|
|
9699
10091
|
}
|
|
9700
10092
|
/**
|
|
9701
10093
|
* Get count of active pools
|
|
9702
10094
|
*/
|
|
9703
10095
|
getPoolCount() {
|
|
9704
|
-
return this.
|
|
10096
|
+
return this.poolCache.size();
|
|
9705
10097
|
}
|
|
9706
10098
|
/**
|
|
9707
10099
|
* Get all active tenant IDs
|
|
@@ -9713,7 +10105,7 @@ var PoolManager = class {
|
|
|
9713
10105
|
* Get the retry configuration
|
|
9714
10106
|
*/
|
|
9715
10107
|
getRetryConfig() {
|
|
9716
|
-
return
|
|
10108
|
+
return this.retryHandler.getConfig();
|
|
9717
10109
|
}
|
|
9718
10110
|
/**
|
|
9719
10111
|
* Pre-warm pools for specified tenants to reduce cold start latency
|
|
@@ -9772,15 +10164,97 @@ var PoolManager = class {
|
|
|
9772
10164
|
details: results
|
|
9773
10165
|
};
|
|
9774
10166
|
}
|
|
10167
|
+
/**
|
|
10168
|
+
* Get current metrics for all pools
|
|
10169
|
+
*
|
|
10170
|
+
* Collects metrics on demand with zero overhead when not called.
|
|
10171
|
+
* Returns raw data that can be formatted for any monitoring system.
|
|
10172
|
+
*
|
|
10173
|
+
* @example
|
|
10174
|
+
* ```typescript
|
|
10175
|
+
* const metrics = manager.getMetrics();
|
|
10176
|
+
* console.log(metrics.pools.total); // 15
|
|
10177
|
+
*
|
|
10178
|
+
* // Format for Prometheus
|
|
10179
|
+
* for (const pool of metrics.pools.tenants) {
|
|
10180
|
+
* gauge.labels(pool.tenantId).set(pool.connections.idle);
|
|
10181
|
+
* }
|
|
10182
|
+
* ```
|
|
10183
|
+
*/
|
|
10184
|
+
getMetrics() {
|
|
10185
|
+
this.ensureNotDisposed();
|
|
10186
|
+
const maxPools = this.config.isolation.maxPools ?? DEFAULT_CONFIG.maxPools;
|
|
10187
|
+
const tenantMetrics = [];
|
|
10188
|
+
for (const [schemaName, entry] of this.poolCache.entries()) {
|
|
10189
|
+
const tenantId = this.tenantIdBySchema.get(schemaName) ?? schemaName;
|
|
10190
|
+
const pool = entry.pool;
|
|
10191
|
+
tenantMetrics.push({
|
|
10192
|
+
tenantId,
|
|
10193
|
+
schemaName,
|
|
10194
|
+
connections: {
|
|
10195
|
+
total: pool.totalCount,
|
|
10196
|
+
idle: pool.idleCount,
|
|
10197
|
+
waiting: pool.waitingCount
|
|
10198
|
+
},
|
|
10199
|
+
lastAccessedAt: new Date(entry.lastAccess).toISOString()
|
|
10200
|
+
});
|
|
10201
|
+
}
|
|
10202
|
+
return {
|
|
10203
|
+
pools: {
|
|
10204
|
+
total: tenantMetrics.length,
|
|
10205
|
+
maxPools,
|
|
10206
|
+
tenants: tenantMetrics
|
|
10207
|
+
},
|
|
10208
|
+
shared: {
|
|
10209
|
+
initialized: this.sharedPool !== null,
|
|
10210
|
+
connections: this.sharedPool ? {
|
|
10211
|
+
total: this.sharedPool.totalCount,
|
|
10212
|
+
idle: this.sharedPool.idleCount,
|
|
10213
|
+
waiting: this.sharedPool.waitingCount
|
|
10214
|
+
} : null
|
|
10215
|
+
},
|
|
10216
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
10217
|
+
};
|
|
10218
|
+
}
|
|
10219
|
+
/**
|
|
10220
|
+
* Check health of all pools and connections
|
|
10221
|
+
*
|
|
10222
|
+
* Verifies the health of tenant pools and optionally the shared database.
|
|
10223
|
+
* Returns detailed status information for monitoring and load balancer integration.
|
|
10224
|
+
*
|
|
10225
|
+
* @example
|
|
10226
|
+
* ```typescript
|
|
10227
|
+
* // Basic health check
|
|
10228
|
+
* const health = await manager.healthCheck();
|
|
10229
|
+
* console.log(health.healthy); // true/false
|
|
10230
|
+
*
|
|
10231
|
+
* // Use with Express endpoint
|
|
10232
|
+
* app.get('/health', async (req, res) => {
|
|
10233
|
+
* const health = await manager.healthCheck();
|
|
10234
|
+
* res.status(health.healthy ? 200 : 503).json(health);
|
|
10235
|
+
* });
|
|
10236
|
+
*
|
|
10237
|
+
* // Check specific tenants only
|
|
10238
|
+
* const health = await manager.healthCheck({
|
|
10239
|
+
* tenantIds: ['tenant-1', 'tenant-2'],
|
|
10240
|
+
* ping: true,
|
|
10241
|
+
* pingTimeoutMs: 3000,
|
|
10242
|
+
* });
|
|
10243
|
+
* ```
|
|
10244
|
+
*/
|
|
10245
|
+
async healthCheck(options = {}) {
|
|
10246
|
+
this.ensureNotDisposed();
|
|
10247
|
+
return this.healthChecker.checkHealth(options);
|
|
10248
|
+
}
|
|
9775
10249
|
/**
|
|
9776
10250
|
* Manually evict a tenant pool
|
|
9777
10251
|
*/
|
|
9778
10252
|
async evictPool(tenantId, reason = "manual") {
|
|
9779
10253
|
const schemaName = this.config.isolation.schemaNameTemplate(tenantId);
|
|
9780
|
-
const entry = this.
|
|
10254
|
+
const entry = this.poolCache.get(schemaName);
|
|
9781
10255
|
if (entry) {
|
|
9782
10256
|
this.debugLogger.logPoolEvicted(tenantId, schemaName, reason);
|
|
9783
|
-
this.
|
|
10257
|
+
this.poolCache.delete(schemaName);
|
|
9784
10258
|
this.tenantIdBySchema.delete(schemaName);
|
|
9785
10259
|
await this.closePool(entry.pool, tenantId);
|
|
9786
10260
|
}
|
|
@@ -9790,10 +10264,9 @@ var PoolManager = class {
|
|
|
9790
10264
|
*/
|
|
9791
10265
|
startCleanup() {
|
|
9792
10266
|
if (this.cleanupInterval) return;
|
|
9793
|
-
const poolTtlMs = this.config.isolation.poolTtlMs ?? DEFAULT_CONFIG.poolTtlMs;
|
|
9794
10267
|
const cleanupIntervalMs = DEFAULT_CONFIG.cleanupIntervalMs;
|
|
9795
10268
|
this.cleanupInterval = setInterval(() => {
|
|
9796
|
-
void this.cleanupIdlePools(
|
|
10269
|
+
void this.cleanupIdlePools();
|
|
9797
10270
|
}, cleanupIntervalMs);
|
|
9798
10271
|
this.cleanupInterval.unref();
|
|
9799
10272
|
}
|
|
@@ -9814,11 +10287,11 @@ var PoolManager = class {
|
|
|
9814
10287
|
this.disposed = true;
|
|
9815
10288
|
this.stopCleanup();
|
|
9816
10289
|
const closePromises = [];
|
|
9817
|
-
for (const [schemaName, entry] of this.
|
|
10290
|
+
for (const [schemaName, entry] of this.poolCache.entries()) {
|
|
9818
10291
|
const tenantId = this.tenantIdBySchema.get(schemaName);
|
|
9819
10292
|
closePromises.push(this.closePool(entry.pool, tenantId ?? schemaName));
|
|
9820
10293
|
}
|
|
9821
|
-
this.
|
|
10294
|
+
await this.poolCache.clear();
|
|
9822
10295
|
this.tenantIdBySchema.clear();
|
|
9823
10296
|
if (this.sharedPool) {
|
|
9824
10297
|
closePromises.push(this.closePool(this.sharedPool, "shared"));
|
|
@@ -9880,18 +10353,13 @@ var PoolManager = class {
|
|
|
9880
10353
|
/**
|
|
9881
10354
|
* Cleanup pools that have been idle for too long
|
|
9882
10355
|
*/
|
|
9883
|
-
async cleanupIdlePools(
|
|
9884
|
-
const
|
|
9885
|
-
const
|
|
9886
|
-
for (const [schemaName, entry] of this.pools.entries()) {
|
|
9887
|
-
if (now - entry.lastAccess > poolTtlMs) {
|
|
9888
|
-
toEvict.push(schemaName);
|
|
9889
|
-
}
|
|
9890
|
-
}
|
|
9891
|
-
for (const schemaName of toEvict) {
|
|
10356
|
+
async cleanupIdlePools() {
|
|
10357
|
+
const evictedSchemas = await this.poolCache.evictExpired();
|
|
10358
|
+
for (const schemaName of evictedSchemas) {
|
|
9892
10359
|
const tenantId = this.tenantIdBySchema.get(schemaName);
|
|
9893
10360
|
if (tenantId) {
|
|
9894
|
-
|
|
10361
|
+
this.debugLogger.logPoolEvicted(tenantId, schemaName, "ttl_expired");
|
|
10362
|
+
this.tenantIdBySchema.delete(schemaName);
|
|
9895
10363
|
}
|
|
9896
10364
|
}
|
|
9897
10365
|
}
|
|
@@ -9943,6 +10411,12 @@ function createTenantManager(config) {
|
|
|
9943
10411
|
async warmup(tenantIds, options) {
|
|
9944
10412
|
return poolManager.warmup(tenantIds, options);
|
|
9945
10413
|
},
|
|
10414
|
+
async healthCheck(options) {
|
|
10415
|
+
return poolManager.healthCheck(options);
|
|
10416
|
+
},
|
|
10417
|
+
getMetrics() {
|
|
10418
|
+
return poolManager.getMetrics();
|
|
10419
|
+
},
|
|
9946
10420
|
async dispose() {
|
|
9947
10421
|
await poolManager.dispose();
|
|
9948
10422
|
}
|