Rework caching
This does a *lot* of things. Essentially, it replaces the existing individual proxy- and autoproxy caches on the bot end with a global cache (in Core) that handles all the caching at once, and automatically invalidates the cache once something changes in the datastore. This allows us to do proxying and autoproxying with *zero database queries* (best-case).
This commit is contained in:
@@ -3,8 +3,11 @@ using System;
|
||||
using App.Metrics;
|
||||
|
||||
using Autofac;
|
||||
using Autofac.Extensions.DependencyInjection;
|
||||
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
using NodaTime;
|
||||
|
||||
@@ -23,6 +26,9 @@ namespace PluralKit.Core
|
||||
builder.RegisterType<DbConnectionFactory>().AsSelf().SingleInstance();
|
||||
builder.RegisterType<PostgresDataStore>().AsSelf().As<IDataStore>();
|
||||
builder.RegisterType<SchemaService>().AsSelf();
|
||||
|
||||
builder.Populate(new ServiceCollection().AddMemoryCache());
|
||||
builder.RegisterType<ProxyCache>().AsSelf().SingleInstance();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="5.0.1" />
|
||||
<PackageReference Include="Dapper" Version="1.60.6" />
|
||||
<PackageReference Include="Dapper.Contrib" Version="1.60.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="3.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="3.1.0" />
|
||||
|
||||
179
PluralKit.Core/ProxyCache.cs
Normal file
179
PluralKit.Core/ProxyCache.cs
Normal file
@@ -0,0 +1,179 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dapper;
|
||||
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
using Serilog;
|
||||
|
||||
namespace PluralKit.Core
|
||||
{
|
||||
public class ProxyCache
|
||||
{
|
||||
// We can NOT depend on IDataStore as that creates a cycle, since it needs access to call the invalidation methods
|
||||
private IMemoryCache _cache;
|
||||
private DbConnectionFactory _db;
|
||||
private ILogger _logger;
|
||||
|
||||
public ProxyCache(IMemoryCache cache, DbConnectionFactory db, ILogger logger)
|
||||
{
|
||||
_cache = cache;
|
||||
_db = db;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task InvalidateSystem(PKSystem system) => InvalidateSystem(system.Id);
|
||||
|
||||
public async Task InvalidateSystem(int systemId)
|
||||
{
|
||||
if (_cache.TryGetValue<CachedAccount>(KeyForSystem(systemId), out var systemCache))
|
||||
{
|
||||
// If we have the system cached here, just invalidate for all the accounts we have in the cache
|
||||
_logger.Debug("Invalidating cache for system {System} and accounts {Accounts}", systemId, systemCache.Accounts);
|
||||
_cache.Remove(KeyForSystem(systemId));
|
||||
foreach (var account in systemCache.Accounts)
|
||||
_cache.Remove(KeyForAccount(account));
|
||||
return;
|
||||
}
|
||||
|
||||
// If we don't, look up the accounts from the database and invalidate *those*
|
||||
|
||||
_cache.Remove(KeyForSystem(systemId));
|
||||
using var conn = await _db.Obtain();
|
||||
var accounts = (await conn.QueryAsync<ulong>("select uid from accounts where system = @System", new {System = systemId})).ToArray();
|
||||
_logger.Debug("Invalidating cache for system {System} and accounts {Accounts}", systemId, accounts);
|
||||
foreach (var account in accounts)
|
||||
_cache.Remove(KeyForAccount(account));
|
||||
}
|
||||
|
||||
public void InvalidateGuild(ulong guild)
|
||||
{
|
||||
_logger.Debug("Invalidating cache for guild {Guild}", guild);
|
||||
_cache.Remove(KeyForGuild(guild));
|
||||
}
|
||||
|
||||
public async Task<GuildConfig> GetGuildDataCached(ulong guild)
|
||||
{
|
||||
if (_cache.TryGetValue<GuildConfig>(KeyForGuild(guild), out var item))
|
||||
{
|
||||
_logger.Verbose("Cache hit for guild {Guild}", guild);
|
||||
return item;
|
||||
}
|
||||
|
||||
// When changing this, also see PostgresDataStore::GetOrCreateGuildConfig
|
||||
using var conn = await _db.Obtain();
|
||||
|
||||
_logger.Verbose("Cache miss for guild {Guild}", guild);
|
||||
var guildConfig = (await conn.QuerySingleOrDefaultAsync<PostgresDataStore.DatabaseCompatibleGuildConfig>(
|
||||
"insert into servers (id) values (@Id) on conflict do nothing; select * from servers where id = @Id",
|
||||
new {Id = guild})).Into();
|
||||
|
||||
_cache.CreateEntry(KeyForGuild(guild))
|
||||
.SetValue(guildConfig)
|
||||
.SetSlidingExpiration(TimeSpan.FromMinutes(5))
|
||||
.SetAbsoluteExpiration(TimeSpan.FromMinutes(30))
|
||||
.Dispose(); // Don't ask, but this *saves* the entry. Somehow.
|
||||
return guildConfig;
|
||||
}
|
||||
|
||||
public async Task<CachedAccount> GetAccountDataCached(ulong account)
|
||||
{
|
||||
if (_cache.TryGetValue<CachedAccount>(KeyForAccount(account), out var item))
|
||||
{
|
||||
_logger.Verbose("Cache hit for account {Account}", account);
|
||||
return item;
|
||||
}
|
||||
|
||||
_logger.Verbose("Cache miss for account {Account}", account);
|
||||
|
||||
var data = await GetAccountData(account);
|
||||
if (data == null)
|
||||
{
|
||||
_logger.Debug("Cached data for account {Account} (no system)", account);
|
||||
|
||||
// If we didn't find any value, set a pretty long expiry and the value to null
|
||||
_cache.CreateEntry(KeyForAccount(account))
|
||||
.SetValue(null)
|
||||
.SetSlidingExpiration(TimeSpan.FromMinutes(5))
|
||||
.SetAbsoluteExpiration(TimeSpan.FromHours(1))
|
||||
.Dispose(); // Don't ask, but this *saves* the entry. Somehow.
|
||||
return null;
|
||||
}
|
||||
|
||||
// If we *did* find the value, cache it for *every account in the system* with a shorter expiry
|
||||
_logger.Debug("Cached data for system {System} and accounts {Account}", data.System.Id, data.Accounts);
|
||||
foreach (var linkedAccount in data.Accounts)
|
||||
{
|
||||
_cache.CreateEntry(KeyForAccount(linkedAccount))
|
||||
.SetValue(data)
|
||||
.SetSlidingExpiration(TimeSpan.FromMinutes(5))
|
||||
.SetAbsoluteExpiration(TimeSpan.FromMinutes(20))
|
||||
.Dispose(); // Don't ask, but this *saves* the entry. Somehow.
|
||||
|
||||
// And also do it for the system itself so we can look up by that
|
||||
_cache.CreateEntry(KeyForSystem(data.System.Id))
|
||||
.SetValue(data)
|
||||
.SetSlidingExpiration(TimeSpan.FromMinutes(5))
|
||||
.SetAbsoluteExpiration(TimeSpan.FromMinutes(20))
|
||||
.Dispose(); // Don't ask, but this *saves* the entry. Somehow.
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private async Task<CachedAccount> GetAccountData(ulong account)
|
||||
{
|
||||
using var conn = await _db.Obtain();
|
||||
|
||||
// Doing this as two queries instead of a two-step join to avoid sending duplicate rows for the system over the network for each member
|
||||
// This *may* be less efficient, haven't done too much stuff about this but having the system ID saved is very useful later on
|
||||
|
||||
var system = await conn.QuerySingleOrDefaultAsync<PKSystem>("select systems.* from accounts inner join systems on systems.id = accounts.system where accounts.uid = @Account", new { Account = account });
|
||||
if (system == null) return null; // No system = no members = no cache value
|
||||
|
||||
// Fetches:
|
||||
// - List of accounts in the system
|
||||
// - List of members in the system
|
||||
// - List of guild settings for the system (for every guild)
|
||||
// - List of guild settings for each member (for every guild)
|
||||
// I'm slightly worried the volume of guild settings will get too much, but for simplicity reasons I decided
|
||||
// against caching them individually per-guild, since I can't imagine they'll be edited *that* much
|
||||
var result = await conn.QueryMultipleAsync(@"
|
||||
select uid from accounts where system = @System;
|
||||
select * from members where system = @System;
|
||||
select * from system_guild where system = @System;
|
||||
select member_guild.* from members inner join member_guild on member_guild.member = members.id where members.system = @System;
|
||||
", new {System = system.Id});
|
||||
|
||||
return new CachedAccount
|
||||
{
|
||||
System = system,
|
||||
Accounts = (await result.ReadAsync<ulong>()).ToArray(),
|
||||
Members = (await result.ReadAsync<PKMember>()).ToArray(),
|
||||
SystemGuild = (await result.ReadAsync<SystemGuildSettings>()).ToArray(),
|
||||
MemberGuild = (await result.ReadAsync<MemberGuildSettings>()).ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
private string KeyForAccount(ulong account) => $"_account_cache_{account}";
|
||||
private string KeyForSystem(int system) => $"_system_cache_{system}";
|
||||
private string KeyForGuild(ulong guild) => $"_guild_cache_{guild}";
|
||||
}
|
||||
|
||||
public class CachedAccount
|
||||
{
|
||||
public PKSystem System;
|
||||
public PKMember[] Members;
|
||||
public SystemGuildSettings[] SystemGuild;
|
||||
public MemberGuildSettings[] MemberGuild;
|
||||
public ulong[] Accounts;
|
||||
|
||||
public SystemGuildSettings SettingsForGuild(ulong guild) =>
|
||||
SystemGuild.FirstOrDefault(s => s.Guild == guild) ?? new SystemGuildSettings();
|
||||
|
||||
public MemberGuildSettings SettingsForMemberGuild(int memberId, ulong guild) =>
|
||||
MemberGuild.FirstOrDefault(m => m.Member == memberId && m.Guild == guild) ?? new MemberGuildSettings();
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ using System.Threading.Tasks;
|
||||
using Dapper;
|
||||
using NodaTime;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
using Serilog;
|
||||
|
||||
namespace PluralKit {
|
||||
@@ -76,6 +78,7 @@ namespace PluralKit {
|
||||
|
||||
public class SystemGuildSettings
|
||||
{
|
||||
public ulong Guild { get; set; }
|
||||
public bool ProxyEnabled { get; set; } = true;
|
||||
|
||||
public AutoproxyMode AutoproxyMode { get; set; } = AutoproxyMode.Off;
|
||||
@@ -84,6 +87,8 @@ namespace PluralKit {
|
||||
|
||||
public class MemberGuildSettings
|
||||
{
|
||||
public int Member { get; set; }
|
||||
public ulong Guild { get; set; }
|
||||
public string DisplayName { get; set; }
|
||||
}
|
||||
|
||||
@@ -425,11 +430,13 @@ namespace PluralKit {
|
||||
public class PostgresDataStore: IDataStore {
|
||||
private DbConnectionFactory _conn;
|
||||
private ILogger _logger;
|
||||
private ProxyCache _cache;
|
||||
|
||||
public PostgresDataStore(DbConnectionFactory conn, ILogger logger)
|
||||
public PostgresDataStore(DbConnectionFactory conn, ILogger logger, ProxyCache cache)
|
||||
{
|
||||
_conn = conn;
|
||||
_logger = logger;
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<PKMember>> GetConflictingProxies(PKSystem system, ProxyTag tag)
|
||||
@@ -467,6 +474,7 @@ namespace PluralKit {
|
||||
settings.AutoproxyMode,
|
||||
settings.AutoproxyMember
|
||||
});
|
||||
await _cache.InvalidateSystem(system);
|
||||
}
|
||||
|
||||
public async Task<PKSystem> CreateSystem(string systemName = null) {
|
||||
@@ -481,6 +489,7 @@ namespace PluralKit {
|
||||
system = await conn.QuerySingleAsync<PKSystem>("insert into systems (hid, name) values (@Hid, @Name) returning *", new { Hid = hid, Name = systemName });
|
||||
|
||||
_logger.Information("Created system {System}", system.Id);
|
||||
// New system has no accounts, therefore nothing gets cached, therefore no need to invalidate caches right here
|
||||
return system;
|
||||
}
|
||||
|
||||
@@ -491,6 +500,7 @@ namespace PluralKit {
|
||||
await conn.ExecuteAsync("insert into accounts (uid, system) values (@Id, @SystemId) on conflict do nothing", new { Id = accountId, SystemId = system.Id });
|
||||
|
||||
_logger.Information("Linked system {System} to account {Account}", system.Id, accountId);
|
||||
await _cache.InvalidateSystem(system);
|
||||
}
|
||||
|
||||
public async Task RemoveAccount(PKSystem system, ulong accountId) {
|
||||
@@ -498,6 +508,7 @@ namespace PluralKit {
|
||||
await conn.ExecuteAsync("delete from accounts where uid = @Id and system = @SystemId", new { Id = accountId, SystemId = system.Id });
|
||||
|
||||
_logger.Information("Unlinked system {System} from account {Account}", system.Id, accountId);
|
||||
await _cache.InvalidateSystem(system);
|
||||
}
|
||||
|
||||
public async Task<PKSystem> GetSystemByAccount(ulong accountId) {
|
||||
@@ -526,12 +537,15 @@ namespace PluralKit {
|
||||
await conn.ExecuteAsync("update systems set name = @Name, description = @Description, tag = @Tag, avatar_url = @AvatarUrl, token = @Token, ui_tz = @UiTz, description_privacy = @DescriptionPrivacy, member_list_privacy = @MemberListPrivacy, front_privacy = @FrontPrivacy, front_history_privacy = @FrontHistoryPrivacy where id = @Id", system);
|
||||
|
||||
_logger.Information("Updated system {@System}", system);
|
||||
await _cache.InvalidateSystem(system);
|
||||
}
|
||||
|
||||
public async Task DeleteSystem(PKSystem system) {
|
||||
using (var conn = await _conn.Obtain())
|
||||
await conn.ExecuteAsync("delete from systems where id = @Id", system);
|
||||
|
||||
_logger.Information("Deleted system {System}", system.Id);
|
||||
await _cache.InvalidateSystem(system);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ulong>> GetSystemAccounts(PKSystem system)
|
||||
@@ -568,6 +582,7 @@ namespace PluralKit {
|
||||
});
|
||||
|
||||
_logger.Information("Created member {Member}", member.Id);
|
||||
await _cache.InvalidateSystem(system);
|
||||
return member;
|
||||
}
|
||||
|
||||
@@ -598,6 +613,7 @@ namespace PluralKit {
|
||||
|
||||
tx.Commit();
|
||||
_logger.Information("Created {MemberCount} members for system {SystemID}", names.Count(), system.Hid);
|
||||
await _cache.InvalidateSystem(system);
|
||||
return results;
|
||||
}
|
||||
}
|
||||
@@ -630,6 +646,7 @@ namespace PluralKit {
|
||||
await conn.ExecuteAsync("update members set name = @Name, display_name = @DisplayName, description = @Description, color = @Color, avatar_url = @AvatarUrl, birthday = @Birthday, pronouns = @Pronouns, proxy_tags = @ProxyTags, keep_proxy = @KeepProxy, member_privacy = @MemberPrivacy where id = @Id", member);
|
||||
|
||||
_logger.Information("Updated member {@Member}", member);
|
||||
await _cache.InvalidateSystem(member.System);
|
||||
}
|
||||
|
||||
public async Task DeleteMember(PKMember member) {
|
||||
@@ -637,6 +654,7 @@ namespace PluralKit {
|
||||
await conn.ExecuteAsync("delete from members where id = @Id", member);
|
||||
|
||||
_logger.Information("Deleted member {@Member}", member);
|
||||
await _cache.InvalidateSystem(member.System);
|
||||
}
|
||||
|
||||
public async Task<MemberGuildSettings> GetMemberGuildSettings(PKMember member, ulong guild)
|
||||
@@ -653,6 +671,7 @@ namespace PluralKit {
|
||||
await conn.ExecuteAsync(
|
||||
"insert into member_guild (member, guild, display_name) values (@Member, @Guild, @DisplayName) on conflict (member, guild) do update set display_name = @Displayname",
|
||||
new {Member = member.Id, Guild = guild, DisplayName = settings.DisplayName});
|
||||
await _cache.InvalidateSystem(member.System);
|
||||
}
|
||||
|
||||
public async Task<ulong> GetMemberMessageCount(PKMember member)
|
||||
@@ -749,7 +768,7 @@ namespace PluralKit {
|
||||
}
|
||||
|
||||
// Same as GuildConfig, but with ISet<ulong> as long[] instead.
|
||||
private struct DatabaseCompatibleGuildConfig
|
||||
public struct DatabaseCompatibleGuildConfig
|
||||
{
|
||||
public ulong Id { get; set; }
|
||||
public ulong? LogChannel { get; set; }
|
||||
@@ -768,6 +787,7 @@ namespace PluralKit {
|
||||
|
||||
public async Task<GuildConfig> GetOrCreateGuildConfig(ulong guild)
|
||||
{
|
||||
// When changing this, also see ProxyCache::GetGuildDataCached
|
||||
using (var conn = await _conn.Obtain())
|
||||
{
|
||||
return (await conn.QuerySingleOrDefaultAsync<DatabaseCompatibleGuildConfig>(
|
||||
@@ -787,6 +807,7 @@ namespace PluralKit {
|
||||
Blacklist = cfg.Blacklist.Select(c => (long) c).ToList()
|
||||
});
|
||||
_logger.Information("Updated guild configuration {@GuildCfg}", cfg);
|
||||
_cache.InvalidateGuild(cfg.Id);
|
||||
}
|
||||
|
||||
public async Task<AuxillaryProxyInformation> GetAuxillaryProxyInformation(ulong guild, PKSystem system, PKMember member)
|
||||
|
||||
Reference in New Issue
Block a user