Skip to content

Commit 3c56efc

Browse files
authored
feat: introduce BufferPool to replace BufferList (#2669)
BufferList really helped simplify a lot of code in our message stream processing, but ultimately is more powerful than we need it to be. Additionally, depending on this package makes maintenance of the driver more difficult over time. This introduces a new type called BufferList which is tailored to our particular use case. NODE-2930
1 parent 07fd317 commit 3c56efc

File tree

6 files changed

+210
-31
lines changed

6 files changed

+210
-31
lines changed

package.json

-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
"bson-ext": "^2.0.0"
2727
},
2828
"dependencies": {
29-
"bl": "^2.2.1",
3029
"bson": "^4.0.4",
3130
"denque": "^1.4.1",
3231
"lodash": "^4.17.20"

src/cmap/message_stream.ts

+8-13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import BufferList = require('bl');
21
import { Duplex, DuplexOptions } from 'stream';
32
import { Response, Msg, BinMsg, Query, WriteProtocolMessageType, MessageHeader } from './commands';
43
import { MongoError, MongoParseError } from '../error';
@@ -11,7 +10,7 @@ import {
1110
CompressorName
1211
} from './wire_protocol/compression';
1312
import type { Document, BSONSerializeOptions } from '../bson';
14-
import type { Callback } from '../utils';
13+
import { BufferPool, Callback } from '../utils';
1514
import type { ClientSession } from '../sessions';
1615

1716
const MESSAGE_HEADER_SIZE = 16;
@@ -48,21 +47,19 @@ export interface OperationDescription extends BSONSerializeOptions {
4847
* @internal
4948
*/
5049
export class MessageStream extends Duplex {
50+
/** @internal */
5151
maxBsonMessageSize: number;
52-
[kBuffer]: BufferList;
52+
/** @internal */
53+
[kBuffer]: BufferPool;
5354

5455
constructor(options: MessageStreamOptions = {}) {
5556
super(options);
56-
5757
this.maxBsonMessageSize = options.maxBsonMessageSize || kDefaultMaxBsonMessageSize;
58-
59-
this[kBuffer] = new BufferList();
58+
this[kBuffer] = new BufferPool();
6059
}
6160

6261
_write(chunk: Buffer, _: unknown, callback: Callback<Buffer>): void {
63-
const buffer = this[kBuffer];
64-
buffer.append(chunk);
65-
62+
this[kBuffer].append(chunk);
6663
processIncomingData(this, callback);
6764
}
6865

@@ -135,7 +132,7 @@ function processIncomingData(stream: MessageStream, callback: Callback<Buffer>)
135132
return;
136133
}
137134

138-
const sizeOfMessage = buffer.readInt32LE(0);
135+
const sizeOfMessage = buffer.peek(4).readInt32LE();
139136
if (sizeOfMessage < 0) {
140137
callback(new MongoParseError(`Invalid message size: ${sizeOfMessage}`));
141138
return;
@@ -155,9 +152,7 @@ function processIncomingData(stream: MessageStream, callback: Callback<Buffer>)
155152
return;
156153
}
157154

158-
const message = buffer.slice(0, sizeOfMessage);
159-
buffer.consume(sizeOfMessage);
160-
155+
const message = buffer.read(sizeOfMessage);
161156
const messageHeader: MessageHeader = {
162157
length: message.readInt32LE(0),
163158
requestId: message.readInt32LE(4),

src/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,8 @@ export type {
288288
ClientMetadata,
289289
ClientMetadataOptions,
290290
MongoDBNamespace,
291-
InterruptableAsyncInterval
291+
InterruptibleAsyncInterval,
292+
BufferPool
292293
} from './utils';
293294
export type { WriteConcern, W, WriteConcernOptions } from './write_concern';
294295
export type { ExecutionResult } from './operations/execute_operation';

src/sdam/monitor.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
now,
44
makeStateMachine,
55
calculateDurationInMs,
6-
makeInterruptableAsyncInterval
6+
makeInterruptibleAsyncInterval
77
} from '../utils';
88
import { EventEmitter } from 'events';
99
import { connect } from '../cmap/connect';
@@ -17,7 +17,7 @@ import {
1717
} from './events';
1818

1919
import { Server } from './server';
20-
import type { InterruptableAsyncInterval, Callback } from '../utils';
20+
import type { InterruptibleAsyncInterval, Callback } from '../utils';
2121
import type { TopologyVersion } from './server_description';
2222
import type { ConnectionOptions } from '../cmap/connection';
2323

@@ -65,7 +65,7 @@ export class Monitor extends EventEmitter {
6565
[kConnection]?: Connection;
6666
[kCancellationToken]: EventEmitter;
6767
/** @internal */
68-
[kMonitorId]?: InterruptableAsyncInterval;
68+
[kMonitorId]?: InterruptibleAsyncInterval;
6969
[kRTTPinger]?: RTTPinger;
7070

7171
constructor(server: Server, options?: Partial<MonitorOptions>) {
@@ -123,7 +123,7 @@ export class Monitor extends EventEmitter {
123123
// start
124124
const heartbeatFrequencyMS = this.options.heartbeatFrequencyMS;
125125
const minHeartbeatFrequencyMS = this.options.minHeartbeatFrequencyMS;
126-
this[kMonitorId] = makeInterruptableAsyncInterval(monitorServer(this), {
126+
this[kMonitorId] = makeInterruptibleAsyncInterval(monitorServer(this), {
127127
interval: heartbeatFrequencyMS,
128128
minInterval: minHeartbeatFrequencyMS,
129129
immediate: true
@@ -153,7 +153,7 @@ export class Monitor extends EventEmitter {
153153
// restart monitoring
154154
const heartbeatFrequencyMS = this.options.heartbeatFrequencyMS;
155155
const minHeartbeatFrequencyMS = this.options.minHeartbeatFrequencyMS;
156-
this[kMonitorId] = makeInterruptableAsyncInterval(monitorServer(this), {
156+
this[kMonitorId] = makeInterruptibleAsyncInterval(monitorServer(this), {
157157
interval: heartbeatFrequencyMS,
158158
minInterval: minHeartbeatFrequencyMS
159159
});

src/utils.ts

+102-5
Original file line numberDiff line numberDiff line change
@@ -961,7 +961,7 @@ export function calculateDurationInMs(started: number): number {
961961
return elapsed < 0 ? 0 : elapsed;
962962
}
963963

964-
export interface InterruptableAsyncIntervalOptions {
964+
export interface InterruptibleAsyncIntervalOptions {
965965
/** The interval to execute a method on */
966966
interval: number;
967967
/** A minimum interval that must elapse before the method is called */
@@ -977,7 +977,7 @@ export interface InterruptableAsyncIntervalOptions {
977977
}
978978

979979
/** @internal */
980-
export interface InterruptableAsyncInterval {
980+
export interface InterruptibleAsyncInterval {
981981
wake(): void;
982982
stop(): void;
983983
}
@@ -991,10 +991,10 @@ export interface InterruptableAsyncInterval {
991991
*
992992
* @param fn - An async function to run on an interval, must accept a `callback` as its only parameter
993993
*/
994-
export function makeInterruptableAsyncInterval(
994+
export function makeInterruptibleAsyncInterval(
995995
fn: (callback: Callback) => void,
996-
options?: Partial<InterruptableAsyncIntervalOptions>
997-
): InterruptableAsyncInterval {
996+
options?: Partial<InterruptibleAsyncIntervalOptions>
997+
): InterruptibleAsyncInterval {
998998
let timerId: NodeJS.Timeout | undefined;
999999
let lastCallTime: number;
10001000
let lastWakeTime: number;
@@ -1155,3 +1155,100 @@ export function isRecord(
11551155

11561156
return isRecord;
11571157
}
1158+
1159+
const kBuffers = Symbol('buffers');
1160+
const kLength = Symbol('length');
1161+
1162+
/**
1163+
* A pool of Buffers which allow you to read them as if they were one
1164+
* @internal
1165+
*/
1166+
export class BufferPool {
1167+
[kBuffers]: Buffer[];
1168+
[kLength]: number;
1169+
1170+
constructor() {
1171+
this[kBuffers] = [];
1172+
this[kLength] = 0;
1173+
}
1174+
1175+
get length(): number {
1176+
return this[kLength];
1177+
}
1178+
1179+
/** Adds a buffer to the internal buffer pool list */
1180+
append(buffer: Buffer): void {
1181+
this[kBuffers].push(buffer);
1182+
this[kLength] += buffer.length;
1183+
}
1184+
1185+
/** Returns the requested number of bytes without consuming them */
1186+
peek(size: number): Buffer {
1187+
return this.read(size, false);
1188+
}
1189+
1190+
/** Reads the requested number of bytes, optionally consuming them */
1191+
read(size: number, consume = true): Buffer {
1192+
if (typeof size !== 'number' || size < 0) {
1193+
throw new TypeError('Parameter size must be a non-negative number');
1194+
}
1195+
1196+
if (size > this[kLength]) {
1197+
return Buffer.alloc(0);
1198+
}
1199+
1200+
let result: Buffer;
1201+
1202+
// read the whole buffer
1203+
if (size === this.length) {
1204+
result = Buffer.concat(this[kBuffers]);
1205+
1206+
if (consume) {
1207+
this[kBuffers] = [];
1208+
this[kLength] = 0;
1209+
}
1210+
}
1211+
1212+
// size is within first buffer, no need to concat
1213+
else if (size <= this[kBuffers][0].length) {
1214+
result = this[kBuffers][0].slice(0, size);
1215+
if (consume) {
1216+
this[kBuffers][0] = this[kBuffers][0].slice(size);
1217+
this[kLength] -= size;
1218+
}
1219+
}
1220+
1221+
// size is beyond first buffer, need to track and copy
1222+
else {
1223+
result = Buffer.allocUnsafe(size);
1224+
1225+
let idx;
1226+
let offset = 0;
1227+
let bytesToCopy = size;
1228+
for (idx = 0; idx < this[kBuffers].length; ++idx) {
1229+
let bytesCopied;
1230+
if (bytesToCopy > this[kBuffers][idx].length) {
1231+
bytesCopied = this[kBuffers][idx].copy(result, offset, 0);
1232+
offset += bytesCopied;
1233+
} else {
1234+
bytesCopied = this[kBuffers][idx].copy(result, offset, 0, bytesToCopy);
1235+
if (consume) {
1236+
this[kBuffers][idx] = this[kBuffers][idx].slice(bytesCopied);
1237+
}
1238+
offset += bytesCopied;
1239+
break;
1240+
}
1241+
1242+
bytesToCopy -= bytesCopied;
1243+
}
1244+
1245+
// compact the internal buffer array
1246+
if (consume) {
1247+
this[kBuffers] = this[kBuffers].slice(idx);
1248+
this[kLength] -= size;
1249+
}
1250+
}
1251+
1252+
return result;
1253+
}
1254+
}

0 commit comments

Comments
 (0)