Bandwidth Throttle Stream
A Node.js and Deno transform stream for throttling bandwidth which distributes available bandwidth evenly between all requests in a "group", accurately simulating the effect of network conditions on simultaneous overlapping requests.
Features
- Idiomatic pipeable Transform API for use in Node.js
- Idiomatic pipeable TransformStream API for use in Deno
- Distributes the desired bandwidth evenly over each second
- Distributes the desired bandwidth evenly between all active requests
- Abortable requests ensure bandwidth is redistributed if a client aborts a request
Contents
Node.js Installation
Firstly, install the package using your package manager of choice.
npm install bandwidth-throttle-stream
You may then import the createBandwidthThrottleGroup()
factory function into your project.
import {createBandwidthThrottleGroup} from 'bandwidth-throttle-stream';
Deno Installation
In Deno, all libraries are imported from URLs as ES modules. Versioned releases of bandwidth-throttle-stream
are available from TBC:
import {createBandwidthThrottleGroup} from 'https://path/to/cdn/bandwidth-throttle-stream@0.2.0/mod.ts'
Usage
Creating a Group
Using the imported createBandwidthThrottleGroup
factory function, we must firstly create a "bandwidth throttle group" which will be configured with a specific throughput in bytes (B) per second.
const bandwidthThrottleGroup = createBandwidthThrottleGroup({
bytesPerSecond: 500000
});
Typically we would create a single group only for a server running a simulation, which all incoming network requests to be throttled are routed through. However, we could also create multiple groups if we wanted to run multiple simulations with different configurations on a single server.
Attaching Throttles
Once we've created a group, we can then attach individual pipeable "throttles" to it, as requests come into our server.
The most simple integration would be to insert the throttle (via .pipe
, or .pipeThrough
) between a readable stream (e.g file system readout, server-side HTTP response), and the response stream of the incoming client request to be throttled.
Node.js example: Piping between readable and writable streams
const throttle = bandwidthThrottleGroup.createBandwidthThrottle(contentLength);
someReadableStream
.pipe(throttle)
.pipe(someWritableStream);
Deno example: Piping between a readable stream and a reader:
const throttle = bandwidthThrottleGroup.createBandwidthThrottle(contentLength);
someReadableStream
.pipeThrough(throttle)
.getReader()
Note that a number value for contentLength
(in "bytes") must be passed when creating an individual throttle. This should be the total size of data for the request being passed through the throttle, and is used to allocate memory upfront in a single Uint8Array
typed array, thus preventing expensive GC calls as backpressure builds up. When throttling HTTP requests, contentLength
can be obtained from the 'content-length'
header, once the headers of the request have arrived:
const contentLength = parseInt(req.get('content-length'))
const { body, headers } = await fetch(destination);
const contentLength = parseInt(headers.get("content-length"));
Handling Output
In most cases however, we require more granular control of data output than simply piping to a writable stream (for example when throttling an HTTP request).
In these cases, we can use any of the Node.js stream events available such as data
and end
:
Node.js example: Hooking into the end
event of a writable stream
request
.pipe(throttle)
.on('data', chunk => response.write(chunk)
.on('end', () => {
response.status(200);
response.end();
});
Deno example: responding to a request with a reader and a status code
import {readerToDenoReader} from 'TBC';
...
await request.respond({
status: 200
body: readerToDenoReader(reader, contentLength),
});
Note that in the Deno example, a reader may be passed directly to request.respond()
allowing real-time streaming of the throttled output. However, the Deno std
server expects a Deno.Reader
as a body
(rather than the standard ReadableStreamDefaultReader
), meaning that conversion is needed between the two.
The readerToDenoReader
util is exposed for this purpose, and must be provided with both a reference to ReadableStreamDefaultReader
(reader
), and the contentLength
of the request.
Configuration Options
Each bandwidth throttle group accepts an optional object of configuration options:
const bandwidthThrottleGroup = createBandwidthThrottleGroup({
bytesPerSecond: 500000
ticksPerSecond: 20
});
The following options are available.
interface IConfig {
bytesPerSecond?: number;
ticksPerSecond?: number;
}
Dynamic Configuration
A group can be reconfigured at any point after creation via its .configure()
method, which accepts the same configuration interface as the createBandwidthThrottleGroup()
factory.
const bandwidthThrottleGroup = createBandwidthThrottleGroup();
bandwidthThrottleGroup.configure({
bytesPerSecond: 6000000
})
Aborted Requests
When a client aborts a requests, its important that we also abort the throttle, ensuring the group can re-balance available bandwidth correctly, and backpressure buffer memory is released.
Node.js example: Handling aborted requests
const throttle = bandwidthThrottleGroup.createBandwidthThrottle(contentLength);
request.on('aborted', () => {
throttle.abort();
});
request
.pipe(throttle)
.pipe(response);
Deno example: Handling aborted requests
const throttle = bandwidthThrottleGroup.createBandwidthThrottle(contentLength);
request
.pipeThrough(throttle)
.getReader()
try {
await request.respond({
status: 200
body: readerToDenoReader(reader, contentLength),
});
} catch(err) {
throttle.abort();
}