From e83ce61877b842f3b6f6109ad8ec8f5a13d7a711 Mon Sep 17 00:00:00 2001 From: LD-Reborn Date: Wed, 7 Jan 2026 01:15:55 +0100 Subject: [PATCH 1/2] Added query cache entry count and capacity to front-end, Fixed ServerGetStatsResult field naming --- .../Controllers/SearchdomainController.cs | 4 +- src/Server/Controllers/ServerController.cs | 24 ++++++- src/Server/Helper/DatabaseHelper.cs | 19 +++++ src/Server/Resources/SharedResources.de.resx | 15 ++++ src/Server/Resources/SharedResources.en.resx | 15 ++++ src/Server/Searchdomain.cs | 20 ++---- src/Server/Views/Home/Index.cshtml | 69 +++++++++++++++++-- src/Shared/Models/ServerModels.cs | 16 +++-- 8 files changed, 155 insertions(+), 27 deletions(-) diff --git a/src/Server/Controllers/SearchdomainController.cs b/src/Server/Controllers/SearchdomainController.cs index 2acf8dd..ffd1e8e 100644 --- a/src/Server/Controllers/SearchdomainController.cs +++ b/src/Server/Controllers/SearchdomainController.cs @@ -286,7 +286,7 @@ public class SearchdomainController : ControllerBase { (Searchdomain? searchdomain_, int? httpStatusCode, string? message) = SearchdomainHelper.TryGetSearchdomain(_domainManager, searchdomain, _logger); if (searchdomain_ is null || httpStatusCode is not null) return StatusCode(httpStatusCode ?? 500, new SearchdomainUpdateResults(){Success = false, Message = message}); - long sizeInBytes = DatabaseHelper.GetSearchdomainDatabaseSize(searchdomain_.helper, searchdomain); - return Ok(new SearchdomainGetDatabaseSizeResult() { SearchdomainDatabaseSizeBytes = sizeInBytes, Success = true }); + long EmbeddingCacheUtilization = DatabaseHelper.GetSearchdomainDatabaseSize(searchdomain_.helper, searchdomain); + return Ok(new SearchdomainGetDatabaseSizeResult() { SearchdomainDatabaseSizeBytes = EmbeddingCacheUtilization, Success = true }); } } diff --git a/src/Server/Controllers/ServerController.cs b/src/Server/Controllers/ServerController.cs index 7fd23a0..494dae3 100644 --- a/src/Server/Controllers/ServerController.cs +++ b/src/Server/Controllers/ServerController.cs @@ -80,6 +80,9 @@ public class ServerController : ControllerBase var sqlHelper = DatabaseHelper.GetSQLHelper(_options.Value); Task entityCountTask = DatabaseHelper.CountEntities(sqlHelper); long queryCacheUtilization = 0; + long queryCacheElementCount = 0; + long queryCacheMaxElementCountAll = 0; + long queryCacheMaxElementCountLoadedSearchdomainsOnly = 0; foreach (string searchdomain in _searchdomainManager.ListSearchdomains()) { if (SearchdomainHelper.IsSearchdomainLoaded(_searchdomainManager, searchdomain)) @@ -87,10 +90,29 @@ public class ServerController : ControllerBase (Searchdomain? searchdomain_, int? httpStatusCode, string? message) = SearchdomainHelper.TryGetSearchdomain(_searchdomainManager, searchdomain, _logger); if (searchdomain_ is null || httpStatusCode is not null) return StatusCode(httpStatusCode ?? 500, new ServerGetStatsResult(){Success = false, Message = message}); queryCacheUtilization += searchdomain_.GetSearchCacheSize(); + queryCacheElementCount += searchdomain_.queryCache.Count; + queryCacheMaxElementCountAll += searchdomain_.queryCache.Capacity; + queryCacheMaxElementCountLoadedSearchdomainsOnly += searchdomain_.queryCache.Capacity; + } else + { + var searchdomainSettings = DatabaseHelper.GetSearchdomainSettings(sqlHelper, searchdomain); + queryCacheMaxElementCountAll += searchdomainSettings.QueryCacheSize; } }; long entityCount = await entityCountTask; - return new ServerGetStatsResult() { Success = true, EntityCount = entityCount, QueryCacheUtilization = queryCacheUtilization, SizeInBytes = size, MaxElementCount = _searchdomainManager.EmbeddingCacheMaxCount, ElementCount = elementCount, EmbeddingsCount = embeddingsCount}; + + return new ServerGetStatsResult() { + Success = true, + EntityCount = entityCount, + QueryCacheUtilization = queryCacheUtilization, + QueryCacheElementCount = queryCacheElementCount, + QueryCacheMaxElementCountAll = queryCacheMaxElementCountAll, + QueryCacheMaxElementCountLoadedSearchdomainsOnly = queryCacheMaxElementCountLoadedSearchdomainsOnly, + EmbeddingCacheUtilization = size, + EmbeddingCacheMaxElementCount = _searchdomainManager.EmbeddingCacheMaxCount, + EmbeddingCacheElementCount = elementCount, + EmbeddingsCount = embeddingsCount + }; } catch (Exception ex) { ElmahExtensions.RaiseError(ex); diff --git a/src/Server/Helper/DatabaseHelper.cs b/src/Server/Helper/DatabaseHelper.cs index c4336c8..5183fbe 100644 --- a/src/Server/Helper/DatabaseHelper.cs +++ b/src/Server/Helper/DatabaseHelper.cs @@ -1,6 +1,7 @@ using System.Configuration; using System.Data.Common; using System.Text; +using System.Text.Json; using MySql.Data.MySqlClient; using Server.Exceptions; using Server.Models; @@ -244,4 +245,22 @@ public class DatabaseHelper(ILogger logger) searchdomainSumReader.Close(); return result; } + + public static SearchdomainSettings GetSearchdomainSettings(SQLHelper helper, string searchdomain) + { + Dictionary parameters = new() + { + ["name"] = searchdomain + }; + DbDataReader reader = helper.ExecuteSQLCommand("SELECT settings from searchdomain WHERE name = @name", parameters); + try + { + reader.Read(); + string settingsString = reader.GetString(0); + return JsonSerializer.Deserialize(settingsString); + } finally + { + reader.Close(); + } + } } \ No newline at end of file diff --git a/src/Server/Resources/SharedResources.de.resx b/src/Server/Resources/SharedResources.de.resx index 04636c2..633a8a9 100644 --- a/src/Server/Resources/SharedResources.de.resx +++ b/src/Server/Resources/SharedResources.de.resx @@ -300,4 +300,19 @@ Searchdomain Datenbank-Auslastung konnte nicht abgerufen werden + + Query-Cache Einträge + + + Query-Cache Kapazität (alle) + + + Anzahl der Einträge, die insgesamt in den Query-Cache passen. Ungeladene Searchdomains werden berücksichtigt. + + + Query-Cache Kapazität (geladen) + + + Anzahl der Einträge, die insgesamt in den Query-Cache der geladenen Searchdomains passen. + \ No newline at end of file diff --git a/src/Server/Resources/SharedResources.en.resx b/src/Server/Resources/SharedResources.en.resx index 000c7bc..f19d653 100644 --- a/src/Server/Resources/SharedResources.en.resx +++ b/src/Server/Resources/SharedResources.en.resx @@ -300,4 +300,19 @@ Unable to fetch searchdomain database utilization + + Query cache entry count + + + Query cache capacity (all) + + + Number of query cache entries that can be stored in the query cache, including searchdomains that are currently not loaded. + + + Query cache capacity (loaded) + + + Number of query cache entries that can be stored in the query cache of all loaded searchdomains. + \ No newline at end of file diff --git a/src/Server/Searchdomain.cs b/src/Server/Searchdomain.cs index ce5cc12..e856947 100644 --- a/src/Server/Searchdomain.cs +++ b/src/Server/Searchdomain.cs @@ -278,15 +278,7 @@ public class Searchdomain public SearchdomainSettings GetSettings() { - Dictionary parameters = new() - { - ["name"] = searchdomain - }; - DbDataReader reader = helper.ExecuteSQLCommand("SELECT settings from searchdomain WHERE name = @name", parameters); - reader.Read(); - string settingsString = reader.GetString(0); - reader.Close(); - return JsonSerializer.Deserialize(settingsString); + return DatabaseHelper.GetSearchdomainSettings(helper, searchdomain); } public void ReconciliateOrInvalidateCacheForNewOrUpdatedEntity(Entity entity) @@ -343,13 +335,13 @@ public class Searchdomain public long GetSearchCacheSize() { - long sizeInBytes = 0; + long EmbeddingCacheUtilization = 0; foreach (var entry in queryCache) { - sizeInBytes += sizeof(int); // string length prefix - sizeInBytes += entry.Key.Length * sizeof(char); // string characters - sizeInBytes += entry.Value.EstimateSize(); + EmbeddingCacheUtilization += sizeof(int); // string length prefix + EmbeddingCacheUtilization += entry.Key.Length * sizeof(char); // string characters + EmbeddingCacheUtilization += entry.Value.EstimateSize(); } - return sizeInBytes; + return EmbeddingCacheUtilization; } } diff --git a/src/Server/Views/Home/Index.cshtml b/src/Server/Views/Home/Index.cshtml index 12f5004..ea70424 100644 --- a/src/Server/Views/Home/Index.cshtml +++ b/src/Server/Views/Home/Index.cshtml @@ -104,6 +104,43 @@ @T["Total query cache utilization"] + + +
+ @T["Query cache entry count"] + +
+ +
+ + @T["Query cache capacity (loaded)"] + + + +
+ +
+
+
+ + +
+ + @T["Query cache capacity (all)"] + + + +
+ +
+
+
@@ -135,6 +172,17 @@ let embeddingcacheEmbeddingCount = document.getElementById("embeddingcacheEmbeddingCount"); showThrobber(embeddingcacheEmbeddingCount); let embeddingcacheElementCountProgressBar = document.getElementById("embeddingcacheElementCountProgressBar"); + + let querycacheCount = document.getElementById("querycacheCount"); + showThrobber(querycacheCount); + let querycacheMaxElementCount = document.getElementById("querycacheMaxElementCount"); + showThrobber(querycacheMaxElementCount); + let querycacheMaxElementCountProgressBar = document.getElementById("querycacheMaxElementCountProgressBar"); + let querycacheLoadedMaxElementCount = document.getElementById("querycacheLoadedMaxElementCount"); + showThrobber(querycacheLoadedMaxElementCount); + let querycacheLoadedElementCountProgressBar = document.getElementById("querycacheLoadedElementCountProgressBar"); + + let healthchecksServer = document.getElementById("healthchecksServer"); let healthchecksAiProvider = document.getElementById("healthchecksAiProvider"); @@ -145,23 +193,34 @@ searchdomainCount.textContent = searchdomains.length; }); getServerStats().then(result => { - let utilization = result.SizeInBytes; - let maxElementCount = result.MaxElementCount; - let elementCount = result.ElementCount; + let utilization = result.EmbeddingCacheUtilization; + let embeddingCacheMaxElementCount = result.EmbeddingCacheMaxElementCount; + let embeddingCacheElementCount = result.ElementCount; let embeddingCount = result.EmbeddingsCount; let entityCount = result.EntityCount; let queryCacheUtilization = result.QueryCacheUtilization; + let queryCacheElementCount = result.QueryCacheElementCount; + let queryCacheMaxElementCountAll = result.QueryCacheMaxElementCountAll; + let queryCacheMaxElementCountLoadedSearchdomainsOnly = result.QueryCacheMaxElementCountLoadedSearchdomainsOnly; hideThrobber(embeddingcacheSize); embeddingcacheSize.textContent = NumberOfBytesAsHumanReadable(utilization); hideThrobber(embeddingcacheElementCount); - embeddingcacheElementCount.textContent = `${elementCount.toLocaleString()} / ${maxElementCount.toLocaleString()}`; + embeddingcacheElementCount.textContent = `${embeddingCacheElementCount.toLocaleString()} / ${embeddingCacheMaxElementCount.toLocaleString()}`; hideThrobber(embeddingcacheEmbeddingCount); embeddingcacheEmbeddingCount.textContent = embeddingCount; - embeddingcacheElementCountProgressBar.style.width = `${elementCount / maxElementCount * 100}%`; + embeddingcacheElementCountProgressBar.style.width = `${embeddingCacheElementCount / embeddingCacheMaxElementCount * 100}%`; hideThrobber(searchdomainEntityCount); searchdomainEntityCount.textContent = entityCount; hideThrobber(totalQuerycacheUtilization); totalQuerycacheUtilization.textContent = NumberOfBytesAsHumanReadable(queryCacheUtilization); + hideThrobber(querycacheMaxElementCount); + querycacheCount.textContent = queryCacheElementCount; + hideThrobber(querycacheCount); + querycacheMaxElementCount.textContent = queryCacheMaxElementCountAll.toLocaleString(); + querycacheMaxElementCountProgressBar.style.width = `${queryCacheElementCount / queryCacheMaxElementCountAll * 100}%`; + hideThrobber(querycacheLoadedMaxElementCount); + querycacheLoadedMaxElementCount.textContent = queryCacheMaxElementCountLoadedSearchdomainsOnly.toLocaleString(); + querycacheLoadedMaxElementCountProgressBar.style.width = `${queryCacheElementCount / queryCacheMaxElementCountLoadedSearchdomainsOnly * 100}%`; }); getHealthCheckStatusAndApply(healthchecksServer, "/healthz/Database"); getHealthCheckStatusAndApply(healthchecksAiProvider, "/healthz/AIProvider"); diff --git a/src/Shared/Models/ServerModels.cs b/src/Shared/Models/ServerModels.cs index 2ad0eae..59905fd 100644 --- a/src/Shared/Models/ServerModels.cs +++ b/src/Shared/Models/ServerModels.cs @@ -10,16 +10,22 @@ public class ServerGetModelsResult : SuccesMessageBaseModel public class ServerGetStatsResult : SuccesMessageBaseModel { - [JsonPropertyName("SizeInBytes")] - public long? SizeInBytes { get; set; } - [JsonPropertyName("MaxElementCount")] - public long? MaxElementCount { get; set; } + [JsonPropertyName("EmbeddingCacheUtilization")] + public long? EmbeddingCacheUtilization { get; set; } + [JsonPropertyName("EmbeddingCacheMaxElementCount")] + public long? EmbeddingCacheMaxElementCount { get; set; } [JsonPropertyName("ElementCount")] - public long? ElementCount { get; set; } + public long? EmbeddingCacheElementCount { get; set; } [JsonPropertyName("EmbeddingsCount")] public long? EmbeddingsCount { get; set; } [JsonPropertyName("EntityCount")] public long? EntityCount { get; set; } + [JsonPropertyName("QueryCacheElementCount")] + public long? QueryCacheElementCount { get; set; } + [JsonPropertyName("QueryCacheMaxElementCountAll")] + public long? QueryCacheMaxElementCountAll { get; set; } + [JsonPropertyName("QueryCacheMaxElementCountLoadedSearchdomainsOnly")] + public long? QueryCacheMaxElementCountLoadedSearchdomainsOnly { get; set; } [JsonPropertyName("QueryCacheUtilization")] public long? QueryCacheUtilization { get; set; } } \ No newline at end of file From e49a7c83ba8a8e8b0ca4159fcb23cfb6f39ab1d4 Mon Sep 17 00:00:00 2001 From: LD-Reborn Date: Wed, 7 Jan 2026 01:52:12 +0100 Subject: [PATCH 2/2] Improved sql connection pool resiliency --- src/Server/Helper/SQLHelper.cs | 30 ++++++++++++++++++++++--- src/Server/Helper/SearchdomainHelper.cs | 2 +- src/Server/SearchdomainManager.cs | 14 ++++++++---- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/Server/Helper/SQLHelper.cs b/src/Server/Helper/SQLHelper.cs index efb24aa..c1dc295 100644 --- a/src/Server/Helper/SQLHelper.cs +++ b/src/Server/Helper/SQLHelper.cs @@ -1,3 +1,4 @@ +using System.Data; using System.Data.Common; using MySql.Data.MySqlClient; @@ -6,6 +7,7 @@ namespace Server.Helper; public class SQLHelper:IDisposable { public MySqlConnection connection; + public DbDataReader? dbDataReader; public string connectionString; public SQLHelper(MySqlConnection connection, string connectionString) { @@ -30,13 +32,15 @@ public class SQLHelper:IDisposable lock (connection) { EnsureConnected(); + EnsureDbReaderIsClosed(); using MySqlCommand command = connection.CreateCommand(); command.CommandText = query; foreach (KeyValuePair parameter in parameters) { command.Parameters.AddWithValue($"@{parameter.Key}", parameter.Value); } - return command.ExecuteReader(); + dbDataReader = command.ExecuteReader(); + return dbDataReader; } } @@ -45,6 +49,7 @@ public class SQLHelper:IDisposable lock (connection) { EnsureConnected(); + EnsureDbReaderIsClosed(); using MySqlCommand command = connection.CreateCommand(); command.CommandText = query; @@ -61,6 +66,7 @@ public class SQLHelper:IDisposable lock (connection) { EnsureConnected(); + EnsureDbReaderIsClosed(); using MySqlCommand command = connection.CreateCommand(); command.CommandText = query; @@ -83,11 +89,29 @@ public class SQLHelper:IDisposable connection.Close(); connection.Open(); } - catch (Exception) + catch (Exception ex) { - throw; // TODO add logging here + ElmahCore.ElmahExtensions.RaiseError(ex); + throw; } } return true; } + + public void EnsureDbReaderIsClosed() + { + int counter = 0; + int sleepTime = 10; + int timeout = 5000; + while (!(dbDataReader?.IsClosed ?? true)) + { + if (counter > timeout / sleepTime) + { + TimeoutException ex = new("Unable to ensure dbDataReader is closed"); + ElmahCore.ElmahExtensions.RaiseError(ex); + throw ex; + } + Thread.Sleep(sleepTime); + } + } } \ No newline at end of file diff --git a/src/Server/Helper/SearchdomainHelper.cs b/src/Server/Helper/SearchdomainHelper.cs index 5b4d256..9be053b 100644 --- a/src/Server/Helper/SearchdomainHelper.cs +++ b/src/Server/Helper/SearchdomainHelper.cs @@ -88,7 +88,7 @@ public class SearchdomainHelper(ILogger logger, DatabaseHelp public Entity? EntityFromJSON(SearchdomainManager searchdomainManager, ILogger logger, JSONEntity jsonEntity) //string json) { - SQLHelper helper = searchdomainManager.helper.DuplicateConnection(); + using SQLHelper helper = searchdomainManager.helper.DuplicateConnection(); Searchdomain searchdomain = searchdomainManager.GetSearchdomain(jsonEntity.Searchdomain); List entityCache = searchdomain.entityCache; AIProvider aIProvider = searchdomain.aIProvider; diff --git a/src/Server/SearchdomainManager.cs b/src/Server/SearchdomainManager.cs index dbc45af..9cc5019 100644 --- a/src/Server/SearchdomainManager.cs +++ b/src/Server/SearchdomainManager.cs @@ -82,12 +82,18 @@ public class SearchdomainManager { DbDataReader reader = helper.ExecuteSQLCommand("SELECT name FROM searchdomain", []); List results = []; - while (reader.Read()) + try { - results.Add(reader.GetString(0)); + while (reader.Read()) + { + results.Add(reader.GetString(0)); + } + return results; + } + finally + { + reader.Close(); } - reader.Close(); - return results; } }