Skip to content

Instantly share code, notes, and snippets.

@gpluscb
Last active April 23, 2020 10:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gpluscb/5f7837782268fe6c818833a5354e1e3e to your computer and use it in GitHub Desktop.
Save gpluscb/5f7837782268fe6c818833a5354e1e3e to your computer and use it in GitHub Desktop.
GatewayIntents, JDAs cache, and chunking

What are intents?

Since the start of this year, there is a new concept in the Discord API: Intents. These intents tell Discord which events your bot receives. Without intents, just presence updates might take up the vast majority of events you receive, and they aren't even useful to most bots! As a fix for that, you could disable guild subscriptions for a while now, but that solution has been superceded by the new intents system.
But with the new intent system, you can just tell Discord that you don't care about such events when your bot connects, and Discord won't send them to you.

What does that have to do with JDAs cache?

Before intents, JDA would chunk guilds before any other events on that guild are executed by default. That just means that JDA sends a request via the gateway to retrieve all members in a guild when loading, and store them in its cache.
After that JDA would just keep the cache updated by looking at what events it receives. If there's an event indicating that a member updated their nickname, JDA would also update that member in its cache.
But here's where a problem sneaks in: With intents, we don't nessecarily get all events, so we can't keep the cache updated all the time. And even worse, without the GUILD_MEMBERS intent, we are not allowed to chunk members!
That means, what we can cache and if we can chunk is dependent on our intents.
So how do we configure our cache?

CacheFlags

Let's take a look at the CacheFlag class: These define what entities JDA should keep a cache of. But as expected, there are restrictions for different intents. For example, we can't use EMOTE if we don't have the GUILD_EMOJIS intent active. All the other CacheFlags also have restrictions on what intents are required.
But there's no CacheFlag for member caching, what's that about? Well the members are a bit special, they get their own class for cache policy, it's called MemberCachePolicy.

MemberCachePolicies

The MemberCachePolicy defines how JDA should try to cache members. Here we have a lot more freedom than with the binary CacheFlags. We are not limited to choosing between "cache all members" and "cache no members", because we have even more configuration options!
First of all, MemberCachePolicy is a functional interface. For every member that could be cached, the cacheMember method is called with the Member object. The member is only cached if that method returns true.
That means, we can cache only members whose username is "Tom" with the following MemberCachePolicy:

MemberCachePolicy policy = (member) -> member.getUser().getName().equals("Tom");

What a lovely use of space ❤️
By the way, the self user will always be cached, regardless of policy.
But doing that for simple rules like "only cache people in voice channels or the owner" would be annoying to write out, so we have useful fields and methods for doing such things more easily. Basically, read the docs.
But wait, there's more: lazy loading
Lazy loading is how JDA loads members if they are not chunked. That means, members are cached as JDA sees them for the first time. Maybe they send typing events, maybe they were included in the guild create event (see guild create), maybe they're in mentioned in a message, maybe they are in whatever events JDA receives. So as always: you need to keep intents in mind. For example, if you use MemberCachePolicy.ALL without the GUILD_MEMBERS intent, JDA will never receive leave events. For that reason, members won't be removed from the cache when they leave a guild. Your cache will just keep growing and keep accumulating more and more garbage. If you want to solve that by, for example, removing old members from the cache automatically, JDA allows you to manually remove members from its cache via Guild#unloadMember/User. Here's an example.

Guild Create

When guilds are loaded, Discord sends some information about members with the Guild Create event. In JDA, that event is represented by the GuildReadyEvent. The information about members you get is also determined by intents: If you don't have the GUILD_PRESENCES intent active, only the bot itself and members in voice channels are sent with that event. If you do have that intent, you should take a look at the concept of large guilds:
If a guild surpasses a certain member limit, it is considered large. At that point, the gateway won't send members that are offline with the guild create event. That limit can be anywhere between 50 and 250 (both inclusive), and you can set it yourself with the DefaultShardManagerBuilder/JDABuilder#setLargeThreshold methods. If a guild is below that limit, it won't need to be chunked, because all members will already be loaded through the guild create event. If you don't have the GUILD_PRESENCES intent active, the concept of large guilds hardly makes any difference.

ChunkingFilter

The ChunkingFilter works very similarly to the MemberCachePolicy. It is a functional interface with one method, filter, that takes a Guild object and returns whether it should be chunked. For basic things like not chunking only specific guilds, read the docs. If a guild is chunked, all members of that guild will always be cached.
And as always, keep in mind your intents. Without GUILD_MEMBERS, you won't be able to chunk anything.

Manual chunking

You can manually request member chunks via Guild#retrieveMembers. That method and Guild#pruneMemberCache can work well together for stuff like counting bots in a guild. Read the docs.

Privileged intents

Currently there are two intents that are privileged: GUILD_MEMBERS and GUILD_PRESENCES. For now, these just need to be activated on your Discord application dashboard. But starting October 7, 2020, your bot will have to go through a manual whitelisting process if your bot is in more than 100 guilds and you want to activate these intents. More info on that is in a github issue and a Discord blogpost. The reason for this comes down to privacy.

Avoiding cache and chunking dependence

But what if you think you need the cache? Well, do you really need it? Let's see:

  • If you need getXById every now and then, see if you can replace it with retrieveXById. Most of the time you can even request the entire X list, at least of a guild via something like retrieveXs, just Guild#retrieveMembers is literally chunking, so it runs over the gateway and needs the GUILD_MEMBERS intent. Keep in mind that these methods may make a request to Discord if the entity is not cached, so you need to make a judgement between speed, amount of requests, ram usage, and possibly the limitations of privileged intents. It's probably worth it to activate CacheFlags for entities you need. If you retrieve members a lot it might be a good idea to set up lazy member loading. However, if you don't have the GUILD_MEMBERS intent, keep in mind that the member cache might be out of date. In that case Guild#retrieveMemberById makes a request even if the member is cached, and updates the member in the cache. JDA#retrieveUserById always makes a request if either GUILD_MEMBERS or GUILD_PRESENCES is disabled. To override that behaviour, use the boolean overload. And keep a look out for retrieveUser/MemberByName methods, they might be added some time in the future.
  • Often times members or users are provided by the event you are working with. So check the docs for event.getMember/getUser methods, but read them carefully. Sometimes these will return null if the given user/member is not cached.
  • If you need to get users from user input, only accept Ids and mentions for that (at least until retrieving users by name is a thing). Then you can either use getMentionedUsers/Members or retrieveUser/MemberById. The getMentionedX methods do not require a cached objects to be present, because Discord sends the mentioned things together with the message.
  • If you want to get the approximate amount of members in a guild and used to do that by getting the cache size, no need to do that. Just use getMemberCount. It will not be updated without the GUILD_MEMBERS intent, but the member count in a single guild should not change that much while the bot is online.

Configuration on the DefaultShardManagerBuilder/JDABuilder

The DefaultShardManagerBuilder and JDABuilder constructors are deprecated. They have been replaced with static create, createLight, createDefault methods.

  • create requires you to specify all the intents you want to use and leaves all the cache settings activated. That means: All the CacheFlags, MemberCachePolicy.ALL and ChunkingFilter.ALL.
  • createLight activates no intents and no caching by default. For what that means specifically, read the docs.
  • createDefault activates all non-privileged intents and everything but Member caching by default. For what that means specifically, read the docs.

As an example, I currently have an extremely simple bot just for doing some evals, here's my JDABuilder code:

JDABuilder.createLight(token, GatewayIntent.DIRECT_MESSAGES).addEventListeners(new EvalBot()).build();

That's it. For more complicated stuff, you can figure out how to enable/disable GatewayIntents and CacheFlags, and set a MemberCachePolicy and a ChunkingFilter, I believe in you. Read the docs.

Some more tricks

If you want to get, for example, the amount of guilds your bot is in, you might naively do that via jda.getGuilds().size(). That's slow, because JDA has to copy all of the guilds into an unmodifiable list. Instead you can use the getGuildCache method: jda.getGuildCache().size(). That would skip the copying. However, if you want to run a stream on the cache, you could naively use the jda.getGuildCache().stream(), but that is slow again. Because..., well because concurrency. Instead you can use applyStream or acceptStream, so that JDA can run your entire stream while holding a lock. Also, you might want to use forEachUnordered instead of forEach, because that will skip copying data again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment