using System.Diagnostics;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.LoadHarness.Scenarios;
/// Result of one store-and-forward drain measurement.
/// Site the buffer belonged to.
/// Messages buffered before the drain began.
/// Wall time to buffer them (concurrent, many origin instances).
/// Buffering throughput.
///
/// Wall time from the first sweep to the first successful delivery. With
/// attemptImmediateDelivery: false the engine stamps LastAttemptAt, so the
/// row is not due until one DefaultRetryInterval (30 s) has passed — this is the
/// configured retry latency, not drain slowness, and is reported separately for that reason.
///
/// Wall time from the first sweep to an empty buffer (includes the retry wait).
///
/// Drain throughput measured from the FIRST delivery to an empty buffer — the engine's
/// actual capacity, and the headline number for register row 50.
///
/// Buffer depth left when the measurement stopped (0 = fully drained).
/// Delivered-count samples during the drain, so a steady rate can be told from a stall-then-burst.
public sealed record StoreAndForwardDrainResult(
string SiteId,
int MessageCount,
double EnqueueSeconds,
double EnqueuePerSecond,
double TimeToFirstDeliverySeconds,
double DrainSeconds,
double DrainPerSecond,
int ResidualDepth,
IReadOnlyList Progress);
/// One observation during the drain.
/// Seconds since the drain began.
/// Cumulative successful deliveries.
/// Remaining buffer depth.
public sealed record DrainProgressSample(double ElapsedSeconds, long Delivered, int Depth);
///
/// Measures store-and-forward buffering and drain throughput (deferred-work register
/// row 50, first half) using the real StoreAndForwardService, the real
/// StoreAndForwardStorage and the real SQLite file — only the delivery target
/// is a counting stub, because what is being measured is the site-local buffer's
/// throughput, not a remote endpoint's.
///
///
/// Phase 1 buffers messages with
/// attemptImmediateDelivery: false, spread across many origin instance names
/// and issued from many concurrent tasks — the "concurrent buffering from multiple
/// instances" WP-4 asks about ([xc-7]). Phase 2 registers a delivery handler
/// that always succeeds and drives sweeps to completion, timing the drain.
///
///
/// The sweep is driven explicitly rather than waiting on the 10 s
/// RetryTimerInterval so the number reported is the engine's drain capacity,
/// not its polling cadence. The per-sweep batch is SweepBatchLimit (500) with
/// SweepTargetParallelism (4) lanes, both at their production defaults.
///
///
public static class StoreAndForwardDrainScenario
{
/// Runs the drain measurement against one site's real S&F engine.
/// The site whose store-and-forward engine is exercised.
/// Messages to buffer.
/// Concurrent enqueue tasks (distinct origin instances).
/// Cancels the measurement.
/// The measured result.
public static async Task RunAsync(
SiteRuntimeFixture site,
int messageCount,
int concurrency,
CancellationToken cancellationToken)
{
var service = site.StoreAndForward;
var payload = $"{{\"site\":\"{site.SiteId}\",\"body\":\"{new string('x', 256)}\"}}";
// Phase 1 — concurrent buffering from many instances, no delivery attempted.
var enqueueWatch = Stopwatch.StartNew();
var perTask = messageCount / concurrency;
var enqueueTasks = new List(concurrency);
for (var t = 0; t < concurrency; t++)
{
var taskIndex = t;
enqueueTasks.Add(Task.Run(async () =>
{
for (var i = 0; i < perTask; i++)
{
await service.EnqueueAsync(
StoreAndForwardCategory.ExternalSystem,
target: $"load-target-{taskIndex % 4}",
payloadJson: payload,
originInstanceName: site.InstanceName(taskIndex),
attemptImmediateDelivery: false);
}
}, cancellationToken));
}
await Task.WhenAll(enqueueTasks);
enqueueWatch.Stop();
var buffered = perTask * concurrency;
// Phase 2 — a delivery target that always succeeds; time the drain to empty.
var delivered = 0L;
service.RegisterDeliveryHandler(StoreAndForwardCategory.ExternalSystem, _ =>
{
Interlocked.Increment(ref delivered);
return Task.FromResult(true);
});
// Sweeps are driven explicitly rather than waiting on the 10 s RetryTimerInterval:
// the number wanted is the engine's drain CAPACITY, not its polling cadence. One
// sweep moves at most SweepBatchLimit (500) messages, so a large backlog needs
// many, and the progress series below is what distinguishes a genuinely slow
// drain from an artefact of this polling loop.
var drainWatch = Stopwatch.StartNew();
var deadline = DateTimeOffset.UtcNow.AddMinutes(10);
var progress = new List();
int depth;
while (true)
{
service.TriggerSweep();
await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
var depths = await service.GetBufferDepthAsync();
depth = depths.Values.Sum();
progress.Add(new DrainProgressSample(
drainWatch.Elapsed.TotalSeconds, Interlocked.Read(ref delivered), depth));
if (depth == 0 || DateTimeOffset.UtcNow > deadline)
break;
}
drainWatch.Stop();
// Split the retry wait from the drain: the first sample with a non-zero delivered
// count marks the moment the backlog actually became due.
var firstDelivery = progress.FirstOrDefault(s => s.Delivered > 0);
var timeToFirstDelivery = firstDelivery?.ElapsedSeconds ?? drainWatch.Elapsed.TotalSeconds;
var activeDrainSeconds = Math.Max(0.001, drainWatch.Elapsed.TotalSeconds - timeToFirstDelivery);
return new StoreAndForwardDrainResult(
SiteId: site.SiteId,
MessageCount: buffered,
EnqueueSeconds: enqueueWatch.Elapsed.TotalSeconds,
EnqueuePerSecond: buffered / Math.Max(0.001, enqueueWatch.Elapsed.TotalSeconds),
TimeToFirstDeliverySeconds: timeToFirstDelivery,
DrainSeconds: drainWatch.Elapsed.TotalSeconds,
DrainPerSecond: Interlocked.Read(ref delivered) / activeDrainSeconds,
ResidualDepth: depth,
Progress: progress);
}
}