@helyx/bot 0.4.1 → 0.4.4
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/CHANGELOG.md +35 -0
- package/README.md +13 -0
- package/dist/cloud-connector.d.ts +7 -0
- package/dist/cloud-connector.js +118 -1
- package/dist/cloud-control-dispatcher.js +84 -0
- package/dist/control-server.d.ts +187 -5
- package/dist/control-server.js +673 -6
- package/dist/developer-operations-module.js +8 -0
- package/dist/discord-owned-message-service.d.ts +39 -0
- package/dist/discord-owned-message-service.js +138 -0
- package/dist/events.d.ts +70 -1
- package/dist/events.js +557 -5
- package/dist/index.js +61 -13
- package/dist/interactions.d.ts +3 -2
- package/dist/interactions.js +66 -31
- package/dist/module-logging.d.ts +1 -0
- package/dist/module-logging.js +19 -1
- package/dist/modules.d.ts +9 -0
- package/dist/modules.js +45 -0
- package/dist/resource-authorization.d.ts +7 -0
- package/dist/resource-authorization.js +12 -0
- package/dist/scheduled-task-runner.d.ts +130 -0
- package/dist/scheduled-task-runner.js +417 -0
- package/dist/shard-cloud-proxy.d.ts +4 -0
- package/dist/shard-cloud-proxy.js +34 -0
- package/dist/shard-contracts.d.ts +574 -23
- package/dist/shard-contracts.js +346 -0
- package/dist/shard-coordinator.d.ts +5 -3
- package/dist/shard-coordinator.js +85 -2
- package/dist/shard-router.d.ts +10 -0
- package/dist/shard-router.js +58 -0
- package/dist/shard-supervisor.d.ts +2 -1
- package/dist/shard-supervisor.js +187 -5
- package/dist/shard-worker.d.ts +8 -2
- package/dist/shard-worker.js +97 -16
- package/dist/support-module.d.ts +9 -0
- package/dist/support-module.js +172 -0
- package/package.json +9 -9
package/dist/shard-router.js
CHANGED
|
@@ -28,6 +28,63 @@ export class ShardControlRouter {
|
|
|
28
28
|
executeDashboardAction(input) {
|
|
29
29
|
return this.#route({ kind: "guild.module.action.execute", ...input });
|
|
30
30
|
}
|
|
31
|
+
listManagedResources(input) {
|
|
32
|
+
return this.#route(shardControlOperationSchema.parse({
|
|
33
|
+
kind: "guild.module.resource.list",
|
|
34
|
+
...input,
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
readManagedResource(input) {
|
|
38
|
+
return this.#route({
|
|
39
|
+
kind: "guild.module.resource.read",
|
|
40
|
+
...input,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
listManagedResourceRelation(input) {
|
|
44
|
+
return this.#route(shardControlOperationSchema.parse({
|
|
45
|
+
kind: "guild.module.resource.relation.list",
|
|
46
|
+
...input,
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
previewManagedResource(input) {
|
|
50
|
+
return this.#route(shardControlOperationSchema.parse({
|
|
51
|
+
kind: "guild.module.resource.preview",
|
|
52
|
+
...input,
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
createManagedResource(input) {
|
|
56
|
+
return this.#route(shardControlOperationSchema.parse({
|
|
57
|
+
kind: "guild.module.resource.create",
|
|
58
|
+
...input,
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
updateManagedResource(input) {
|
|
62
|
+
return this.#route(shardControlOperationSchema.parse({
|
|
63
|
+
kind: "guild.module.resource.update",
|
|
64
|
+
...input,
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
deleteManagedResource(input) {
|
|
68
|
+
return this.#route({
|
|
69
|
+
kind: "guild.module.resource.delete",
|
|
70
|
+
...input,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
executeManagedResourceAction(input) {
|
|
74
|
+
return this.#route({
|
|
75
|
+
kind: "guild.module.resource.action.execute",
|
|
76
|
+
...input,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
sendSupportNotification(input) {
|
|
80
|
+
if (!this.input.notificationGuildId)
|
|
81
|
+
throw new BotControlTransportError(404, "The support notification channel is not configured");
|
|
82
|
+
return this.#route({
|
|
83
|
+
kind: "support.notification.send",
|
|
84
|
+
guildId: this.input.notificationGuildId,
|
|
85
|
+
...input,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
31
88
|
listPrivacyProviders() {
|
|
32
89
|
return this.#routeLeader({ kind: "privacy.providers.list" });
|
|
33
90
|
}
|
|
@@ -171,6 +228,7 @@ function unwrapResponse(response) {
|
|
|
171
228
|
const accepted = shardControlResponseSchema.parse(response);
|
|
172
229
|
if (!accepted.ok)
|
|
173
230
|
throw new BotControlTransportError(accepted.status === 400 ||
|
|
231
|
+
accepted.status === 403 ||
|
|
174
232
|
accepted.status === 404 ||
|
|
175
233
|
accepted.status === 409 ||
|
|
176
234
|
accepted.status === 504
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { BotEnvironment } from "@helyx/config";
|
|
2
2
|
import { type DatabasePool } from "@helyx/database";
|
|
3
|
-
import type { ModuleDefinition } from "@helyx/sdk";
|
|
3
|
+
import type { ModuleDefinition, ScheduledTaskExecutionResult } from "@helyx/sdk";
|
|
4
4
|
import { type ShardCloudRequest, type ShardIdentity, type ShardProcessHealth } from "./shard-contracts.js";
|
|
5
5
|
import { type ShardCoordinator, type ShardGenerationDescriptor, type ShardLeaderLease, type ShardLease } from "./shard-coordinator.js";
|
|
6
6
|
import type { BotHandle } from "./index.js";
|
|
7
7
|
export declare const STEADY_STATE_LEASE_LOSS_BOUND_MS: number;
|
|
8
|
+
export declare function deferScheduledRoutingFailures(route: () => Promise<ScheduledTaskExecutionResult>): Promise<ScheduledTaskExecutionResult>;
|
|
8
9
|
export declare function startShardSupervisor(environment: BotEnvironment, processEnvironment: NodeJS.ProcessEnv, workerEntry: string): Promise<BotHandle>;
|
|
9
10
|
export declare function buildShardWorkerEnvironment(source: NodeJS.ProcessEnv, identity: ShardIdentity, restartCount: number): NodeJS.ProcessEnv;
|
|
10
11
|
export declare function shardRuntimeCompositionHash(modules: readonly ModuleDefinition[], internalGuildId?: string): string;
|
package/dist/shard-supervisor.js
CHANGED
|
@@ -13,13 +13,15 @@ import { BotControlService, createBotControlServer, mapBotControlError, } from "
|
|
|
13
13
|
import { createManagementModule } from "./management-module.js";
|
|
14
14
|
import { buildApplicationCommandDefinitions } from "./interactions.js";
|
|
15
15
|
import { resolveModulePackageSpecifiers } from "./module-configuration.js";
|
|
16
|
-
import { loadConfiguredModulePackages } from "./modules.js";
|
|
16
|
+
import { deriveSecureRandomSelectionKey, loadConfiguredModulePackages, platformServiceOptions, secureRandomSelectionWorkerKeyName, } from "./modules.js";
|
|
17
17
|
import { createPermissionsModule } from "./permissions-module.js";
|
|
18
|
-
import { SHARD_CONTROL_MAX_BYTES, SHARD_HEALTH_STALE_MS, assertShardMessageSize, controlOperationGuildId, executeShardControlOperation, gatewayBotInformationSchema, shardCloudRequestSchema, shardHealthSnapshotSchema, workerMessageSchema, } from "./shard-contracts.js";
|
|
18
|
+
import { SHARD_CONTROL_MAX_BYTES, SHARD_HEALTH_STALE_MS, assertShardMessageSize, controlOperationGuildId, executeShardControlOperation, gatewayBotInformationSchema, guildIdToShardId, shardCloudRequestSchema, shardHealthSnapshotSchema, workerMessageSchema, } from "./shard-contracts.js";
|
|
19
19
|
import { RedisShardCoordinator, } from "./shard-coordinator.js";
|
|
20
20
|
import { ShardControlRouter } from "./shard-router.js";
|
|
21
21
|
import { createDeveloperOperationsModule } from "./developer-operations-module.js";
|
|
22
22
|
import { SupervisorCloudDiscordDirectory } from "./supervisor-cloud-directory.js";
|
|
23
|
+
import { createSupportModule } from "./support-module.js";
|
|
24
|
+
import { createScheduledTaskRunner, schedulerOperationalHealth, } from "./scheduled-task-runner.js";
|
|
23
25
|
const LEASE_RENEW_INTERVAL_MS = 10_000;
|
|
24
26
|
const STARTUP_LEASE_RENEW_INTERVAL_MS = 5_000;
|
|
25
27
|
const LEASE_VALIDATION_TIMEOUT_MS = 1_000;
|
|
@@ -29,6 +31,18 @@ const WORKER_SHUTDOWN_MS = 5_000;
|
|
|
29
31
|
const MAX_RESTARTS_PER_WINDOW = 5;
|
|
30
32
|
const RESTART_WINDOW_MS = 10 * 60 * 1_000;
|
|
31
33
|
export const STEADY_STATE_LEASE_LOSS_BOUND_MS = LEASE_RENEW_INTERVAL_MS + LEASE_VALIDATION_TIMEOUT_MS + WORKER_SHUTDOWN_MS;
|
|
34
|
+
export async function deferScheduledRoutingFailures(route) {
|
|
35
|
+
try {
|
|
36
|
+
return await route();
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return {
|
|
40
|
+
outcome: "deferred",
|
|
41
|
+
delayMs: 1_000,
|
|
42
|
+
safeCode: "routing_unavailable",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
32
46
|
export async function startShardSupervisor(environment, processEnvironment, workerEntry) {
|
|
33
47
|
if (environment.HELYX_SHARD_MODE === "off")
|
|
34
48
|
throw new Error("Shard supervisor requires sharding to be enabled");
|
|
@@ -65,6 +79,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
65
79
|
const localLeases = new Map();
|
|
66
80
|
const healthByShard = new Map();
|
|
67
81
|
const pendingControl = new Map();
|
|
82
|
+
const pendingScheduled = new Map();
|
|
68
83
|
const restartHistory = new Map();
|
|
69
84
|
const restartCounts = new Map();
|
|
70
85
|
const workers = new Map();
|
|
@@ -75,6 +90,9 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
75
90
|
let shardCount = 1;
|
|
76
91
|
let privacyService;
|
|
77
92
|
let manager;
|
|
93
|
+
let scheduledTasks;
|
|
94
|
+
let scheduledModules = [];
|
|
95
|
+
let workersRoutable = false;
|
|
78
96
|
let promoteToLeader = () => Promise.resolve(false);
|
|
79
97
|
try {
|
|
80
98
|
await checkDatabaseConnection(database);
|
|
@@ -112,6 +130,9 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
112
130
|
getGuildResources: () => Promise.reject(new Error("Guild resources are worker-owned")),
|
|
113
131
|
sendToChannel: () => Promise.reject(new Error("Guild resources are worker-owned")),
|
|
114
132
|
deleteChannelMessage: () => Promise.reject(new Error("Guild resources are worker-owned")),
|
|
133
|
+
editChannelMessage: () => Promise.reject(new Error("Guild resources are worker-owned")),
|
|
134
|
+
addChannelMessageReaction: () => Promise.reject(new Error("Guild resources are worker-owned")),
|
|
135
|
+
resolveActor: () => Promise.reject(new Error("Guild resources are worker-owned")),
|
|
115
136
|
};
|
|
116
137
|
const permissionsModule = createPermissionsModule(featureModules);
|
|
117
138
|
const managementModule = createManagementModule(featureModules, unavailableResources);
|
|
@@ -123,12 +144,15 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
123
144
|
configuredOwnerUserIds: environment.HELYX_OWNER_USER_IDS,
|
|
124
145
|
})
|
|
125
146
|
: undefined;
|
|
147
|
+
const supportModule = createSupportModule(cloudConnector);
|
|
126
148
|
const publicModules = [
|
|
127
149
|
managementModule,
|
|
128
150
|
permissionsModule,
|
|
151
|
+
supportModule,
|
|
129
152
|
...(cloudManagementModule ? [cloudManagementModule] : []),
|
|
130
153
|
...featureModules,
|
|
131
154
|
];
|
|
155
|
+
scheduledModules = publicModules;
|
|
132
156
|
const operationsModule = environment.DISCORD_INTERNAL_GUILD_ID
|
|
133
157
|
? createDeveloperOperationsModule({
|
|
134
158
|
internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID,
|
|
@@ -142,6 +166,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
142
166
|
coreFunctions: [
|
|
143
167
|
managementModule,
|
|
144
168
|
permissionsModule,
|
|
169
|
+
supportModule,
|
|
145
170
|
...(cloudManagementModule ? [cloudManagementModule] : []),
|
|
146
171
|
].map(({ manifest }) => manifest),
|
|
147
172
|
modules: featureModules.map(({ manifest }) => manifest),
|
|
@@ -160,7 +185,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
160
185
|
assertStartupLeaseHealthy();
|
|
161
186
|
privacyDatabase = await createPrivacyDatabase(environment, database);
|
|
162
187
|
assertStartupLeaseHealthy();
|
|
163
|
-
const services = new Map(createPlatformServices(database, publicModules));
|
|
188
|
+
const services = new Map(createPlatformServices(database, publicModules, platformServiceOptions(environment, publicModules)));
|
|
164
189
|
privacyService = new BotControlService(database, publicModules, unavailableResources, new ServiceRegistry(services), privacyDatabase
|
|
165
190
|
? new PostgresPrivacyProviderExecutor({
|
|
166
191
|
readPool: database,
|
|
@@ -176,6 +201,11 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
176
201
|
privacy: privacyService,
|
|
177
202
|
localLeaderLease: leaderLease,
|
|
178
203
|
...(coordinator ? { coordinator } : {}),
|
|
204
|
+
...(environment.DISCORD_INTERNAL_GUILD_ID
|
|
205
|
+
? {
|
|
206
|
+
notificationGuildId: environment.DISCORD_INTERNAL_GUILD_ID,
|
|
207
|
+
}
|
|
208
|
+
: {}),
|
|
179
209
|
}));
|
|
180
210
|
await startLeaderSingletons();
|
|
181
211
|
assertStartupLeaseHealthy();
|
|
@@ -194,7 +224,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
194
224
|
onError: (error) => logger.error({ event: "shard.redis_error", error: safeError(error) }, "Shard coordination failed"),
|
|
195
225
|
});
|
|
196
226
|
supervisorId = coordinator.supervisorId;
|
|
197
|
-
await coordinator.start((targetShardId, request) => dispatchLocal(targetShardId, request), (request) => executeLeaderControl(request), (request) => executeLeaderCloud(request));
|
|
227
|
+
await coordinator.start((targetShardId, request) => dispatchLocal(targetShardId, request), (request) => executeLeaderControl(request), (request) => executeLeaderCloud(request), (targetShardId, request) => dispatchLocalScheduled(targetShardId, request));
|
|
198
228
|
deploymentOrder = await coordinator.assertGeneration(generationDescriptor, deploymentOrder);
|
|
199
229
|
leaderLease = await coordinator.acquireLeader(generationDescriptor, deploymentOrder);
|
|
200
230
|
startStartupLeaseMaintenance();
|
|
@@ -249,6 +279,11 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
249
279
|
...(privacyService ? { privacy: privacyService } : {}),
|
|
250
280
|
...(leaderLease ? { localLeaderLease: leaderLease } : {}),
|
|
251
281
|
...(coordinator ? { coordinator } : {}),
|
|
282
|
+
...(environment.DISCORD_INTERNAL_GUILD_ID
|
|
283
|
+
? {
|
|
284
|
+
notificationGuildId: environment.DISCORD_INTERNAL_GUILD_ID,
|
|
285
|
+
}
|
|
286
|
+
: {}),
|
|
252
287
|
});
|
|
253
288
|
if (!leaderLease)
|
|
254
289
|
cloudConnector.setControlHandler(router);
|
|
@@ -263,6 +298,9 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
263
298
|
});
|
|
264
299
|
await reserveInitialSessionStarts(claimedIds.length, gateway);
|
|
265
300
|
await spawnInIdentifyBuckets(claimedIds, gateway.session_start_limit.max_concurrency);
|
|
301
|
+
workersRoutable = true;
|
|
302
|
+
if (leaderLease)
|
|
303
|
+
startScheduledTasks();
|
|
266
304
|
assertStartupLeaseHealthy();
|
|
267
305
|
if (environment.HELYX_BOT_CONTROL_SECRET) {
|
|
268
306
|
controlServer = await createBotControlServer({
|
|
@@ -442,6 +480,15 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
442
480
|
pending.resolve(message.data);
|
|
443
481
|
return;
|
|
444
482
|
}
|
|
483
|
+
if (message.data.type === "helyx.scheduled.response") {
|
|
484
|
+
const pending = pendingScheduled.get(message.data.requestId);
|
|
485
|
+
if (!pending || pending.shardId !== shardId)
|
|
486
|
+
return;
|
|
487
|
+
pendingScheduled.delete(message.data.requestId);
|
|
488
|
+
clearTimeout(pending.timer);
|
|
489
|
+
pending.resolve(message.data);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
445
492
|
if (message.data.type === "helyx.cloud.request") {
|
|
446
493
|
const response = leaderLease
|
|
447
494
|
? await executeLeaderCloud(message.data)
|
|
@@ -478,6 +525,24 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
478
525
|
});
|
|
479
526
|
});
|
|
480
527
|
}
|
|
528
|
+
function dispatchLocalScheduled(shardId, request) {
|
|
529
|
+
const shard = workers.get(shardId);
|
|
530
|
+
if (!shard?.process)
|
|
531
|
+
return Promise.resolve(unavailableScheduled(request.requestId));
|
|
532
|
+
return new Promise((resolve) => {
|
|
533
|
+
const timer = setTimeout(() => {
|
|
534
|
+
pendingScheduled.delete(request.requestId);
|
|
535
|
+
resolve(unavailableScheduled(request.requestId, "shard_timeout"));
|
|
536
|
+
}, Math.max(1, new Date(request.deadlineAt).getTime() - Date.now()));
|
|
537
|
+
timer.unref();
|
|
538
|
+
pendingScheduled.set(request.requestId, { shardId, resolve, timer });
|
|
539
|
+
void shard.send(request).catch(() => {
|
|
540
|
+
clearTimeout(timer);
|
|
541
|
+
pendingScheduled.delete(request.requestId);
|
|
542
|
+
resolve(unavailableScheduled(request.requestId));
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
}
|
|
481
546
|
async function executeLeaderControl(request) {
|
|
482
547
|
if (!leaderLease || !privacyService)
|
|
483
548
|
return unavailableControl(request.requestId);
|
|
@@ -549,6 +614,23 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
549
614
|
return failedCloud(parsed.requestId, "Cloud request expired");
|
|
550
615
|
result = await cloudConnector.rotate();
|
|
551
616
|
break;
|
|
617
|
+
case "support.bug.create":
|
|
618
|
+
if (cloudRequestExpired(parsed))
|
|
619
|
+
return failedCloud(parsed.requestId, "Support request expired");
|
|
620
|
+
result = await cloudConnector.submitBug({
|
|
621
|
+
operation: "support.bug.create.v1",
|
|
622
|
+
reporterDiscordUserId: parsed.operation.reporterDiscordUserId,
|
|
623
|
+
guildId: parsed.operation.guildId,
|
|
624
|
+
moduleOrArea: parsed.operation.moduleOrArea,
|
|
625
|
+
summary: parsed.operation.summary,
|
|
626
|
+
whatHappened: parsed.operation.whatHappened,
|
|
627
|
+
...(parsed.operation.extraDetail
|
|
628
|
+
? { extraDetail: parsed.operation.extraDetail }
|
|
629
|
+
: {}),
|
|
630
|
+
interactionId: parsed.operation.interactionId,
|
|
631
|
+
correlationId: parsed.operation.correlationId,
|
|
632
|
+
});
|
|
633
|
+
break;
|
|
552
634
|
}
|
|
553
635
|
const response = {
|
|
554
636
|
version: 1,
|
|
@@ -700,6 +782,72 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
700
782
|
retentionTimer.unref();
|
|
701
783
|
await cloudConnector?.start();
|
|
702
784
|
cloudStarted = true;
|
|
785
|
+
if (workersRoutable)
|
|
786
|
+
startScheduledTasks();
|
|
787
|
+
}
|
|
788
|
+
function startScheduledTasks() {
|
|
789
|
+
if (scheduledTasks || !leaderLease || !workersRoutable)
|
|
790
|
+
return;
|
|
791
|
+
scheduledTasks = createScheduledTaskRunner({
|
|
792
|
+
database,
|
|
793
|
+
modules: scheduledModules,
|
|
794
|
+
route: routeScheduledTask,
|
|
795
|
+
logger,
|
|
796
|
+
workerId: `shard-leader:${leaderLease.generation}:${String(leaderLease.fencingToken)}`,
|
|
797
|
+
});
|
|
798
|
+
scheduledTasks.start();
|
|
799
|
+
}
|
|
800
|
+
async function routeScheduledTask(task, signal) {
|
|
801
|
+
if (!task.guildId)
|
|
802
|
+
return {
|
|
803
|
+
outcome: "requires_review",
|
|
804
|
+
safeCode: "guild_route_required",
|
|
805
|
+
};
|
|
806
|
+
if (!leaderLease || signal.aborted)
|
|
807
|
+
return {
|
|
808
|
+
outcome: "deferred",
|
|
809
|
+
delayMs: 1_000,
|
|
810
|
+
safeCode: "leader_unavailable",
|
|
811
|
+
};
|
|
812
|
+
return deferScheduledRoutingFailures(async () => {
|
|
813
|
+
const shardId = guildIdToShardId(task.guildId, shardCount);
|
|
814
|
+
const lease = localLeases.get(shardId) ??
|
|
815
|
+
(coordinator ? await coordinator.getShardLease(shardId) : undefined);
|
|
816
|
+
if (!lease || lease.generation !== generationDescriptor.generation)
|
|
817
|
+
return {
|
|
818
|
+
outcome: "deferred",
|
|
819
|
+
delayMs: 1_000,
|
|
820
|
+
safeCode: "shard_unavailable",
|
|
821
|
+
};
|
|
822
|
+
const request = {
|
|
823
|
+
version: 1,
|
|
824
|
+
type: "helyx.scheduled.request",
|
|
825
|
+
requestId: randomUUID(),
|
|
826
|
+
deadlineAt: new Date(Date.now() + 10_000).toISOString(),
|
|
827
|
+
identity: {
|
|
828
|
+
version: 1,
|
|
829
|
+
mode: shardMode,
|
|
830
|
+
scope: environment.HELYX_SHARD_SCOPE,
|
|
831
|
+
generation: lease.generation,
|
|
832
|
+
deploymentOrder: lease.deploymentOrder,
|
|
833
|
+
supervisorId: lease.supervisorId,
|
|
834
|
+
shardId,
|
|
835
|
+
shardCount,
|
|
836
|
+
fencingToken: lease.fencingToken,
|
|
837
|
+
},
|
|
838
|
+
task: {
|
|
839
|
+
...task,
|
|
840
|
+
guildId: task.guildId,
|
|
841
|
+
},
|
|
842
|
+
};
|
|
843
|
+
assertShardMessageSize(request);
|
|
844
|
+
const response = lease.supervisorId === supervisorId
|
|
845
|
+
? await dispatchLocalScheduled(shardId, request)
|
|
846
|
+
: coordinator
|
|
847
|
+
? await coordinator.routeRemoteScheduled(lease, request)
|
|
848
|
+
: unavailableScheduled(request.requestId);
|
|
849
|
+
return response.result;
|
|
850
|
+
});
|
|
703
851
|
}
|
|
704
852
|
function startStartupLeaseMaintenance() {
|
|
705
853
|
if (!coordinator || startupLeaseTimer)
|
|
@@ -749,6 +897,11 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
749
897
|
}
|
|
750
898
|
async function buildHealth() {
|
|
751
899
|
const now = Date.now();
|
|
900
|
+
const scheduler = scheduledTasks
|
|
901
|
+
? await withTimeout(scheduledTasks.getHealth(), LEASE_VALIDATION_TIMEOUT_MS)
|
|
902
|
+
.then(schedulerOperationalHealth)
|
|
903
|
+
.catch(() => ({ status: "unavailable" }))
|
|
904
|
+
: { status: "unavailable" };
|
|
752
905
|
let redisReady = true;
|
|
753
906
|
if (coordinator)
|
|
754
907
|
await withTimeout(coordinator.checkReady(), LEASE_VALIDATION_TIMEOUT_MS).catch(() => {
|
|
@@ -802,7 +955,8 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
802
955
|
? "stale"
|
|
803
956
|
: readyShardCount === shardCount &&
|
|
804
957
|
coordinationRedis !== "unavailable" &&
|
|
805
|
-
databaseHealthy
|
|
958
|
+
databaseHealthy &&
|
|
959
|
+
scheduler.status === "available"
|
|
806
960
|
? "healthy"
|
|
807
961
|
: readyShardCount === 0
|
|
808
962
|
? "unavailable"
|
|
@@ -818,6 +972,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
818
972
|
redis: coordinationRedis,
|
|
819
973
|
database: databaseHealthy ? "healthy" : "unavailable",
|
|
820
974
|
},
|
|
975
|
+
scheduler,
|
|
821
976
|
...(sessionBudget ? { sessionStarts: sessionBudget } : {}),
|
|
822
977
|
shards,
|
|
823
978
|
});
|
|
@@ -832,6 +987,14 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
832
987
|
clearInterval(startupLeaseTimer);
|
|
833
988
|
if (retentionTimer)
|
|
834
989
|
clearInterval(retentionTimer);
|
|
990
|
+
workersRoutable = false;
|
|
991
|
+
await scheduledTasks?.stop().catch(() => undefined);
|
|
992
|
+
scheduledTasks = undefined;
|
|
993
|
+
for (const [requestId, pending] of pendingScheduled) {
|
|
994
|
+
clearTimeout(pending.timer);
|
|
995
|
+
pending.resolve(unavailableScheduled(requestId, "supervisor_stopping"));
|
|
996
|
+
}
|
|
997
|
+
pendingScheduled.clear();
|
|
835
998
|
await controlServer?.close().catch(() => undefined);
|
|
836
999
|
await Promise.allSettled([...workers.values()].map(async (shard) => {
|
|
837
1000
|
if (!shard.process)
|
|
@@ -965,6 +1128,7 @@ export function buildShardWorkerEnvironment(source, identity, restartCount) {
|
|
|
965
1128
|
"DISCORD_TOKEN",
|
|
966
1129
|
"DISCORD_CLIENT_ID",
|
|
967
1130
|
"DISCORD_INTERNAL_GUILD_ID",
|
|
1131
|
+
"HELYX_SUPPORT_STAFF_CHANNEL_ID",
|
|
968
1132
|
"HELYX_MODULE_PACKAGES",
|
|
969
1133
|
"HELYX_MODULE_CATALOGUE_PATH",
|
|
970
1134
|
"HELYX_CLOUD_CONNECT_ENABLED",
|
|
@@ -988,8 +1152,14 @@ export function buildShardWorkerEnvironment(source, identity, restartCount) {
|
|
|
988
1152
|
"TMP",
|
|
989
1153
|
]);
|
|
990
1154
|
const environment = Object.fromEntries(Object.entries(source).filter(([key, value]) => allowed.has(key) && value !== undefined));
|
|
1155
|
+
const selectionKey = source.HELYX_DATA_ENCRYPTION_KEY
|
|
1156
|
+
? Buffer.from(deriveSecureRandomSelectionKey(source.HELYX_DATA_ENCRYPTION_KEY)).toString("base64")
|
|
1157
|
+
: undefined;
|
|
991
1158
|
return {
|
|
992
1159
|
...environment,
|
|
1160
|
+
...(selectionKey
|
|
1161
|
+
? { [secureRandomSelectionWorkerKeyName]: selectionKey }
|
|
1162
|
+
: {}),
|
|
993
1163
|
SHARDING_MANAGER: "true",
|
|
994
1164
|
SHARDING_MANAGER_MODE: "process",
|
|
995
1165
|
SHARDS: String(identity.shardId),
|
|
@@ -1042,6 +1212,18 @@ function unavailableControl(requestId, status = 503) {
|
|
|
1042
1212
|
: "The owning shard is temporarily unavailable",
|
|
1043
1213
|
};
|
|
1044
1214
|
}
|
|
1215
|
+
function unavailableScheduled(requestId, safeCode = "shard_unavailable") {
|
|
1216
|
+
return {
|
|
1217
|
+
version: 1,
|
|
1218
|
+
type: "helyx.scheduled.response",
|
|
1219
|
+
requestId,
|
|
1220
|
+
result: {
|
|
1221
|
+
outcome: "deferred",
|
|
1222
|
+
delayMs: 1_000,
|
|
1223
|
+
safeCode,
|
|
1224
|
+
},
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1045
1227
|
function failedCloud(requestId, message) {
|
|
1046
1228
|
return {
|
|
1047
1229
|
version: 1,
|
package/dist/shard-worker.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type CommandRegistration } from "./interactions.js";
|
|
2
|
-
import { type ShardIdentity } from "./shard-contracts.js";
|
|
2
|
+
import { type ShardIdentity, type ShardScheduledTaskRequest, type ShardScheduledTaskResponse } from "./shard-contracts.js";
|
|
3
3
|
import { type BotHandle } from "./index.js";
|
|
4
|
+
import { type ScheduledTaskRoute } from "./scheduled-task-runner.js";
|
|
4
5
|
export declare const WORKER_ORPHAN_TERMINATION_BOUND_MS: number;
|
|
5
6
|
export declare function createLeaseHeartbeatWatchdog(onExpired: () => void, timeoutMs?: number): {
|
|
6
7
|
heartbeat(): void;
|
|
@@ -17,8 +18,11 @@ export declare function startShardWorker(processEnvironment?: NodeJS.ProcessEnv,
|
|
|
17
18
|
fencingToken: number;
|
|
18
19
|
deploymentOrder?: number | undefined;
|
|
19
20
|
}): Promise<BotHandle>;
|
|
21
|
+
export declare function executeShardScheduledTaskRequest(identity: ShardIdentity, request: ShardScheduledTaskRequest, route: ScheduledTaskRoute, now?: number): Promise<ShardScheduledTaskResponse>;
|
|
20
22
|
export declare function parseShardWorkerLaunchContract(value: string | undefined): ShardIdentity;
|
|
21
23
|
export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.ProcessEnv, identity: ShardIdentity): {
|
|
24
|
+
HELYX_INTERNAL_SECURE_RANDOM_SELECTION_KEY?: string;
|
|
25
|
+
HELYX_DATA_ENCRYPTION_KEY: undefined;
|
|
22
26
|
HELYX_SHARD_WORKERS_PER_REPLICA: number;
|
|
23
27
|
HELYX_SHARD_SUPERVISOR_DATABASE_POOL_MAX: number | undefined;
|
|
24
28
|
HELYX_SHARD_WORKER_DATABASE_POOL_MAX: number | undefined;
|
|
@@ -30,6 +34,7 @@ export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.P
|
|
|
30
34
|
APPROVED_ROLES: string[];
|
|
31
35
|
HELYX_MODULE_PACKAGES: string[];
|
|
32
36
|
HELYX_MODULE_CATALOGUE_PATH: string | undefined;
|
|
37
|
+
HELYX_SUPPORT_STAFF_CHANNEL_ID: string | undefined;
|
|
33
38
|
HELYX_BOT_CONTROL_PORT: number;
|
|
34
39
|
HELYX_PRIVACY_ENABLED: boolean;
|
|
35
40
|
HELYX_CLOUD_CONNECT_ENABLED: boolean;
|
|
@@ -45,9 +50,9 @@ export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.P
|
|
|
45
50
|
LOG_LEVEL: "error" | "fatal" | "warn" | "info" | "debug" | "trace" | "silent";
|
|
46
51
|
NODE_ENV: "development" | "test" | "production";
|
|
47
52
|
HELYX_BOT_CONTROL_SECRET?: string | undefined;
|
|
53
|
+
HELYX_SUPPORT_SERVICE_SECRET?: string | undefined;
|
|
48
54
|
HELYX_PRIVACY_EXECUTOR_DATABASE_URL?: string | undefined;
|
|
49
55
|
HELYX_PRIVACY_EXECUTOR_DATABASE_SSL?: "disable" | "require" | "verify-full" | undefined;
|
|
50
|
-
HELYX_DATA_ENCRYPTION_KEY?: string | undefined;
|
|
51
56
|
HELYX_SHARD_COUNT?: number | undefined;
|
|
52
57
|
HELYX_SHARD_REDIS_URL?: string | undefined;
|
|
53
58
|
HELYX_SHARD_REDIS_NAMESPACE?: string | undefined;
|
|
@@ -58,5 +63,6 @@ export declare function shardWorkerCommandRegistration(input: {
|
|
|
58
63
|
internalGuildId?: string;
|
|
59
64
|
prepare: NonNullable<CommandRegistration["prepare"]>;
|
|
60
65
|
operationsModuleId?: string;
|
|
66
|
+
supportModuleId: string;
|
|
61
67
|
}): CommandRegistration;
|
|
62
68
|
//# sourceMappingURL=shard-worker.d.ts.map
|