Skip to content

Commit d5956a5

Browse files
committed
feat: detect sharding mode
1 parent 237b246 commit d5956a5

File tree

9 files changed

+120
-21
lines changed

9 files changed

+120
-21
lines changed

apps/test-bot/src/app/commands/(general)/help.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export const chatInput: ChatInputCommand = async (ctx) => {
3232
title: 'Help',
3333
description: commands,
3434
footer: {
35-
text: `Bot Version: ${botVersion}`,
35+
text: `Bot Version: ${botVersion} | Shard ID ${interaction.guild?.shardId ?? 'N/A'}`,
3636
},
3737
color: 0x7289da,
3838
timestamp: new Date().toISOString(),
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { ShardingManager } from 'discord.js';
2+
import { join } from 'node:path';
3+
4+
process.loadEnvFile('./.env');
5+
6+
const manager = new ShardingManager(join(import.meta.dirname, 'index.js'), {
7+
token: process.env.DISCORD_TOKEN,
8+
totalShards: 2,
9+
mode: 'worker',
10+
});
11+
12+
manager.on('shardCreate', (shard) => console.log(`Launched shard ${shard.id}`));
13+
14+
await manager.spawn();
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
title: Sharding your bot
3+
description: Learn how to shard your bot in CommandKit.
4+
---
5+
6+
# Sharding your bot
7+
8+
Sharding is a method of splitting your bot into multiple processes, or "shards". This is useful for large bots that have a lot of guilds, as it allows you to distribute the load across multiple processes. Discord actually requires sharding for bots in more than 2,500 guilds, so understanding how to implement it is crucial as your bot grows.
9+
10+
Sharding is a built-in feature of discord.js, and CommandKit does not alter the way sharding works. In this guide, we will cover how to shard your bot using CommandKit. To learn more about sharding in discord.js, check out the [discord.js documentation](https://discordjs.guide/sharding).
11+
12+
## When to use sharding
13+
14+
You should consider implementing sharding in the following scenarios:
15+
16+
- Your bot is in, or approaching, 2,500 guilds (required by Discord)
17+
- You're experiencing memory or performance issues with a single process
18+
- You want to distribute your bot's workload across multiple cores/machines
19+
- You're planning for future scaling of your bot
20+
21+
## Creating a sharding manager file
22+
23+
You can simply create a new file in your source directory named `sharding-manager.ts` and CommandKit will automatically detect it and use it as the sharding manager. This file will be responsible for creating the shards and managing them.
24+
25+
```ts
26+
import { ShardingManager } from 'discord.js';
27+
import { join } from 'node:path';
28+
29+
process.loadEnvFile('./.env');
30+
31+
const manager = new ShardingManager(join(import.meta.dirname, 'index.js'), {
32+
token: process.env.DISCORD_TOKEN,
33+
totalShards: 2,
34+
mode: 'worker',
35+
});
36+
37+
manager.on('shardCreate', (shard) => console.log(`Launched shard ${shard.id}`));
38+
39+
await manager.spawn();
40+
```
41+
42+
:::info
43+
If you're confused about `index.js` being used, this is an autogenerated entrypoint file that sets up the CommandKit environment. When running `commandkit start` or `commandkit dev`, CommandKit automatically detects the entrypoint file (either `sharding-manager.js` or `index.js`) and loads it.
44+
:::

packages/cache/src/use-cache.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const fnStore = new Map<
1111
{
1212
key: string;
1313
hash: string;
14-
ttl: number;
14+
ttl?: number;
1515
original: GenericFunction;
1616
memo: GenericFunction;
1717
}
@@ -28,7 +28,7 @@ export interface CacheContext {
2828
/** Custom name for the cache entry */
2929
name?: string;
3030
/** Time-to-live in milliseconds */
31-
ttl?: number;
31+
ttl?: number | null;
3232
};
3333
}
3434

@@ -84,7 +84,7 @@ function useCache<R extends any[], F extends AsyncFunction<R>>(
8484
{
8585
params: {
8686
name: keyHash,
87-
ttl: resolvedTTL ?? DEFAULT_TTL,
87+
ttl: resolvedTTL,
8888
},
8989
},
9090
async () => {
@@ -110,16 +110,16 @@ function useCache<R extends any[], F extends AsyncFunction<R>>(
110110
if (result != null) {
111111
// Get the final key name (might have been modified by cacheTag)
112112
const finalKey = context.params.name!;
113-
const ttl = context.params.ttl ?? DEFAULT_TTL;
113+
const ttl = context.params.ttl;
114114

115115
// Store the result
116-
await provider.set(finalKey, result, ttl);
116+
await provider.set(finalKey, result, ttl ?? undefined);
117117

118118
// Update function store
119119
fnStore.set(keyHash, {
120120
key: finalKey,
121121
hash: keyHash,
122-
ttl,
122+
ttl: ttl ?? undefined,
123123
original: fn,
124124
memo,
125125
});
@@ -210,7 +210,7 @@ export function cacheLife(ttl: number | string): void {
210210
}
211211

212212
if (ttl == null || !['string', 'number'].includes(typeof ttl)) {
213-
throw new Error('cacheLife() must be called with a ttl.');
213+
throw new Error('cacheLife() must be called with a ttl value.');
214214
}
215215

216216
context.params.ttl = typeof ttl === 'string' ? ms(ttl) : ttl;

packages/commandkit/src/cli/common.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ export function write(message: any) {
1313
process.stdout.write('\n');
1414
}
1515

16+
export function findEntrypoint(dir: string) {
17+
const target = join(dir, 'sharding-manager.js');
18+
19+
// if sharding manager exists, return that file instead
20+
if (fs.existsSync(target)) return target;
21+
22+
return join(dir, 'index.js');
23+
}
24+
1625
/**
1726
* @returns {never}
1827
*/

packages/commandkit/src/cli/development.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ import { ChildProcess } from 'node:child_process';
1010
import { setTimeout as sleep } from 'node:timers/promises';
1111
import { randomUUID } from 'node:crypto';
1212
import { HMREventType } from '../utils/constants';
13+
import { findEntrypoint } from './common';
1314

1415
async function buildAndStart(configPath: string, skipStart = false) {
1516
const config = await loadConfigFile(configPath);
16-
const mainFile = join('.commandkit', 'index.js');
17+
const mainFile = findEntrypoint('.commandkit');
1718

1819
await buildApplication({
1920
configPath,

packages/commandkit/src/cli/production.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { join } from 'path';
22
import { loadConfigFile } from '../config/loader';
33
import { createAppProcess } from './app-process';
44
import { existsSync } from 'fs';
5-
import { panic } from './common';
5+
import { findEntrypoint, panic } from './common';
66
import { buildApplication } from './build';
77
import { CompilerPlugin, isCompilerPlugin } from '../plugins';
88
import { createSpinner } from './utils';
@@ -11,7 +11,7 @@ export async function bootstrapProductionServer(configPath?: string) {
1111
process.env.COMMANDKIT_BOOTSTRAP_MODE = 'production';
1212
const cwd = configPath || process.cwd();
1313
const config = await loadConfigFile(cwd);
14-
const mainFile = join(config.distDir, 'index.js');
14+
const mainFile = findEntrypoint(config.distDir);
1515

1616
if (!existsSync(mainFile)) {
1717
panic(

packages/devtools/src/server/app.ts

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@ import { join } from 'node:path';
22
import { api } from './api';
33
import express from 'express';
44
import { Server } from 'node:http';
5-
import CommandKit from 'commandkit';
5+
import CommandKit, { Logger } from 'commandkit';
66
import { setCommandKit, setConfig } from './store';
7-
import type { Client } from 'discord.js';
87
import cors from 'cors';
98

109
const staticDir = join(__dirname, '..', '..', 'ui');
@@ -26,9 +25,39 @@ export async function startServer(
2625
) {
2726
setCommandKit(commandkit);
2827
setConfig({ credential });
29-
return new Promise<Server>((resolve) => {
30-
const server = app.listen(port, () => {
31-
resolve(server);
32-
});
28+
29+
let currentPort = port;
30+
const maxRetries = 10; // Limit the number of retries to prevent infinite loop
31+
let retries = 0;
32+
33+
return new Promise<Server>((resolve, reject) => {
34+
function attemptToListen() {
35+
try {
36+
const server = app.listen(currentPort, () => {
37+
resolve(server);
38+
});
39+
40+
server.on('error', (error: NodeJS.ErrnoException) => {
41+
// Handle specific error for port already in use
42+
if (error.code === 'EADDRINUSE' && retries < maxRetries) {
43+
Logger.warn(
44+
`Port ${currentPort} is already in use, trying port ${currentPort + 1}`,
45+
);
46+
currentPort++;
47+
retries++;
48+
attemptToListen();
49+
} else {
50+
// For other errors or if we've exceeded max retries
51+
Logger.error('Server failed to start:', error);
52+
reject(error);
53+
}
54+
});
55+
} catch (error) {
56+
Logger.error('Unexpected error starting server:', error);
57+
reject(error);
58+
}
59+
}
60+
61+
attemptToListen();
3362
});
3463
}

packages/redis/src/index.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export class RedisCache extends CacheProvider {
6868
return undefined;
6969
}
7070

71-
return JSON.parse(value) as T;
71+
return this.deserialize(value) as T;
7272
}
7373

7474
/**
@@ -78,12 +78,14 @@ export class RedisCache extends CacheProvider {
7878
* @param ttl The time-to-live for the cache entry in milliseconds.
7979
*/
8080
public async set<T>(key: string, value: T, ttl?: number): Promise<void> {
81-
const serialized = await this.serialize(value);
81+
const serialized = this.serialize(value);
82+
const finalValue =
83+
serialized instanceof Promise ? await serialized : serialized;
8284

8385
if (typeof ttl === 'number') {
84-
await this.redis.set(key, serialized, 'PX', ttl);
86+
await this.redis.set(key, finalValue, 'PX', ttl);
8587
} else {
86-
await this.redis.set(key, serialized);
88+
await this.redis.set(key, finalValue);
8789
}
8890
}
8991

0 commit comments

Comments
 (0)