UNPKG

@google-cloud/pubsub

Version:

Cloud Pub/Sub Client Library for Node.js

1,092 lines 39 kB
"use strict"; /*! * Copyright 2018 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.Subscriber = exports.Message = exports.SubscriberSpans = exports.AckError = exports.SubscriberCloseBehaviors = exports.AckResponses = exports.logs = exports.StatusError = void 0; const precise_date_1 = require("@google-cloud/precise-date"); const projectify_1 = require("@google-cloud/projectify"); const promisify_1 = require("@google-cloud/promisify"); const defer = require("p-defer"); const histogram_1 = require("./histogram"); const lease_manager_1 = require("./lease-manager"); const message_queues_1 = require("./message-queues"); const message_stream_1 = require("./message-stream"); const default_options_1 = require("./default-options"); const tracing = require("./telemetry-tracing"); const temporal_1 = require("./temporal"); const events_1 = require("events"); const util_1 = require("./util"); const logs_1 = require("./logs"); var message_stream_2 = require("./message-stream"); Object.defineProperty(exports, "StatusError", { enumerable: true, get: function () { return message_stream_2.StatusError; } }); /** * Loggers. Exported for unit tests. * * @private */ exports.logs = { slowAck: logs_1.logs.pubsub.sublog('slow-ack'), ackNack: logs_1.logs.pubsub.sublog('ack-nack'), debug: logs_1.logs.pubsub.sublog('debug'), }; exports.AckResponses = { PermissionDenied: 'PERMISSION_DENIED', FailedPrecondition: 'FAILED_PRECONDITION', Success: 'SUCCESS', Invalid: 'INVALID', Other: 'OTHER', }; /** * Enum values for behaviors of the Subscriber.close() method. */ exports.SubscriberCloseBehaviors = { NackImmediately: 'NACK', WaitForProcessing: 'WAIT', }; /** * Specifies how long before the final close timeout, in WaitForProcessing mode, * that we should give up and start shutting down cleanly. */ const FINAL_NACK_TIMEOUT = temporal_1.Duration.from({ seconds: 1 }); /** * Thrown when an error is detected in an ack/nack/modack call, when * exactly-once delivery is enabled on the subscription. This will * only be thrown for actual errors that can't be retried. */ class AckError extends Error { errorCode; constructor(errorCode, message) { let finalMessage = `${errorCode}`; if (message) { finalMessage += ` : ${message}`; } super(finalMessage); this.errorCode = errorCode; } } exports.AckError = AckError; /** * Tracks the various spans related to subscriber/receive tracing. * * @private */ class SubscriberSpans { parent; // These are always attached to a message. constructor(parent) { this.parent = parent; } // Start a flow control span if needed. flowStart() { if (!this.flow) { this.flow = tracing.PubsubSpans.createReceiveFlowSpan(this.parent); } } // End any flow control span. flowEnd() { if (this.flow) { this.flow.end(); this.flow = undefined; } } // Emit an event for starting to send an ack. ackStart() { tracing.PubsubEvents.ackStart(this.parent); } // Emit an event for the ack having been sent. ackEnd() { tracing.PubsubEvents.ackEnd(this.parent); } // Emit an event for calling ack. ackCall() { if (this.processing) { tracing.PubsubEvents.ackCalled(this.processing); } } // Emit an event for starting to send a nack. nackStart() { tracing.PubsubEvents.nackStart(this.parent); } // Emit an event for the nack having been sent. nackEnd() { tracing.PubsubEvents.nackEnd(this.parent); } // Emit an event for calling nack. nackCall() { if (this.processing) { tracing.PubsubEvents.nackCalled(this.processing); } } // Emit an event for starting to send a modAck. modAckStart(deadline, isInitial) { tracing.PubsubEvents.modAckStart(this.parent, deadline, isInitial); } // Emit an event for the modAck having been sent. modAckEnd() { tracing.PubsubEvents.modAckEnd(this.parent); } // Emit an event for calling modAck. // Note that we don't currently support users calling modAck directly, but // this may be used in the future for things like fully managed pull // subscriptions. modAckCall(deadline) { if (this.processing) { tracing.PubsubEvents.modAckCalled(this.processing, deadline); } } // Start a scheduler span if needed. // Note: This is not currently used in Node, because there is no // scheduler process, due to the way messages are delivered one at a time. schedulerStart() { if (!this.scheduler) { this.scheduler = tracing.PubsubSpans.createReceiveSchedulerSpan(this.parent); } } // End any scheduler span. schedulerEnd() { if (this.scheduler) { this.scheduler.end(); this.scheduler = undefined; } } // Start a processing span if needed. // This is for user processing, during on('message') delivery. processingStart(subName) { if (!this.processing) { this.processing = tracing.PubsubSpans.createReceiveProcessSpan(this.parent, subName); } } // End any processing span. processingEnd() { if (this.processing) { this.processing.end(); this.processing = undefined; } } // If we shut down before processing can finish. shutdown() { tracing.PubsubEvents.shutdown(this.parent); } flow; scheduler; processing; } exports.SubscriberSpans = SubscriberSpans; /** * Date object with nanosecond precision. Supports all standard Date arguments * in addition to several custom types. * * @external PreciseDate * @see {@link https://github.com/googleapis/nodejs-precise-date|PreciseDate} */ /** * Message objects provide a simple interface for users to get message data and * acknowledge the message. * * @example * ``` * subscription.on('message', message => { * // { * // ackId: 'RUFeQBJMJAxESVMrQwsqWBFOBCEhPjA', * // attributes: {key: 'value'}, * // data: Buffer.from('Hello, world!'), * // id: '1551297743043', * // orderingKey: 'ordering-key', * // publishTime: new PreciseDate('2019-02-27T20:02:19.029534186Z'), * // received: 1551297743043, * // length: 13 * // } * }); * ``` */ class Message { ackId; attributes; data; deliveryAttempt; id; orderingKey; publishTime; received; _handledPromise; _handled; _length; _subscriber; _ackFailed; _dispatched; /** * @private * * Tracks a telemetry tracing parent span through the receive process. This will * be the original publisher-side span if we have one; otherwise we'll create * a "publisher" span to hang new subscriber spans onto. * * This needs to be declared explicitly here, because having a public class * implement a private interface seems to confuse TypeScript. (And it's needed * in unit tests.) */ parentSpan; /** * We'll save the state of the subscription's exactly once delivery flag at the * time the message was received. This is pretty much only for tracing, as we will * generally use the live state of the subscription to figure out how to respond. * * @private * @internal */ isExactlyOnceDelivery; /** * @private * * Ends any open subscribe telemetry tracing span. */ endParentSpan() { this.parentSpan?.end(); delete this.parentSpan; } /** * @private * * Tracks subscriber-specific telemetry objects through the library. */ subSpans; /** * @hideconstructor * * @param {Subscriber} sub The parent subscriber. * @param {object} message The raw message response. */ constructor(sub, { ackId, message, deliveryAttempt }) { /** * This ID is used to acknowledge the message. * * @name Message#ackId * @type {string} */ this.ackId = ackId; /** * Optional attributes for this message. * * @name Message#attributes * @type {object} */ this.attributes = message.attributes || {}; /** * The message data as a Buffer. * * @name Message#data * @type {Buffer} */ this.data = message.data; /** * Delivery attempt counter is 1 + (the sum of number of NACKs and number of * ack_deadline exceeds) for this message. * * @name Message#deliveryAttempt * @type {number} */ this.deliveryAttempt = Number(deliveryAttempt || 0); /** * ID of the message, assigned by the server when the message is published. * Guaranteed to be unique within the topic. * * @name Message#id * @type {string} */ this.id = message.messageId; /** * Identifies related messages for which publish order should be respected. * If a `Subscription` has `enableMessageOrdering` set to `true`, messages * published with the same `orderingKey` value will be delivered to * subscribers in the order in which they are received by the Pub/Sub * system. * * **EXPERIMENTAL:** This feature is part of a closed alpha release. This * API might be changed in backward-incompatible ways and is not recommended * for production use. It is not subject to any SLA or deprecation policy. * * @name Message#orderingKey * @type {string} */ this.orderingKey = message.orderingKey; /** * The time at which the message was published. * * @name Message#publishTime * @type {external:PreciseDate} */ this.publishTime = new precise_date_1.PreciseDate(message.publishTime); /** * The time at which the message was recieved by the subscription. * * @name Message#received * @type {number} */ this.received = Date.now(); /** * Telemetry tracing objects. * * @private */ this.subSpans = new SubscriberSpans(this); /** * Save the state of the subscription into the message for later tracing. * * @private * @internal */ this.isExactlyOnceDelivery = sub.isExactlyOnceDelivery; this._dispatched = false; this._handled = false; this._handledPromise = defer(); this._length = this.data.length; this._subscriber = sub; } /** * The length of the message data. * * @type {number} */ get length() { return this._length; } /** * Resolves when the message has been handled fully; a handled message may * not have any further operations performed on it. * * @private */ get handledPromise() { return this._handledPromise.promise; } /** * When this message is dispensed to user callback code, this should be called. * The time between the dispatch and the handledPromise resolving is when the * message is with the user. * * @private */ dispatched() { if (!this._dispatched) { this.subSpans.processingStart(this._subscriber.name); this._dispatched = true; } } /** * @private * @returns True if this message has been dispatched to user callback code. */ isDispatched() { return this._dispatched; } /** * Sets this message's exactly once delivery acks to permanent failure. This is * meant for internal library use only. * * @private */ ackFailed(error) { this._ackFailed = error; } /** * Acknowledges the message. * * @example * ``` * subscription.on('message', message => { * message.ack(); * }); * ``` */ ack() { if (!this._handled) { this._handled = true; this.subSpans.ackCall(); this.subSpans.processingEnd(); void this._subscriber.ack(this); this._handledPromise.resolve(); } } /** * Acknowledges the message, expecting a response (for exactly-once delivery subscriptions). * If exactly-once delivery is not enabled, this will immediately resolve successfully. * * @example * ``` * subscription.on('message', async (message) => { * const response = await message.ackWithResponse(); * }); * ``` */ async ackWithResponse() { if (!this._subscriber.isExactlyOnceDelivery) { this.ack(); return exports.AckResponses.Success; } if (this._ackFailed) { throw this._ackFailed; } if (!this._handled) { this._handled = true; this.subSpans.ackCall(); this.subSpans.processingEnd(); try { return await this._subscriber.ackWithResponse(this); } catch (e) { this.ackFailed(e); throw e; } finally { this._handledPromise.resolve(); } } else { return exports.AckResponses.Invalid; } } /** * Modifies the ack deadline. * At present time, this should generally not be called by users. * * @param {number} deadline The number of seconds to extend the deadline. * @private */ modAck(deadline) { if (!this._handled) { this.subSpans.modAckCall(temporal_1.Duration.from({ seconds: deadline })); void this._subscriber.modAck(this, deadline); } } /** * Modifies the ack deadline, expecting a response (for exactly-once delivery subscriptions). * If exactly-once delivery is not enabled, this will immediately resolve successfully. * At present time, this should generally not be called by users. * * @param {number} deadline The number of seconds to extend the deadline. * @private */ async modAckWithResponse(deadline) { if (!this._subscriber.isExactlyOnceDelivery) { this.modAck(deadline); return exports.AckResponses.Success; } if (this._ackFailed) { throw this._ackFailed; } if (!this._handled) { this.subSpans.modAckCall(temporal_1.Duration.from({ seconds: deadline })); try { return await this._subscriber.modAckWithResponse(this, deadline); } catch (e) { this.ackFailed(e); throw e; } } else { return exports.AckResponses.Invalid; } } /** * Removes the message from our inventory and schedules it to be redelivered. * * @example * ``` * subscription.on('message', message => { * message.nack(); * }); * ``` */ nack() { if (!this._handled) { this._handled = true; this.subSpans.nackCall(); this.subSpans.processingEnd(); void this._subscriber.nack(this); this._handledPromise.resolve(); } } /** * Removes the message from our inventory and schedules it to be redelivered, * with the modAck response being returned (for exactly-once delivery subscriptions). * If exactly-once delivery is not enabled, this will immediately resolve successfully. * * @example * ``` * subscription.on('message', async (message) => { * const response = await message.nackWithResponse(); * }); * ``` */ async nackWithResponse() { if (!this._subscriber.isExactlyOnceDelivery) { this.nack(); return exports.AckResponses.Success; } if (this._ackFailed) { throw this._ackFailed; } if (!this._handled) { this._handled = true; this.subSpans.nackCall(); this.subSpans.processingEnd(); try { return await this._subscriber.nackWithResponse(this); } catch (e) { this.ackFailed(e); throw e; } finally { this._handledPromise.resolve(); } } else { return exports.AckResponses.Invalid; } } } exports.Message = Message; const minAckDeadlineForExactlyOnceDelivery = temporal_1.Duration.from({ seconds: 60 }); /** * Subscriber class is used to manage all message related functionality. * * @private * @class * * @param {Subscription} subscription The corresponding subscription. * @param {SubscriberOptions} options The subscriber options. */ class Subscriber extends events_1.EventEmitter { ackDeadline; maxMessages; maxBytes; useLegacyFlowControl; isOpen; maxExtensionTime; _acks; _histogram; _inventory; _latencies; _modAcks; _name; _options; _stream; _subscription; // We keep this separate from ackDeadline, because ackDeadline could // end up being bound by min/max deadline configs. _99th; subscriptionProperties; constructor(subscription, options = {}) { super(); this.ackDeadline = default_options_1.defaultOptions.subscription.startingAckDeadline.totalOf('second'); this._99th = this.ackDeadline; this.maxMessages = default_options_1.defaultOptions.subscription.maxOutstandingMessages; this.maxBytes = default_options_1.defaultOptions.subscription.maxOutstandingBytes; this.maxExtensionTime = default_options_1.defaultOptions.subscription.maxExtensionTime; this.useLegacyFlowControl = false; this.isOpen = false; this._histogram = new histogram_1.Histogram({ min: 10, max: 600 }); this._latencies = new histogram_1.Histogram(); this._subscription = subscription; this.setOptions(options); } /** * Update our ack extension time that will be used by the lease manager * for sending modAcks. * * Should not be called from outside this class, except for unit tests. * * @param {number} [ackTimeSeconds] The number of seconds that the last * ack took after the message was received. If this is undefined, then * we won't update the histogram, but we will still recalculate the * ackDeadline based on the situation. * * @private */ updateAckDeadline(ackTimeSeconds) { // Start with the value we already have. let ackDeadline = this.ackDeadline; // If we got an ack time reading, update the histogram (and ackDeadline). if (ackTimeSeconds) { this._histogram.add(ackTimeSeconds); this._99th = ackDeadline = this._histogram.percentile(99); } // Grab our current min/max deadline values, based on whether exactly-once // delivery is enabled, and the defaults. const [minDeadline, maxDeadline] = this.getMinMaxDeadlines(); if (minDeadline) { ackDeadline = Math.max(ackDeadline, minDeadline.totalOf('second')); } if (maxDeadline) { ackDeadline = Math.min(ackDeadline, maxDeadline.totalOf('second')); } // Set the bounded result back. this.ackDeadline = ackDeadline; } getMinMaxDeadlines() { // If this is an exactly-once delivery subscription, and the user // didn't set their own minimum ack periods, set it to the default // for exactly-once delivery. const defaultMinDeadline = this.isExactlyOnceDelivery ? minAckDeadlineForExactlyOnceDelivery : default_options_1.defaultOptions.subscription.minAckDeadline; const defaultMaxDeadline = default_options_1.defaultOptions.subscription.maxAckDeadline; // Pull in any user-set min/max. const minDeadline = this._options.minAckDeadline ?? defaultMinDeadline; const maxDeadline = this._options.maxAckDeadline ?? defaultMaxDeadline; return [minDeadline, maxDeadline]; } /** * Returns true if an exactly-once delivery subscription has been detected. * * @private */ get isExactlyOnceDelivery() { if (!this.subscriptionProperties) { return false; } return !!this.subscriptionProperties.exactlyOnceDeliveryEnabled; } /** * Sets our subscription properties from incoming messages. * * @param {SubscriptionProperties} subscriptionProperties The new properties. * @private */ setSubscriptionProperties(subscriptionProperties) { const previouslyEnabled = this.isExactlyOnceDelivery; this.subscriptionProperties = subscriptionProperties; // Update ackDeadline in case the flag switched. if (previouslyEnabled !== this.isExactlyOnceDelivery) { this.updateAckDeadline(); // For exactly-once delivery, make sure the subscription ack deadline is 60. // (Otherwise fall back to the default of 10 seconds.) const subscriptionAckDeadlineSeconds = this.isExactlyOnceDelivery ? 60 : 10; this._stream.setStreamAckDeadline(temporal_1.Duration.from({ seconds: subscriptionAckDeadlineSeconds })); } } /** * The 99th percentile of request latencies. * * @type {number} * @private */ get modAckLatency() { const latency = this._latencies.percentile(99); let bufferTime = 0; if (this._modAcks) { bufferTime = this._modAcks.maxMilliseconds; } return latency * 1000 + bufferTime; } /** * The full name of the Subscription. * * @type {string} * @private */ get name() { if (!this._name) { const { name, projectId } = this._subscription; this._name = (0, projectify_1.replaceProjectIdToken)(name, projectId); } return this._name; } /** * Acknowledges the supplied message. * * @param {Message} message The message to acknowledge. * @returns {Promise<void>} * @private */ async ack(message) { const ackTimeSeconds = (Date.now() - message.received) / 1000; this.updateAckDeadline(ackTimeSeconds); exports.logs.ackNack.info('message (ID %s, ackID %s) ack', message.id, message.ackId); if (ackTimeSeconds > this._99th) { exports.logs.slowAck.info('message (ID %s, ackID %s) ack took longer than the 99th percentile of message processing time (%s s)', message.id, message.ackId, ackTimeSeconds); } tracing.PubsubEvents.ackStart(message); // Ignore this in this version of the method (but hook catch // to avoid unhandled exceptions). const resultPromise = this._acks.add(message); resultPromise.catch(() => { }); await this._acks.onFlush(); tracing.PubsubEvents.ackEnd(message); message.endParentSpan(); this._inventory.remove(message); } /** * Acknowledges the supplied message, expecting a response (for exactly * once subscriptions). * * @param {Message} message The message to acknowledge. * @returns {Promise<AckResponse>} * @private */ async ackWithResponse(message) { const ackTimeSeconds = (Date.now() - message.received) / 1000; this.updateAckDeadline(ackTimeSeconds); exports.logs.ackNack.info('message (ID %s, ackID %s) ack with response', message.id, message.ackId); if (ackTimeSeconds > this._99th) { exports.logs.slowAck.info('message (ID %s, ackID %s) ack took longer than the 99th percentile (%s s)', message.id, message.ackId, ackTimeSeconds); } tracing.PubsubEvents.ackStart(message); await this._acks.add(message); tracing.PubsubEvents.ackEnd(message); message.endParentSpan(); this._inventory.remove(message); // No exception means Success. return exports.AckResponses.Success; } async #awaitTimeoutAndCheck(promise, timeout) { const result = await (0, util_1.awaitWithTimeout)(promise, timeout); if (result.exception || result.timedOut) { // Don't try to deal with errors at this point, just warn-log. if (result.timedOut === false) { // This wasn't a timeout. exports.logs.debug.warn('Error during Subscriber.close(): %j', result.exception); } } } /** * Closes the subscriber, stopping the reception of new messages and shutting * down the underlying stream. The behavior of the returned Promise will depend * on the closeOptions in the subscriber options. * * @returns {Promise<void>} A promise that resolves when the subscriber is closed * and pending operations are flushed or the timeout is reached. * * @private */ async close() { if (!this.isOpen) { return; } // Always close the stream right away so we don't receive more messages. this.isOpen = false; this._stream.destroy(); const options = this._options.closeOptions; // If no behavior is specified, default to Wait. const behavior = options?.behavior ?? exports.SubscriberCloseBehaviors.WaitForProcessing; // The timeout can't realistically be longer than the longest time we're willing // to lease messages. let timeout = (0, temporal_1.atMost)(options?.timeout ?? this.maxExtensionTime, this.maxExtensionTime); // If the user specified a zero timeout, just bail immediately. if (!timeout.milliseconds) { this._inventory.clear(); return; } // Warn the user if the timeout is too short for NackImmediately. if (temporal_1.Duration.compare(timeout, FINAL_NACK_TIMEOUT) < 0) { exports.logs.debug.warn('Subscriber.close() timeout is less than the final shutdown time (%i ms). This may result in lost nacks.', timeout.milliseconds); } // If we're in WaitForProcessing mode, then we first need to derive a NackImmediately // timeout point. If everything finishes before then, we also want to go ahead and bail cleanly. const shutdownStart = Date.now(); if (behavior === exports.SubscriberCloseBehaviors.WaitForProcessing && !this._inventory.isEmpty) { const waitTimeout = timeout.subtract(FINAL_NACK_TIMEOUT); const emptyPromise = new Promise(r => { this._inventory.on('empty', r); }); await this.#awaitTimeoutAndCheck(emptyPromise, waitTimeout); } // Now we head into immediate shutdown mode with what time is left. timeout = timeout.subtract({ milliseconds: Date.now() - shutdownStart, }); if (timeout.milliseconds <= 0) { // This probably won't work out, but go through the motions. timeout = temporal_1.Duration.from({ milliseconds: 0 }); } // Grab everything left in inventory. This includes messages that have already // been dispatched to user callbacks. const remaining = this._inventory.clear(); remaining.forEach(m => m.nack()); // Wait for user callbacks to complete. const flushCompleted = this._waitForFlush(); await this.#awaitTimeoutAndCheck(flushCompleted, timeout); // Clean up OTel spans for any remaining messages. remaining.forEach(m => { m.subSpans.shutdown(); m.endParentSpan(); }); this.emit('close'); this._acks.close(); this._modAcks.close(); } /** * Gets the subscriber client instance. * * @returns {Promise<object>} * @private */ async getClient() { const pubsub = this._subscription.pubsub; const [client] = await (0, promisify_1.promisify)(pubsub.getClient_).call(pubsub, { client: 'SubscriberClient', }); return client; } /** * Modifies the acknowledge deadline for the provided message. * * @param {Message} message The message to modify. * @param {number} deadline The deadline in seconds. * @returns {Promise<void>} * @private */ async modAck(message, deadline) { const startTime = Date.now(); const responsePromise = this._modAcks.add(message, deadline); responsePromise.catch(() => { }); await this._modAcks.onFlush(); const latency = (Date.now() - startTime) / 1000; this._latencies.add(latency); } /** * Modifies the acknowledge deadline for the provided message, expecting * a reply (for exactly-once delivery subscriptions). * * @param {Message} message The message to modify. * @param {number} deadline The deadline. * @returns {Promise<AckResponse>} * @private */ async modAckWithResponse(message, deadline) { const startTime = Date.now(); await this._modAcks.add(message, deadline); const latency = (Date.now() - startTime) / 1000; this._latencies.add(latency); // No exception means Success. return exports.AckResponses.Success; } /** * Modfies the acknowledge deadline for the provided message and then removes * it from our inventory. * * @param {Message} message The message. * @return {Promise<void>} * @private */ async nack(message) { exports.logs.ackNack.info('message (ID %s, ackID %s) nack', message.id, message.ackId); const nackTimeSeconds = (Date.now() - message.received) / 1000; if (nackTimeSeconds > this._99th) { exports.logs.slowAck.info('message (ID %s, ackID %s) nack took longer than the 99th percentile (%s s)', message.id, message.ackId, nackTimeSeconds); } message.subSpans.nackStart(); await this.modAck(message, 0); message.subSpans.nackEnd(); message.endParentSpan(); this._inventory.remove(message); } /** * Modfies the acknowledge deadline for the provided message and then removes * it from our inventory, expecting a response from modAck (for * exactly-once delivery subscriptions). * * @param {Message} message The message. * @return {Promise<AckResponse>} * @private */ async nackWithResponse(message) { exports.logs.ackNack.info('message (ID %s, ackID %s) nack with response', message.id, message.ackId); const nackTimeSeconds = (Date.now() - message.received) / 1000; if (nackTimeSeconds > this._99th) { exports.logs.slowAck.info('message (ID %s, ackID %s) nack took longer than the 99th percentile (%s s)', message.id, message.ackId, nackTimeSeconds); } message.subSpans.nackStart(); const response = await this.modAckWithResponse(message, 0); message.subSpans.nackEnd(); message.endParentSpan(); return response; } /** * Starts pulling messages. * @private */ open() { const { batching, flowControl, streamingOptions } = this._options; this._acks = new message_queues_1.AckQueue(this, batching); this._modAcks = new message_queues_1.ModAckQueue(this, batching); this._inventory = new lease_manager_1.LeaseManager(this, flowControl); this._stream = new message_stream_1.MessageStream(this, streamingOptions); this._stream .on('error', err => this.emit('error', err)) .on('debug', msg => this.emit('debug', msg)) .on('data', (data) => this._onData(data)) .once('close', () => this.close()); this._inventory .on('full', () => this._stream.pause()) .on('free', () => this._stream.resume()); this._stream.start().catch(err => { this.emit('error', err); void this.close(); }); this.isOpen = true; } /** * Sets subscriber options. * * @param {SubscriberOptions} options The options. * @private */ setOptions(options) { this._options = options; this.useLegacyFlowControl = options.useLegacyFlowControl || false; if (options.flowControl) { this.maxMessages = options.flowControl.maxMessages || default_options_1.defaultOptions.subscription.maxOutstandingMessages; this.maxBytes = options.flowControl.maxBytes || default_options_1.defaultOptions.subscription.maxOutstandingBytes; // In the event that the user has specified the maxMessages option, we // want to make sure that the maxStreams option isn't higher. // It doesn't really make sense to open 5 streams if the user only wants // 1 message at a time. if (!options.streamingOptions) { options.streamingOptions = {}; } const { maxStreams = default_options_1.defaultOptions.subscription.maxStreams } = options.streamingOptions; options.streamingOptions.maxStreams = Math.min(maxStreams, this.maxMessages); } if (this._inventory) { this._inventory.setOptions(this._options.flowControl); } this.updateAckDeadline(); } /** * Retrieves our effective options. This is mostly for unit test use. * * @private * @returns {SubscriberOptions} The options. */ getOptions() { return this._options; } /** * Constructs a telemetry span from the incoming message. * * @param {Message} message One of the received messages * @private */ createParentSpan(message) { const enabled = tracing.isEnabled(); if (enabled) { tracing.extractSpan(message, this.name); } } /** * Callback to be invoked when a new message is available. * * New messages will be added to the subscribers inventory, which in turn will * automatically extend the messages ack deadline until either: * a. the user acks/nacks it * b. the maxExtension option is hit * * If the message puts us at/over capacity, then we'll pause our message * stream until we've freed up some inventory space. * * New messages must immediately issue a ModifyAckDeadline request * (aka receipt) to confirm with the backend that we did infact receive the * message and its ok to start ticking down on the deadline. * * @private */ _onData(response) { // Grab the subscription properties for exactly-once delivery and ordering flags. if (response.subscriptionProperties) { this.setSubscriptionProperties(response.subscriptionProperties); } const { receivedMessages } = response; for (const data of receivedMessages) { const message = new Message(this, data); this.createParentSpan(message); if (this.isOpen) { if (this.isExactlyOnceDelivery) { // For exactly-once delivery, we must validate that we got a valid // lease on the message before actually leasing it. message.subSpans.modAckStart(temporal_1.Duration.from({ seconds: this.ackDeadline }), true); message .modAckWithResponse(this.ackDeadline) .then(() => { this._inventory.add(message); }) .catch(() => { // Temporary failures will retry, so if an error reaches us // here, that means a permanent failure. Silently drop these. this._discardMessage(message); }) .finally(() => { message.subSpans.modAckEnd(); }); } else { message.subSpans.modAckStart(temporal_1.Duration.from({ seconds: this.ackDeadline }), true); message.modAck(this.ackDeadline); message.subSpans.modAckEnd(); this._inventory.add(message); } } else { message.subSpans.shutdown(); message.nack(); } } } // Internal: This is here to provide a hook for unit testing, at least for now. _discardMessage(message) { message; } /** * Returns a promise that will resolve once all pending requests have settled. * * @private * * @returns {Promise<void>} */ async _waitForFlush() { const promises = []; // Flush any batched requests immediately. if (this._acks.numPendingRequests) { promises.push(this._acks.onFlush()); this._acks.flush('message count').catch(() => { }); } if (this._modAcks.numPendingRequests) { promises.push(this._modAcks.onFlush()); this._modAcks.flush('message count').catch(() => { }); } // Now, prepare the drain promises. if (this._acks.numInFlightRequests) { promises.push(this._acks.onDrain()); } if (this._modAcks.numInFlightRequests) { promises.push(this._modAcks.onDrain()); } // Wait for the flush promises. await Promise.all(promises); } } exports.Subscriber = Subscriber; //# sourceMappingURL=subscriber.js.map