fjodor-rybakov / discord-nestjs

👾 NestJS package for discord.js
MIT License
271 stars 49 forks source link

Add support for sharding out of the box. #911

Open KieronWiltshire opened 2 years ago

KieronWiltshire commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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:

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:

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.

DJDavid98 commented 1 year ago

@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 commented 1 year ago

@DJDavid98 not sure I understand your issue

DJDavid98 commented 1 year ago

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 commented 1 year ago

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

DJDavid98 commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

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

DJDavid98 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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