diff --git a/AyCode.Core/Helpers/TaskHelper.cs b/AyCode.Core/Helpers/TaskHelper.cs index 4cc1ac2..3aa3dbb 100644 --- a/AyCode.Core/Helpers/TaskHelper.cs +++ b/AyCode.Core/Helpers/TaskHelper.cs @@ -1,7 +1,27 @@ -namespace AyCode.Core.Helpers +using System.Runtime.CompilerServices; +using AyCode.Core.Loggers; + +namespace AyCode.Core.Helpers { public static class TaskHelper { + /// + /// Optional ambient logger for fire-and-forget faults. Set once at startup (after the DI + /// container is built) in each host's entry point, e.g. + /// TaskHelper.Logger = new LoggerClient<TaskHelper>(writers);. + /// When null, faults are swallowed (legacy behaviour). A per-call logger argument + /// passed to any Forget overload overrides this ambient instance. + /// + public static IAcLoggerBase? Logger { get; set; } + + /// + /// Logs a fire-and-forget fault. Normal cancellation is filtered out by the callers, + /// so anything reaching here is a real fault worth recording. Resolves the logger as + /// "explicit per-call argument, else ambient "; both null → silent. + /// + private static void LogForgetFault(Exception ex, IAcLoggerBase? logger, string caller, int line) + => (logger ?? Logger)?.Error($"Fire-and-forget faulted @ {caller}:{line}", ex); + public static bool WaitTo(Func predicate, int msTimeout = 10000, int msDelay = 5, int msFirstDelay = 0) => WaitToAsync(predicate, msTimeout, msDelay, msFirstDelay).GetAwaiter().GetResult(); @@ -48,56 +68,68 @@ return predicate(); } - public static void Forget(this Task task) + public static void Forget(this Task task, IAcLoggerBase? logger = null, [CallerMemberName] string caller = "", [CallerLineNumber] int line = 0) { - if (!task.IsCompleted || task.IsFaulted) - _ = ForgetAwaited(task); + if (!task.IsCompleted || task.IsFaulted) _ = ForgetAwaited(task, logger, caller, line); + return; - static async Task ForgetAwaited(Task task) + static async Task ForgetAwaited(Task task, IAcLoggerBase? logger, string caller, int line) { try { await task.ConfigureAwait(false); } - catch + catch (OperationCanceledException) { - // Swallow exception - fire and forget semantics + // Normal cancellation — not a fault. + } + catch (Exception ex) + { + LogForgetFault(ex, logger, caller, line); } } } - public static void Forget(this ValueTask task) + public static void Forget(this ValueTask task, IAcLoggerBase? logger = null, [CallerMemberName] string caller = "", [CallerLineNumber] int line = 0) { - if (!task.IsCompleted || task.IsFaulted) - _ = ForgetAwaited(task); + if (!task.IsCompleted || task.IsFaulted) _ = ForgetAwaited(task, logger, caller, line); + return; - static async Task ForgetAwaited(ValueTask task) + static async Task ForgetAwaited(ValueTask task, IAcLoggerBase? logger, string caller, int line) { try { await task.ConfigureAwait(false); } - catch + catch (OperationCanceledException) { - // Swallow exception - fire and forget semantics + // Normal cancellation — not a fault. + } + catch (Exception ex) + { + LogForgetFault(ex, logger, caller, line); } } } - public static void Forget(this ValueTask task) + public static void Forget(this ValueTask task, IAcLoggerBase? logger = null, [CallerMemberName] string caller = "", [CallerLineNumber] int line = 0) { - if (!task.IsCompleted || task.IsFaulted) - _ = ForgetAwaited(task); + if (!task.IsCompleted || task.IsFaulted) _ = ForgetAwaited(task, logger, caller, line); + return; - static async Task ForgetAwaited(ValueTask task) + static async Task ForgetAwaited(ValueTask task, IAcLoggerBase? logger, string caller, int line) { try { await task.ConfigureAwait(false); } - catch + catch (OperationCanceledException) { - // Swallow exception - fire and forget semantics + // Normal cancellation — not a fault. + } + catch (Exception ex) + { + LogForgetFault(ex, logger, caller, line); } } } diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/TestOrderItemListDataSource.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/TestOrderItemListDataSource.cs index 0c0a7b1..e9672e3 100644 --- a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/TestOrderItemListDataSource.cs +++ b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/TestOrderItemListDataSource.cs @@ -7,5 +7,5 @@ namespace AyCode.Services.Server.Tests.SignalRs.SignalRDatasources; public class TestOrderItemListDataSource : AcSignalRDataSource> { public TestOrderItemListDataSource(AcSignalRClientBase signalRClient, SignalRCrudTags crudTags) - : base(signalRClient, crudTags) { } + : base(signalRClient, crudTags, null) { } } \ No newline at end of file diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/TestOrderItemObservableDataSource.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/TestOrderItemObservableDataSource.cs index dff2ddf..a9e2c81 100644 --- a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/TestOrderItemObservableDataSource.cs +++ b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/TestOrderItemObservableDataSource.cs @@ -8,5 +8,5 @@ namespace AyCode.Services.Server.Tests.SignalRs.SignalRDatasources; public class TestOrderItemObservableDataSource : AcSignalRDataSource> { public TestOrderItemObservableDataSource(AcSignalRClientBase signalRClient, SignalRCrudTags crudTags) - : base(signalRClient, crudTags) { } + : base(signalRClient, crudTags, null) { } } \ No newline at end of file diff --git a/AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs b/AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs index 008d2be..c7ffdc3 100644 --- a/AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs +++ b/AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs @@ -1,15 +1,17 @@ -using AyCode.Core.Enums; +using AyCode.Core.Compression; +using AyCode.Core.Enums; using AyCode.Core.Extensions; using AyCode.Core.Helpers; using AyCode.Core.Interfaces; +using AyCode.Core.Loggers; +using AyCode.Core.Serializers; +using AyCode.Core.Serializers.Binaries; using AyCode.Services.SignalRs; +using Castle.Core.Logging; using System.Collections; using System.Collections.ObjectModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using AyCode.Core.Compression; -using AyCode.Core.Serializers; -using AyCode.Core.Serializers.Binaries; namespace AyCode.Services.Server.SignalRs { @@ -218,8 +220,12 @@ namespace AyCode.Services.Server.SignalRs protected int FindIndexInnerListUnsafe(TId id) => InnerList.FindIndex(x => IdEquals(x.Id, id)); protected TDataItem? FirstOrDefaultInnerListUnsafe(TId id) => InnerList.FirstOrDefault(x => IdEquals(x.Id, id)); - public AcSignalRDataSource(AcSignalRClientBase signalRClient, SignalRCrudTags signalRCrudTags, object[]? contextIds = null) + private IAcLoggerBase? _logger; + + public AcSignalRDataSource(AcSignalRClientBase signalRClient, SignalRCrudTags signalRCrudTags, IAcLoggerBase? logger, object[]? contextIds = null) { + _logger = logger; + ContextIds = contextIds; SignalRCrudTags = signalRCrudTags; SignalRClient = signalRClient; @@ -277,8 +283,7 @@ namespace AyCode.Services.Server.SignalRs var rawBytes = await SignalRClient.GetAllAsync(SignalRCrudTags.GetAllMessageTag, GetContextParams()) ?? throw new NullReferenceException("LoadDataSource; null response"); - await LoadDataSourceFromResponseData(rawBytes, SerializerType, - false, false, clearChangeTracking); + await LoadDataSourceFromResponseData(rawBytes, SerializerType, false, false, clearChangeTracking); } finally { @@ -301,11 +306,9 @@ namespace AyCode.Services.Server.SignalRs { try { - var rawBytes = await responseTask - ?? throw new NullReferenceException("LoadDataSourceAsync; null response"); + var rawBytes = await responseTask ?? throw new NullReferenceException("LoadDataSourceAsync; null response"); - await LoadDataSourceFromResponseData(rawBytes, SerializerType, - false, false, clearChangeTracking); + await LoadDataSourceFromResponseData(rawBytes, SerializerType, false, false, clearChangeTracking); } finally { @@ -314,6 +317,39 @@ namespace AyCode.Services.Server.SignalRs }).Unwrap(); } + /// + /// Wraps a deserialize/populate call so a fault is logged with the concrete + /// type before it propagates. Placed directly around the deserialize call (inside any BeginUpdate/EndUpdate + /// scope, before EndUpdate runs) so the genuine root exception is captured even if EndUpdate's NotifyReset + /// throws a secondary exception that would otherwise overwrite it. Rethrows — callers' behaviour is unchanged. + /// + private void TryDeserialize(Action deserialize) + { + try + { + deserialize(); + } + catch (Exception ex) + { + _logger?.Error($"Deserialize failed for {typeof(TDataItem).Name}", ex); + throw; + } + } + + /// + private T TryDeserialize(Func deserialize) + { + try + { + return deserialize(); + } + catch (Exception ex) + { + _logger?.Error($"Deserialize failed for {typeof(TDataItem).Name}", ex); + throw; + } + } + /// /// Loads data source from response data. /// responseData is either a typed object (protocol deserialized) or byte[] (raw path). @@ -336,7 +372,7 @@ namespace AyCode.Services.Server.SignalRs observable.BeginUpdate(); try { - AcBinaryDeserializer.PopulateMerge(rawBytes, 0, rawBytes.Length, InnerList); + TryDeserialize(() => AcBinaryDeserializer.PopulateMerge(rawBytes, 0, rawBytes.Length, InnerList)); } finally { @@ -345,7 +381,7 @@ namespace AyCode.Services.Server.SignalRs } else { - AcBinaryDeserializer.Populate(rawBytes, 0, rawBytes.Length, InnerList); + TryDeserialize(() => AcBinaryDeserializer.Populate(rawBytes, 0, rawBytes.Length, InnerList)); } } else @@ -364,10 +400,8 @@ namespace AyCode.Services.Server.SignalRs else { TIList? fromSource; - if (serializerType == AcSerializerType.Binary) - fromSource = AcBinaryDeserializer.Deserialize(rawBytes, 0, rawBytes.Length); - else - fromSource = GzipHelper.DecompressToString(rawBytes).JsonTo(); + if (serializerType == AcSerializerType.Binary) fromSource = TryDeserialize(() => AcBinaryDeserializer.Deserialize(rawBytes, 0, rawBytes.Length)); + else fromSource = GzipHelper.DecompressToString(rawBytes).JsonTo(); if (fromSource != null) { @@ -417,17 +451,23 @@ namespace AyCode.Services.Server.SignalRs if (InnerList is IAcObservableCollection observable2) { observable2.BeginUpdate(); - try { AcBinaryDeserializer.PopulateMerge(reBytes, 0, reBytes.Length, InnerList); } - finally { observable2.EndUpdate(); } + try + { + TryDeserialize(() => AcBinaryDeserializer.PopulateMerge(reBytes, 0, reBytes.Length, InnerList)); + } + finally + { + observable2.EndUpdate(); + } } else { - AcBinaryDeserializer.Populate(reBytes, 0, reBytes.Length, InnerList); + TryDeserialize(() => AcBinaryDeserializer.Populate(reBytes, 0, reBytes.Length, InnerList)); } } else { - var fromSource = AcBinaryDeserializer.Deserialize(reBytes, 0, reBytes.Length); + var fromSource = TryDeserialize(() => AcBinaryDeserializer.Deserialize(reBytes, 0, reBytes.Length)); if (fromSource != null) { ClearUnsafe(clearChangeTracking);