Compare commits
1 Commits
0e8cab5d3f
...
31324ae82d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31324ae82d |
@@ -26,10 +26,25 @@ class DemandDispatcher {
|
||||
this._gate.fire(demand);
|
||||
}
|
||||
|
||||
// Returns a promise that resolves when THIS demand's dispatch settles.
|
||||
// If superseded by a later fireAndWait while parked, the promise
|
||||
// resolves with the LatestWinsGate SUPERSEDED sentinel
|
||||
// ({ superseded: true }) — callers can branch on it without try/catch.
|
||||
fireAndWait(demand) {
|
||||
return this._gate.fireAndWait(demand);
|
||||
}
|
||||
|
||||
drain() {
|
||||
return this._gate.drain();
|
||||
}
|
||||
|
||||
// Cancels any parked pending value so it cannot run. The currently
|
||||
// in-flight dispatch (if any) still runs to completion. A parked
|
||||
// fireAndWait promise resolves with the SUPERSEDED sentinel.
|
||||
cancelPending() {
|
||||
if (this._gate._pending) this._gate._supersedePending();
|
||||
}
|
||||
|
||||
get inFlight() {
|
||||
return this._gate.size > 0;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ const optimizer = require('./optimizer');
|
||||
const GroupEfficiency = require('./efficiency/groupEfficiency');
|
||||
const control = require('./control/strategies');
|
||||
const io = require('./io/output');
|
||||
const DemandDispatcher = require('./dispatch/demandDispatcher');
|
||||
|
||||
const ACTIVE_STATES = new Set(['operational', 'accelerating', 'decelerating']);
|
||||
|
||||
@@ -43,11 +44,13 @@ class MachineGroup extends BaseDomain {
|
||||
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 } };
|
||||
|
||||
// Latest-wins gate kept inline (not DemandDispatcher) so awaiting
|
||||
// handleInput in tests blocks until dispatch completes. See
|
||||
// turnoff-deadlock.integration.test.js — _delayedCall is pinned.
|
||||
this._dispatchInFlight = false;
|
||||
this._delayedCall = null;
|
||||
// Latest-wins demand gate. Awaiting handleInput resolves when THIS
|
||||
// call's dispatch settles (LatestWinsGate.fireAndWait); a parked
|
||||
// call that is later superseded resolves with { superseded: true }.
|
||||
this._demandDispatcher = new DemandDispatcher(
|
||||
{ logger: this.logger },
|
||||
(payload) => this._runDispatch(payload.source, payload.demand, payload.powerCap, payload.priorityList),
|
||||
);
|
||||
this._shutdownInFlight = new Set();
|
||||
|
||||
this.operatingPoint = new GroupOperatingPoint({
|
||||
@@ -230,22 +233,11 @@ class MachineGroup extends BaseDomain {
|
||||
}));
|
||||
}
|
||||
|
||||
// Returns when THIS call's dispatch settles. If overwritten by a later
|
||||
// handleInput() while parked behind an in-flight dispatch, resolves
|
||||
// with the LatestWinsGate.SUPERSEDED sentinel ({ superseded: true }).
|
||||
async handleInput(source, demand, powerCap = Infinity, priorityList = null) {
|
||||
if (this._dispatchInFlight) {
|
||||
this._delayedCall = { source, demand, powerCap, priorityList };
|
||||
return;
|
||||
}
|
||||
this._dispatchInFlight = true;
|
||||
try {
|
||||
return await this._runDispatch(source, demand, powerCap, priorityList);
|
||||
} finally {
|
||||
this._dispatchInFlight = false;
|
||||
if (this._delayedCall) {
|
||||
const next = this._delayedCall;
|
||||
this._delayedCall = null;
|
||||
await this.handleInput(next.source, next.demand, next.powerCap, next.priorityList);
|
||||
}
|
||||
}
|
||||
return this._demandDispatcher.fireAndWait({ source, demand, powerCap, priorityList });
|
||||
}
|
||||
|
||||
async _runDispatch(source, demand, powerCap, priorityList) {
|
||||
@@ -286,8 +278,9 @@ class MachineGroup extends BaseDomain {
|
||||
}
|
||||
|
||||
async turnOffAllMachines() {
|
||||
// Cancel any deferred dispatch — turnOff is latest user intent.
|
||||
this._delayedCall = null;
|
||||
// Cancel any parked demand — turnOff is latest user intent so a
|
||||
// pending fireAndWait must not re-engage pumps post-shutdown.
|
||||
this._demandDispatcher.cancelPending();
|
||||
await Promise.all(Object.entries(this.machines).map(async ([id, machine]) => {
|
||||
if (this._shutdownInFlight.has(id)) return;
|
||||
if (this.isMachineActive(id)) {
|
||||
|
||||
@@ -116,16 +116,27 @@ test('repeated turnOffAllMachines reaches idle (serializes concurrent shutdowns)
|
||||
'delayedMove must be cleared after shutdown');
|
||||
});
|
||||
|
||||
test('turnOffAllMachines clears MGC._delayedCall to cancel any deferred dispatch', async () => {
|
||||
test('turnOffAllMachines cancels any parked demand so it cannot re-engage pumps', async () => {
|
||||
// PS sends a 1% keep-alive while MGC is mid-dispatch. MGC parks it in
|
||||
// _delayedCall. PS then crosses stopLevel and calls turnOffAllMachines.
|
||||
// Without clearing _delayedCall, MGC's finally block fires the parked
|
||||
// 1% call AFTER the shutdown — re-engaging the pump.
|
||||
// its demand dispatcher's latest-wins slot. PS then crosses stopLevel
|
||||
// and calls turnOffAllMachines. Without cancelPending(), the parked
|
||||
// 1% call would fire AFTER the shutdown — re-engaging the pump.
|
||||
const { mgc } = buildGroup();
|
||||
mgc._delayedCall = { source: 'parent', demand: 1, powerCap: Infinity, priorityList: null };
|
||||
const gate = mgc._demandDispatcher._gate;
|
||||
// Pin a fake in-flight dispatch then park a pending call behind it.
|
||||
gate._inFlight = true;
|
||||
const parked = mgc.handleInput('parent', 1, Infinity, null);
|
||||
|
||||
await mgc.turnOffAllMachines();
|
||||
|
||||
assert.equal(mgc._delayedCall, null,
|
||||
'turnOff must cancel any deferred dispatch so it cannot re-engage pumps post-shutdown');
|
||||
// Re-open the gate: the in-flight pin is artificial. Awaiting the
|
||||
// parked promise must yield the SUPERSEDED sentinel (i.e. it was
|
||||
// cancelled, not run).
|
||||
const res = await parked;
|
||||
assert.ok(res && res.superseded === true,
|
||||
'parked demand must resolve as superseded after turnOffAllMachines cancels it');
|
||||
// Idle now — pending slot must be clear.
|
||||
assert.equal(gate._pending, null,
|
||||
'turnOff must cancel any parked demand so it cannot re-engage pumps post-shutdown');
|
||||
gate._inFlight = false;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user