UNPKG

@nestjs/microservices

Version:

Nest - modern, fast, powerful node.js web framework (@microservices)

256 lines (255 loc) • 11.2 kB
import { Buffer } from 'buffer'; import { StringDecoder } from 'string_decoder'; import { CorruptedPacketLengthException } from '../errors/corrupted-packet-length.exception.js'; import { DrainTimeoutException } from '../errors/drain-timeout.exception.js'; import { IncompleteMessageTimeoutException } from '../errors/incomplete-message-timeout.exception.js'; import { MaxPacketLengthExceededException } from '../errors/max-packet-length-exceeded.exception.js'; import { MaxSendBufferSizeExceededException } from '../errors/max-send-buffer-size-exceeded.exception.js'; import { TcpSocket } from './tcp-socket.js'; const DEFAULT_MAX_BUFFER_SIZE = (512 * 1024 * 1024) / 4; // 512 MBs in characters with 4 bytes per character (32-bit) const DEFAULT_INCOMPLETE_MESSAGE_TIMEOUT = 30_000; const DEFAULT_MAX_SEND_BUFFER_SIZE = 128 * 1024 * 1024; // 128 MBs in bytes const DEFAULT_DRAIN_TIMEOUT = 30_000; const MAX_CONTENT_LENGTH_SIZE = 32; // in characters, a string length never has more than 16 digits export class JsonSocket extends TcpSocket { contentLength = null; buffer = ''; stringDecoder = new StringDecoder(); delimiter = '#'; maxBufferSize; incompleteMessageTimeout; maxSendBufferSize; drainTimeout; pauseOnBackpressure; incompleteMessageTimer = null; drainTimer = null; isBackpressured = false; writtenLength = 0; constructor(socket, options) { super(socket); this.maxBufferSize = options?.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE; this.incompleteMessageTimeout = options?.incompleteMessageTimeout ?? DEFAULT_INCOMPLETE_MESSAGE_TIMEOUT; this.maxSendBufferSize = options?.maxSendBufferSize ?? DEFAULT_MAX_SEND_BUFFER_SIZE; this.drainTimeout = options?.drainTimeout ?? DEFAULT_DRAIN_TIMEOUT; this.pauseOnBackpressure = options?.pauseOnBackpressure ?? true; this.socket.on("close" /* TcpEventsMap.CLOSE */, () => { this.clearIncompleteMessageTimer(); this.clearDrainTimer(); }); } handleSend(message, callback) { const data = this.formatMessageData(message); const flushed = this.socket.write(data, 'utf-8', callback); this.writtenLength += data.length; // "write" returns false once the outgoing buffer is above the socket's // high-water mark. Without honouring it, a peer that issues requests but // never reads the responses makes the process buffer every response in // memory. Reading is suspended until the peer catches up, which also stops // further requests from that peer being turned into more queued responses. if (flushed === false && this.pauseOnBackpressure) { this.applyBackpressure(); } this.assertSendBufferWithinLimit(); } /** * Suspending reads bounds a peer that keeps asking for more, but responses * to requests already in flight still queue up behind a peer that reads * nothing at all. This caps how much may be held for one connection, which * mirrors "maxBufferSize" on the receiving side. */ assertSendBufferWithinLimit() { const bufferedBytes = this.socket.writableLength; if (!this.maxSendBufferSize || !(bufferedBytes > this.maxSendBufferSize)) { return; } this.handleDataError(new MaxSendBufferSizeExceededException(bufferedBytes, this.maxSendBufferSize), { destroy: true }); } handleData(dataRaw) { const data = Buffer.isBuffer(dataRaw) ? this.stringDecoder.write(dataRaw) : dataRaw; this.buffer += data; try { this.processBuffer(); } finally { this.refreshIncompleteMessageTimer(); } } processBuffer() { // Iterative loop replaces recursion to prevent stack overflow on pipelined // TCP messages (e.g. many small frames arriving in one read event). while (true) { // Stop turning buffered frames into responses while the outgoing buffer // is backed up. Whatever is left stays in the buffer and is picked up // again once the socket drains. if (this.isBackpressured) { break; } if (this.buffer.length > this.maxBufferSize) { const bufferLength = this.buffer.length; this.buffer = ''; throw new MaxPacketLengthExceededException(bufferLength); } if (this.contentLength === null) { const i = this.buffer.indexOf(this.delimiter); /** * Check if the buffer has the delimiter (#), * if not, the end of the buffer string might be in the middle of a content length string */ if (i === -1) { // Data that cannot be a content length would otherwise be kept and // scanned again on every read until a delimiter arrives if (this.buffer.length > MAX_CONTENT_LENGTH_SIZE) { const rawContentLength = this.buffer.substring(0, MAX_CONTENT_LENGTH_SIZE); this.buffer = ''; throw new CorruptedPacketLengthException(rawContentLength); } break; } const rawContentLength = this.buffer.substring(0, i); this.contentLength = parseInt(rawContentLength, 10); if (isNaN(this.contentLength)) { this.contentLength = null; this.buffer = ''; throw new CorruptedPacketLengthException(rawContentLength); } this.buffer = this.buffer.substring(i + 1); } if (this.contentLength !== null) { const length = this.buffer.length; if (length === this.contentLength) { this.handleMessage(this.buffer); // handleMessage resets contentLength and buffer; next iteration will break } else if (length > this.contentLength) { const message = this.buffer.substring(0, this.contentLength); const rest = this.buffer.substring(this.contentLength); this.handleMessage(message); // resets this.buffer to '' this.buffer = rest; // restore remaining data for next iteration continue; } else { // Incomplete message — wait for more data break; } } else { break; } } } applyBackpressure() { if (this.isBackpressured) { return; } this.isBackpressured = true; // Reads are paused by us, so the peer cannot make progress on an inbound // packet. Resume its idle timeout only after outgoing data has drained. this.clearIncompleteMessageTimer(); this.socket.pause(); this.socket.once("drain" /* TcpEventsMap.DRAIN */, () => this.releaseBackpressure()); this.armDrainTimer(); } releaseBackpressure() { this.isBackpressured = false; this.clearDrainTimer(); this.socket.resume(); // Frames that arrived before the socket was paused may still be buffered. try { this.processBuffer(); } catch (e) { this.handleDataError(e); } finally { this.refreshIncompleteMessageTimer(); } } /** * Arms a timer while reads are suspended, as only the peer, by reading, * can bring about the "drain" that resumes them. Whenever it fires, it is * armed again if some of the queued data has been written out since, so * it only drops a peer once nothing at all has been written out for a * full period - a slow but reading peer is given more time. */ armDrainTimer() { if (!this.drainTimeout) { return; } const drainedLength = this.getDrainedLength(); this.drainTimer = setTimeout(() => this.handleDrainTimeout(drainedLength), this.drainTimeout); this.drainTimer.unref?.(); } clearDrainTimer() { if (!this.drainTimer) { return; } clearTimeout(this.drainTimer); this.drainTimer = null; } handleDrainTimeout(drainedLength) { this.drainTimer = null; if (this.getDrainedLength() > drainedLength) { this.armDrainTimer(); return; } // A FIN would only queue up behind the responses the peer is not reading. this.handleDataError(new DrainTimeoutException(this.socket.writableLength, this.drainTimeout), { destroy: true }); } /** * How much of the data written so far has left the outgoing buffer. Unlike * "writableLength", it does not move when more responses are queued, only * when a pending write completes, which is as fine-grained as Node reports * progress. Sockets do not decode strings, so "writableLength" counts them * by length, as "writtenLength" does. */ getDrainedLength() { return this.writtenLength - this.socket.writableLength; } /** * Arms an idle timer whenever a packet is only partially received, and * disarms it as soon as the buffer is drained. The timer is refreshed on * every read, so it only fires when the peer stops sending mid-packet - * a slow but progressing transfer is never interrupted. */ refreshIncompleteMessageTimer() { this.clearIncompleteMessageTimer(); const hasIncompleteMessage = this.buffer.length > 0 || this.contentLength !== null; if (this.isBackpressured || !this.incompleteMessageTimeout || !hasIncompleteMessage) { return; } this.incompleteMessageTimer = setTimeout(() => this.handleIncompleteMessageTimeout(), this.incompleteMessageTimeout); this.incompleteMessageTimer.unref?.(); } clearIncompleteMessageTimer() { if (!this.incompleteMessageTimer) { return; } clearTimeout(this.incompleteMessageTimer); this.incompleteMessageTimer = null; } handleIncompleteMessageTimeout() { const bufferedLength = this.buffer.length; this.buffer = ''; this.contentLength = null; this.incompleteMessageTimer = null; // The peer already stopped talking mid-packet, so do not wait on a FIN. this.handleDataError(new IncompleteMessageTimeoutException(bufferedLength, this.incompleteMessageTimeout), { destroy: true }); } handleMessage(message) { this.contentLength = null; this.buffer = ''; this.emitMessage(message); } formatMessageData(message) { const messageData = JSON.stringify(message); const length = messageData.length; const data = length + this.delimiter + messageData; return data; } }