lifion-kinesis
Version:
Lifion client for Amazon Kinesis Data streams
280 lines (255 loc) • 11.1 kB
JavaScript
/**
* Module with statics to handle stream management.
*
* @module stream
* @private
*/
import equal from 'fast-deep-equal';
import { promisify } from 'node:util';
const CONSUMER_STATE_CHECK_DELAY = 3000;
const wait = promisify(setTimeout);
/**
* Checks if the given stream exists. If the stream is getting deleted or in the middle of an
* update, it will wait for the status change completion. If the stream exists, the stream ARN and
* created-on timestamp is returned. If the stream doesn't exist, the ARN is set to `null`.
*
* @param {Object} params - The parameters.
* @param {Object} params.client - An instance of the Kinesis client.
* @param {Object} params.logger - An instance of a logger.
* @param {string} params.streamName - The name of the stream to check for.
* @fulfil {Object} - An object with `streamArn` (the stream ARN) and `streamCreatedOn` (the
* stream creation timestamp). If the stream doesn't exist, `streamArn` is set to `null`.
* @returns {Promise}
* @memberof module:stream
*/
async function checkIfStreamExists({ client, logger, streamName }) {
try {
const params = { StreamName: streamName };
const { StreamDescriptionSummary } = await client.describeStreamSummary(params);
const { StreamARN, StreamCreationTimestamp, StreamStatus } = StreamDescriptionSummary;
if (StreamStatus === 'DELETING') {
logger.debug('Waiting for the stream to complete deletion…');
await client.waitFor('streamNotExists', params);
logger.debug('The stream is now gone.');
return { streamArn: null };
}
if (StreamStatus && StreamStatus !== 'ACTIVE') {
logger.debug('Waiting for the stream to be active…');
await client.waitFor('streamExists', params);
logger.debug('The stream is now active.');
}
return {
streamArn: StreamARN,
streamCreatedOn: StreamCreationTimestamp.toISOString()
};
} catch (err) {
if (err.code === 'ResourceNotFoundException') {
return { streamArn: null };
}
logger.error(err);
throw err;
}
}
/**
* Checks if the given stream is tagged as specified. If the stream is currently tagged with
* different tags, the tags are merged and the stream is tagged with them. If the stream has no
* tags, it will get tagged with the specified ones.
*
* @param {Object} params - The parameters.
* @param {Object} params.client - An instance of the Kinesis client.
* @param {Object} params.logger - An instance of a logger.
* @param {string} params.streamName - The stream to check the tags for.
* @param {Object} params.tags - The wanted tags.
* @fulfil {undefined}
* @returns {Promise}
* @memberof module:stream
*/
async function confirmStreamTags({ client, logger, streamName, tags }) {
const params = { StreamName: streamName };
const { Tags } = await client.listTagsForStream(params);
const existingTags = Tags.reduce((obj, { Key, Value }) => ({ ...obj, [Key]: Value }), {});
const mergedTags = { ...existingTags, ...tags };
if (!equal(existingTags, mergedTags)) {
await client.addTagsToStream({ ...params, Tags: mergedTags });
logger.debug(`The stream tags have been updated.`);
} else {
logger.debug('The stream is already tagged as required.');
}
}
/**
* Ensures that the stream is encrypted as specified. If not encrypted, the stream will get
* encrypted and the call won't resolve until the stream update process has completed.
*
* @param {Object} params - The parameters.
* @param {Object} params.client - An instance of the Kinesis client.
* @param {Object} params.encryption - The encryption options to enforce.
* @param {string} params.encryption.keyId - The GUID for the customer-managed AWS KMS key
* to use for encryption. This value can be a globally unique identifier, a fully
* specified ARN to either an alias or a key, or an alias name prefixed by "alias/".
* @param {string} params.encryption.type - The encryption type to use.
* @param {Object} params.logger - An instance of the logger.
* @param {string} params.streamName - The stream to check for encryption.
* @fulfil {undefined}
* @returns {Promise}
* @memberof module:stream
*/
async function ensureStreamEncription(params) {
const { client, encryption, logger, streamName: StreamName } = params;
const { keyId: KeyId, type: EncryptionType } = encryption;
const { StreamDescriptionSummary } = await client.describeStreamSummary({ StreamName });
if (StreamDescriptionSummary.EncryptionType === 'NONE') {
logger.debug('Trying to encrypt the stream…');
await client.startStreamEncryption({ EncryptionType, KeyId, StreamName });
logger.debug('Waiting for the stream to update…');
await client.waitFor('streamExists', { StreamName });
logger.debug('The stream is now encrypted.');
} else {
logger.debug('The stream is already encrypted.');
}
}
/**
* Ensures that the specified stream exists. If it doesn't exist, it process to create it and
* wait until the new stream is activated and ready to go.
*
* @param {Object} params - The parameters.
* @param {Object} params.client - An intance of the Kinesis client.
* @param {boolean} params.createStreamIfNeeded - Whether if the Kinesis stream should
* be automatically created if it doesn't exist upon connection.
* @param {Object} params.logger - An instance of a logger.
* @param {number} params.shardCount - The number of shards that the newly-created stream
* will use (if the `createStreamIfNeeded` option is set)
* @param {string} params.streamName - The name of the stream to check/create.
* @fulfil {Object} - An object with `streamArn` (the stream ARN) and `streamCreatedOn` (the
* stream creation timestamp). If the stream doesn't exist, `streamArn` is set to `null`.
* @returns {Promise}
* @memberof module:stream
*/
async function ensureStreamExists(params) {
const { client, createStreamIfNeeded, logger, shardCount, streamName } = params;
logger.debug(`Verifying the "${streamName}" stream exists and it's active…`);
const { streamArn, streamCreatedOn } = await checkIfStreamExists(params);
if (createStreamIfNeeded && streamArn === null) {
logger.debug('Trying to create the stream…');
const awsParams = { StreamName: streamName };
await client.createStream({ ...awsParams, ShardCount: shardCount });
logger.debug('Waiting for the new stream to be active…');
const { StreamDescription } = await client.waitFor('streamExists', awsParams);
logger.debug('The new stream is now active.');
const { StreamARN, StreamCreationTimestamp } = StreamDescription;
return {
streamArn: StreamARN,
streamCreatedOn: StreamCreationTimestamp.toISOString()
};
}
logger.debug("The stream exists and it's active.");
return { streamArn, streamCreatedOn };
}
/**
* Retrieves a list of the enhanced fan-out consumers registered for the stream. If any of the
* enhanced consumers is changing status, it will wait until they all are active.
*
* @param {Object} params - The parameters.
* @param {Object} params.client - An instance of the Kinesis client.
* @param {Object} params.logger - An instance of a logger.
* @param {string} params.streamArn - The ARN of the stream to retrieve enhanced consumers from.
* @fulfil {Array<Object>} - An array of objects with the properties `arn` (the ARN of the
* enhanced consumer), and `status`.
* @returns {Promise}
* @memberof module:stream
*/
async function getEnhancedConsumers(params) {
const { client, logger, streamArn } = params;
const { Consumers } = await client.listStreamConsumers({ StreamARN: streamArn });
const consumers = Consumers.reduce(
(result, consumer) => ({
...result,
[consumer.ConsumerName]: {
arn: consumer.ConsumerARN,
status: consumer.ConsumerStatus
}
}),
{}
);
const shouldWaitForConsumer = Object.keys(consumers).some(
(consumerName) => consumers[consumerName].status !== 'ACTIVE'
);
if (shouldWaitForConsumer) {
logger.debug(`Waiting until all enhanced consumers are active…`);
await wait(CONSUMER_STATE_CHECK_DELAY);
return getEnhancedConsumers(params);
}
return consumers;
}
/**
* Returns an object with the information of the stream shards. For each pair, the key corresponds
* to the shard ID, while the value stores the details for the shard.
*
* @param {Object} params - The parameters.
* @param {Object} params.client - An instance of the Kinesis client.
* @param {Object} params.logger - An instance of a logger.
* @param {string} params.streamName - The name of the stream to get shards for.
* @fulfil {Object} - The shard information as an object hashed by shard ID.
* @returns {Promise}
* @memberof module:stream
*/
async function getStreamShards({ client, logger, streamName }) {
logger.debug(`Retrieving shards for the "${streamName}" stream…`);
const { Shards } = await client.listShards({ StreamName: streamName });
const shards = Shards.reduce((obj, item) => {
const { ParentShardId, SequenceNumberRange, ShardId } = item;
return {
...obj,
[ShardId]: {
parent: ParentShardId || null,
startingSequenceNumber: SequenceNumberRange.StartingSequenceNumber
}
};
}, {});
Object.keys(shards).forEach((id) => {
const shard = shards[id];
const { parent } = shard;
if (parent && !shards[parent]) {
shard.parent = null;
}
});
return shards;
}
/**
* Registers a new enhanced fan-out consumer for the given stream. The call won't resolve until
* the new enhanced consumer has become active.
*
* @param {Object} params - The parameters.
* @param {Object} params.client - An instance of the Kinesis client.
* @param {string} params.consumerName - The name for the new enhanced fan-out consumer.
* @param {Object} params.logger - An instance of a logger.
* @param {string} params.streamArn - The ARN of the stream to register the consumer on.
* @fulfil {undefined}
* @returns {Promise}
* @memberof module:stream
*/
async function registerEnhancedConsumer({ client, consumerName, logger, streamArn }) {
logger.debug(`Registering enhanced consumer "${consumerName}"…`);
await client.registerStreamConsumer({
ConsumerName: consumerName,
StreamARN: streamArn
});
let ConsumerStatus;
logger.debug(`Waiting for the new enhanced consumer "${consumerName}" to be active…`);
do {
await wait(CONSUMER_STATE_CHECK_DELAY);
const { Consumers } = await client.listStreamConsumers({ StreamARN: streamArn });
const consumer = Consumers.find((i) => i.ConsumerName === consumerName);
if (consumer) ({ ConsumerStatus } = consumer);
else ConsumerStatus = null;
} while (ConsumerStatus !== 'ACTIVE');
logger.debug(`The enhanced consumer "${consumerName}" is now active.`);
}
export {
checkIfStreamExists,
confirmStreamTags,
ensureStreamEncription,
ensureStreamExists,
getEnhancedConsumers,
getStreamShards,
registerEnhancedConsumer
};