1+ import type { BackpressureMetrics } from "./backpressureMetrics.js" ;
2+
3+ export interface BackpressureLogger {
4+ info ( message : string , meta ?: Record < string , unknown > ) : void ;
5+ }
6+
17export type BackpressureVerdict = {
28 engaged : boolean ;
39 /** Epoch ms the verdict was produced. Used for consumer-side staleness fail-open. */
@@ -31,6 +37,13 @@ export type BackpressureMonitorOptions = {
3137 rampMs ?: number ;
3238 /** Injectable RNG for the resume ramp; defaults to Math.random. */
3339 random ?: ( ) => number ;
40+ /**
41+ * When true, the gates are inert (never skip dequeues, never freeze scale-up).
42+ * computeEngaged() still reflects the real signal so it can be observed.
43+ */
44+ dryRun ?: boolean ;
45+ logger ?: BackpressureLogger ;
46+ metrics ?: BackpressureMetrics ;
3447} ;
3548
3649const DEFAULT_REFRESH_INTERVAL_MS = 1000 ;
@@ -41,7 +54,9 @@ export class BackpressureMonitor {
4154 private wasEngaged = false ;
4255 private releasedAt ?: number ;
4356
44- constructor ( private readonly opts : BackpressureMonitorOptions ) { }
57+ constructor ( private readonly opts : BackpressureMonitorOptions ) {
58+ this . opts . metrics ?. dryRun . set ( this . opts . dryRun ? 1 : 0 ) ;
59+ }
4560
4661 start ( ) : void {
4762 if ( ! this . opts . enabled ) {
@@ -63,11 +78,11 @@ export class BackpressureMonitor {
6378 }
6479
6580 /**
66- * Hard backpressure state: true while the (fresh) verdict says engaged. This is
67- * the signal for freezing consumer-pool scale-up - distinct from the dequeue
68- * gate, which additionally ramps after release. Hot-path read, no I/O .
81+ * Raw hard backpressure state: true while the (fresh) verdict says engaged,
82+ * ignoring dry-run. Used for observability/metrics so the real signal is
83+ * visible even when the gates are inert .
6984 */
70- isEngaged ( ) : boolean {
85+ computeEngaged ( ) : boolean {
7186 const verdict = this . verdict ;
7287 if ( verdict ?. engaged !== true ) {
7388 return false ;
@@ -81,9 +96,25 @@ export class BackpressureMonitor {
8196 return true ;
8297 }
8398
84- /** Hot-path read: synchronous, never performs I/O. */
99+ /**
100+ * Effective hard state: the signal for freezing consumer-pool scale-up. Inert
101+ * (false) in dry-run. Hot-path read, no I/O.
102+ */
103+ isEngaged ( ) : boolean {
104+ return this . opts . dryRun ? false : this . computeEngaged ( ) ;
105+ }
106+
107+ /** Hot-path read: synchronous, never performs I/O. Inert (false) in dry-run. */
85108 shouldSkipDequeue ( ) : boolean {
86- if ( this . isEngaged ( ) ) {
109+ const wouldSkip = this . computeShouldSkip ( ) ;
110+ if ( wouldSkip ) {
111+ this . opts . metrics ?. skipsTotal . inc ( { dry_run : this . opts . dryRun ? "true" : "false" } ) ;
112+ }
113+ return this . opts . dryRun ? false : wouldSkip ;
114+ }
115+
116+ private computeShouldSkip ( ) : boolean {
117+ if ( this . computeEngaged ( ) ) {
87118 return true ;
88119 }
89120
@@ -113,6 +144,14 @@ export class BackpressureMonitor {
113144 // Track the engaged→released transition to anchor the resume ramp. Based on
114145 // the raw refreshed verdict, not the staleness-adjusted read.
115146 const nowEngaged = this . verdict ?. engaged === true ;
147+ this . opts . metrics ?. engaged . set ( nowEngaged ? 1 : 0 ) ;
148+
149+ if ( nowEngaged !== this . wasEngaged ) {
150+ this . opts . logger ?. info ( "backpressure verdict changed" , {
151+ engaged : nowEngaged ,
152+ dryRun : ! ! this . opts . dryRun ,
153+ } ) ;
154+ }
116155 if ( this . wasEngaged && ! nowEngaged ) {
117156 this . releasedAt = Date . now ( ) ;
118157 }
0 commit comments