Split linkDown() into canCarry() and droppedWhileDraining()
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 34s
Test / test (24) (pull_request) Successful in 40s
Test / test (26) (pull_request) Successful in 33s
Mirror / push (push) Waiting to run

This commit is contained in:
2026-09-28 01:31:39 +02:00
parent 1371137fef
commit 8d443bf670
4 changed files with 16 additions and 16 deletions
+2 -2
View File
@@ -761,12 +761,12 @@ rule and an index of the titles below.
round trip before the bind is answered, so gating on `closed` let a send arriving in that window go round trip before the bind is answered, so gating on `closed` let a send arriving in that window go
out unbound and come back `ESME_RINVBNDSTS`. `LinkGate` owns the answer instead — `shut(returning)` out unbound and come back `ESME_RINVBNDSTS`. `LinkGate` owns the answer instead — `shut(returning)`
on every teardown, `open()` only once `comeBackUp()` has a bound link — and on every teardown, `open()` only once `comeBackUp()` has a bound link — and
`OutgoingRequests.linkDown()` reads it rather than `closed`. The bind itself cannot wait for what it `OutgoingRequests.canCarry()` reads it rather than `closed`. The bind itself cannot wait for what it
creates, so `pastDrain()` lets the three bind commands past the gate and the window, the same door creates, so `pastDrain()` lets the three bind commands past the gate and the window, the same door
`unbind()` takes through `now()`. The gate is told what happened and never reads back into the `unbind()` takes through `now()`. The gate is told what happened and never reads back into the
session: a collaborator that has to ask does not own its decision, which is how the first cut ended session: a collaborator that has to ask does not own its decision, which is how the first cut ended
up answering the same question two different ways at admit and at release. For the same reason the up answering the same question two different ways at admit and at release. For the same reason the
retry in `pastDrain()` asks `gate.isUp()` rather than `linkDown()`, which also reads the socket — a retry in `pastDrain()` asks `gate.isUp()` rather than `canCarry()`, which also reads the socket — a
condition that loops on something the gate does not gate on spins against a gate that admits it condition that loops on something the gate does not gate on spins against a gate that admits it
straight back. `LinkGate.returning` is a copy of `retrying()` taken at teardown, and stays true straight back. `LinkGate.returning` is a copy of `retrying()` taken at teardown, and stays true
only because nothing stops the reconnect loop without `emitClose()` following it: `drain()` and only because nothing stops the reconnect loop without `emitClose()` following it: `drain()` and
+10 -5
View File
@@ -51,9 +51,14 @@ export class OutgoingRequests {
this.window = new SendWindow({ limit: options.maxOutstanding, log: options.log }); this.window = new SendWindow({ limit: options.maxOutstanding, log: options.log });
} }
/** Read through a method: a drop can land while a request is awaiting. */ /** Whether a request made now reaches the socket. Read through a method: a drop can land while one awaits. */
linkDown(): boolean { canCarry(): boolean {
return !this.gate.isUp() || this.transport.sock.destroyed; return this.gate.isUp() && !this.transport.sock.destroyed;
}
/** The link went while draining, so the teardown settled what the drain waited on, not the peer. */
droppedWhileDraining(): boolean {
return this.draining && !this.canCarry();
} }
/** A link is up and bound, so everything held for one goes out on it. */ /** A link is up and bound, so everything held for one goes out on it. */
@@ -84,8 +89,8 @@ export class OutgoingRequests {
if (wrong) return Promise.resolve({ err: wrong }); if (wrong) return Promise.resolve({ err: wrong });
// A drain on a live link. A link that is down is the gate's answer, which says closed instead. // With no link, the gate answers instead, and says closed.
if (this.draining && !this.linkDown()) { if (this.draining && this.canCarry()) {
return Promise.resolve({ err: new Error('Session is shutting down') }); return Promise.resolve({ err: new Error('Session is shutting down') });
} }
+3 -3
View File
@@ -333,7 +333,8 @@ export class Session extends EventEmitter<SessionEvents> {
this.reconnectLoop?.stop(); this.reconnectLoop?.stop();
this.outgoing.stopAccepting(); this.outgoing.stopAccepting();
if (this.outgoing.linkDown()) return {}; // No link, so nothing is on the wire to wait out.
if (!this.outgoing.canCarry()) return {};
const timeout = this.options.shutdownTimeout ?? defaults.shutdownTimeout; const timeout = this.options.shutdownTimeout ?? defaults.shutdownTimeout;
const deadline = timeout > 0 ? Date.now() + timeout : 0; const deadline = timeout > 0 ? Date.now() + timeout : 0;
@@ -341,8 +342,7 @@ export class Session extends EventEmitter<SessionEvents> {
const messages = await this.incoming.drain(this.answering(timeout), signal); const messages = await this.incoming.drain(this.answering(timeout), signal);
const requests = await this.outgoing.drain(leftOf(deadline), signal); const requests = await this.outgoing.drain(leftOf(deadline), signal);
// The window empties on a teardown too, which settles everything the link was carrying. if (this.outgoing.droppedWhileDraining()) {
if (this.outgoing.linkDown()) {
return { err: new Error('The session closed before the drain finished') }; return { err: new Error('The session closed before the drain finished') };
} }
+1 -6
View File
@@ -199,15 +199,10 @@ next work ([decision](docs/decisions.md#internals-and-tests)).
### Locality — next, ahead of everything below; 5–6 today, and the gate is 7 ### Locality — next, ahead of everything below; 5–6 today, and the gate is 7
- [ ] **Split the two questions `OutgoingRequests.linkDown()` answers.** `Session.drain()` calls it
twice for opposite conclusions — "nothing to drain, success" and "the link died under us,
failure" — and `outgoing-requests.ts` reads it a third way. Two named predicates. Named by 7
of 9 readers, who each reconstructed the ordering by hand.
- [ ] **Name `pastDrain()`'s retry condition and what makes the loop end.** The exit is a - [ ] **Name `pastDrain()`'s retry condition and what makes the loop end.** The exit is a
three-term disjunction over two collaborators, whose comment covers the first term only, and three-term disjunction over two collaborators, whose comment covers the first term only, and
the method is named for what it bypasses. Do not change what it asks: `gate.isUp()` rather than the method is named for what it bypasses. Do not change what it asks: `gate.isUp()` rather than
`linkDown()` is deliberate and recorded. `canCarry()` is deliberate and recorded.
- [ ] **Replace `resolveBody`'s `settles` boolean with the decision it stands for.** One boolean - [ ] **Replace `resolveBody`'s `settles` boolean with the decision it stands for.** One boolean
chooses both whether to overwrite `data_coding` and which params to read it from, across four chooses both whether to overwrite `data_coding` and which params to read it from, across four