feat(transport): counter for import-session cap rejections (register row 66)

Row 53 shipped the scadabridge.central.transport.import_sessions.open gauge
and a Warning log on a cap hit, but a log line is not something an operator
can alert on. Adds the companion counter

    scadabridge.central.transport.import_sessions.rejected

on the existing ZB.MOM.WW.ScadaBridge meter, so no ObservedMeters allowlist
change is needed (an unlisted meter exports nothing, silently).

The emit point is deliberately narrow. BundleSessionStore.Open tests the cap
TWICE — once on entry and again after EvictExpired() — and only the second
test is a rejection: a store full of merely expired sessions reclaims a slot
and admits the caller. Counting the first would make the series fire
routinely on any node whose sweep had not yet run, turning a page-worthy
signal into background noise. Re-Opening an already-open session id skips
the cap block entirely and is likewise not counted.

The two instruments are diagnostic as a pair: rejections rising while the
gauge oscillates below the cap is genuine concurrency (raise the cap);
rejections rising while the gauge sits pinned at the cap is a session leak
(register row 43's signature), where raising the cap only defers the problem.

Tests assert a delta over a scoped action rather than an absolute value, so
they do not inherit the register row 32 static-state flake that makes
QueueDepthGaugeTests fail only under the full concurrent suite. Negative
controls cover under-cap, expired-reclaim, and duplicate-session-id.

Tests: 11 passed in BundleSessionObservabilityTests (6 existing + 5 new).
This commit is contained in:
Joseph Doherty
2026-08-15 12:42:56 -04:00
parent ef876b38c5
commit 90ce099928
5 changed files with 175 additions and 1 deletions
@@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Transport;
using ZB.MOM.WW.ScadaBridge.Commons.Observability;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;
namespace ZB.MOM.WW.ScadaBridge.Transport.Import;
@@ -115,6 +116,15 @@ public sealed class BundleSessionStore : IBundleSessionStore
EvictExpired();
if (_sessions.Count >= cap)
{
// Register row 66: the alertable half of row 53's observability. The
// Warning below carries the diagnosis but a log line cannot be alerted
// on; this counter can. Deliberately incremented HERE, after
// EvictExpired() has already failed to free a slot — the outer check
// at the top of this block is not a rejection (a store full of stale
// entries reclaims and succeeds), so counting there would inflate the
// series with imports that were never actually turned away.
ScadaBridgeTelemetry.RecordImportSessionRejected();
// Register row 53: this rejection used to be silent apart from the
// exception the caller saw. A cap hit is either genuine concurrency
// (many operators importing at once) or a session leak like register