lunibee 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/index.js +22 -10
- package/dist/core/index.js.map +8 -8
- package/dist/index.js +403 -38
- package/dist/index.js.map +17 -15
- package/dist/managers/base.d.ts +7 -0
- package/dist/managers/index.js +22 -10
- package/dist/managers/index.js.map +8 -8
- package/dist/rest/index.js +7 -2
- package/dist/rest/index.js.map +3 -3
- package/dist/sharding/cluster.d.ts +28 -0
- package/dist/sharding/host.d.ts +7 -2
- package/dist/sharding/index.d.ts +2 -0
- package/dist/sharding/index.js +376 -28
- package/dist/sharding/index.js.map +11 -9
- package/dist/sharding/leader.d.ts +34 -0
- package/dist/sharding/remote.d.ts +18 -0
- package/dist/sharding/thread.d.ts +30 -0
- package/dist/structures/index.js +3 -3
- package/dist/structures/index.js.map +3 -3
- package/package.json +16 -15
package/dist/sharding/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// packages/sharding/package.json
|
|
3
3
|
var package_default = {
|
|
4
4
|
name: "@lunibee/sharding",
|
|
5
|
-
version: "0.3.
|
|
5
|
+
version: "0.3.3",
|
|
6
6
|
type: "module",
|
|
7
7
|
module: "./src/index.ts",
|
|
8
8
|
types: "./src/index.ts",
|
|
@@ -1479,14 +1479,109 @@ function parseParentFrame(value) {
|
|
|
1479
1479
|
}
|
|
1480
1480
|
}
|
|
1481
1481
|
|
|
1482
|
+
// packages/sharding/src/thread.ts
|
|
1483
|
+
import { EventEmitter } from "events";
|
|
1484
|
+
import {
|
|
1485
|
+
isMainThread,
|
|
1486
|
+
parentPort,
|
|
1487
|
+
Worker,
|
|
1488
|
+
workerData
|
|
1489
|
+
} from "worker_threads";
|
|
1490
|
+
var THREAD_MARKER = "__lunibeeClusterThread";
|
|
1491
|
+
|
|
1492
|
+
class ThreadChild extends EventEmitter {
|
|
1493
|
+
pid;
|
|
1494
|
+
exitCode = null;
|
|
1495
|
+
signalCode = null;
|
|
1496
|
+
connected = true;
|
|
1497
|
+
#worker;
|
|
1498
|
+
#signal;
|
|
1499
|
+
constructor(worker) {
|
|
1500
|
+
super();
|
|
1501
|
+
this.#worker = worker;
|
|
1502
|
+
this.pid = worker.threadId;
|
|
1503
|
+
worker.on("message", (frame) => this.emit("message", frame));
|
|
1504
|
+
worker.on("error", (error) => this.emit("error", error));
|
|
1505
|
+
worker.once("exit", (code) => {
|
|
1506
|
+
this.connected = false;
|
|
1507
|
+
if (this.#signal) {
|
|
1508
|
+
this.signalCode = this.#signal;
|
|
1509
|
+
this.emit("exit", null, this.#signal);
|
|
1510
|
+
} else {
|
|
1511
|
+
this.exitCode = code;
|
|
1512
|
+
this.emit("exit", code, null);
|
|
1513
|
+
}
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
send(frame) {
|
|
1517
|
+
if (!this.connected)
|
|
1518
|
+
return false;
|
|
1519
|
+
try {
|
|
1520
|
+
this.#worker.postMessage(frame);
|
|
1521
|
+
return true;
|
|
1522
|
+
} catch {
|
|
1523
|
+
return false;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
kill(signal = "SIGTERM") {
|
|
1527
|
+
if (!this.connected)
|
|
1528
|
+
return false;
|
|
1529
|
+
this.#signal ??= signal;
|
|
1530
|
+
this.#worker.terminate();
|
|
1531
|
+
return true;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
function threadFork(options = {}) {
|
|
1535
|
+
return (script, env) => {
|
|
1536
|
+
const defined = {};
|
|
1537
|
+
for (const [key, value] of Object.entries(env))
|
|
1538
|
+
if (value !== undefined)
|
|
1539
|
+
defined[key] = value;
|
|
1540
|
+
const worker = new Worker(script, {
|
|
1541
|
+
env: defined,
|
|
1542
|
+
workerData: { [THREAD_MARKER]: true },
|
|
1543
|
+
...options.resourceLimits ? { resourceLimits: options.resourceLimits } : {}
|
|
1544
|
+
});
|
|
1545
|
+
return new ThreadChild(worker);
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
var endpoint;
|
|
1549
|
+
function threadEndpoint() {
|
|
1550
|
+
const port = parentPort;
|
|
1551
|
+
if (isMainThread || !port || !workerData?.[THREAD_MARKER])
|
|
1552
|
+
return;
|
|
1553
|
+
endpoint ??= portEndpoint(port);
|
|
1554
|
+
return endpoint;
|
|
1555
|
+
}
|
|
1556
|
+
function portEndpoint(port) {
|
|
1557
|
+
const wrapped = {
|
|
1558
|
+
send: (message) => {
|
|
1559
|
+
port.postMessage(message);
|
|
1560
|
+
return true;
|
|
1561
|
+
},
|
|
1562
|
+
on: (event, listener) => {
|
|
1563
|
+
if (event === "message")
|
|
1564
|
+
port.on("message", listener);
|
|
1565
|
+
return wrapped;
|
|
1566
|
+
},
|
|
1567
|
+
off: (event, listener) => {
|
|
1568
|
+
if (event === "message")
|
|
1569
|
+
port.off("message", listener);
|
|
1570
|
+
return wrapped;
|
|
1571
|
+
}
|
|
1572
|
+
};
|
|
1573
|
+
return wrapped;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1482
1576
|
// packages/sharding/src/worker.ts
|
|
1483
1577
|
class ClusterWorker {
|
|
1484
1578
|
static #current;
|
|
1485
1579
|
static current() {
|
|
1486
1580
|
if (!ClusterWorker.#current) {
|
|
1487
1581
|
const context = readClusterContext();
|
|
1488
|
-
|
|
1489
|
-
|
|
1582
|
+
const endpoint = typeof process.send === "function" ? process : threadEndpoint();
|
|
1583
|
+
if (context && endpoint)
|
|
1584
|
+
ClusterWorker.#current = new ClusterWorker(context, endpoint);
|
|
1490
1585
|
}
|
|
1491
1586
|
return ClusterWorker.#current;
|
|
1492
1587
|
}
|
|
@@ -1671,8 +1766,13 @@ class IpcTransport {
|
|
|
1671
1766
|
}
|
|
1672
1767
|
}
|
|
1673
1768
|
function defaultTransport(name) {
|
|
1674
|
-
if (process.env.LUNIBEE_SHARD_BUS === "ipc"
|
|
1675
|
-
|
|
1769
|
+
if (process.env.LUNIBEE_SHARD_BUS === "ipc") {
|
|
1770
|
+
if (typeof process.send === "function")
|
|
1771
|
+
return new IpcTransport;
|
|
1772
|
+
const thread = threadEndpoint();
|
|
1773
|
+
if (thread)
|
|
1774
|
+
return new IpcTransport(thread);
|
|
1775
|
+
}
|
|
1676
1776
|
return new BroadcastChannelTransport(name);
|
|
1677
1777
|
}
|
|
1678
1778
|
|
|
@@ -2119,7 +2219,7 @@ class IdentifyCoordinator {
|
|
|
2119
2219
|
}
|
|
2120
2220
|
|
|
2121
2221
|
// packages/sharding/src/remote.ts
|
|
2122
|
-
import { EventEmitter } from "events";
|
|
2222
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
2123
2223
|
import { randomBytes } from "crypto";
|
|
2124
2224
|
|
|
2125
2225
|
// packages/sharding/src/remote-protocol.ts
|
|
@@ -2270,7 +2370,7 @@ function parseToCoordinator(text, maxBytes = MAX_REMOTE_FRAME_BYTES) {
|
|
|
2270
2370
|
}
|
|
2271
2371
|
|
|
2272
2372
|
// packages/sharding/src/remote.ts
|
|
2273
|
-
class RemoteChild extends
|
|
2373
|
+
class RemoteChild extends EventEmitter2 {
|
|
2274
2374
|
host;
|
|
2275
2375
|
cluster;
|
|
2276
2376
|
epoch;
|
|
@@ -2316,6 +2416,7 @@ class RemoteChild extends EventEmitter {
|
|
|
2316
2416
|
class RemoteHosts {
|
|
2317
2417
|
#options;
|
|
2318
2418
|
#links = new Map;
|
|
2419
|
+
#droppedAt = new Map;
|
|
2319
2420
|
#waiters = new Set;
|
|
2320
2421
|
#hooks;
|
|
2321
2422
|
#server;
|
|
@@ -2353,6 +2454,13 @@ class RemoteHosts {
|
|
|
2353
2454
|
connected() {
|
|
2354
2455
|
return [...this.#links.keys()].sort();
|
|
2355
2456
|
}
|
|
2457
|
+
isConnected(hostId) {
|
|
2458
|
+
return this.#links.has(hostId);
|
|
2459
|
+
}
|
|
2460
|
+
fencedAt(hostId) {
|
|
2461
|
+
const dropped = this.#droppedAt.get(hostId);
|
|
2462
|
+
return dropped === undefined || this.#links.has(hostId) ? 0 : dropped + this.#options.lease * 2;
|
|
2463
|
+
}
|
|
2356
2464
|
start() {
|
|
2357
2465
|
if (this.#server)
|
|
2358
2466
|
return;
|
|
@@ -2585,6 +2693,7 @@ class RemoteHosts {
|
|
|
2585
2693
|
children: new Map
|
|
2586
2694
|
};
|
|
2587
2695
|
this.#links.set(id, link);
|
|
2696
|
+
this.#droppedAt.delete(id);
|
|
2588
2697
|
const lease = this.#options.lease;
|
|
2589
2698
|
this.#post(link, {
|
|
2590
2699
|
t: "welcome",
|
|
@@ -2617,6 +2726,9 @@ class RemoteHosts {
|
|
|
2617
2726
|
if (this.#links.get(link.id) !== link)
|
|
2618
2727
|
return;
|
|
2619
2728
|
this.#links.delete(link.id);
|
|
2729
|
+
this.#droppedAt.set(link.id, Date.now());
|
|
2730
|
+
if (this.#droppedAt.size > 1000)
|
|
2731
|
+
this.#droppedAt.delete(this.#droppedAt.keys().next().value);
|
|
2620
2732
|
if (code !== undefined)
|
|
2621
2733
|
try {
|
|
2622
2734
|
link.ws.close(code, reason.slice(0, 100));
|
|
@@ -2751,7 +2863,13 @@ class ClusterManager {
|
|
|
2751
2863
|
#stopPromise;
|
|
2752
2864
|
#wasStarted = false;
|
|
2753
2865
|
#stopping = false;
|
|
2866
|
+
#generation = 0;
|
|
2867
|
+
#leading = false;
|
|
2868
|
+
#renewTimer;
|
|
2869
|
+
#lastRenew = 0;
|
|
2870
|
+
#renewing = false;
|
|
2754
2871
|
#restartTimers = new Set;
|
|
2872
|
+
#fencing = new Map;
|
|
2755
2873
|
#startTimers = new Map;
|
|
2756
2874
|
#pauses = new Set;
|
|
2757
2875
|
#readyWaiters = new Set;
|
|
@@ -2799,6 +2917,11 @@ class ClusterManager {
|
|
|
2799
2917
|
if (options.shardIds && typeof options.shardCount === "number")
|
|
2800
2918
|
validateShardIds(options.shardIds, options.shardCount);
|
|
2801
2919
|
this.#auto = options.shardCount === "auto";
|
|
2920
|
+
const ttl = options.leader?.ttl;
|
|
2921
|
+
if (options.leader && (typeof options.leader.lock?.acquire !== "function" || typeof options.leader.lock.release !== "function"))
|
|
2922
|
+
throw new TypeError("leader.lock must have acquire() and release().");
|
|
2923
|
+
if (ttl !== undefined && (!Number.isInteger(ttl) || ttl < 100))
|
|
2924
|
+
throw new RangeError("leader.ttl must be an integer of at least 100.");
|
|
2802
2925
|
this.#options = { ...options };
|
|
2803
2926
|
this.#supervisor = new ShardSupervisor({
|
|
2804
2927
|
restartDelay: options.restartDelay,
|
|
@@ -2807,7 +2930,7 @@ class ClusterManager {
|
|
|
2807
2930
|
this.#tracker = new RequestTracker("parent", options.maxPendingRequests);
|
|
2808
2931
|
if (options.remote)
|
|
2809
2932
|
this.#remote = new RemoteHosts(options.remote, {
|
|
2810
|
-
join: (id) => this.#
|
|
2933
|
+
join: (id) => this.#hostJoined(id),
|
|
2811
2934
|
leave: (id, reason) => this.#emit("hostLeave", id, reason),
|
|
2812
2935
|
error: (error) => this.#emit("error", error)
|
|
2813
2936
|
});
|
|
@@ -2881,8 +3004,15 @@ class ClusterManager {
|
|
|
2881
3004
|
};
|
|
2882
3005
|
}
|
|
2883
3006
|
async#start() {
|
|
3007
|
+
const generation = this.#generation;
|
|
3008
|
+
const stale = () => this.#stopping || generation !== this.#generation;
|
|
3009
|
+
if (this.#options.leader && !this.#leading) {
|
|
3010
|
+
await this.#lead();
|
|
3011
|
+
if (stale())
|
|
3012
|
+
return;
|
|
3013
|
+
}
|
|
2884
3014
|
const { count, maxConcurrency, budget } = await this.#resolveGateway();
|
|
2885
|
-
if (
|
|
3015
|
+
if (stale())
|
|
2886
3016
|
return;
|
|
2887
3017
|
const ids = this.#options.shardIds ?? Array.from({ length: count }, (_, i) => i);
|
|
2888
3018
|
validateShardIds(ids, count);
|
|
@@ -2890,11 +3020,11 @@ class ClusterManager {
|
|
|
2890
3020
|
const chunks = allocateShards(ids, clusterCount);
|
|
2891
3021
|
assertCompleteAllocation(chunks, ids);
|
|
2892
3022
|
const placement = await this.#placeHosts().catch((error) => {
|
|
2893
|
-
if (
|
|
3023
|
+
if (stale())
|
|
2894
3024
|
return;
|
|
2895
3025
|
throw error;
|
|
2896
3026
|
});
|
|
2897
|
-
if (!placement ||
|
|
3027
|
+
if (!placement || stale())
|
|
2898
3028
|
return;
|
|
2899
3029
|
this.shardCount = count;
|
|
2900
3030
|
this.#wasStarted = true;
|
|
@@ -2920,7 +3050,7 @@ class ClusterManager {
|
|
|
2920
3050
|
for (const chunk of active)
|
|
2921
3051
|
this.#hostOf.set(chunk.id, placement[chunk.id % placement.length]);
|
|
2922
3052
|
for (const [index, chunk] of active.entries()) {
|
|
2923
|
-
if (
|
|
3053
|
+
if (stale())
|
|
2924
3054
|
return;
|
|
2925
3055
|
for (const shard of chunk.shards)
|
|
2926
3056
|
this.#owner.set(shard, chunk.id);
|
|
@@ -2929,7 +3059,7 @@ class ClusterManager {
|
|
|
2929
3059
|
if (index < active.length - 1 && gap > 0)
|
|
2930
3060
|
await this.#pause(gap);
|
|
2931
3061
|
}
|
|
2932
|
-
if (
|
|
3062
|
+
if (stale())
|
|
2933
3063
|
return;
|
|
2934
3064
|
if (this.#options.waitForReady)
|
|
2935
3065
|
await this.ready();
|
|
@@ -2939,6 +3069,104 @@ class ClusterManager {
|
|
|
2939
3069
|
}, this.#options.autoScaleInterval);
|
|
2940
3070
|
}
|
|
2941
3071
|
}
|
|
3072
|
+
get isLeader() {
|
|
3073
|
+
return !this.#options.leader || this.#leading;
|
|
3074
|
+
}
|
|
3075
|
+
async#lead() {
|
|
3076
|
+
const { lock, ttl = 1e4 } = this.#options.leader;
|
|
3077
|
+
const generation = this.#generation;
|
|
3078
|
+
while (!this.#stopping && generation === this.#generation) {
|
|
3079
|
+
const started = Date.now();
|
|
3080
|
+
let held = false;
|
|
3081
|
+
try {
|
|
3082
|
+
held = await withTimeout(lock.acquire(ttl), ttl / 2);
|
|
3083
|
+
} catch (error) {
|
|
3084
|
+
this.#emit("error", asError(error));
|
|
3085
|
+
}
|
|
3086
|
+
if (this.#stopping || generation !== this.#generation) {
|
|
3087
|
+
if (held)
|
|
3088
|
+
lock.release().catch(() => {
|
|
3089
|
+
return;
|
|
3090
|
+
});
|
|
3091
|
+
return;
|
|
3092
|
+
}
|
|
3093
|
+
if (held) {
|
|
3094
|
+
this.#leading = true;
|
|
3095
|
+
this.#lastRenew = started;
|
|
3096
|
+
this.#startRenewing(ttl);
|
|
3097
|
+
this.#emit("leaderAcquired");
|
|
3098
|
+
const lease = this.#options.remote ? this.#options.remote.lease ?? 15000 : 0;
|
|
3099
|
+
if (lease > 0)
|
|
3100
|
+
await this.#pause(lease * 2);
|
|
3101
|
+
return;
|
|
3102
|
+
}
|
|
3103
|
+
await this.#pause(Math.floor(ttl / 2));
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
#startRenewing(ttl) {
|
|
3107
|
+
this.#stopRenewing();
|
|
3108
|
+
const lock = this.#options.leader.lock;
|
|
3109
|
+
this.#renewTimer = setInterval(() => {
|
|
3110
|
+
if (Date.now() - this.#lastRenew > ttl / 2) {
|
|
3111
|
+
this.#standDown("The leader lock could not be renewed.");
|
|
3112
|
+
return;
|
|
3113
|
+
}
|
|
3114
|
+
if (this.#renewing)
|
|
3115
|
+
return;
|
|
3116
|
+
this.#renewing = true;
|
|
3117
|
+
const started = Date.now();
|
|
3118
|
+
lock.acquire(ttl).then((held) => {
|
|
3119
|
+
this.#renewing = false;
|
|
3120
|
+
if (!this.#leading)
|
|
3121
|
+
return;
|
|
3122
|
+
if (held)
|
|
3123
|
+
this.#lastRenew = started;
|
|
3124
|
+
else
|
|
3125
|
+
this.#standDown("Another coordinator holds the leader lock.");
|
|
3126
|
+
}, (error) => {
|
|
3127
|
+
this.#renewing = false;
|
|
3128
|
+
this.#emit("error", asError(error));
|
|
3129
|
+
});
|
|
3130
|
+
}, Math.max(10, Math.floor(ttl / 4)));
|
|
3131
|
+
this.#renewTimer.unref?.();
|
|
3132
|
+
}
|
|
3133
|
+
#stopRenewing() {
|
|
3134
|
+
if (this.#renewTimer)
|
|
3135
|
+
clearInterval(this.#renewTimer);
|
|
3136
|
+
this.#renewTimer = undefined;
|
|
3137
|
+
}
|
|
3138
|
+
async#resign() {
|
|
3139
|
+
this.#stopRenewing();
|
|
3140
|
+
if (!this.#leading)
|
|
3141
|
+
return;
|
|
3142
|
+
this.#leading = false;
|
|
3143
|
+
const { lock, ttl = 1e4 } = this.#options.leader;
|
|
3144
|
+
try {
|
|
3145
|
+
await withTimeout(lock.release(), ttl / 2);
|
|
3146
|
+
} catch (error) {
|
|
3147
|
+
this.#emit("error", asError(error));
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
#standDown(reason) {
|
|
3151
|
+
if (!this.#leading)
|
|
3152
|
+
return;
|
|
3153
|
+
this.#stopRenewing();
|
|
3154
|
+
this.#leading = false;
|
|
3155
|
+
this.#stopping = true;
|
|
3156
|
+
this.#halt();
|
|
3157
|
+
for (const cluster of this.#infos.values()) {
|
|
3158
|
+
cluster.state = "stopped";
|
|
3159
|
+
try {
|
|
3160
|
+
cluster.process.kill("SIGKILL");
|
|
3161
|
+
} catch {}
|
|
3162
|
+
}
|
|
3163
|
+
this.clusters.clear();
|
|
3164
|
+
this.#startPromise = undefined;
|
|
3165
|
+
this.#remote?.stop();
|
|
3166
|
+
this.#stopping = false;
|
|
3167
|
+
this.#emit("leaderLost", reason);
|
|
3168
|
+
this.spawn().catch((error) => this.#emit("error", asError(error)));
|
|
3169
|
+
}
|
|
2942
3170
|
async#placeHosts() {
|
|
2943
3171
|
const remote = this.#remote;
|
|
2944
3172
|
const options = this.#options.remote;
|
|
@@ -3074,14 +3302,59 @@ class ClusterManager {
|
|
|
3074
3302
|
attempt: info.restarts,
|
|
3075
3303
|
delay
|
|
3076
3304
|
});
|
|
3305
|
+
this.#restartLater(info, delay);
|
|
3306
|
+
}
|
|
3307
|
+
#restartLater(info, delay) {
|
|
3077
3308
|
const timer = setTimeout(() => {
|
|
3078
3309
|
this.#restartTimers.delete(timer);
|
|
3310
|
+
this.#fencing.delete(info.id);
|
|
3079
3311
|
if (this.#stopping || info.state !== "restarting")
|
|
3080
3312
|
return;
|
|
3081
|
-
this.#
|
|
3313
|
+
const wait = this.#relocate(info);
|
|
3314
|
+
if (wait > 0)
|
|
3315
|
+
this.#fencing.set(info.id, this.#restartLater(info, wait));
|
|
3316
|
+
else
|
|
3317
|
+
this.#launch(info.id, info.shards);
|
|
3082
3318
|
}, delay);
|
|
3083
3319
|
timer.unref?.();
|
|
3084
3320
|
this.#restartTimers.add(timer);
|
|
3321
|
+
return timer;
|
|
3322
|
+
}
|
|
3323
|
+
#hostJoined(hostId) {
|
|
3324
|
+
this.#emit("hostJoin", hostId);
|
|
3325
|
+
for (const [id, timer] of [...this.#fencing]) {
|
|
3326
|
+
const info = this.#infos.get(id);
|
|
3327
|
+
if (this.#hostOf.get(id) !== hostId || !info)
|
|
3328
|
+
continue;
|
|
3329
|
+
clearTimeout(timer);
|
|
3330
|
+
this.#restartTimers.delete(timer);
|
|
3331
|
+
this.#fencing.delete(id);
|
|
3332
|
+
if (!this.#stopping && info.state === "restarting")
|
|
3333
|
+
this.#launch(info.id, info.shards);
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
#relocate(info) {
|
|
3337
|
+
const remote = this.#remote;
|
|
3338
|
+
const host = this.#hostOf.get(info.id) ?? "local";
|
|
3339
|
+
if (!remote || this.#options.remote?.moveClusters === false || host === "local" || remote.isConnected(host))
|
|
3340
|
+
return 0;
|
|
3341
|
+
const wait = remote.fencedAt(host) - Date.now();
|
|
3342
|
+
if (wait > 0)
|
|
3343
|
+
return wait;
|
|
3344
|
+
const places = this.#options.remote?.local ? ["local", ...remote.connected()] : remote.connected();
|
|
3345
|
+
if (places.length === 0)
|
|
3346
|
+
return 0;
|
|
3347
|
+
const load = new Map(places.map((place) => [place, 0]));
|
|
3348
|
+
for (const [cluster, place] of this.#hostOf) {
|
|
3349
|
+
const state = this.#infos.get(cluster)?.state;
|
|
3350
|
+
if (load.has(place) && state !== "failed" && state !== "stopped")
|
|
3351
|
+
load.set(place, load.get(place) + 1);
|
|
3352
|
+
}
|
|
3353
|
+
const target = places.reduce((best, place) => load.get(place) < load.get(best) ? place : best);
|
|
3354
|
+
this.#hostOf.set(info.id, target);
|
|
3355
|
+
info.host = target;
|
|
3356
|
+
this.#emit("clusterMoved", info, { from: host, to: target });
|
|
3357
|
+
return 0;
|
|
3085
3358
|
}
|
|
3086
3359
|
async#reassign(failed) {
|
|
3087
3360
|
const shardIds = [...failed.shards];
|
|
@@ -3503,14 +3776,18 @@ class ClusterManager {
|
|
|
3503
3776
|
cluster.state = "stopped";
|
|
3504
3777
|
this.clusters.clear();
|
|
3505
3778
|
this.#startPromise = undefined;
|
|
3506
|
-
if (!keepRemote)
|
|
3779
|
+
if (!keepRemote) {
|
|
3507
3780
|
this.#remote?.stop();
|
|
3781
|
+
await this.#resign();
|
|
3782
|
+
}
|
|
3508
3783
|
this.#stopping = false;
|
|
3509
3784
|
}
|
|
3510
3785
|
#halt() {
|
|
3786
|
+
this.#generation++;
|
|
3511
3787
|
for (const timer of this.#restartTimers)
|
|
3512
3788
|
clearTimeout(timer);
|
|
3513
3789
|
this.#restartTimers.clear();
|
|
3790
|
+
this.#fencing.clear();
|
|
3514
3791
|
for (const id of [...this.#startTimers.keys()])
|
|
3515
3792
|
this.#clearStartTimer(id);
|
|
3516
3793
|
for (const wake of [...this.#pauses])
|
|
@@ -3565,9 +3842,20 @@ class ClusterManager {
|
|
|
3565
3842
|
this.clusters.clear();
|
|
3566
3843
|
this.#startPromise = undefined;
|
|
3567
3844
|
this.#remote?.stop();
|
|
3845
|
+
this.#resign();
|
|
3568
3846
|
this.#stopping = false;
|
|
3569
3847
|
}
|
|
3570
3848
|
}
|
|
3849
|
+
function withTimeout(promise, ms) {
|
|
3850
|
+
let timer;
|
|
3851
|
+
const timeout = new Promise((_, reject) => {
|
|
3852
|
+
timer = setTimeout(() => reject(new Error(`The leader lock did not answer within ${ms} ms.`)), ms);
|
|
3853
|
+
});
|
|
3854
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
3855
|
+
}
|
|
3856
|
+
function asError(error) {
|
|
3857
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
3858
|
+
}
|
|
3571
3859
|
// packages/sharding/src/host.ts
|
|
3572
3860
|
import { fork as fork2 } from "child_process";
|
|
3573
3861
|
class ClusterHost {
|
|
@@ -3575,6 +3863,10 @@ class ClusterHost {
|
|
|
3575
3863
|
#running = new Map;
|
|
3576
3864
|
#listeners = new Map;
|
|
3577
3865
|
#socket;
|
|
3866
|
+
#urls;
|
|
3867
|
+
#urlIndex = 0;
|
|
3868
|
+
#unreachable = 0;
|
|
3869
|
+
#accepted = false;
|
|
3578
3870
|
#lease = 15000;
|
|
3579
3871
|
#lastRx = 0;
|
|
3580
3872
|
#watchdog;
|
|
@@ -3591,18 +3883,24 @@ class ClusterHost {
|
|
|
3591
3883
|
throw new RangeError("secret must be at least 16 characters.");
|
|
3592
3884
|
if (!options.script?.trim())
|
|
3593
3885
|
throw new TypeError("A worker script path is required.");
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
url
|
|
3597
|
-
|
|
3598
|
-
|
|
3886
|
+
const urls = Array.isArray(options.url) ? [...options.url] : [options.url];
|
|
3887
|
+
if (urls.length === 0 || urls.length > 16)
|
|
3888
|
+
throw new TypeError("url must list 1 to 16 coordinators.");
|
|
3889
|
+
for (const raw of urls) {
|
|
3890
|
+
let url;
|
|
3891
|
+
try {
|
|
3892
|
+
url = new URL(raw);
|
|
3893
|
+
} catch {
|
|
3894
|
+
throw new TypeError("url must be an absolute ws:// or wss:// URL.");
|
|
3895
|
+
}
|
|
3896
|
+
if (url.protocol !== "ws:" && url.protocol !== "wss:")
|
|
3897
|
+
throw new TypeError("url must start with ws:// or wss://.");
|
|
3898
|
+
if (url.username || url.password)
|
|
3899
|
+
throw new TypeError("url must not contain credentials.");
|
|
3900
|
+
if (url.protocol === "ws:" && !options.allowInsecure && !isLoopbackHost(url.hostname))
|
|
3901
|
+
throw new TypeError("url must use wss:// to reach another machine (allowInsecure: true overrides this for a network you fully trust).");
|
|
3599
3902
|
}
|
|
3600
|
-
|
|
3601
|
-
throw new TypeError("url must start with ws:// or wss://.");
|
|
3602
|
-
if (url.username || url.password)
|
|
3603
|
-
throw new TypeError("url must not contain credentials.");
|
|
3604
|
-
if (url.protocol === "ws:" && !options.allowInsecure && !isLoopbackHost(url.hostname))
|
|
3605
|
-
throw new TypeError("url must use wss:// to reach another machine (allowInsecure: true overrides this for a network you fully trust).");
|
|
3903
|
+
this.#urls = urls;
|
|
3606
3904
|
this.#options = options;
|
|
3607
3905
|
this.#delay = options.reconnectDelay ?? 1000;
|
|
3608
3906
|
}
|
|
@@ -3634,6 +3932,7 @@ class ClusterHost {
|
|
|
3634
3932
|
return Promise.resolve();
|
|
3635
3933
|
this.#closed = false;
|
|
3636
3934
|
this.#busyAttempts = 0;
|
|
3935
|
+
this.#unreachable = 0;
|
|
3637
3936
|
return new Promise((resolve, reject) => {
|
|
3638
3937
|
this.#firstConnect = { resolve, reject };
|
|
3639
3938
|
this.#open();
|
|
@@ -3665,7 +3964,8 @@ class ClusterHost {
|
|
|
3665
3964
|
});
|
|
3666
3965
|
}
|
|
3667
3966
|
#open() {
|
|
3668
|
-
|
|
3967
|
+
this.#accepted = false;
|
|
3968
|
+
const socket = new WebSocket(this.#urls[this.#urlIndex]);
|
|
3669
3969
|
this.#socket = socket;
|
|
3670
3970
|
socket.addEventListener("message", (event) => this.#message(socket, event.data));
|
|
3671
3971
|
socket.addEventListener("error", () => {});
|
|
@@ -3727,6 +4027,7 @@ class ClusterHost {
|
|
|
3727
4027
|
return;
|
|
3728
4028
|
case "welcome":
|
|
3729
4029
|
this.#lease = message.leaseMs;
|
|
4030
|
+
this.#accepted = true;
|
|
3730
4031
|
this.#connected = true;
|
|
3731
4032
|
this.#delay = this.#options.reconnectDelay ?? 1000;
|
|
3732
4033
|
this.#startWatchdog();
|
|
@@ -3841,6 +4142,13 @@ class ClusterHost {
|
|
|
3841
4142
|
const refused = code === RemoteClose.AuthFailed || code === RemoteClose.UnknownHost || code === RemoteClose.DuplicateHost || code === RemoteClose.VersionMismatch;
|
|
3842
4143
|
const first = this.#firstConnect;
|
|
3843
4144
|
const busy = code === RemoteClose.TooManyConnections || code === RemoteClose.HelloTimeout;
|
|
4145
|
+
const accepted = this.#accepted;
|
|
4146
|
+
if (this.#urls.length > 1)
|
|
4147
|
+
this.#urlIndex = (this.#urlIndex + 1) % this.#urls.length;
|
|
4148
|
+
if (first && !refused && !busy && !accepted && ++this.#unreachable < this.#urls.length) {
|
|
4149
|
+
this.#open();
|
|
4150
|
+
return;
|
|
4151
|
+
}
|
|
3844
4152
|
if (first && busy && ++this.#busyAttempts < (this.#options.connectAttempts ?? 10)) {
|
|
3845
4153
|
this.#retryLater();
|
|
3846
4154
|
return;
|
|
@@ -3868,6 +4176,43 @@ class ClusterHost {
|
|
|
3868
4176
|
function defaultFork(script, env) {
|
|
3869
4177
|
return fork2(script, [], { env });
|
|
3870
4178
|
}
|
|
4179
|
+
// packages/sharding/src/leader.ts
|
|
4180
|
+
var ACQUIRE = `local v = redis.call('GET', KEYS[1])
|
|
4181
|
+
if v == ARGV[1] then redis.call('PEXPIRE', KEYS[1], ARGV[2]) return 1 end
|
|
4182
|
+
if not v then redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2]) return 1 end
|
|
4183
|
+
return 0`;
|
|
4184
|
+
var RELEASE = `if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) end
|
|
4185
|
+
return 0`;
|
|
4186
|
+
|
|
4187
|
+
class RedisLeaderLock {
|
|
4188
|
+
#client;
|
|
4189
|
+
#key;
|
|
4190
|
+
#token;
|
|
4191
|
+
constructor(options) {
|
|
4192
|
+
if (typeof options?.client?.send !== "function")
|
|
4193
|
+
throw new TypeError("RedisLeaderLock needs a client with send().");
|
|
4194
|
+
this.#client = options.client;
|
|
4195
|
+
this.#key = options.key ?? "lunibee:cluster:leader";
|
|
4196
|
+
this.#token = options.token ?? crypto.randomUUID();
|
|
4197
|
+
if (!this.#key || !this.#token)
|
|
4198
|
+
throw new TypeError("key and token must not be empty.");
|
|
4199
|
+
}
|
|
4200
|
+
async acquire(ttlMs) {
|
|
4201
|
+
if (!Number.isInteger(ttlMs) || ttlMs < 1)
|
|
4202
|
+
throw new RangeError("ttlMs must be a positive integer.");
|
|
4203
|
+
const reply = await this.#client.send("EVAL", [
|
|
4204
|
+
ACQUIRE,
|
|
4205
|
+
"1",
|
|
4206
|
+
this.#key,
|
|
4207
|
+
this.#token,
|
|
4208
|
+
String(ttlMs)
|
|
4209
|
+
]);
|
|
4210
|
+
return Number(reply) === 1;
|
|
4211
|
+
}
|
|
4212
|
+
async release() {
|
|
4213
|
+
await this.#client.send("EVAL", [RELEASE, "1", this.#key, this.#token]);
|
|
4214
|
+
}
|
|
4215
|
+
}
|
|
3871
4216
|
|
|
3872
4217
|
// packages/sharding/src/index.ts
|
|
3873
4218
|
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -4258,6 +4603,7 @@ export {
|
|
|
4258
4603
|
IpcTransport,
|
|
4259
4604
|
MAX_GUILDS_PER_SHARD,
|
|
4260
4605
|
REMOTE_PROTOCOL_VERSION,
|
|
4606
|
+
RedisLeaderLock,
|
|
4261
4607
|
RemoteClose,
|
|
4262
4608
|
RequestTracker,
|
|
4263
4609
|
ShardBus,
|
|
@@ -4271,7 +4617,9 @@ export {
|
|
|
4271
4617
|
isClusterWorker,
|
|
4272
4618
|
readClusterContext,
|
|
4273
4619
|
shardsForGuilds,
|
|
4620
|
+
threadEndpoint,
|
|
4621
|
+
threadFork,
|
|
4274
4622
|
validateShardIds
|
|
4275
4623
|
};
|
|
4276
4624
|
|
|
4277
|
-
//# debugId=
|
|
4625
|
+
//# debugId=D3CF0E421E8E307864756E2164756E21
|