prisma-sharding 0.0.6 → 0.0.7
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 +135 -58
- package/dist/cli/migrate.js +198 -47
- package/dist/cli/studio.js +152 -84
- package/dist/cli/test.js +145 -41
- package/dist/cli/update.js +195 -46
- package/dist/cli/utils/command.js +232 -0
- package/dist/cli/utils/postgres.js +55 -0
- package/dist/index.js +395 -89
- package/dist/index.mjs +395 -89
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -131,6 +131,11 @@ var ShardRouter = class {
|
|
|
131
131
|
if (this.strategy === "consistent-hash") {
|
|
132
132
|
this.initializeConsistentHashRing();
|
|
133
133
|
}
|
|
134
|
+
this.sortedRingHashes = Array.from(this.consistentHashRing.keys()).sort((a, b) => a - b);
|
|
135
|
+
this.randomShardWeights = this.shardIds.map(
|
|
136
|
+
(shardId) => config.shardWeights?.get(shardId) ?? 1
|
|
137
|
+
);
|
|
138
|
+
this.totalRandomWeight = this.randomShardWeights.reduce((sum, weight) => sum + weight, 0);
|
|
134
139
|
}
|
|
135
140
|
initializeConsistentHashRing() {
|
|
136
141
|
for (const shardId of this.shardIds) {
|
|
@@ -155,25 +160,49 @@ var ShardRouter = class {
|
|
|
155
160
|
return hash % shardCount;
|
|
156
161
|
}
|
|
157
162
|
getIndexConsistentHash(key) {
|
|
163
|
+
return this.shardIds.indexOf(this.getConsistentHashShardId(key));
|
|
164
|
+
}
|
|
165
|
+
getConsistentHashShardId(key) {
|
|
158
166
|
const hash = hashString(key);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
167
|
+
let low = 0;
|
|
168
|
+
let high = this.sortedRingHashes.length - 1;
|
|
169
|
+
let matchIndex = 0;
|
|
170
|
+
while (low <= high) {
|
|
171
|
+
const middle = Math.floor((low + high) / 2);
|
|
172
|
+
if (this.sortedRingHashes[middle] >= hash) {
|
|
173
|
+
matchIndex = middle;
|
|
174
|
+
high = middle - 1;
|
|
175
|
+
} else {
|
|
176
|
+
low = middle + 1;
|
|
165
177
|
}
|
|
166
178
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
179
|
+
if (low >= this.sortedRingHashes.length) {
|
|
180
|
+
matchIndex = 0;
|
|
181
|
+
}
|
|
182
|
+
return this.consistentHashRing.get(this.sortedRingHashes[matchIndex]);
|
|
170
183
|
}
|
|
171
184
|
getShardId(key) {
|
|
185
|
+
if (this.shardIds.length === 0) {
|
|
186
|
+
throw new RoutingError("No shards available");
|
|
187
|
+
}
|
|
188
|
+
if (this.strategy === "consistent-hash") {
|
|
189
|
+
return this.getConsistentHashShardId(key);
|
|
190
|
+
}
|
|
172
191
|
const index = this.getShardIndex(key);
|
|
173
192
|
return this.shardIds[index] || `shard_${index + 1}`;
|
|
174
193
|
}
|
|
175
194
|
getRandomShardIndex() {
|
|
176
|
-
|
|
195
|
+
if (this.shardIds.length === 0) {
|
|
196
|
+
throw new RoutingError("No shards available");
|
|
197
|
+
}
|
|
198
|
+
let selection = Math.random() * this.totalRandomWeight;
|
|
199
|
+
for (let index = 0; index < this.randomShardWeights.length; index++) {
|
|
200
|
+
selection -= this.randomShardWeights[index];
|
|
201
|
+
if (selection < 0) {
|
|
202
|
+
return index;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return this.shardIds.length - 1;
|
|
177
206
|
}
|
|
178
207
|
getRandomShardId() {
|
|
179
208
|
const index = this.getRandomShardIndex();
|
|
@@ -181,18 +210,235 @@ var ShardRouter = class {
|
|
|
181
210
|
}
|
|
182
211
|
};
|
|
183
212
|
|
|
213
|
+
// src/constants/internal.ts
|
|
214
|
+
var INTERNAL_DEFAULTS = {
|
|
215
|
+
HEALTH_CHECK_TIMEOUT_MS: 5e3,
|
|
216
|
+
CROSS_SHARD_CONCURRENCY: 4,
|
|
217
|
+
CROSS_SHARD_TIMEOUT_MS: 1e4,
|
|
218
|
+
CLI_COMMAND_TIMEOUT_MS: 12e4,
|
|
219
|
+
CLI_FORCE_KILL_GRACE_MS: 5e3,
|
|
220
|
+
CLI_MAX_OUTPUT_LENGTH: 1e6,
|
|
221
|
+
CLI_TEST_TCP_TIMEOUT_MS: 5e3,
|
|
222
|
+
CLI_TEST_COMMAND_TIMEOUT_MS: 15e3,
|
|
223
|
+
STUDIO_BASE_PORT: 51212,
|
|
224
|
+
STUDIO_REUSE_EXISTING: true,
|
|
225
|
+
STUDIO_STRICT_PORT_CHECK: false,
|
|
226
|
+
STUDIO_START_TIMEOUT_MS: 15e3,
|
|
227
|
+
STUDIO_STABILITY_MS: 500,
|
|
228
|
+
STUDIO_SHUTDOWN_TIMEOUT_MS: 5e3
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// src/utils/sanitize.ts
|
|
232
|
+
var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
233
|
+
var maskDatabaseUrl = (url) => {
|
|
234
|
+
try {
|
|
235
|
+
const parsed = new URL(url);
|
|
236
|
+
if (parsed.password) {
|
|
237
|
+
parsed.password = "***";
|
|
238
|
+
}
|
|
239
|
+
return parsed.toString();
|
|
240
|
+
} catch {
|
|
241
|
+
return url.replace(/:[^:@]+@/, ":***@");
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
var sanitizeDatabaseText = (output, databaseUrls) => {
|
|
245
|
+
let sanitized = output;
|
|
246
|
+
for (const url of databaseUrls) {
|
|
247
|
+
sanitized = sanitized.replace(new RegExp(escapeRegExp(url), "g"), maskDatabaseUrl(url));
|
|
248
|
+
try {
|
|
249
|
+
const password = new URL(url).password;
|
|
250
|
+
if (password.length >= 4) {
|
|
251
|
+
sanitized = sanitized.replace(
|
|
252
|
+
new RegExp(escapeRegExp(decodeURIComponent(password)), "g"),
|
|
253
|
+
"***"
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
} catch {
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return sanitized.replace(
|
|
260
|
+
/(postgres(?:ql)?:\/\/[^:/\s]+:)[^@\s]+(@)/gi,
|
|
261
|
+
"$1***$2"
|
|
262
|
+
);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// src/core/executor.ts
|
|
266
|
+
var timeoutError = (shardId, timeoutMs) => new Error(`Shard ${shardId} operation timed out after ${timeoutMs}ms`);
|
|
267
|
+
var CrossShardExecutor = class {
|
|
268
|
+
constructor(config = {}) {
|
|
269
|
+
this.metrics = {
|
|
270
|
+
operationCount: 0,
|
|
271
|
+
timeoutCount: 0,
|
|
272
|
+
failuresByShard: /* @__PURE__ */ new Map()
|
|
273
|
+
};
|
|
274
|
+
this.concurrency = config.concurrency ?? INTERNAL_DEFAULTS.CROSS_SHARD_CONCURRENCY;
|
|
275
|
+
this.timeoutMs = config.timeoutMs ?? INTERNAL_DEFAULTS.CROSS_SHARD_TIMEOUT_MS;
|
|
276
|
+
}
|
|
277
|
+
orderedTargets(targets) {
|
|
278
|
+
return targets.map((target, index) => ({ target, index })).sort((left, right) => {
|
|
279
|
+
if (left.target.isHealthy !== right.target.isHealthy) {
|
|
280
|
+
return left.target.isHealthy ? -1 : 1;
|
|
281
|
+
}
|
|
282
|
+
const leftLatency = left.target.latencyMs >= 0 ? left.target.latencyMs : Number.POSITIVE_INFINITY;
|
|
283
|
+
const rightLatency = right.target.latencyMs >= 0 ? right.target.latencyMs : Number.POSITIVE_INFINITY;
|
|
284
|
+
return leftLatency - rightLatency || left.index - right.index;
|
|
285
|
+
}).map(({ target }) => target);
|
|
286
|
+
}
|
|
287
|
+
recordFailure(shardId, error) {
|
|
288
|
+
this.metrics.failuresByShard.set(
|
|
289
|
+
shardId,
|
|
290
|
+
(this.metrics.failuresByShard.get(shardId) ?? 0) + 1
|
|
291
|
+
);
|
|
292
|
+
if (error.message.includes("timed out")) {
|
|
293
|
+
this.metrics.timeoutCount++;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
withTimeout(shardId, operation) {
|
|
297
|
+
return new Promise((resolve, reject) => {
|
|
298
|
+
let settled = false;
|
|
299
|
+
const finish = (callback) => {
|
|
300
|
+
if (settled) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
settled = true;
|
|
304
|
+
clearTimeout(timer);
|
|
305
|
+
callback();
|
|
306
|
+
};
|
|
307
|
+
const timer = setTimeout(() => {
|
|
308
|
+
finish(() => reject(timeoutError(shardId, this.timeoutMs)));
|
|
309
|
+
}, this.timeoutMs);
|
|
310
|
+
Promise.resolve().then(operation).then(
|
|
311
|
+
(result) => finish(() => resolve(result)),
|
|
312
|
+
(error) => finish(() => reject(error))
|
|
313
|
+
);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
async executeAll(targets, operation) {
|
|
317
|
+
this.metrics.operationCount++;
|
|
318
|
+
const originalIndexes = new Map(targets.map((target, index) => [target.shardId, index]));
|
|
319
|
+
const ordered = this.orderedTargets(targets);
|
|
320
|
+
const results = new Array(targets.length);
|
|
321
|
+
let nextIndex = 0;
|
|
322
|
+
const worker = async () => {
|
|
323
|
+
while (nextIndex < ordered.length) {
|
|
324
|
+
const target = ordered[nextIndex++];
|
|
325
|
+
const resultIndex = originalIndexes.get(target.shardId);
|
|
326
|
+
try {
|
|
327
|
+
const result = await this.withTimeout(
|
|
328
|
+
target.shardId,
|
|
329
|
+
() => operation(target.client, target.shardId)
|
|
330
|
+
);
|
|
331
|
+
results[resultIndex] = { shardId: target.shardId, result, error: void 0 };
|
|
332
|
+
} catch (error) {
|
|
333
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
334
|
+
this.recordFailure(target.shardId, normalizedError);
|
|
335
|
+
results[resultIndex] = {
|
|
336
|
+
shardId: target.shardId,
|
|
337
|
+
result: null,
|
|
338
|
+
error: normalizedError
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
const workerCount = Math.min(this.concurrency, ordered.length);
|
|
344
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
345
|
+
return results;
|
|
346
|
+
}
|
|
347
|
+
findFirst(targets, operation) {
|
|
348
|
+
this.metrics.operationCount++;
|
|
349
|
+
const ordered = this.orderedTargets(targets);
|
|
350
|
+
return new Promise((resolve) => {
|
|
351
|
+
if (ordered.length === 0) {
|
|
352
|
+
resolve({ result: null, shardId: null });
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
let nextIndex = 0;
|
|
356
|
+
let active = 0;
|
|
357
|
+
let completed = 0;
|
|
358
|
+
let found = false;
|
|
359
|
+
const launch = () => {
|
|
360
|
+
while (!found && active < this.concurrency && nextIndex < ordered.length) {
|
|
361
|
+
const target = ordered[nextIndex++];
|
|
362
|
+
active++;
|
|
363
|
+
this.withTimeout(target.shardId, () => operation(target.client)).then((result) => {
|
|
364
|
+
if (!found && result !== null && result !== void 0) {
|
|
365
|
+
found = true;
|
|
366
|
+
resolve({ result, shardId: target.shardId });
|
|
367
|
+
}
|
|
368
|
+
}).catch((error) => {
|
|
369
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
370
|
+
this.recordFailure(target.shardId, normalizedError);
|
|
371
|
+
}).finally(() => {
|
|
372
|
+
active--;
|
|
373
|
+
completed++;
|
|
374
|
+
if (!found && completed === ordered.length) {
|
|
375
|
+
resolve({ result: null, shardId: null });
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
launch();
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
launch();
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
|
|
184
387
|
// src/core/manager.ts
|
|
388
|
+
var isObject = (value) => typeof value === "object" && value !== null;
|
|
389
|
+
var isConnectable = (client) => isObject(client) && typeof client.$connect === "function";
|
|
390
|
+
var isDisconnectable = (client) => isObject(client) && typeof client.$disconnect === "function";
|
|
391
|
+
var isQueryable = (client) => isObject(client) && typeof client.$queryRaw === "function";
|
|
392
|
+
var withTimeout = (operation, timeoutMs, timeoutMessage) => new Promise((resolve, reject) => {
|
|
393
|
+
let settled = false;
|
|
394
|
+
const timer = setTimeout(() => {
|
|
395
|
+
if (!settled) {
|
|
396
|
+
settled = true;
|
|
397
|
+
reject(new Error(timeoutMessage));
|
|
398
|
+
}
|
|
399
|
+
}, timeoutMs);
|
|
400
|
+
operation.then(
|
|
401
|
+
(result) => {
|
|
402
|
+
if (!settled) {
|
|
403
|
+
settled = true;
|
|
404
|
+
clearTimeout(timer);
|
|
405
|
+
resolve(result);
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
(error) => {
|
|
409
|
+
if (!settled) {
|
|
410
|
+
settled = true;
|
|
411
|
+
clearTimeout(timer);
|
|
412
|
+
reject(error);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
);
|
|
416
|
+
});
|
|
185
417
|
var ShardManager = class {
|
|
186
418
|
constructor(config) {
|
|
187
419
|
this.instances = /* @__PURE__ */ new Map();
|
|
420
|
+
this.unhealthyAccessWarnings = /* @__PURE__ */ new Set();
|
|
188
421
|
this.healthCheckInterval = null;
|
|
422
|
+
this.healthCheckInFlight = false;
|
|
423
|
+
this.healthCheckPromise = null;
|
|
189
424
|
this.config = config;
|
|
425
|
+
this.executor = new CrossShardExecutor();
|
|
190
426
|
}
|
|
191
427
|
async initialize() {
|
|
192
428
|
this.config.logger.info(`Initializing ${this.config.shards.length} shard(s)...`);
|
|
193
|
-
|
|
194
|
-
this.
|
|
429
|
+
try {
|
|
430
|
+
for (const shardConfig of this.config.shards) {
|
|
431
|
+
this.initializeShard(shardConfig);
|
|
432
|
+
}
|
|
433
|
+
} catch (error) {
|
|
434
|
+
await this.disconnectInstances();
|
|
435
|
+
this.instances.clear();
|
|
436
|
+
throw error;
|
|
195
437
|
}
|
|
438
|
+
void this.performHealthChecks(true).catch((error) => {
|
|
439
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
440
|
+
this.config.logger.error(`Initial shard health verification failed: ${message}`);
|
|
441
|
+
});
|
|
196
442
|
this.startHealthChecks();
|
|
197
443
|
this.config.logger.info("All shards initialized successfully");
|
|
198
444
|
}
|
|
@@ -214,7 +460,8 @@ var ShardManager = class {
|
|
|
214
460
|
});
|
|
215
461
|
this.config.logger.info(`Shard ${shardConfig.id} initialized`);
|
|
216
462
|
} catch (error) {
|
|
217
|
-
|
|
463
|
+
const message = sanitizeDatabaseText(String(error), [shardConfig.url]);
|
|
464
|
+
this.config.logger.error(`Failed to initialize shard ${shardConfig.id}: ${message}`);
|
|
218
465
|
throw new ConnectionError(`Failed to initialize shard ${shardConfig.id}`, shardConfig.id);
|
|
219
466
|
}
|
|
220
467
|
}
|
|
@@ -222,58 +469,108 @@ var ShardManager = class {
|
|
|
222
469
|
if (this.healthCheckInterval) {
|
|
223
470
|
clearInterval(this.healthCheckInterval);
|
|
224
471
|
}
|
|
225
|
-
this.healthCheckInterval = setInterval(
|
|
226
|
-
|
|
472
|
+
this.healthCheckInterval = setInterval(() => {
|
|
473
|
+
if (!this.healthCheckInFlight) {
|
|
474
|
+
void this.performHealthChecks(false);
|
|
475
|
+
}
|
|
227
476
|
}, this.config.healthCheckIntervalMs);
|
|
477
|
+
this.healthCheckInterval.unref?.();
|
|
228
478
|
}
|
|
229
|
-
async
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (
|
|
235
|
-
await client.$
|
|
479
|
+
async verifyShard(instance, warmConnection) {
|
|
480
|
+
const startTime = Date.now();
|
|
481
|
+
const previousHealth = instance.health;
|
|
482
|
+
try {
|
|
483
|
+
const verification = async () => {
|
|
484
|
+
if (warmConnection && isConnectable(instance.client)) {
|
|
485
|
+
await instance.client.$connect();
|
|
236
486
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
...instance.health,
|
|
240
|
-
isHealthy: true,
|
|
241
|
-
latencyMs,
|
|
242
|
-
lastChecked: /* @__PURE__ */ new Date(),
|
|
243
|
-
consecutiveFailures: 0
|
|
244
|
-
};
|
|
245
|
-
} catch (error) {
|
|
246
|
-
const consecutiveFailures = instance.health.consecutiveFailures + 1;
|
|
247
|
-
const isHealthy = consecutiveFailures < this.config.circuitBreakerThreshold;
|
|
248
|
-
instance.health = {
|
|
249
|
-
...instance.health,
|
|
250
|
-
isHealthy,
|
|
251
|
-
latencyMs: -1,
|
|
252
|
-
lastChecked: /* @__PURE__ */ new Date(),
|
|
253
|
-
errorCount: instance.health.errorCount + 1,
|
|
254
|
-
consecutiveFailures
|
|
255
|
-
};
|
|
256
|
-
if (!isHealthy) {
|
|
257
|
-
this.config.logger.error(
|
|
258
|
-
`Shard ${instance.config.id} marked unhealthy after ${consecutiveFailures} consecutive failures`
|
|
259
|
-
);
|
|
487
|
+
if (isQueryable(instance.client)) {
|
|
488
|
+
await instance.client.$queryRaw`SELECT 1`;
|
|
260
489
|
}
|
|
490
|
+
};
|
|
491
|
+
await withTimeout(
|
|
492
|
+
verification(),
|
|
493
|
+
INTERNAL_DEFAULTS.HEALTH_CHECK_TIMEOUT_MS,
|
|
494
|
+
`Health check timed out for shard ${instance.config.id}`
|
|
495
|
+
);
|
|
496
|
+
instance.health = {
|
|
497
|
+
...previousHealth,
|
|
498
|
+
isHealthy: true,
|
|
499
|
+
latencyMs: Date.now() - startTime,
|
|
500
|
+
lastChecked: /* @__PURE__ */ new Date(),
|
|
501
|
+
consecutiveFailures: 0
|
|
502
|
+
};
|
|
503
|
+
this.unhealthyAccessWarnings.delete(instance.config.id);
|
|
504
|
+
if (!previousHealth.isHealthy && previousHealth.consecutiveFailures > 0) {
|
|
505
|
+
this.config.logger.info(`Shard ${instance.config.id} recovered`);
|
|
261
506
|
}
|
|
507
|
+
} catch (error) {
|
|
508
|
+
const consecutiveFailures = previousHealth.consecutiveFailures + 1;
|
|
509
|
+
const wasVerifiedHealthy = previousHealth.isHealthy;
|
|
510
|
+
const isHealthy = warmConnection ? false : wasVerifiedHealthy && consecutiveFailures < this.config.circuitBreakerThreshold;
|
|
511
|
+
instance.health = {
|
|
512
|
+
...previousHealth,
|
|
513
|
+
isHealthy,
|
|
514
|
+
latencyMs: -1,
|
|
515
|
+
lastChecked: /* @__PURE__ */ new Date(),
|
|
516
|
+
errorCount: previousHealth.errorCount + 1,
|
|
517
|
+
consecutiveFailures
|
|
518
|
+
};
|
|
519
|
+
const message = sanitizeDatabaseText(
|
|
520
|
+
error instanceof Error ? error.message : String(error),
|
|
521
|
+
[instance.config.url]
|
|
522
|
+
);
|
|
523
|
+
const isFirstFailure = previousHealth.errorCount === 0;
|
|
524
|
+
const becameUnhealthy = previousHealth.isHealthy && !isHealthy;
|
|
525
|
+
if (isFirstFailure || becameUnhealthy) {
|
|
526
|
+
this.config.logger.error(
|
|
527
|
+
`Health check failed for ${instance.config.id}: ${message}${!isHealthy ? "; shard marked unhealthy" : ""}`
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
performHealthChecks(warmConnections) {
|
|
533
|
+
if (this.healthCheckInFlight) {
|
|
534
|
+
return this.healthCheckPromise ?? Promise.resolve();
|
|
535
|
+
}
|
|
536
|
+
this.healthCheckInFlight = true;
|
|
537
|
+
const operation = this.executor.executeAll(this.getExecutionTargets(), async (_client, shardId) => {
|
|
538
|
+
const instance = this.instances.get(shardId);
|
|
539
|
+
if (instance) {
|
|
540
|
+
await this.verifyShard(instance, warmConnections);
|
|
541
|
+
}
|
|
542
|
+
return null;
|
|
543
|
+
}).then(() => void 0).finally(() => {
|
|
544
|
+
this.healthCheckInFlight = false;
|
|
545
|
+
this.healthCheckPromise = null;
|
|
262
546
|
});
|
|
263
|
-
|
|
547
|
+
this.healthCheckPromise = operation;
|
|
548
|
+
return operation;
|
|
549
|
+
}
|
|
550
|
+
getExecutionTargets() {
|
|
551
|
+
return Array.from(this.instances.values()).map((instance) => ({
|
|
552
|
+
shardId: instance.config.id,
|
|
553
|
+
client: instance.client,
|
|
554
|
+
isHealthy: instance.health.isHealthy,
|
|
555
|
+
latencyMs: instance.health.latencyMs
|
|
556
|
+
}));
|
|
264
557
|
}
|
|
265
558
|
getClient(shardId) {
|
|
266
559
|
const instance = this.instances.get(shardId);
|
|
267
560
|
if (!instance) {
|
|
268
561
|
throw new ConnectionError(`Shard ${shardId} not found`, shardId);
|
|
269
562
|
}
|
|
270
|
-
if (!instance.health.isHealthy) {
|
|
563
|
+
if (!instance.health.isHealthy && !this.unhealthyAccessWarnings.has(shardId)) {
|
|
564
|
+
this.unhealthyAccessWarnings.add(shardId);
|
|
271
565
|
this.config.logger.warn(`Accessing unhealthy shard ${shardId}`);
|
|
272
566
|
}
|
|
273
567
|
return instance.client;
|
|
274
568
|
}
|
|
275
569
|
getClientByIndex(index) {
|
|
276
|
-
const shardId =
|
|
570
|
+
const shardId = this.getShardIds()[index];
|
|
571
|
+
if (!shardId) {
|
|
572
|
+
throw new ConnectionError(`Shard index ${index} not found`, String(index));
|
|
573
|
+
}
|
|
277
574
|
return this.getClient(shardId);
|
|
278
575
|
}
|
|
279
576
|
getAllClients() {
|
|
@@ -294,56 +591,38 @@ var ShardManager = class {
|
|
|
294
591
|
getAllHealth() {
|
|
295
592
|
return Array.from(this.instances.values()).map((instance) => instance.health);
|
|
296
593
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
Array.from(this.instances.entries()).map(async ([shardId, instance]) => {
|
|
300
|
-
try {
|
|
301
|
-
const result = await operation(instance.client, shardId);
|
|
302
|
-
return { shardId, result, error: void 0 };
|
|
303
|
-
} catch (error) {
|
|
304
|
-
return { shardId, result: null, error };
|
|
305
|
-
}
|
|
306
|
-
})
|
|
307
|
-
);
|
|
308
|
-
return results.map((result) => {
|
|
309
|
-
if (result.status === "fulfilled") {
|
|
310
|
-
return result.value;
|
|
311
|
-
}
|
|
312
|
-
return {
|
|
313
|
-
shardId: "unknown",
|
|
314
|
-
result: null,
|
|
315
|
-
error: result.reason
|
|
316
|
-
};
|
|
317
|
-
});
|
|
594
|
+
executeOnAll(operation) {
|
|
595
|
+
return this.executor.executeAll(this.getExecutionTargets(), operation);
|
|
318
596
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
for (const res of results) {
|
|
322
|
-
if (res.result !== null && res.result !== void 0) {
|
|
323
|
-
return { result: res.result, shardId: res.shardId };
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
return { result: null, shardId: null };
|
|
597
|
+
findFirst(operation) {
|
|
598
|
+
return this.executor.findFirst(this.getExecutionTargets(), operation);
|
|
327
599
|
}
|
|
328
|
-
async
|
|
329
|
-
this.config.logger.info("Shutting down...");
|
|
330
|
-
if (this.healthCheckInterval) {
|
|
331
|
-
clearInterval(this.healthCheckInterval);
|
|
332
|
-
this.healthCheckInterval = null;
|
|
333
|
-
}
|
|
600
|
+
async disconnectInstances() {
|
|
334
601
|
const disconnects = Array.from(this.instances.values()).map(async (instance) => {
|
|
335
602
|
try {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
await client.$disconnect();
|
|
603
|
+
if (isDisconnectable(instance.client)) {
|
|
604
|
+
await instance.client.$disconnect();
|
|
339
605
|
}
|
|
340
606
|
this.config.logger.info(`Shard ${instance.config.id} disconnected`);
|
|
341
607
|
} catch (error) {
|
|
342
|
-
|
|
608
|
+
const message = sanitizeDatabaseText(String(error), [instance.config.url]);
|
|
609
|
+
this.config.logger.error(`Error disconnecting shard ${instance.config.id}: ${message}`);
|
|
343
610
|
}
|
|
344
611
|
});
|
|
345
612
|
await Promise.allSettled(disconnects);
|
|
613
|
+
}
|
|
614
|
+
async shutdown() {
|
|
615
|
+
this.config.logger.info("Shutting down...");
|
|
616
|
+
if (this.healthCheckInterval) {
|
|
617
|
+
clearInterval(this.healthCheckInterval);
|
|
618
|
+
this.healthCheckInterval = null;
|
|
619
|
+
}
|
|
620
|
+
if (this.healthCheckPromise) {
|
|
621
|
+
await this.healthCheckPromise;
|
|
622
|
+
}
|
|
623
|
+
await this.disconnectInstances();
|
|
346
624
|
this.instances.clear();
|
|
625
|
+
this.unhealthyAccessWarnings.clear();
|
|
347
626
|
this.config.logger.info("Shutdown complete");
|
|
348
627
|
}
|
|
349
628
|
};
|
|
@@ -362,17 +641,34 @@ var PrismaSharding = class {
|
|
|
362
641
|
if (!config.shards || config.shards.length === 0) {
|
|
363
642
|
throw new ConfigError(ERROR_MESSAGES.NO_SHARDS);
|
|
364
643
|
}
|
|
365
|
-
if (
|
|
644
|
+
if (typeof config.createClient !== "function") {
|
|
366
645
|
throw new ConfigError(ERROR_MESSAGES.MISSING_CLIENT_FACTORY);
|
|
367
646
|
}
|
|
647
|
+
const shardIds = /* @__PURE__ */ new Set();
|
|
368
648
|
for (const shard of config.shards) {
|
|
649
|
+
if (!shard.id || shard.id.trim().length === 0) {
|
|
650
|
+
throw new ConfigError("Shard ID must not be empty");
|
|
651
|
+
}
|
|
652
|
+
if (shardIds.has(shard.id)) {
|
|
653
|
+
throw new ConfigError(`Duplicate shard ID: "${shard.id}"`);
|
|
654
|
+
}
|
|
655
|
+
shardIds.add(shard.id);
|
|
369
656
|
if (!shard.url || !validateUrl(shard.url)) {
|
|
370
657
|
throw new ConfigError(ERROR_MESSAGES.INVALID_SHARD_URL(shard.id));
|
|
371
658
|
}
|
|
659
|
+
if (shard.weight !== void 0 && (!Number.isFinite(shard.weight) || shard.weight <= 0)) {
|
|
660
|
+
throw new ConfigError(`Weight for shard "${shard.id}" must be positive`);
|
|
661
|
+
}
|
|
372
662
|
}
|
|
373
663
|
if (config.strategy && config.strategy !== "modulo" && config.strategy !== "consistent-hash") {
|
|
374
664
|
throw new ConfigError(ERROR_MESSAGES.INVALID_STRATEGY(config.strategy));
|
|
375
665
|
}
|
|
666
|
+
if (config.healthCheckIntervalMs !== void 0 && (!Number.isFinite(config.healthCheckIntervalMs) || config.healthCheckIntervalMs <= 0)) {
|
|
667
|
+
throw new ConfigError("healthCheckIntervalMs must be positive");
|
|
668
|
+
}
|
|
669
|
+
if (config.circuitBreakerThreshold !== void 0 && (!Number.isFinite(config.circuitBreakerThreshold) || config.circuitBreakerThreshold <= 0)) {
|
|
670
|
+
throw new ConfigError("circuitBreakerThreshold must be positive");
|
|
671
|
+
}
|
|
376
672
|
}
|
|
377
673
|
async connect() {
|
|
378
674
|
if (this.connected) {
|
|
@@ -386,10 +682,20 @@ var PrismaSharding = class {
|
|
|
386
682
|
circuitBreakerThreshold: this.config.circuitBreakerThreshold ?? DEFAULTS.CIRCUIT_BREAKER_THRESHOLD,
|
|
387
683
|
logger: this.logger
|
|
388
684
|
});
|
|
389
|
-
|
|
685
|
+
try {
|
|
686
|
+
await this.manager.initialize();
|
|
687
|
+
} catch (error) {
|
|
688
|
+
this.manager = null;
|
|
689
|
+
this.router = null;
|
|
690
|
+
this.connected = false;
|
|
691
|
+
throw error;
|
|
692
|
+
}
|
|
390
693
|
this.router = new ShardRouter({
|
|
391
694
|
strategy: this.config.strategy ?? "modulo",
|
|
392
695
|
shardIds: this.manager.getShardIds(),
|
|
696
|
+
shardWeights: new Map(
|
|
697
|
+
this.config.shards.map((shard) => [shard.id, shard.weight ?? 1])
|
|
698
|
+
),
|
|
393
699
|
logger: this.logger
|
|
394
700
|
});
|
|
395
701
|
this.connected = true;
|