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