feat: initialize manufacturing progress dashboard
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace DongfangHydro.Dashboard.Api.Realtime;
|
||||
|
||||
public sealed class DashboardHub : Hub;
|
||||
@@ -0,0 +1,112 @@
|
||||
using System.Collections.Concurrent;
|
||||
using DongfangHydro.Dashboard.Api.Contracts;
|
||||
using DongfangHydro.Dashboard.Api.Services;
|
||||
|
||||
namespace DongfangHydro.Dashboard.Api.Realtime;
|
||||
|
||||
public sealed class DashboardUpdateQueue(
|
||||
IDashboardNotifier notifier,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ILogger<DashboardUpdateQueue> logger)
|
||||
: BackgroundService, IDashboardUpdateQueue
|
||||
{
|
||||
private const int MaximumAttempts = 5;
|
||||
private readonly ConcurrentDictionary<Guid, QueuedUpdate> pendingByOrder = new();
|
||||
private readonly SemaphoreSlim signal = new(0, 1);
|
||||
private int signalScheduled;
|
||||
|
||||
public ValueTask EnqueueAsync(
|
||||
UpdateNodeProgressResultDto update,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
pendingByOrder.AddOrUpdate(
|
||||
update.Order.Id,
|
||||
_ => new QueuedUpdate(update),
|
||||
(_, _) => new QueuedUpdate(update));
|
||||
ScheduleWork();
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
await signal.WaitAsync(stoppingToken);
|
||||
Interlocked.Exchange(ref signalScheduled, 0);
|
||||
|
||||
foreach (var orderId in pendingByOrder.Keys)
|
||||
{
|
||||
if (pendingByOrder.TryRemove(orderId, out var item))
|
||||
{
|
||||
await PublishLatestAsync(orderId, item, stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
if (!pendingByOrder.IsEmpty)
|
||||
{
|
||||
ScheduleWork();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PublishLatestAsync(
|
||||
Guid orderId,
|
||||
QueuedUpdate initial,
|
||||
CancellationToken stoppingToken)
|
||||
{
|
||||
var current = initial;
|
||||
var attempt = 1;
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
if (pendingByOrder.TryRemove(orderId, out var newer))
|
||||
{
|
||||
current = newer;
|
||||
attempt = 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var scope = scopeFactory.CreateAsyncScope();
|
||||
var overview = await scope.ServiceProvider
|
||||
.GetRequiredService<DashboardQueryService>()
|
||||
.GetOverviewAsync(stoppingToken);
|
||||
await notifier.PublishUpdateAsync(current.Update, overview, stoppingToken);
|
||||
return;
|
||||
}
|
||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogWarning(
|
||||
exception,
|
||||
"Dashboard update broadcast failed on attempt {Attempt} for order {OrderId}.",
|
||||
attempt,
|
||||
orderId);
|
||||
|
||||
if (attempt >= MaximumAttempts)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pendingByOrder.ContainsKey(orderId))
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(attempt * 2), stoppingToken);
|
||||
attempt += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ScheduleWork()
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref signalScheduled, 1, 0) == 0)
|
||||
{
|
||||
signal.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record QueuedUpdate(UpdateNodeProgressResultDto Update);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using DongfangHydro.Dashboard.Api.Contracts;
|
||||
|
||||
namespace DongfangHydro.Dashboard.Api.Realtime;
|
||||
|
||||
public interface IDashboardNotifier
|
||||
{
|
||||
Task PublishUpdateAsync(
|
||||
UpdateNodeProgressResultDto update,
|
||||
DashboardOverviewDto overview,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using DongfangHydro.Dashboard.Api.Contracts;
|
||||
|
||||
namespace DongfangHydro.Dashboard.Api.Realtime;
|
||||
|
||||
public interface IDashboardUpdateQueue
|
||||
{
|
||||
ValueTask EnqueueAsync(
|
||||
UpdateNodeProgressResultDto update,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using DongfangHydro.Dashboard.Api.Contracts;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace DongfangHydro.Dashboard.Api.Realtime;
|
||||
|
||||
public sealed class SignalRDashboardNotifier(IHubContext<DashboardHub> hubContext)
|
||||
: IDashboardNotifier
|
||||
{
|
||||
public async Task PublishUpdateAsync(
|
||||
UpdateNodeProgressResultDto update,
|
||||
DashboardOverviewDto overview,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var clients = hubContext.Clients.All;
|
||||
await Task.WhenAll(
|
||||
clients.SendAsync("nodeUpdated", update.Node, cancellationToken),
|
||||
clients.SendAsync("orderUpdated", update.Order, cancellationToken),
|
||||
clients.SendAsync("trendAppended", update.TrendPoint, cancellationToken),
|
||||
clients.SendAsync("overviewUpdated", overview, cancellationToken),
|
||||
update.RiskEvent is null
|
||||
? Task.CompletedTask
|
||||
: clients.SendAsync("riskEventCreated", update.RiskEvent, cancellationToken));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user