Compare commits

...

1 Commits

Author SHA1 Message Date
Rene De Ren
df74ea0fac Serialize handleInput dispatches via _dispatchInFlight gate
Mirrors rotatingMachine state.delayedMove. PS ticks demand into MGC at
1 Hz but a real pump ramp takes several seconds; before this gate every
PS tick aborted the in-flight optimalControl and started a new one, so
pumps never reached their setpoint. Live observation: 120 aborts / 2
min, pump_a drifting to 138 m³/h while pump_b stayed clamped at minFlow
60 m³/h ("near_curve_edge").

While a dispatch is in flight, the latest {source, demand, powerCap,
priorityList} is parked in _delayedCall and the new call returns.
The in-flight dispatch's finally block picks up the latest delayed
value when it settles. Latest-wins — intermediate demands are stomped
because they were obsolete by the time the pumps were ready for them.

Regression test in superproject:
test/mgc-overactive-demand-serialization.integration.test.js
30 concurrent demand calls now produce ≤ 5 aborts (was 30).

All existing tests still pass: 21 MGC integration + 7 cross-node
integration (incl. realistic-startup-timing, inflow-overcapacity-
stability, ps-mgc-flow-contract, idle-startup-deadlock).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 09:14:59 +02:00

View File

@@ -74,6 +74,16 @@ class MachineGroup {
// Combination curve data
this.dynamicTotals = { flow: { min: Infinity, max: 0 }, power: { min: Infinity, max: 0 } , NCog : 0};
this.absoluteTotals = { flow: { min: Infinity, max: 0 }, power: { min: Infinity, max: 0 }};
// Dispatch serialization. PS ticks demand into MGC at 1 Hz, but
// a real pump ramp takes several seconds — without this gate
// every PS tick aborts the in-flight dispatch and starts a new
// one, so pumps never reach their setpoint. Mirrors
// rotatingMachine state.delayedMove: while a dispatch is in
// flight the latest demand is parked here for pickup when the
// current dispatch settles. Latest-wins.
this._dispatchInFlight = false;
this._delayedCall = null;
//this always last in the constructor
this.childRegistrationUtils = new childRegistrationUtils(this);
@@ -1274,6 +1284,37 @@ class MachineGroup {
async handleInput(source, demand, powerCap = Infinity, priorityList = null) {
// Serialize dispatches: if a previous handleInput is still
// awaiting pump movements, park the latest demand and return.
// The in-flight dispatch's `finally` block will pick it up.
// See rotatingMachine state.delayedMove for the analogous
// pattern at the pump level.
if (this._dispatchInFlight) {
this._delayedCall = { source, demand, powerCap, priorityList };
this.logger.debug(`Dispatch in flight; deferring demand=${demand} until current pump moves complete.`);
return;
}
this._dispatchInFlight = true;
try {
return await this._runDispatch(source, demand, powerCap, priorityList);
} finally {
this._dispatchInFlight = false;
// Pick up the latest deferred call (intermediate values were
// stomped while we were busy — only the last one matters).
if (this._delayedCall) {
const next = this._delayedCall;
this._delayedCall = null;
this.logger.debug(`Dispatch finished; picking up deferred demand=${next.demand}.`);
// Recursive call re-enters the gate; safe because
// _dispatchInFlight has been reset to false above.
await this.handleInput(next.source, next.demand, next.powerCap, next.priorityList);
}
}
}
async _runDispatch(source, demand, powerCap = Infinity, priorityList = null) {
const demandQ = parseFloat(demand);
if(!Number.isFinite(demandQ)){