diff --git a/C4IT_DataHistoryProvider_Base/Publish/F4SD-Cockpit-Server.dll b/C4IT_DataHistoryProvider_Base/Publish/F4SD-Cockpit-Server.dll index 909a9bd..00ed442 100644 Binary files a/C4IT_DataHistoryProvider_Base/Publish/F4SD-Cockpit-Server.dll and b/C4IT_DataHistoryProvider_Base/Publish/F4SD-Cockpit-Server.dll differ diff --git a/F4SD-Cockpit-ServerCore/DataHistoryCollectorM42Wpm.cs b/F4SD-Cockpit-ServerCore/DataHistoryCollectorM42Wpm.cs index 2901c91..4627dec 100644 --- a/F4SD-Cockpit-ServerCore/DataHistoryCollectorM42Wpm.cs +++ b/F4SD-Cockpit-ServerCore/DataHistoryCollectorM42Wpm.cs @@ -38,6 +38,7 @@ namespace C4IT.DataHistoryProvider private const string constUrlGetPickupValues = "m42Services/api/c4itf4sdwebapi/getpickup/{0}?group={1}"; private const string constUrlGetRoleMeberships = "m42services/api/c4itf4sdwebapi/getrolememberships/?sid={0}"; private const string constUrlGetTicketOverviewCounts = "m42Services/api/c4itf4sdwebapi/getticketoverviewcounts?sid={0}&scope={1}&keys={2}"; + private const string constUrlGetTicketOverviewCountsByRoles = "m42Services/api/c4itf4sdwebapi/getticketoverviewcountsbyroles"; private const string constUrlGetTicketOverviewRelations = "m42Services/api/c4itf4sdwebapi/getticketoverviewrelations?sid={0}&scope={1}&key={2}&count={3}"; private const string constUrlGetDataQueryRelationItems = "m42Services/api/dataquery/relationitems"; private const string constUrlGetDataQueryRelationItemsCount = "m42Services/api/dataquery/relationitems/count"; @@ -66,11 +67,16 @@ namespace C4IT.DataHistoryProvider private readonly Timer onlineTimer = null; - private bool IsOnline = false; - - private Dictionary UserWebClientCache = new Dictionary(); - - private readonly cDataHistoryCollector _collector; + private bool IsOnline = false; + + private Dictionary UserWebClientCache = new Dictionary(); + + private readonly object _ticketOverviewCacheLock = new object(); + private readonly Dictionary _ticketOverviewPersonalCache = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _ticketOverviewRoleCache = new Dictionary(); + private readonly Dictionary _ticketOverviewRoleListCache = new Dictionary(StringComparer.OrdinalIgnoreCase); + + private readonly cDataHistoryCollector _collector; private static class _OnlineCheckCriticalSection { @@ -88,12 +94,38 @@ namespace C4IT.DataHistoryProvider public string Language; } - private class WebClientCacheEntry - { - public cM42WebClient WebClient; - public DateTime LastUsed; - public DateTime ValidUntil; - } + private class WebClientCacheEntry + { + public cM42WebClient WebClient; + public DateTime LastUsed; + public DateTime ValidUntil; + } + + private sealed class TicketOverviewCountCacheEntry + { + public Dictionary Counts { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + public DateTime ExpiresAtUtc { get; set; } + } + + private sealed class TicketOverviewRoleListCacheEntry + { + public List RoleIds { get; set; } = new List(); + public DateTime ExpiresAtUtc { get; set; } + } + + private sealed class TicketOverviewCountsByRolesRequest + { + public string Sid { get; set; } + public List RoleGuids { get; set; } = new List(); + public List Keys { get; set; } = new List(); + } + + private sealed class TicketOverviewCountsByRolesResponse + { + [JsonProperty("countsByRole")] + public Dictionary> CountsByRole { get; set; } + = new Dictionary>(StringComparer.OrdinalIgnoreCase); + } public class cM42ApiTokenModel { @@ -601,34 +633,295 @@ namespace C4IT.DataHistoryProvider return null; } - public async Task> GetTicketOverviewCountsAsync(IEnumerable keys, bool useRoleScope, cF4sdWebRequestInfo requestInfo, int LogDeep, CancellationToken Token) + private TimeSpan GetTicketOverviewCacheDuration(bool useRoleScope) + { + var ticketConfig = Collector?.GetGlobalConfig()?.TicketConfiguration; + var minutes = useRoleScope + ? ticketConfig?.OverviewPollingRole ?? cF4sdTicketConfig.DefaultOverviewPollingRole + : ticketConfig?.OverviewPollingPersonal ?? cF4sdTicketConfig.DefaultOverviewPollingPersonal; + + if (minutes < 1) + minutes = 1; + + return TimeSpan.FromMinutes(minutes); + } + + private bool TryGetTicketOverviewPersonalCache(string sid, DateTime now, out Dictionary counts) + { + counts = null; + if (string.IsNullOrWhiteSpace(sid)) + return false; + + lock (_ticketOverviewCacheLock) + { + if (_ticketOverviewPersonalCache.TryGetValue(sid, out var entry)) + { + if (entry.ExpiresAtUtc > now) + { + counts = new Dictionary(entry.Counts, StringComparer.OrdinalIgnoreCase); + return true; + } + + _ticketOverviewPersonalCache.Remove(sid); + } + } + + return false; + } + + private bool TryGetTicketOverviewRoleCache(Guid roleId, DateTime now, out Dictionary counts) + { + counts = null; + if (roleId == Guid.Empty) + return false; + + lock (_ticketOverviewCacheLock) + { + if (_ticketOverviewRoleCache.TryGetValue(roleId, out var entry)) + { + if (entry.ExpiresAtUtc > now) + { + counts = new Dictionary(entry.Counts, StringComparer.OrdinalIgnoreCase); + return true; + } + + _ticketOverviewRoleCache.Remove(roleId); + } + } + + return false; + } + + private void SetTicketOverviewPersonalCache(string sid, Dictionary counts, DateTime expiresAtUtc) + { + if (string.IsNullOrWhiteSpace(sid)) + return; + + lock (_ticketOverviewCacheLock) + { + _ticketOverviewPersonalCache[sid] = new TicketOverviewCountCacheEntry + { + Counts = new Dictionary(counts ?? new Dictionary(), StringComparer.OrdinalIgnoreCase), + ExpiresAtUtc = expiresAtUtc + }; + } + } + + private void SetTicketOverviewRoleCache(Guid roleId, Dictionary counts, DateTime expiresAtUtc) + { + if (roleId == Guid.Empty) + return; + + lock (_ticketOverviewCacheLock) + { + _ticketOverviewRoleCache[roleId] = new TicketOverviewCountCacheEntry + { + Counts = new Dictionary(counts ?? new Dictionary(), StringComparer.OrdinalIgnoreCase), + ExpiresAtUtc = expiresAtUtc + }; + } + } + + private bool TryGetTicketOverviewRoleIdsFromCache(string sid, DateTime now, out List roleIds) + { + roleIds = null; + if (string.IsNullOrWhiteSpace(sid)) + return false; + + lock (_ticketOverviewCacheLock) + { + if (_ticketOverviewRoleListCache.TryGetValue(sid, out var entry)) + { + if (entry.ExpiresAtUtc > now) + { + roleIds = new List(entry.RoleIds); + return true; + } + + _ticketOverviewRoleListCache.Remove(sid); + } + } + + return false; + } + + private void SetTicketOverviewRoleIdsCache(string sid, IEnumerable roleIds, DateTime expiresAtUtc) + { + if (string.IsNullOrWhiteSpace(sid)) + return; + + var normalized = (roleIds ?? Enumerable.Empty()) + .Where(id => id != Guid.Empty) + .Distinct() + .ToList(); + + lock (_ticketOverviewCacheLock) + { + _ticketOverviewRoleListCache[sid] = new TicketOverviewRoleListCacheEntry + { + RoleIds = normalized, + ExpiresAtUtc = expiresAtUtc + }; + } + } + + private async Task> GetTicketOverviewRoleIdsAsync(string sid, DateTime now, TimeSpan ttl, CancellationToken token) + { + if (TryGetTicketOverviewRoleIdsFromCache(sid, now, out var cached)) + return cached; + + var userInfo = await GetM42UserInfoAsync(sid, token); + var roles = userInfo?.Roles ?? new List(); + var roleIds = roles + .Where(r => r != null && r.Id != Guid.Empty) + .Select(r => r.Id) + .Distinct() + .ToList(); + + SetTicketOverviewRoleIdsCache(sid, roleIds, now.Add(ttl)); + return roleIds; + } + + private async Task>> FetchTicketOverviewCountsByRolesAsync( + IEnumerable roleIds, + IReadOnlyCollection requestedKeys, + cF4sdWebRequestInfo requestInfo, + int logDeep, + CancellationToken token) { MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); } - if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); } - var _startTime = DateTime.UtcNow; + if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(logDeep, CM, requestInfo.id, requestInfo.created); } + var startTime = DateTime.UtcNow; try { - if (!await CheckOnline()) - return new Dictionary(StringComparer.OrdinalIgnoreCase); - - var sid = requestInfo?.userInfo?.AdSid; - if (string.IsNullOrWhiteSpace(sid)) - return new Dictionary(StringComparer.OrdinalIgnoreCase); - - var scope = useRoleScope ? "role" : "personal"; - var normalizedKeys = (keys ?? Enumerable.Empty()) - .Where(k => !string.IsNullOrWhiteSpace(k)) - .Distinct(StringComparer.OrdinalIgnoreCase) + var roleList = (roleIds ?? Enumerable.Empty()) + .Where(id => id != Guid.Empty) + .Distinct() .ToList(); - var keyParam = normalizedKeys.Count > 0 - ? HttpUtility.UrlEncode(string.Join(",", normalizedKeys)) - : string.Empty; - var url = string.Format(constUrlGetTicketOverviewCounts, HttpUtility.UrlEncode(sid), scope, keyParam); - var wc = await GetWebClient(requestInfo, Token); - var res = await wc.HttpEnh.GetAsync(url, Token); - if (Token.IsCancellationRequested) + if (roleList.Count == 0) + return new Dictionary>(); + + var request = new TicketOverviewCountsByRolesRequest + { + Sid = requestInfo?.userInfo?.AdSid, + RoleGuids = roleList, + Keys = requestedKeys?.Where(k => !string.IsNullOrWhiteSpace(k)).ToList() ?? new List() + }; + + var jsonBody = JsonConvert.SerializeObject(request, Formatting.None, + new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); + var content = new StringContent(jsonBody, Encoding.UTF8, "application/json"); + + var wc = await GetWebClient(requestInfo, token); + var res = await wc.HttpEnh.PostAsync(constUrlGetTicketOverviewCountsByRoles, content, token); + if (token.IsCancellationRequested) + return new Dictionary>(); + + if (res?.IsSuccessStatusCode == true) + { + var json = await res.Content.ReadAsStringAsync(); + if (!string.IsNullOrWhiteSpace(json)) + { + var response = JsonConvert.DeserializeObject(json); + if (response?.CountsByRole != null) + { + var result = new Dictionary>(); + foreach (var entry in response.CountsByRole) + { + if (!Guid.TryParse(entry.Key, out var roleId) || roleId == Guid.Empty) + continue; + + var counts = entry.Value ?? new Dictionary(); + result[roleId] = new Dictionary(counts, StringComparer.OrdinalIgnoreCase); + } + return result; + } + } + } + + StartOnlineValidation(); + } + catch (Exception E) + { + LogException(E); + } + finally + { + if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(logDeep, CM, requestInfo.id, requestInfo.created, startTime); } + if (CM != null) LogMethodEnd(CM); + } + + return new Dictionary>(); + } + + private Dictionary SumTicketOverviewCounts(IEnumerable> sources) + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (sources == null) + return result; + + foreach (var counts in sources) + { + if (counts == null) + continue; + + foreach (var kvp in counts) + { + if (string.IsNullOrWhiteSpace(kvp.Key)) + continue; + + result[kvp.Key] = result.TryGetValue(kvp.Key, out var existing) + ? existing + kvp.Value + : kvp.Value; + } + } + + return result; + } + + private Dictionary FilterTicketOverviewCounts(Dictionary counts, IReadOnlyCollection requestedKeys) + { + if (counts == null) + return new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (requestedKeys == null || requestedKeys.Count == 0) + return new Dictionary(counts, StringComparer.OrdinalIgnoreCase); + + var filtered = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var key in requestedKeys) + { + if (string.IsNullOrWhiteSpace(key)) + continue; + + filtered[key] = counts.TryGetValue(key, out var value) ? value : 0; + } + + return filtered; + } + + private async Task> FetchTicketOverviewCountsAsync( + string sid, + string scope, + IReadOnlyCollection requestedKeys, + cF4sdWebRequestInfo requestInfo, + CancellationToken token) + { + try + { + var keyParam = requestedKeys != null && requestedKeys.Count > 0 + ? HttpUtility.UrlEncode(string.Join(",", requestedKeys)) + : null; + + var url = string.Format(constUrlGetTicketOverviewCounts, HttpUtility.UrlEncode(sid), scope, keyParam ?? string.Empty); + if (string.IsNullOrWhiteSpace(keyParam)) + { + url = $"m42Services/api/c4itf4sdwebapi/getticketoverviewcounts?sid={HttpUtility.UrlEncode(sid)}&scope={scope}"; + } + var wc = await GetWebClient(requestInfo, token); + var res = await wc.HttpEnh.GetAsync(url, token); + if (token.IsCancellationRequested) return new Dictionary(StringComparer.OrdinalIgnoreCase); if (res?.IsSuccessStatusCode == true) @@ -661,6 +954,86 @@ namespace C4IT.DataHistoryProvider { LogException(E); } + + return new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + public async Task> GetTicketOverviewCountsAsync(IEnumerable keys, bool useRoleScope, cF4sdWebRequestInfo requestInfo, int LogDeep, CancellationToken Token) + { + MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); } + if (cPerformanceLogger.IsActive && requestInfo != null) { if (CM == null) CM = MethodBase.GetCurrentMethod(); cPerformanceLogger.LogPerformanceStart(LogDeep, CM, requestInfo.id, requestInfo.created); } + var _startTime = DateTime.UtcNow; + + try + { + if (!await CheckOnline()) + return new Dictionary(StringComparer.OrdinalIgnoreCase); + + var sid = requestInfo?.userInfo?.AdSid; + if (string.IsNullOrWhiteSpace(sid)) + return new Dictionary(StringComparer.OrdinalIgnoreCase); + + var normalizedKeys = (keys ?? Enumerable.Empty()) + .Where(k => !string.IsNullOrWhiteSpace(k)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + var now = DateTime.UtcNow; + var ttl = GetTicketOverviewCacheDuration(useRoleScope); + + if (!useRoleScope) + { + if (TryGetTicketOverviewPersonalCache(sid, now, out var cachedPersonal)) + return FilterTicketOverviewCounts(cachedPersonal, normalizedKeys); + + var counts = await FetchTicketOverviewCountsAsync(sid, "personal", normalizedKeys, requestInfo, Token); + if (Token.IsCancellationRequested) + return new Dictionary(StringComparer.OrdinalIgnoreCase); + + SetTicketOverviewPersonalCache(sid, counts, now.Add(ttl)); + return FilterTicketOverviewCounts(counts, normalizedKeys); + } + + var roleIds = await GetTicketOverviewRoleIdsAsync(sid, now, ttl, Token); + if (roleIds == null || roleIds.Count == 0) + return new Dictionary(StringComparer.OrdinalIgnoreCase); + + var countsByRole = new Dictionary>(); + var missingRoles = new List(); + + foreach (var roleId in roleIds) + { + if (TryGetTicketOverviewRoleCache(roleId, now, out var cachedRole)) + { + countsByRole[roleId] = cachedRole; + } + else + { + missingRoles.Add(roleId); + } + } + + if (missingRoles.Count > 0) + { + var fetched = await FetchTicketOverviewCountsByRolesAsync(missingRoles, normalizedKeys, requestInfo, LogDeep + 1, Token); + var expiresAt = DateTime.UtcNow.Add(ttl); + + foreach (var roleId in missingRoles) + { + if (!fetched.TryGetValue(roleId, out var roleCounts)) + roleCounts = new Dictionary(StringComparer.OrdinalIgnoreCase); + + SetTicketOverviewRoleCache(roleId, roleCounts, expiresAt); + countsByRole[roleId] = roleCounts; + } + } + + var summed = SumTicketOverviewCounts(countsByRole.Values); + return FilterTicketOverviewCounts(summed, normalizedKeys); + } + catch (Exception E) + { + LogException(E); + } finally { if (cPerformanceLogger.IsActive && requestInfo != null) { cPerformanceLogger.LogPerformanceEnd(LogDeep, CM, requestInfo.id, requestInfo.created, _startTime); }