Skip to content

add Cache Manager #39

@switchupcb

Description

@switchupcb

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.

  1. Add CacheManager interface which defines necessary functions used throughout the cache manager.
  2. 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.
  3. Use sync.Map to 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.
  4. Define functions that setup various caches (supporting 3) [i.e Guild, Channel, etc].
  5. Define functions that ease cache retrieval and updates.
  6. Create tests.
  7. Add documentation in contribution.
  8. Add examples in _examples/bot.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions