Compare commits

2 Commits

Author SHA1 Message Date
lilleman 0b9b210fb2 Keep a hold's release behind MessageHold, and require its listener count
Test / lint (pull_request) Successful in 23s
Test / test (18) (pull_request) Successful in 31s
Test / test (20) (pull_request) Successful in 31s
Test / test (22) (pull_request) Successful in 31s
Test / test (24) (pull_request) Successful in 31s
Test / test (26) (pull_request) Successful in 32s
Mirror / push (push) Successful in 5s
2026-09-28 01:03:15 +02:00
lilleman 9dda1a6917 Release a held message through the hold it was given 2026-09-28 01:02:43 +02:00
3 changed files with 34 additions and 33 deletions
+20 -16
View File
@@ -25,26 +25,29 @@ type Held = {
pduObjs: PduObject[]; pduObjs: PduObject[];
}; };
type HoldEntry = {
isHeld: () => boolean;
release: () => void;
};
/** One message offered to the application, held until it is answered or every listener gives up. */ /** One message offered to the application, held until it is answered or every listener gives up. */
export class MessageHold { export class MessageHold {
private readonly held: HeldMessages; private readonly entry: HoldEntry;
private readonly pduObjs: PduObject[];
private working: number; private working: number;
constructor(held: HeldMessages, pduObjs: PduObject[], listeners: number) { constructor(entry: HoldEntry, listeners: number) {
this.held = held; this.entry = entry;
this.pduObjs = pduObjs;
this.working = listeners; this.working = listeners;
} }
/** Whether a drain is still waiting for this message to be answered. */ /** Whether a drain is still waiting for this message to be answered. */
isHeld(): boolean { isHeld(): boolean {
return this.held.has(this.pduObjs); return this.entry.isHeld();
} }
/** A turn later, so a listener sending its receipt straight after the response still holds. */ /** A turn later, so a listener sending its receipt straight after the response still holds. */
answered(): void { answered(): void {
setImmediate(() => { this.held.release(this.pduObjs); }); setImmediate(() => { this.entry.release(); });
} }
/** A rejection leaves the other listeners running, so only the last one to fail gives the message up. */ /** A rejection leaves the other listeners running, so only the last one to fail gives the message up. */
@@ -54,9 +57,9 @@ export class MessageHold {
if (this.working <= 0) this.answered(); if (this.working <= 0) this.answered();
} }
/** A message nobody took is not work a shutdown can wait for. */ /** At once, for a message nobody took: that is not work a shutdown can wait for. */
untaken(): void { release(): void {
this.held.release(this.pduObjs); this.entry.release();
} }
} }
@@ -94,9 +97,11 @@ export class HeldMessages {
return this.held.full || this.octets >= this.maxOctets; return this.held.full || this.octets >= this.maxOctets;
} }
/** Holds a message about to be offered to `listeners` listeners. */ hold(pduObjs: PduObject[], listeners: number): MessageHold {
hold(pduObjs: PduObject[], listeners = 1): MessageHold { const hold = new MessageHold({
const hold = new MessageHold(this, pduObjs, listeners); isHeld: () => this.has(pduObjs),
release: () => { this.release(pduObjs); },
}, listeners);
const key = keyOf(pduObjs); const key = keyOf(pduObjs);
if (key === undefined) return hold; if (key === undefined) return hold;
@@ -118,14 +123,13 @@ export class HeldMessages {
return hold; return hold;
} }
/** Whether a drain is still waiting for this message to be answered. */ private has(pduObjs: PduObject[]): boolean {
has(pduObjs: PduObject[]): boolean {
const key = keyOf(pduObjs); const key = keyOf(pduObjs);
return key !== undefined && this.held.get(key)?.pduObjs === pduObjs; return key !== undefined && this.held.get(key)?.pduObjs === pduObjs;
} }
release(pduObjs: PduObject[]): void { private release(pduObjs: PduObject[]): void {
const key = keyOf(pduObjs); const key = keyOf(pduObjs);
// Identity, not the key: a wrapped sequence number must not release someone else's message. // Identity, not the key: a wrapped sequence number must not release someone else's message.
+1 -2
View File
@@ -324,12 +324,11 @@ export class IncomingRequests {
bindAllows: cmdName => this.deps.bindAllows(cmdName), bindAllows: cmdName => this.deps.bindAllows(cmdName),
lostLink: () => this.linkGeneration !== generation, lostLink: () => this.linkGeneration !== generation,
onAnswered: () => { hold.answered(); }, onAnswered: () => { hold.answered(); },
// Past the refusal only while a drain is still waiting for this message; an ordinary send after.
send: input => (hold.isHeld() ? this.deps.sendPastDrain(input) : this.deps.send(input)), send: input => (hold.isHeld() ? this.deps.sendPastDrain(input) : this.deps.send(input)),
}); });
this.holds.set(sms, hold); this.holds.set(sms, hold);
if (!this.deps.offerSms(sms)) hold.untaken(); if (!this.deps.offerSms(sms)) hold.release();
} }
} }
+13 -15
View File
@@ -1502,15 +1502,14 @@ describe('held message bounds', () => {
test('is full at its count, and a re-used sequence number replaces rather than adding', () => { test('is full at its count, and a re-used sequence number replaces rather than adding', () => {
const held = new HeldMessages({ log: silentLog, max: 2, maxOctets: 1_000_000, timeout: 10_000 }); const held = new HeldMessages({ log: silentLog, max: 2, maxOctets: 1_000_000, timeout: 10_000 });
const first = message(1); const first = held.hold(message(1), 1);
held.hold(first); held.hold(message(2), 1);
held.hold(message(2)); held.hold(message(2), 1);
held.hold(message(2));
assert.equal(held.size, 2); assert.equal(held.size, 2);
assert.equal(held.full(), true); assert.equal(held.full(), true);
assert.equal(held.has(first), true); assert.equal(first.isHeld(), true);
held.clear(); held.clear();
}); });
@@ -1519,23 +1518,22 @@ describe('held message bounds', () => {
test('is full at its octet cap, until a message leaves by any way out', () => { test('is full at its octet cap, until a message leaves by any way out', () => {
let now = 0; let now = 0;
const held = new HeldMessages({ log: silentLog, max: 10, maxOctets: 2000, now: () => now, timeout: 10_000 }); const held = new HeldMessages({ log: silentLog, max: 10, maxOctets: 2000, now: () => now, timeout: 10_000 });
const answered = message(1); const answered = held.hold(message(1), 1);
held.hold(answered);
assert.equal(held.full(), false); assert.equal(held.full(), false);
held.hold(message(2)); held.hold(message(2), 1);
assert.equal(held.full(), true); assert.equal(held.full(), true);
held.release(answered); answered.release();
assert.equal(held.full(), false, 'after a release'); assert.equal(held.full(), false, 'after a release');
held.hold(message(3)); held.hold(message(3), 1);
now = 20_000; now = 20_000;
held.sweep(); held.sweep();
now = 0; now = 0;
assert.equal(held.full(), false, 'after a sweep'); assert.equal(held.full(), false, 'after a sweep');
held.hold(message(4)); held.hold(message(4), 1);
held.hold(message(5)); held.hold(message(5), 1);
held.clear(); held.clear();
assert.equal(held.full(), false, 'after a clear'); assert.equal(held.full(), false, 'after a clear');
@@ -1624,11 +1622,11 @@ describe('held message bounds', () => {
let now = 0; let now = 0;
const held = new HeldMessages({ log: silentLog, max: 10, maxOctets: 1_000_000, now: () => now, timeout: 60 }); const held = new HeldMessages({ log: silentLog, max: 10, maxOctets: 1_000_000, now: () => now, timeout: 60 });
held.hold(message(1)); held.hold(message(1), 1);
now = 61; now = 61;
// The next message sweeps the one that expired, so only the new one is still waited for. // The next message sweeps the one that expired, so only the new one is still waited for.
held.hold(message(2)); held.hold(message(2), 1);
assert.equal(held.size, 1); assert.equal(held.size, 1);
@@ -1640,7 +1638,7 @@ describe('held message bounds', () => {
let now = 0; let now = 0;
const held = new HeldMessages({ log: silentLog, max: 10, maxOctets: 1_000_000, now: () => now, timeout: 60 }); const held = new HeldMessages({ log: silentLog, max: 10, maxOctets: 1_000_000, now: () => now, timeout: 60 });
held.hold(message(1)); held.hold(message(1), 1);
const waiting = held.idle(1000, undefined); const waiting = held.idle(1000, undefined);