From e725ca5864ade9a4b56dbbe71d86afcc0f0d9a72 Mon Sep 17 00:00:00 2001 From: barelyprofessional <150058423+barelyprofessional@users.noreply.github.com> Date: Sun, 26 Apr 2026 20:30:56 -0500 Subject: [PATCH] Moved to Lazy and a static class for handling Redis connections with some methods to make it easier to work with JSON. Completely untested. --- .../Commands/Kasino/RouletteCommand.cs | 6 +- .../Services/ConversationContextManager.cs | 9 +- KfChatDotNetBot/Services/KasinoKrash.cs | 9 +- KfChatDotNetBot/Services/KasinoMines.cs | 9 +- KfChatDotNetBot/Services/KasinoRain.cs | 9 +- KfChatDotNetBot/Services/Redis.cs | 110 ++++++++++++++++++ 6 files changed, 128 insertions(+), 24 deletions(-) create mode 100644 KfChatDotNetBot/Services/Redis.cs diff --git a/KfChatDotNetBot/Commands/Kasino/RouletteCommand.cs b/KfChatDotNetBot/Commands/Kasino/RouletteCommand.cs index df657ca..a9e01d5 100644 --- a/KfChatDotNetBot/Commands/Kasino/RouletteCommand.cs +++ b/KfChatDotNetBot/Commands/Kasino/RouletteCommand.cs @@ -61,8 +61,7 @@ public class RouletteCommand : ICommand var settings = await SettingsProvider.GetMultipleValuesAsync([ BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay, BuiltIn.Keys.KasinoRouletteEnabled, - BuiltIn.Keys.KasinoRouletteCountdownDuration, - BuiltIn.Keys.BotRedisConnectionString + BuiltIn.Keys.KasinoRouletteCountdownDuration ]); // Check if roulette is enabled @@ -84,8 +83,7 @@ public class RouletteCommand : ICommand return; } - var redis = await ConnectionMultiplexer.ConnectAsync(settings[BuiltIn.Keys.BotRedisConnectionString].Value!); - _redisDb = redis.GetDatabase(); + _redisDb = Redis.Multiplexer.GetDatabase(); var countdownDuration = TimeSpan.FromSeconds( settings[BuiltIn.Keys.KasinoRouletteCountdownDuration].ToType()); diff --git a/KfChatDotNetBot/Services/ConversationContextManager.cs b/KfChatDotNetBot/Services/ConversationContextManager.cs index f125d7b..eb93cea 100644 --- a/KfChatDotNetBot/Services/ConversationContextManager.cs +++ b/KfChatDotNetBot/Services/ConversationContextManager.cs @@ -37,15 +37,14 @@ public class ConversationContextManager public ConversationContextManager() { - var connectionString = SettingsProvider.GetValueAsync(BuiltIn.Keys.BotRedisConnectionString).Result; - if (string.IsNullOrEmpty(connectionString.Value)) + if (!Redis.IsAvailable) { - Logger.Error($"Can't initialize the Nora ConversationContextManager service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString}"); + Logger.Error($"Can't initialize the Nora ConversationContextManager service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString} " + + $"or the Redis client failed to connect"); throw new InvalidOperationException("Redis isn't configured"); } - var redis = ConnectionMultiplexer.Connect(connectionString.Value); - _redisDb = redis.GetDatabase(); + _redisDb = Redis.Multiplexer.GetDatabase(); } public static string GetContextKeyAsync(string mode, int userId, int roomId) diff --git a/KfChatDotNetBot/Services/KasinoKrash.cs b/KfChatDotNetBot/Services/KasinoKrash.cs index 1e8f770..b0a4d25 100644 --- a/KfChatDotNetBot/Services/KasinoKrash.cs +++ b/KfChatDotNetBot/Services/KasinoKrash.cs @@ -23,15 +23,14 @@ public class KasinoKrash : IDisposable { _kfChatBot = kfChatBot; _ct = ct; - var connectionString = SettingsProvider.GetValueAsync(BuiltIn.Keys.BotRedisConnectionString).Result; - if (string.IsNullOrEmpty(connectionString.Value)) + if (!Redis.IsAvailable) { - _logger.Error($"Can't initialize the Kasino Krash service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString}"); + _logger.Error($"Can't initialize the Kasino Krash service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString} " + + $"or the Redis service failed to connect"); return; } - var redis = ConnectionMultiplexer.Connect(connectionString.Value); - _redisDb = redis.GetDatabase(); + _redisDb = Redis.Multiplexer.GetDatabase(); //attempt to pull a game from the db in case the bot crashed while a game was ongoing. if so it will restart the run TheGame = GetKrashState().Result; if (TheGame != null) _ = RunGame(); diff --git a/KfChatDotNetBot/Services/KasinoMines.cs b/KfChatDotNetBot/Services/KasinoMines.cs index 27ccd13..e2a8187 100644 --- a/KfChatDotNetBot/Services/KasinoMines.cs +++ b/KfChatDotNetBot/Services/KasinoMines.cs @@ -241,15 +241,14 @@ public class KasinoMines public KasinoMines(ChatBot kfChatBot, int gamblerId) { _kfChatBot = kfChatBot; - var connectionString = SettingsProvider.GetValueAsync(BuiltIn.Keys.BotRedisConnectionString).Result; - if (string.IsNullOrEmpty(connectionString.Value)) + if (!Redis.IsAvailable) { - _logger.Error($"Can't initialize the Kasino Mines service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString}"); + _logger.Error($"Can't initialize the Kasino Mines service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString} " + + $"or the Redis service failed to connect"); return; } - var redis = ConnectionMultiplexer.Connect(connectionString.Value); - _redisDb = redis.GetDatabase(); + _redisDb = Redis.Multiplexer.GetDatabase(); GetSavedGames(gamblerId).Wait(); } diff --git a/KfChatDotNetBot/Services/KasinoRain.cs b/KfChatDotNetBot/Services/KasinoRain.cs index 34e79ce..ac5ccec 100644 --- a/KfChatDotNetBot/Services/KasinoRain.cs +++ b/KfChatDotNetBot/Services/KasinoRain.cs @@ -22,15 +22,14 @@ public class KasinoRain : IDisposable { _kfChatBot = kfChatBot; _ct = ct; - var connectionString = SettingsProvider.GetValueAsync(BuiltIn.Keys.BotRedisConnectionString).Result; - if (string.IsNullOrEmpty(connectionString.Value)) + if (!Redis.IsAvailable) { - _logger.Error($"Can't initialize the Kasino Rain service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString}"); + _logger.Error($"Can't initialize the Kasino Rain service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString} " + + $"or the Redis service failed to connect"); return; } - var redis = ConnectionMultiplexer.Connect(connectionString.Value); - _redisDb = redis.GetDatabase(); + _redisDb = Redis.Multiplexer.GetDatabase(); _rainTimerTask = Task.Run(RainTimerTask, ct); } diff --git a/KfChatDotNetBot/Services/Redis.cs b/KfChatDotNetBot/Services/Redis.cs new file mode 100644 index 0000000..0fb6d86 --- /dev/null +++ b/KfChatDotNetBot/Services/Redis.cs @@ -0,0 +1,110 @@ +using KfChatDotNetBot.Settings; +using System.Text.Json; +using NLog; +using StackExchange.Redis; + +namespace KfChatDotNetBot.Services; + +public static class Redis +{ + public static bool IsAvailable => LazyMultiplexer.IsValueCreated; + // Claude told me this will act like a singleton ConnectionMultiplexer + // while keeping things nice and convenient with static methods + // FYI the exception will be thrown once, cached for the lifetime of the application + // If you configure a Redis connection string, you MUST restart the application + // https://learn.microsoft.com/en-us/dotnet/api/system.lazy-1?view=net-10.0#:~:text=Exception%20caching,-When + private static readonly Lazy LazyMultiplexer = + new(() => + { + var logger = LogManager.GetCurrentClassLogger(); + var connectionString = SettingsProvider.GetValueAsync(BuiltIn.Keys.BotRedisConnectionString).Result.Value; + if (string.IsNullOrEmpty(connectionString)) + { + logger.Error($"Could not initiate the lazy connection multiplexer for the Redis service as the " + + $"connection string is not configured in {BuiltIn.Keys.BotRedisConnectionString}. " + + $"Redis won't be available to anything that relies on it. " + + $"If you do configure {BuiltIn.Keys.BotRedisConnectionString}, YOU MUST RESTART THE BOT"); + throw new InvalidOperationException(); + } + + try + { + return ConnectionMultiplexer.Connect( + SettingsProvider.GetValueAsync(BuiltIn.Keys.BotRedisConnectionString).Result.Value ?? + throw new InvalidOperationException( + $"{BuiltIn.Keys.BotRedisConnectionString} not defined, cannot connect to Redis")); + } + catch (Exception e) + { + logger.Error($"Caught an exception when connecting to Redis at {connectionString}"); + logger.Error(e); + throw; + } + }); + + // You can just grab this from wherever if you want a ready to go Redis connection + // ReSharper disable once MemberCanBePrivate.Global + public static ConnectionMultiplexer Multiplexer => LazyMultiplexer.Value; + + private static IDatabase Db => Multiplexer.GetDatabase(); + + /// + /// Fetches a key from Redis asynchronously and deserializes its JSON value to T. + /// Returns default(T) if the key doesn't exist. + /// + /// Redis key + public static async Task GetJsonAsync(string key) + { + var value = await Db.StringGetAsync(key); + if (value.IsNullOrEmpty) + return default; + + return JsonSerializer.Deserialize(value.ToString()); + } + + /// + /// Fetches a key from Redis synchronously and deserializes its JSON value to T. + /// Returns default(T) if the key doesn't exist. + /// + /// Redis key + public static T? GetJson(string key) + { + var value = Db.StringGet(key); + if (value.IsNullOrEmpty) + return default; + + return JsonSerializer.Deserialize(value.ToString()); + } + + /// + /// Asynchronously set a key to a given object serialized using JSON + /// + /// Redis key + /// Object that you wish to serialize + /// Expiration (null means never expires) + /// Redis behavior whether the key has a value or not + /// When.Always = set the value regardless of whether the key has a value + /// When.Exists = only set the value if the key has a value already + /// When.NotExists = only set the value if the key has no value + /// + public static async Task SetJsonAsync(string key, object value, TimeSpan? expires = null, When when = When.Always) + { + await Db.StringSetAsync(key, JsonSerializer.Serialize(value), expires, when); + } + + /// + /// Synchronously set a key to a given object serialized using JSON + /// + /// Redis key + /// Object that you wish to serialize + /// Expiration (null means never expires) + /// Redis behavior whether the key has a value or not + /// When.Always = set the value regardless of whether the key has a value + /// When.Exists = only set the value if the key has a value already + /// When.NotExists = only set the value if the key has no value + /// + public static void SetJson(string key, object value, TimeSpan? expires = null, When when = When.Always) + { + Db.StringSet(key, JsonSerializer.Serialize(value), expires, when); + } +}