From 1b92365b7a17f6a062e3613c19a7ba3cf75c1308 Mon Sep 17 00:00:00 2001 From: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> Date: Fri, 3 Mar 2023 09:19:24 -0600 Subject: [PATCH] fix: more docs for big bot (#2829) --- website/docs/bigbot/step-1-decisions.md | 1 + website/docs/bigbot/step-2-rest.md | 5 +- website/docs/bigbot/step-3-gateway.md | 183 ++++++++++++++++++++++++ 3 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 website/docs/bigbot/step-3-gateway.md diff --git a/website/docs/bigbot/step-1-decisions.md b/website/docs/bigbot/step-1-decisions.md index c0e7f0d08..64b181ee0 100644 --- a/website/docs/bigbot/step-1-decisions.md +++ b/website/docs/bigbot/step-1-decisions.md @@ -1,5 +1,6 @@ --- sidebar_position: 2 +sidebar_label: Step 1 - Decisions --- # Decisions To Make Before Continuing diff --git a/website/docs/bigbot/step-2-rest.md b/website/docs/bigbot/step-2-rest.md index d96a0cce8..b84a6ff71 100644 --- a/website/docs/bigbot/step-2-rest.md +++ b/website/docs/bigbot/step-2-rest.md @@ -1,5 +1,6 @@ --- sidebar_position: 3 +sidebar_label: Step 2 - REST --- # Proxy REST @@ -43,7 +44,7 @@ dotenv.config() import { REST } from './rest.ts' -const REST_AUTHORIZATION = process.env.REST_AUTHORIZATION as string +const AUTHORIZATION = process.env.AUTHORIZATION as string const app = express() @@ -56,7 +57,7 @@ app.use( app.use(express.json()) app.all('/*', async (req, res) => { - if (!REST_AUTHORIZATION || REST_AUTHORIZATION !== req.headers.authorization) { + if (!AUTHORIZATION || AUTHORIZATION !== req.headers.authorization) { return res.status(401).json({ error: 'Invalid authorization key.' }) } diff --git a/website/docs/bigbot/step-3-gateway.md b/website/docs/bigbot/step-3-gateway.md new file mode 100644 index 000000000..acf047f6d --- /dev/null +++ b/website/docs/bigbot/step-3-gateway.md @@ -0,0 +1,183 @@ +--- +sidebar_position: 4 +sidebar_label: Step 3 - Gateway +--- + +# Standalone Gateway + +Sweet, it is time to start our gateway code. By now, you should have already built your rest process, as we will need it shortly. + +## Understanding The Concepts + +Let's take a minute to understand the unique design approach of discordeno's gateway system. In Discordeno, we opt for maximum flexibility and scalability at the cost of user experience. This means it is a bit harder to use Discordeno but by far, it should be able to do anything you can dream of much easier. + +In Discordeno, we have 2 main portions of the gateway system. We have what we call the *manager* and we have the *Shard*. These are important to keep in mind going forward. They work similar to how a a worker system is designed. You have 1 main process that manages all the other ones. + +**Key Points:** + +- Any Shard can communicate directly to your event handler *(bot)* process. +- Bot process should communicate to the manager only. + +Let's say the bot process needs to execute some code on some shard such as fetching members, changing bot's status, or anything else. The ideal way to do this is the bot process sends a request to the gateway manager process which sends a request to the shard process which can send it back to the bot process directly. All of this will help make it easily scale horizontally, which we will start to see below as we code. + +## Creating The Manager + +## Preparing Connecting To REST + +Before we begin making a gateway manager, we need to first prepare a connection to our REST manager. Go ahead and make a file called `services/gateway/rest/index.ts`. + +```ts +import { createRestManager } from '@discordeno/rest'; + +export const REST = createRestManager({ + // YOUR BOT TOKEN HERE + token: process.env.TOKEN, + baseUrl: process.env.REST_URL, + authorization: process.env.AUTHORIZATION, +}); +``` + +The `baseUrl` should be pointed at the server where you are hosting your REST manager we created earlier in Step 2. This will make sure that requests sent from the gateway are sent to your proxy REST process and NOT sent directly to discord. + + +## Preparing Our Gateway Manager + +We are going to proceed with the understanding that we have 5,000 shards *5,000,000 servers*. Let's make a file called `services/gateway/manager.ts` + +```ts +import { createGatewayManager } from '@discordeno/gateway'; +import { logger } from '@discordeno/utils'; +import { REST } from '../rest/index.ts'; + +export const GATEWAY = createGatewayManager({ + token: process.env.TOKEN, + intents: Intents.Guilds | Intents.GuildMessages, + shardsPerWorker: 500, + totalWorkers: 10, + connection = await REST.getSessionInfo(), +}); + +// More code to be added here but first you need to understand this part. +``` + +Now let's break it down. + +### Worker & Server Confusion + +The `shardsPerWorker` property represents how many shards we will run per **server**. This property is called `perWorker` because for mid sized bots that don't require separate dedicated servers it uses *worker threads* to mitigate the load. Here we are going to be aiming to scale much much larger so we need to think bigger. In our case, what we are telling our gateway manager, is that it should create 500 shards per **server**. Sounds like a lot? Yes, but no problem! Those shards will then be split across *worker threads* on each server. + +The `totalWorkers` property represents the number of **servers** we have available for shards. For example, if we have 10 dedicated servers available to us, this will allow the manager to spread out the load across 10 total **servers**. + +:::tip +You can adjust the amount of **shardsPerWorker** and **totalWorkers** to fit your specific needs. +::: + +### Setting Up Gateway To Shard Communication + +Continuing from the code above, now we can start telling our gateway how to communicate to our shards. We do this bit by bit. + +```ts +export const GATEWAY = createGatewayManager({ + token: process.env.TOKEN, + intents: Intents.Guilds | Intents.GuildMessages, + shardsPerWorker: 500, + totalWorkers: 10, + connection = await REST.getSessionInfo(), +}); + +GATEWAY.tellWorkerToIdentify = async function (workerId, shardId, bucketId) { + const url = process.env[`SERVER_URL_${workerId}`]; + if (!url) return logger.error(`No server URL found for server #${workerId}. Unable to start Shard #${shardId}`); + + await fetch(url, { + method: "POST", + headers: { + authorization: process.env.AUTHORIZATION, + }, + body: JSON.stringify({ type: "IDENTIFY_SHARD", shardId }) + }).then(res => res.json()).catch(logger.error); +} + +// More code to be added here but first you need to understand this part. +``` + +Here, we are overriding the built in method on the gateway manager called `tellWorkerToIdentify`. Internally, this function just simply starts a new shard as by default the lib supports small bots. For our case, we are going to make it get the server url from a `.env` file +and then send the request to identify it. + +:::tip +For the purposes of the guide, we are using `fetch` to communicate between servers but you can use any communication system you prefer. I highly recommend taking the time to optimize this portion with a more performant communication system. Ideal recommendation would be gRPC. +::: + +Now, let's go ahead and set up the server where we will receive this and start a Shard. + +### Setting Up Sharder + +Just like before, we are going to make another http listener to listen for incoming events and delegate them outwords. Make a file called `services/gateway/sharding/index.ts` + +```ts +import { DiscordenoShard } from '@discordeno/gateway'; +import events from './events.js'; +import dotenv from 'dotenv'; +import express from 'express'; +dotenv.config(); + +const AUTHORIZATION = process.env.AUTHORIZATION as string; +const SHARDS = new Collection(); + +const app = express(); + +app.use( + express.urlencoded({ + extended: true, + }), +); + +app.use(express.json()); + +app.all('/*', async (req, res) => { + if (!AUTHORIZATION || AUTHORIZATION !== req.headers.authorization) { + return res.status(401).json({ error: 'Invalid authorization key.' }); + } + + try { + // Identify A Shard + switch (req.body.type) { + case 'IDENTIFY_SHARD': { + logger.info(`[Shard] identifying ${SHARDS.has(req.body.shardId) ? 'existing' : 'new'} shard (${shardId})`); + const shard = SHARDS.get(req.body.shardId) ?? new DiscordenoShard({ + id: shardId, + connection: { + compress: this.compress, + intents: this.intents, + properties: this.properties, + token: this.token, + totalShards: this.totalShards, + url: this.url, + version: this.version, + }, + // TODO: Enable this in the next portion of the guide. + // events, + }); + + SHARDS.set(shard.id, shard); + await shard.identify(); + return res.status(200).json({ + identified: true, + shardId: req.body.shardId, + workerId: process.env.WORKER_ID + }); + } + default: + logger.error(`[Shard] Unknown request received. ${JSON.stringify(req.body)}`); + return res.status(404).json({ message: "Unknown request received.", status: 404 }) + } + } catch (error: any) { + console.log(error) + res.status(500).json(error) + } +}) + +app.listen(process.env.SHARD_SERVER_PORT, () => { + console.log(`Listening at ${process.env.SERVER_URL}`) +}) +```