Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for sharding out of the box. #911

Open
KieronWiltshire opened this issue Oct 31, 2022 · 15 comments
Open

Add support for sharding out of the box. #911

KieronWiltshire opened this issue Oct 31, 2022 · 15 comments
Labels
documentation Improvements or additions to documentation

Comments

@KieronWiltshire
Copy link

Is your feature request related to a problem? Please describe.
I tried creating 2 apps within nestjs and using the shard manager to instantiate the other, but I keep getting an error, SHARDING_READY_TIMEOUT Shard 0's Client took too long to become ready..

Describe the solution you'd like
I'd like to be able to specify within the config the sharding requirements and have it all handled behind the scenes.

Describe alternatives you've considered
I've tried creating a single file I could specify in the sharding manager and a separate nestjs app under a monorepo with no success.

@fjodor-rybakov
Copy link
Owner

Hi! Can you provide minimal reproduction repository? NestJS and discord.js are not directly related in any way. How would you like to see sharding out of the box?

@KieronWiltshire
Copy link
Author

Yeah I understand that NestJS and discord aren't directly related. To be honest, I'd prefer something like this...

|- src
    main.ts
    app.module.ts
    bot
        - bot.module.ts
        - bot.gateway.ts

then in app.module.ts it would be awesome to do something like

const manager = new ShardManager(path.join(__dirname, 'bot', 'bot.module.ts'), { token: configService.get<string>('discord.bot.token')});
manager.spawn();

@fjodor-rybakov
Copy link
Owner

fjodor-rybakov commented Oct 31, 2022

Yeah I understand that NestJS and discord aren't directly related. To be honest, I'd prefer something like this...

|- src
    main.ts
    app.module.ts
    bot
        - bot.module.ts
        - bot.gateway.ts

then in app.module.ts it would be awesome to do something like

const manager = new ShardManager(path.join(__dirname, 'bot', 'bot.module.ts'), { token: configService.get<string>('discord.bot.token')});
manager.spawn();

app.module.ts is not a bootstrap. You must specify path to the main.ts file.

/* spawn-shards.ts */

async function createShardingManager() {
  const appContext = await NestFactory.createApplicationContext(ConfigModule);
  const configService = appContext.get(ConfigService);
  
  const manager = new ShardManager(/* path to main.ts/js */, { token: configService.get<string>('discord.bot.token')});

  appContext.close();

  return manager;
}

createShardingManager().then((manager) => {
  manager.spawn();
});

@KieronWiltshire
Copy link
Author

KieronWiltshire commented Oct 31, 2022

For anyone coming across this, I have found a solution using @fjodor-rybakov help, follow the steps below. Hopefully the documentation will be updated to include this.

In your src directory, create the following files:

  • bot.ts
  • server.ts
  • shared.module.ts

You may also need to add a webpack.config.js file to your root directory which exports the bot.ts as it's not automatically exported with the application due to how the bot.ts file is used within another process that webpack is unable to detect. You can use the following snippet:

const Path = require('path');

module.exports = function (options) {
    return {
        ...options,
        entry: {
            server: options.entry,
            bot: Path.join(__dirname, 'src', 'bot.ts')
        },
        output: {
            filename: '[name].js'
        }
    };
};

Secondly, change your entryPoint in your nest-cli.json file to server. The server.ts file will become our entry point for instantiating the HTTP server and the Bot. If you do not need a HTTP server, then you can make the adjustments yourself, this example assumes you want both.

Steps:

  • Remove the call to the bootstrap function in main.ts and export the function instead. You can keep everything in your main.ts as it was aside from these changes. This will be your HTTP server.
  • Copy the modules you want your HTTP server and Bot to share, and put them in a new shared.module.ts, I have provided an example below.
  • Inside the bot.module.ts file, you're going to want to load up the DiscordModule.forRootAsync and DiscordModule.forFeature which should exist from when you created it following this package's readme file. You'll also want to include the SharedModule for anything you wish to use within the bot. I have provided an example below.
  • Inside the bot.ts file, you'll want to copy the code below.
  • For your server.ts file you'll also want to copy the code below.
  • Then just type in npm run start exactly how you did before, except now it will start the server.ts file which in turn launches the HTTP server and creates a ShardingManager instance which in turn loads your bot.ts as shard processes.

shared.module.ts (example)

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import appConfig from './config/app.config';
import discordConfig from './config/discord.config';

@Module({
    imports: [
        ConfigModule.forRoot({
            cache: true,
            isGlobal: true,
            load: [
                appConfig,
                discordConfig,
            ]
        }),
    ],
})
export class SharedModule {}

app.module.ts (example)

import { Module } from '@nestjs/common';
import { SharedModule } from "./shared.module";

@Module({
  imports: [
    SharedModule
  ],
})
export class AppModule {}

main.ts (example)

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ConfigService } from '@nestjs/config';

export async function bootstrap() {
  const app = await NestFactory.create(AppModule, { cors: true });
  const config = app.get(ConfigService);

  if (config.get<boolean>('app.debug')) {
    app.getHttpAdapter().getInstance().set('json spaces', 2);
  }

  const port = config.get<string>('app.port');
  await app.listen(port);
}

bot.ts (required)

import { NestFactory } from '@nestjs/core';
import { BotModule } from "./bot/bot.module";

async function bootstrap() {
    const app = await NestFactory.createApplicationContext(BotModule);
}

bootstrap();

server.ts (required)

import { NestFactory } from "@nestjs/core";
import { ConfigService } from "@nestjs/config";
import * as Path from "path";
import { ShardingManager } from 'discord.js';
import { SharedModule } from "./shared.module";
import { bootstrap } from './main';

async function createShardingManager() {
    const appContext = await NestFactory.createApplicationContext(SharedModule);
    const config = appContext.get(ConfigService);

    const manager = new ShardingManager(Path.join(__dirname, 'bot.js'), {
        token: config.get<string>('discord.bot.token')
    });

    return manager;
}

bootstrap().then(() => createShardingManager()).then((manager) => {
    manager.spawn();

    manager.on("shardCreate", shard => {
        shard.on('reconnecting', () => {
            console.log(`Reconnecting shard: [${shard.id}]`);
        });
        shard.on('spawn', () => {
            console.log(`Spawned shard: [${shard.id}]`);
        });
        shard.on('ready', () => {
            console.log(` Shard [${shard.id}] is ready`);
        });
        shard.on('death', () => {
            console.log(`Died shard: [${shard.id}]`);
        });
        shard.on('error', (err)=>{
            console.log(`Error in  [${shard.id}] with : ${err} `)
            shard.respawn()
        })
    });
});

bot.module.ts (required)

import { Module } from '@nestjs/common';
import { DiscordModule } from '@discord-nestjs/core';
import { BotGateway } from './bot.gateway'
import { GatewayIntentBits } from "discord.js";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { SharedModule } from "../shared.module";

@Module({
    imports: [
        SharedModule,
        DiscordModule.forRootAsync({
            imports: [ConfigModule],
            useFactory: (config: ConfigService) => ({
                token: config.get<string>('discord.bot.token'),
                discordClientOptions: {
                    intents: [
                        GatewayIntentBits.Guilds,
                        GatewayIntentBits.GuildMembers,
                        GatewayIntentBits.GuildWebhooks,
                        GatewayIntentBits.GuildInvites,
                        GatewayIntentBits.GuildMessages,
                        GatewayIntentBits.DirectMessages,
                        GatewayIntentBits.MessageContent
                    ],
                },
            }),
            inject: [ConfigService],
        }),
        DiscordModule.forFeature()
    ],
    providers: [BotGateway]
})
export class BotModule {}

My directory structure looks something as shown below. My advice would be to keep all your bot related code within the bot sub directory.

|- [src]
     |- [bot]
         |- bot.gateway.ts
         |- bot.module.ts
     |- [config]
         |- app.config.ts
         |- discord.config.ts
     |- app.module.ts
     |- bot.ts
     |- main.ts
     |- server.ts
     |- shared.module.ts

Note, if you're using the discord-hybrid-sharding package found here, then just change your server.ts to use that shard manager instead of the built in discord.js. That will allow you to scale using discord-cross-hosting package found here.

@fjodor-rybakov fjodor-rybakov added the documentation Improvements or additions to documentation label Jan 15, 2023
@DJDavid98
Copy link

@KieronWiltshire Thank you for your comment, however I'm wondering if there is anything to prevent each shard from registering the commands. My understanding is that by default the library will register all commands as application commands, and if there are multiple shards each of them is going to try to perform that registration

@KieronWiltshire
Copy link
Author

@DJDavid98 not sure I understand your issue

@DJDavid98
Copy link

DJDavid98 commented May 12, 2023

The library register the commands by default, as per the docs:

  • registerCommandOptions - Specific registration of slash commands(If option is not set, global commands will be registered)

So each time a shard starts up, it will register the global commands, that is my understanding at least. My existing bot did not use this and there all I did was only let shard 0 update the commands, but I'm not sure such filtering is possible here.

@KieronWiltshire
Copy link
Author

KieronWiltshire commented May 12, 2023

I'm still not sure I understand your problem. Link me to the docs in question please.

@DJDavid98
Copy link

DJDavid98 commented May 12, 2023

Here are the docs: https://github.com/fjodor-rybakov/discord-nestjs/tree/master/packages/core#%E2%84%B9%EF%B8%8F-automatic-registration-of-slash-commands-

Without sharding, the BotModule exists in a single instance, registers the commands globally upon registration, no issue there.

As soon as you introduce sharding, the BotModule module will be instantiated for each shard and will execute the command registration logic independently. This is my primary concern, that if you start 10 shards, the bot will register its commands 10 times.

@KieronWiltshire
Copy link
Author

KieronWiltshire commented May 12, 2023

@DJDavid98 why does that bother you? in theory thats how it works... thats how it's suggested in the Discordjs docs or at least implied anyway... https://discordjs.guide/sharding/#when-to-shard

What is the exact problem you're having?

@DJDavid98
Copy link

Command registration needs to happen only once during application startup. By running the command registration multiple times there is a possibility for race conditions as multiple processes simultaneously start registering commands, and in case the removeCommandsBefore option is provided it will potentially try to delete and re-register commands multiple times during a single application start. I manually started 50 shards for my bot and each of them is running this registration process, which is quite wasteful.

Screenshot of Nestjs application log output showing multiple instances of "All guild commands removed!" and "All guild commands are registered!" messages

@KieronWiltshire
Copy link
Author

I see, but other than it being "wasteful" it works as intended right?

@DJDavid98
Copy link

Technically speaking, for my essentially "hello world" bot at this stage, yes, it currently does. However for a larger bot with multiple commands where registering them might take longer, I feel like this can cause issues later down the line. I specifically wanted to ask if there is some way you are aware of to prevent this. I tried to look into using trigger-based registration based on the options shown in the docs, but when it comes to that option they are a bit lacking.

@KieronWiltshire
Copy link
Author

To be fair, I've never created a full featured discord bot. I've always just loved poking around at things, the intentions are there to build a bot at some point, but not in my immediate scope for the project I work on. So for full transparency, I have no idea if sharding in this manner actually works at scale. I personally just struggled to get this working, and I thought if I ever was going to make a bot using NestJS, I'd want to make sure it can absolutely be done. So I went out and did some research on this and came back with the method above, but again other than getting it to just "work," I have no idea.

Now with all that being said, if you'd want to try something else instead of discord-nestjs, I would highly recommend Necord. I wrote up the sharding part into the docs on there and the main dev is extremely active in taking feature requests etc... so if this is still an issue over on the necord package, bring it up to the dev on the necord discord. He will gladly patch any issues you're facing I'm almost sure!

I'm sorry I can't be of more help to you :(

@DJDavid98
Copy link

Alright, thanks for the suggestion, I will check it out

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

3 participants