-
Notifications
You must be signed in to change notification settings - Fork 3
Description
Problem
Users (developers) want an easy way to fetch information from the Discord Environment that the bot has access to.
The Disgo Cache Manager aims to solve this.
Caching
The difference between a "cache" and a "cache manager" is that a "cache manager" manages other "caches". The entire point of a cache is to minimize load on the application and network. However, the optimal way to do this will be dependent on the users (developers) application and code. Such that implementing a standard method of caching that every bot adheres to is an anti-pattern. Other Go Discord API Wrappers are either based on a cache (i.e Disgord), or implement a mandatory cache (i.e DiscordGo State). At minimum, this adds overhead to the program. In the worst case, it adds complexity to the end user. Let's analyze the following code.
Caching Overhead
The following code from Disgord showcases how a cache adds overhead.
// bypasses local cache
client.CurrentUser().Get(disgord.IgnoreCache)
client.Guild(guildID).GetMembers(disgord.IgnoreCache)
// always checks the local cache first
client.CurrentUser().Get()
client.Guild(guildID).GetMembers()The problem here is not necessarily that the user will always have to specify the usage of a cache, but that the cache is always involved. It does not matter if the user creates a program that has no use for the cache: Providing an option to ignore the cache implies that requests are always cached. When this is the case, a large amount of memory is spent storing unnecessary entries (especially given the nature of Discord's Models). In the case of Disgord, it is stated that "the cache is immutable by default", such that "every incoming and outgoing data of the cache is deep copied". This adds even more overhead for applications which handle millions of requests.
Caching Complexity
The second issue with mandatory caching is the complexity that is added to the developer. In Disgord's case, you are unable to control your cache and unable to prevent data from being stored. In other cases, it can be even more problematic. Let's analyze the following code from DiscordGo.
// ChannelValue is a utility function for casting option value to channel object.
// s : Session object, if not nil, function additionally fetches all channel's data
func (o ApplicationCommandInteractionDataOption) ChannelValue(s *Session) *Channel {
if o.Type != ApplicationCommandOptionChannel {
panic("ChannelValue called on data option of type " + o.Type.String())
}
chanID := o.Value.(string)
if s == nil {
return &Channel{ID: chanID}
}
ch, err := s.State.Channel(chanID)
if err != nil {
...The following code takes a string ID value and turns it into a channel. Not too bad of an idea. However, the context of this function is that it's called after a user (developer) has received an event (with Application Command Options) from Discord. Such that the user (developer) may not expect the remaining channel data to come from a cache, but from Discord itself.
When the object is in the cache (and a session parameter is provided), the program becomes incorrect: The state of the channel from the cache is not guaranteed to match the state of the Discord Channel. When the object is not in the cache, the program adds overhead by creating an additional blocking network call; in a function for "casting" nonetheless. However, the latter behavior is stated.
In a similar manner to other API Wrappers, DiscordGo's cache (State) is structured in a way that does not allow the user (developer) to manage cached resources (due to unexpected fields). Such that developer is only able to solve the problem of incorrectness by manually calling the network themselves, defeating the purpose of the "typecast" function.
Solution
The solution to this problem is to implement a separated Cache Manager module. This cache manager should make it easy for the user to setup caching, but also operate the cache themselves. In this way, the user can ensure correctness of their program while minimizing overhead of the cache. In addition, external caching solutions (such as Redis or MemCache) can be used by making the cache an exported interface.
Implementation
Users (developers) would add the Cache Manager to their application. Then, the cache manager can be used in a manner similar to the rate limiter.
- Add
CacheManager interfacewhich defines necessary functions used throughout the cache manager. - Create a struct that implements the
CacheManager. During development of the actual cache manager, this in addition to the following steps may be completed prior to 1. - Use
sync.Mapto create a cache struct that stores objects in a given manner, such that they can be retrieved in a given manner, without data race issues. No specifics are provided in this step because a solution that is flexible to the end-user has yet to be designed. The solution must account for caching by the bot and by resource. - Define functions that setup various caches (supporting 3) [i.e Guild, Channel, etc].
- Define functions that ease cache retrieval and updates.
- Create tests.
- Add documentation in contribution.
- Add examples in
_examples/bot.