Skip to content

Commit

Permalink
feat(cache_in_memory)!: In-memory cache with generic types (twilight-…
Browse files Browse the repository at this point in the history
…rs#2179)

Adds support for using custom structs for cached representation of
Discord API data via generics on `InMemoryCache` and a set of traits for
cached data that is required for caching functionality.

The `DefaultInMemoryCache` type alias is added for an implementation
with the default types provided by the cache crate. Migration is very
straightforward, replacing `InMemoryCache` with `DefaultInMemoryCache`
in code is sufficient. 

This could potentially reduce memory usage drastically when structs
are optimized properly by implementors.

Signed-off-by: Jens Reidel <adrian@travitia.xyz>
Co-authored-by: Valdemar Erk <valdemar@erk.dev>
  • Loading branch information
Gelbpunkt and Erk- authored Feb 20, 2024
1 parent fd89d05 commit e6c124c
Show file tree
Hide file tree
Showing 50 changed files with 1,964 additions and 654 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ bot's token. You must also depend on `tokio`, `twilight-cache-inmemory`,

```rust,no_run
use std::{env, error::Error, sync::Arc};
use twilight_cache_inmemory::{InMemoryCache, ResourceType};
use twilight_cache_inmemory::{DefaultInMemoryCache, ResourceType};
use twilight_gateway::{Event, EventTypeFlags, Intents, Shard, ShardId, StreamExt as _};
use twilight_http::Client as HttpClient;
Expand All @@ -142,7 +142,7 @@ async fn main() -> anyhow::Result<()> {
// Since we only care about new messages, make the cache only
// cache new messages.
let cache = InMemoryCache::builder()
let cache = DefaultInMemoryCache::builder()
.resource_types(ResourceType::MESSAGE)
.build();
Expand Down
4 changes: 2 additions & 2 deletions book/src/chapter_1_crates/section_4_cache_inmemory.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ Process new messages that come over a shard into the cache:
# #[tokio::main]
# async fn main() -> Result<(), Box<dyn std::error::Error>> {
use std::env;
use twilight_cache_inmemory::InMemoryCache;
use twilight_cache_inmemory::DefaultInMemoryCache;
use twilight_gateway::{EventTypeFlags, Intents, Shard, ShardId, StreamExt as _};
let token = env::var("DISCORD_TOKEN")?;
let mut shard = Shard::new(ShardId::ONE, token, Intents::GUILD_MESSAGES);
let cache = InMemoryCache::new();
let cache = DefaultInMemoryCache::new();
while let Some(item) = shard.next_event(EventTypeFlags::all()).await {
let Ok(event) = item else {
Expand Down
5 changes: 2 additions & 3 deletions book/src/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,8 @@ in from a channel:

```rust,no_run
use std::{env, error::Error, sync::Arc};
use twilight_cache_inmemory::{InMemoryCache, ResourceType};
use twilight_cache_inmemory::{DefaultInMemoryCache, ResourceType};
use twilight_gateway::{Event, EventTypeFlags, Intents, Shard, ShardId, StreamExt as _};
use twilight_http::Client as HttpClient;
#[tokio::main]
Expand All @@ -68,7 +67,7 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
let http = Arc::new(HttpClient::new(token));
// Since we only care about messages, make the cache only process messages.
let cache = InMemoryCache::builder()
let cache = DefaultInMemoryCache::builder()
.resource_types(ResourceType::MESSAGE)
.build();
Expand Down
5 changes: 5 additions & 0 deletions examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,17 @@ serde_json = { version = "1" }
tokio = { default-features = false, features = ["macros", "signal", "rt-multi-thread"], version = "1.0" }
tracing = "0.1"
tracing-subscriber = { default-features = false, features = ["fmt", "tracing-log"], version = "0.3" }
twilight-cache-inmemory = { path = "../twilight-cache-inmemory", features = ["permission-calculator"] }
twilight-gateway = { path = "../twilight-gateway" }
twilight-http = { path = "../twilight-http" }
twilight-lavalink = { path = "../twilight-lavalink" }
twilight-model = { path = "../twilight-model" }
twilight-standby = { path = "../twilight-standby" }

[[example]]
name = "cache-optimization"
path = "cache-optimization/main.rs"

[[example]]
name = "gateway-queue-http"
path = "gateway-queue-http.rs"
Expand Down
70 changes: 70 additions & 0 deletions examples/cache-optimization/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//! This example demonstrates the usage of [`InMemoryCache`] with custom cached
//! types. The actual fields stored here are kept to a minimum for the sake of
//! simplicity, in reality you may want to store a lot more information.
use std::env;
use twilight_gateway::{Event, EventTypeFlags, Intents, Shard, ShardId, StreamExt as _};
use twilight_http::Client;

mod models;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();

let event_types = EventTypeFlags::MESSAGE_CREATE
| EventTypeFlags::GUILD_CREATE
| EventTypeFlags::GUILD_UPDATE
| EventTypeFlags::GUILD_DELETE
| EventTypeFlags::MEMBER_ADD
| EventTypeFlags::MEMBER_REMOVE;

let mut shard = Shard::new(
ShardId::ONE,
env::var("DISCORD_TOKEN")?,
Intents::GUILDS
| Intents::GUILD_MEMBERS
| Intents::GUILD_MESSAGES
| Intents::MESSAGE_CONTENT,
);

let client = Client::new(env::var("DISCORD_TOKEN")?);

let cache = models::CustomInMemoryCache::new();

while let Some(item) = shard.next_event(event_types).await {
let Ok(event) = item else {
tracing::warn!(source = ?item.unwrap_err(), "error receiving event");

continue;
};

cache.update(&event);

if let Event::MessageCreate(msg) = event {
if !msg.content.starts_with("!guild-info") {
continue;
}

let Some(guild_id) = msg.guild_id else {
continue;
};

let Some(guild) = cache.guild(guild_id) else {
continue;
};

let text = format!(
"The owner of this server is <@{}>. The guild currently has {} members.",
guild.owner_id,
guild
.member_count
.map_or(String::from("N/A"), |c| c.to_string()),
);

client.create_message(msg.channel_id).content(&text).await?;
}
}

Ok(())
}
63 changes: 63 additions & 0 deletions examples/cache-optimization/models/channel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use twilight_cache_inmemory::CacheableChannel;
use twilight_model::{
channel::{permission_overwrite::PermissionOverwrite, Channel, ChannelType},
id::{
marker::{ChannelMarker, GuildMarker},
Id,
},
util::Timestamp,
};

#[derive(Clone, Debug, PartialEq)]
pub struct MinimalCachedChannel {
pub id: Id<ChannelMarker>,
pub guild_id: Option<Id<GuildMarker>>,
pub kind: ChannelType,
pub parent_id: Option<Id<ChannelMarker>>,
}

impl From<Channel> for MinimalCachedChannel {
fn from(channel: Channel) -> Self {
Self {
id: channel.id,
guild_id: channel.guild_id,
kind: channel.kind,
parent_id: channel.parent_id,
}
}
}

impl PartialEq<Channel> for MinimalCachedChannel {
fn eq(&self, other: &Channel) -> bool {
self.id == other.id
&& self.guild_id == other.guild_id
&& self.kind == other.kind
&& self.parent_id == other.parent_id
}
}

impl CacheableChannel for MinimalCachedChannel {
fn guild_id(&self) -> Option<Id<GuildMarker>> {
self.guild_id
}

fn id(&self) -> Id<ChannelMarker> {
self.id
}

fn kind(&self) -> ChannelType {
self.kind
}

fn parent_id(&self) -> Option<Id<ChannelMarker>> {
self.parent_id
}

fn permission_overwrites(&self) -> Option<&[PermissionOverwrite]> {
None
}

fn set_last_pin_timestamp(&mut self, _timestamp: Option<Timestamp>) {
// We don't store this information, so this is a no-op
}
}
28 changes: 28 additions & 0 deletions examples/cache-optimization/models/current_user.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use twilight_cache_inmemory::CacheableCurrentUser;
use twilight_model::{
id::{marker::UserMarker, Id},
user::CurrentUser,
};

#[derive(Clone, Debug, PartialEq)]
pub struct MinimalCachedCurrentUser {
pub id: Id<UserMarker>,
}

impl From<CurrentUser> for MinimalCachedCurrentUser {
fn from(value: CurrentUser) -> Self {
Self { id: value.id }
}
}

impl PartialEq<CurrentUser> for MinimalCachedCurrentUser {
fn eq(&self, other: &CurrentUser) -> bool {
self.id == other.id
}
}

impl CacheableCurrentUser for MinimalCachedCurrentUser {
fn id(&self) -> Id<UserMarker> {
self.id
}
}
24 changes: 24 additions & 0 deletions examples/cache-optimization/models/emoji.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use twilight_cache_inmemory::CacheableEmoji;
use twilight_model::{
guild::Emoji,
id::{marker::EmojiMarker, Id},
};

#[derive(Clone, Debug, PartialEq)]
pub struct MinimalCachedEmoji {
pub id: Id<EmojiMarker>,
}

impl From<Emoji> for MinimalCachedEmoji {
fn from(value: Emoji) -> Self {
Self { id: value.id }
}
}

impl PartialEq<Emoji> for MinimalCachedEmoji {
fn eq(&self, other: &Emoji) -> bool {
self.id == other.id
}
}

impl CacheableEmoji for MinimalCachedEmoji {}
66 changes: 66 additions & 0 deletions examples/cache-optimization/models/guild.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use twilight_cache_inmemory::CacheableGuild;
use twilight_model::{
gateway::payload::incoming::GuildUpdate,
guild::Guild,
id::{
marker::{GuildMarker, UserMarker},
Id,
},
};

#[derive(Clone, Debug, PartialEq)]
pub struct MinimalCachedGuild {
pub id: Id<GuildMarker>,
pub owner_id: Id<UserMarker>,
pub member_count: Option<u64>,
}

impl From<Guild> for MinimalCachedGuild {
fn from(guild: Guild) -> Self {
Self {
id: guild.id,
owner_id: guild.owner_id,
member_count: guild.member_count,
}
}
}

impl PartialEq<Guild> for MinimalCachedGuild {
fn eq(&self, other: &Guild) -> bool {
self.id == other.id
&& self.owner_id == other.owner_id
&& self.member_count == other.member_count
}
}

impl CacheableGuild for MinimalCachedGuild {
fn id(&self) -> Id<GuildMarker> {
self.id
}

fn owner_id(&self) -> Id<UserMarker> {
self.owner_id
}

fn set_unavailable(&mut self, _unavailable: bool) {
// We don't store this information, so this is a no-op
}

fn update_with_guild_update(&mut self, guild_update: &GuildUpdate) {
self.id = guild_update.id;
self.owner_id = guild_update.owner_id;
self.member_count = guild_update.member_count;
}

fn increase_member_count(&mut self, amount: u64) {
if let Some(count) = self.member_count.as_mut() {
*count += amount;
}
}

fn decrease_member_count(&mut self, amount: u64) {
if let Some(count) = self.member_count.as_mut() {
*count -= amount;
}
}
}
24 changes: 24 additions & 0 deletions examples/cache-optimization/models/guild_integration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use twilight_cache_inmemory::CacheableGuildIntegration;
use twilight_model::{
guild::GuildIntegration,
id::{marker::IntegrationMarker, Id},
};

#[derive(Clone, Debug, PartialEq)]
pub struct MinimalCachedGuildIntegration {
pub id: Id<IntegrationMarker>,
}

impl From<GuildIntegration> for MinimalCachedGuildIntegration {
fn from(integration: GuildIntegration) -> Self {
Self { id: integration.id }
}
}

impl PartialEq<GuildIntegration> for MinimalCachedGuildIntegration {
fn eq(&self, other: &GuildIntegration) -> bool {
self.id == other.id
}
}

impl CacheableGuildIntegration for MinimalCachedGuildIntegration {}
Loading

0 comments on commit e6c124c

Please sign in to comment.