From 493d6de86fbe67b9fae137b9470f5b0aa643d3a5 Mon Sep 17 00:00:00 2001 From: Faolain Date: Thu, 5 Feb 2026 23:36:33 -0400 Subject: [PATCH] fix(shared-log): guard persistCoordinate against TOCTOU race during shutdown During test teardown (and potentially production close), _close() can complete between the caller's !this.closed check in findLeaders and persistCoordinate's actual execution. At that point entryCoordinatesIndex is undefined, causing an unhandled promise rejection. This adds: - this.closed bail-out at method entry - entryCoordinatesIndex/replicationRangeIndex existence checks - try/catch wrapper that swallows errors if closed during execution Validated: reduces unhandled errors from 6 to 2 in integration tests. The remaining 2 errors originate from @peerbit/program RPC layer. Co-Authored-By: Claude Opus 4.6 --- .../programs/data/shared-log/src/index.ts | 78 +++++++++++-------- 1 file changed, 45 insertions(+), 33 deletions(-) diff --git a/packages/programs/data/shared-log/src/index.ts b/packages/programs/data/shared-log/src/index.ts index 6549b9c1e..cccbcd42e 100644 --- a/packages/programs/data/shared-log/src/index.ts +++ b/packages/programs/data/shared-log/src/index.ts @@ -3673,45 +3673,57 @@ export class SharedLog< replicas: number; prev?: EntryReplicated; }) { - let assignedToRangeBoundary = shouldAssignToRangeBoundary( - properties.leaders, - properties.replicas, - ); + // Guard against TOCTOU race: _close() may complete between the caller's + // !this.closed check and this method's execution, tearing down indices. + if (this.closed) return; + if (!this._entryCoordinatesIndex) return; + if (!this._replicationRangeIndex) return; - if ( - properties.prev && - properties.prev.assignedToRangeBoundary === assignedToRangeBoundary - ) { - return; // no change - } + try { + let assignedToRangeBoundary = shouldAssignToRangeBoundary( + properties.leaders, + properties.replicas, + ); - const cidObject = cidifyString(properties.entry.hash); - const hashNumber = this.indexableDomain.numbers.bytesToNumber( - cidObject.multihash.digest, - ); + if ( + properties.prev && + properties.prev.assignedToRangeBoundary === assignedToRangeBoundary + ) { + return; // no change + } - await this.entryCoordinatesIndex.put( - new this.indexableDomain.constructorEntry({ - assignedToRangeBoundary, - coordinates: properties.coordinates, - meta: properties.entry.meta, - hash: properties.entry.hash, - hashNumber, - }), - ); + const cidObject = cidifyString(properties.entry.hash); + const hashNumber = this.indexableDomain.numbers.bytesToNumber( + cidObject.multihash.digest, + ); - for (const coordinate of properties.coordinates) { - this.coordinateToHash.add(coordinate, properties.entry.hash); - } + await this.entryCoordinatesIndex.put( + new this.indexableDomain.constructorEntry({ + assignedToRangeBoundary, + coordinates: properties.coordinates, + meta: properties.entry.meta, + hash: properties.entry.hash, + hashNumber, + }), + ); + + for (const coordinate of properties.coordinates) { + this.coordinateToHash.add(coordinate, properties.entry.hash); + } - if (properties.entry.meta.next.length > 0) { - await this.entryCoordinatesIndex.del({ - query: new Or( - properties.entry.meta.next.map( - (x) => new StringMatch({ key: "hash", value: x }), + if (properties.entry.meta.next.length > 0) { + await this.entryCoordinatesIndex.del({ + query: new Or( + properties.entry.meta.next.map( + (x) => new StringMatch({ key: "hash", value: x }), + ), ), - ), - }); + }); + } + } catch (err) { + // If closed by the time error fires, this was a shutdown race + if (this.closed) return; + throw err; } }