Skip to content

Commit 82b8983

Browse files
committed
Fix node.js stream blockage in mxstream channels
When the node.js implementation of a `MultiplexingStream` channel receives more data than the highWatermark (16KB) limit, a flowing stream stops flowing permanently, blocking all communication in that direction. This fixes the problem by calling `resume()` on the stream when flowing has been stopped by that particular data buffer.
1 parent 026821f commit 82b8983

File tree

2 files changed

+62
-0
lines changed

2 files changed

+62
-0
lines changed

src/nerdbank-streams/src/Channel.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,8 +216,17 @@ export class ChannelClass extends Channel {
216216
}
217217

218218
public onContent(buffer: Buffer | null) {
219+
const priorReadableFlowing = this._duplex.readableFlowing
220+
219221
this._duplex.push(buffer)
220222

223+
// Large buffer pushes can switch a stream from flowing to non-flowing
224+
// when it meets or exceeds the highWaterMark. We need to resume the stream
225+
// in this case so that the user can continue to receive data.
226+
if (priorReadableFlowing && this._duplex.readableFlowing === false) {
227+
this._duplex.resume()
228+
}
229+
221230
// We should find a way to detect when we *actually* share the received buffer with the Channel's user
222231
// and only report consumption when they receive the buffer from us so that we effectively apply
223232
// backpressure to the remote party based on our user's actual consumption rather than continually allocating memory.

src/nerdbank-streams/src/tests/MultiplexingStream.spec.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,59 @@ import { Channel } from '../Channel'
88
import CancellationToken from 'cancellationtoken'
99
import * as assert from 'assert'
1010
import { nextTick } from 'process'
11+
import { Duplex } from 'stream'
12+
13+
it('highWatermark threshold does not clog', async () => {
14+
// Brokered service
15+
let bytesToReceive = 0
16+
let receivedAllBytes = new Deferred()
17+
function receiver(pipe: Duplex) {
18+
let lengths: number[] = []
19+
pipe.on('data', (data: Buffer) => {
20+
lengths.push(data.length)
21+
22+
bytesToReceive -= data.length
23+
// console.log(`recv ${data.length}. ${bytesToReceive} remaining`)
24+
if (bytesToReceive <= 0) {
25+
receivedAllBytes.resolve(undefined)
26+
}
27+
})
28+
}
29+
30+
// IServiceBroker
31+
const { first: localServicePipe, second: servicePipe } = FullDuplexStream.CreatePair()
32+
receiver(localServicePipe)
33+
34+
// MultiplexingStreamServiceBroker
35+
const simulatedMxStream = FullDuplexStream.CreatePair()
36+
const [mx1, mx2] = await Promise.all([MultiplexingStream.CreateAsync(simulatedMxStream.first), MultiplexingStream.CreateAsync(simulatedMxStream.second)])
37+
const [local, remote] = await Promise.all([mx1.offerChannelAsync(''), mx2.acceptChannelAsync('')])
38+
servicePipe.pipe(local.stream)
39+
local.stream.pipe(servicePipe)
40+
41+
global.test_servicePipe = servicePipe
42+
global.test_d = local.stream
43+
global.test_localServicePipe = localServicePipe
44+
45+
// brokered service client
46+
function writeHelper(buffer: Buffer): boolean {
47+
bytesToReceive += buffer.length
48+
const result = remote.stream.write(buffer)
49+
// console.log('written', buffer.length, result)
50+
return result
51+
}
52+
for (let i = 15; i < 20; i++) {
53+
const buffer = Buffer.alloc(i * 1024)
54+
writeHelper(buffer)
55+
await nextTickAsync()
56+
writeHelper(Buffer.alloc(10))
57+
await nextTickAsync()
58+
}
59+
60+
if (bytesToReceive > 0) {
61+
await receivedAllBytes.promise
62+
}
63+
})
1164
;[1, 2, 3].forEach(protocolMajorVersion => {
1265
describe(`MultiplexingStream v${protocolMajorVersion}`, () => {
1366
let mx1: MultiplexingStream

0 commit comments

Comments
 (0)