Refactor logger config, improve SignalR log writer errors
- Make AcLoggerOptions.AppType nullable; resolve AppType via config or DI default parameter - Update AddAcLoggerFactory to accept host default AppType - Enhance AcSignaRClientLogItemWriter: robust error handling, dropped log tracking, single error reporting per failure state - Update logger README for new AppType precedence - Document AcSignalRDataSource threading/event model in ISSUES - Add diagnostic commands to settings.local.json
This commit is contained in:
parent
fe02550f79
commit
b210517826
|
|
@ -152,7 +152,17 @@
|
|||
"PowerShell(dotnet build AyCode.Services\\\\AyCode.Services.csproj --nologo -v q 2>$null)",
|
||||
"PowerShell(dotnet test AyCode.Services.Server.Tests\\\\AyCode.Services.Server.Tests.csproj --nologo --no-restore --filter \"FullyQualifiedName~AcBinaryHubProtocolConcurrencyTests\" 2>$null)",
|
||||
"Bash(git -C \"H:/Applications/Aycode/Source/AyCode.Blazor\" diff --stat HEAD)",
|
||||
"Bash(git -C \"H:/Applications/Aycode/Source/AyCode.Blazor\" diff HEAD -- AyCode.Blazor.Components/Components/Grids/MgGridBase.cs)"
|
||||
"Bash(git -C \"H:/Applications/Aycode/Source/AyCode.Blazor\" diff HEAD -- AyCode.Blazor.Components/Components/Grids/MgGridBase.cs)",
|
||||
"Bash(find \"C:/Users/Fullepi/.nuget/packages/devexpress.blazor/25.1.3\" -name \"*.xml\" 2>/dev/null | head -3)",
|
||||
"Bash(ls \"C:/Users/Fullepi/.nuget/packages/\" 2>/dev/null | grep -i devexpress | head -5; find \"C:/Users/Fullepi/.nuget/packages\" -maxdepth 4 -iname \"DevExpress.Blazor*.xml\" 2>/dev/null | head -3)",
|
||||
"Bash(git -C \"H:/Applications/Mango/Source/NopCommerce.Common/4.70/Plugins/Nop.Plugin.Misc.AIPlugin\" rev-parse --show-toplevel)",
|
||||
"Bash(git -C \"H:/Applications/Mango/Source\" rev-parse --show-toplevel)",
|
||||
"Bash(git -C \"H:/Applications/Mango/Source/NopCommerce.Common/4.70/Plugins\" log --oneline -5 -- \"Nop.Plugin.Misc.AIPlugin/docs/SIGNALR/README.md\")",
|
||||
"Bash(git -C \"H:/Applications/Mango/Source/NopCommerce.Common/4.70/Plugins\" show HEAD:\"Nop.Plugin.Misc.AIPlugin/docs/SIGNALR/README.md\")",
|
||||
"Bash(git -C \"H:/Applications/Mango/Source/NopCommerce.Common/4.70/Plugins\" show 13e96ea:\"Nop.Plugin.Misc.AIPlugin/docs/SIGNALR/README.md\")",
|
||||
"Bash(git -C \"H:/Applications/Mango/Source/NopCommerce.Common/4.70/Plugins\" restore --source 13e96ea -- \"Nop.Plugin.Misc.AIPlugin/docs/SIGNALR/README.md\")",
|
||||
"Bash(git -C \"H:/Applications/Mango/Source/NopCommerce.Common/4.70/Plugins\" status --short)",
|
||||
"Bash(exit 1)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,10 @@ namespace AyCode.Core.Loggers;
|
|||
/// </summary>
|
||||
public sealed class AcLoggerOptions
|
||||
{
|
||||
/// <summary>Application type stamped on every log entry. Default: <see cref="AppType.Server"/>.</summary>
|
||||
public AppType AppType { get; set; } = AppType.Server;
|
||||
/// <summary>Application type stamped on every log entry. <c>null</c> = a config nem adja meg — ilyenkor az
|
||||
/// <c>AddAcLoggerFactory(defaultAppType)</c> kód-defaultja érvényesül (host-onként a helyes érték: Web/Mobile/Server).
|
||||
/// Ha az appsettings kimondja, MINDIG a config nyer.</summary>
|
||||
public AppType? AppType { get; set; }
|
||||
|
||||
/// <summary>Global minimum log level. Entries below this level are discarded. Default: <see cref="LogLevel.Info"/>.</summary>
|
||||
public LogLevel LogLevel { get; set; } = LogLevel.Info;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using AyCode.Core.Enums;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
|
|
@ -23,31 +24,39 @@ public static class AcLoggerServiceExtensions
|
|||
/// Concrete logger type. Must expose a public constructor with signature
|
||||
/// <c>(AppType, LogLevel, string?, params IAcLogWriterBase[])</c>.
|
||||
/// </typeparam>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="defaultAppType">A host kód-defaultja (pl. WASM/MAUI kliensen <see cref="AppType.Web"/>) — CSAK akkor
|
||||
/// érvényesül, ha az appsettings (<see cref="AcLoggerOptions.AppType"/>) nem adja meg; ha a config kimondja, az nyer.</param>
|
||||
public static IServiceCollection AddAcLoggerFactory<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TLogger>(
|
||||
this IServiceCollection services)
|
||||
this IServiceCollection services, AppType defaultAppType = AppType.Server)
|
||||
where TLogger : AcLoggerBase
|
||||
{
|
||||
return services.AddAcLoggerFactory<TLogger, IAcLogWriterBase>();
|
||||
return services.AddAcLoggerFactory<TLogger, IAcLogWriterBase>(defaultAppType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a logger factory with a custom writer marker interface — e.g. a client-only marker
|
||||
/// to keep server-only and client-only writers separated at the DI level.
|
||||
/// </summary>
|
||||
/// <typeparam name="TLogger">Concrete logger type. See <see cref="AddAcLoggerFactory{TLogger}"/> for ctor requirements.</typeparam>
|
||||
/// <typeparam name="TLogger">Concrete logger type. See <see cref="AddAcLoggerFactory{TLogger}(IServiceCollection, AppType)"/> for ctor requirements.</typeparam>
|
||||
/// <typeparam name="TWriterBase">Writer marker interface; must extend <see cref="IAcLogWriterBase"/>.</typeparam>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="defaultAppType">A host kód-defaultja — csak config-érték hiányában érvényesül (a config nyer).</param>
|
||||
public static IServiceCollection AddAcLoggerFactory<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TLogger, TWriterBase>(
|
||||
this IServiceCollection services)
|
||||
this IServiceCollection services, AppType defaultAppType = AppType.Server)
|
||||
where TLogger : AcLoggerBase
|
||||
where TWriterBase : class, IAcLogWriterBase
|
||||
{
|
||||
services.AddSingleton<Func<string, TLogger>>(sp =>
|
||||
{
|
||||
var opts = sp.GetRequiredService<IOptions<AcLoggerOptions>>().Value;
|
||||
// Prioritás: appsettings (ha megadva) → a hívó defaultAppType-ja. Így a host kódja hordozza a helyes
|
||||
// alapértéket (kliensen Web/Mobile), a config pedig runtime felülírást ad újrafordítás nélkül.
|
||||
var appType = opts.AppType ?? defaultAppType;
|
||||
return categoryName =>
|
||||
{
|
||||
var writers = sp.GetServices<TWriterBase>().Cast<IAcLogWriterBase>().ToArray();
|
||||
return (TLogger)Activator.CreateInstance(typeof(TLogger), opts.AppType, opts.LogLevel, categoryName, writers)!;
|
||||
return (TLogger)Activator.CreateInstance(typeof(TLogger), appType, opts.LogLevel, categoryName, writers)!;
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -10,16 +10,26 @@ namespace AyCode.Services.Loggers
|
|||
public class AcSignaRClientLogItemWriter : AcLogItemWriterBase<AcLogItemClient>, IAcLogWriterClientBase
|
||||
{
|
||||
private readonly HubConnection _hubConnection;
|
||||
private readonly string _fullHubName;
|
||||
|
||||
// A log-csatorna a SAJÁT hibáit nem logolhatja önmagán át (rekurzió) → Console.Error.
|
||||
// Spam-védelem: hiba-állapotonként EGY jelentés; sikeres küldés után a kapu újraélesedik, és
|
||||
// helyreálláskor kiírjuk, hány log-item veszett el. (Egyszerű mezők — a torz dupla-jelentés ártalmatlan.)
|
||||
private bool _failureReported;
|
||||
private long _droppedCount;
|
||||
|
||||
public AcSignaRClientLogItemWriter(string fullHubName, AppType appType = AppType.Web, LogLevel logLevel = LogLevel.Detail, string? categoryName = null) : base(appType, logLevel, categoryName)
|
||||
{
|
||||
_fullHubName = fullHubName;
|
||||
|
||||
_hubConnection = new HubConnectionBuilder()
|
||||
.WithUrl(fullHubName)
|
||||
//.WithAutomaticReconnect()
|
||||
//.AddMessagePackProtocol(options => options.SerializerOptions = MessagePackSerializerOptions.Standard.WithSecurity(MessagePackSecurity.UntrustedData))
|
||||
.Build();
|
||||
|
||||
_hubConnection.StartAsync().Forget();
|
||||
// A .Forget() logger nélkül elnyelné az induló kapcsolat hibáját → jelentő wrapper.
|
||||
StartConnectionReportedAsync().Forget();
|
||||
}
|
||||
|
||||
public async Task StartConnection()
|
||||
|
|
@ -42,16 +52,56 @@ namespace AyCode.Services.Loggers
|
|||
|
||||
protected override async void WriteLogItemCallback(AcLogItemClient logItem)
|
||||
{
|
||||
//Ez fontos, mert a signalR nem küldi a DateTime.Kind-ot! A szeró oldalon kap egy utc DateTime-ot, ami a kliens lokális DateTime-ját tartalmazza! - J.
|
||||
logItem.TimeStampUtc = logItem.TimeStampUtc.ToUniversalTime();
|
||||
// Teljes try/catch: async void — egy StartAsync/SendAsync-kivétel különben kezeletlenül szökne el.
|
||||
try
|
||||
{
|
||||
//Ez fontos, mert a signalR nem küldi a DateTime.Kind-ot! A szeró oldalon kap egy utc DateTime-ot, ami a kliens lokális DateTime-ját tartalmazza! - J.
|
||||
logItem.TimeStampUtc = logItem.TimeStampUtc.ToUniversalTime();
|
||||
|
||||
await StartConnection();
|
||||
await StartConnection();
|
||||
|
||||
if (_hubConnection.State != HubConnectionState.Connected)
|
||||
return;
|
||||
if (_hubConnection.State != HubConnectionState.Connected)
|
||||
{
|
||||
_droppedCount++;
|
||||
ReportFailureOnce($"connection not available ({_hubConnection.State}), log item DROPPED", null);
|
||||
return;
|
||||
}
|
||||
|
||||
//var refreshToken = Setting.UserBasicDetails?.RefreshToken ?? Guid.NewGuid().ToString("N");
|
||||
_hubConnection.SendAsync("AddLogItem", /*refreshToken,*/ logItem).Forget();
|
||||
//var refreshToken = Setting.UserBasicDetails?.RefreshToken ?? Guid.NewGuid().ToString("N");
|
||||
await _hubConnection.SendAsync("AddLogItem", /*refreshToken,*/ logItem);
|
||||
|
||||
ReportRecoveryIfNeeded();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_droppedCount++;
|
||||
ReportFailureOnce("send failed, log item DROPPED", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartConnectionReportedAsync()
|
||||
{
|
||||
try { await StartConnection(); }
|
||||
catch (Exception ex) { ReportFailureOnce("initial connect failed", ex); }
|
||||
}
|
||||
|
||||
/// <summary>Hiba-állapotonként EGYSZER ír a Console.Error-ra (nem spamel) — a következő sikeres küldés élesíti újra.</summary>
|
||||
private void ReportFailureOnce(string message, Exception? ex)
|
||||
{
|
||||
if (_failureReported) return;
|
||||
_failureReported = true;
|
||||
|
||||
Console.Error.WriteLine($"[{nameof(AcSignaRClientLogItemWriter)}] {message}; url: {_fullHubName}; droppedCount: {_droppedCount}{(ex == null ? string.Empty : $"; ex: {ex.GetType().Name}: {ex.Message}")}");
|
||||
}
|
||||
|
||||
/// <summary>Sikeres küldésnél: ha hiba-állapotból jöttünk vissza, jelentjük a kiesés alatt elveszett logok számát.</summary>
|
||||
private void ReportRecoveryIfNeeded()
|
||||
{
|
||||
if (!_failureReported) return;
|
||||
_failureReported = false;
|
||||
|
||||
Console.Error.WriteLine($"[{nameof(AcSignaRClientLogItemWriter)}] connection recovered; url: {_fullHubName}; DROPPED items while down: {_droppedCount}");
|
||||
_droppedCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,43 @@ private Task SaveTrackingItemUnsafeAsync(TrackingItem<TDataItem, TId> ti)
|
|||
|
||||
---
|
||||
|
||||
## ACCORE-SIGDS-I-H3M8: DataSource mutations and events run on background threads (WASM-safe; theoretical race under Blazor Server/MAUI)
|
||||
|
||||
**Severity:** Minor (theoretical — no repro; current consumers are single-threaded WASM/Hybrid) · **Status:** Open · **Area:** `AcSignalRDataSource` threading model
|
||||
|
||||
> **Note:** documents accepted current behaviour — not scheduled for change.
|
||||
|
||||
### Description
|
||||
The DataSource does its collection work on non-UI threads:
|
||||
- **Bulk loads**: `LoadDataSourceFromResponseData` → `BeginUpdate()` → `PopulateMerge` → `EndUpdate()` runs on the
|
||||
SignalR receive pipeline; `EndUpdate` raises the collection Reset notification on that thread.
|
||||
- **Per-item save confirms**: `ProcessSavedResponseItem` (thread-pool continuation via
|
||||
`ContinueWith(TaskScheduler.Default)`, or the SignalR response callback) mutates the tracked item in place
|
||||
(`CopyTo` under `lock (_syncRoot)` — no Begin/EndUpdate) and then fires `OnDataSourceItemChanged` on the same
|
||||
background thread.
|
||||
|
||||
Consequences for subscribers:
|
||||
- `OnDataSourceLoaded` / `OnDataSourceItemChanged` / `OnSyncingStateChanged` handlers execute on the event
|
||||
thread — any UI work must be marshalled by the subscriber (e.g. `InvokeAsync`).
|
||||
- On single-threaded WebAssembly (the current deployment model) there is no true parallelism — safe.
|
||||
- Under Blazor Server / MAUI a theoretical race exists: a bulk-load mutation on the SignalR thread can
|
||||
interleave with a UI-thread enumeration of the same collection (any render), risking
|
||||
`InvalidOperationException` (collection modified) or an inconsistent view.
|
||||
|
||||
### Root cause
|
||||
By design: the transport delivers responses on its own pipeline and the DataSource processes them where they
|
||||
arrive; marshalling is left to subscribers (the DataSource has no UI dependency).
|
||||
|
||||
### Known workaround
|
||||
UI-layer subscribers marshal their render/refresh work via `InvokeAsync` (already the practice). A Server/MAUI
|
||||
deployment would additionally need the collection mutations themselves dispatched to the UI context — see fix direction.
|
||||
|
||||
### Fix direction (if ever needed)
|
||||
Optional marshal delegate on the DataSource (e.g. a consumer-settable `Func<Action, Task>` dispatcher) so
|
||||
loads/merges/`CopyTo` run on the consumer's UI context under Server/MAUI. Not planned while all consumers are WASM/Hybrid.
|
||||
|
||||
---
|
||||
|
||||
## ACCORE-SIGDS-I-W2H6: BinarySearch is public but throws NotImplementedException
|
||||
|
||||
**Status:** Open
|
||||
|
|
|
|||
Loading…
Reference in New Issue