UNPKG

firehoser_es5

Version:

es5 version of firehoser. A wrapper around AWS Kinesis Firehose with retry logic and custom queuing behavior.

338 lines (281 loc) 15.5 kB
'use strict'; var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } // require("babel-polyfill"); var _ = require('lodash'); var AWS = require('aws-sdk'); var async = require('async'); var JaySchema = require('jayschema'); var moment = require('moment'); var Promise = require('es6-promise').Promise; var schemaValidator = new JaySchema(); var DeliveryStream = function () { function DeliveryStream(name) { var awsConfig = arguments.length <= 1 || arguments[1] === undefined ? null : arguments[1]; var schema = arguments.length <= 2 || arguments[2] === undefined ? null : arguments[2]; var retryInterval = arguments.length <= 3 || arguments[3] === undefined ? 1500 : arguments[3]; var firehose = arguments.length <= 4 || arguments[4] === undefined ? null : arguments[4]; var logger = arguments.length <= 5 || arguments[5] === undefined ? null : arguments[5]; _classCallCheck(this, DeliveryStream); this.maxIngestion = 400; this.maxDrains = 3; this.maxRetries = 40; this.name = name; if (awsConfig !== null) { AWS.config.update(awsConfig); } this.schema = schema; this.retryInterval = retryInterval; this.firehose = firehose ? firehose : new AWS.Firehose({ params: { DeliveryStreamName: name } }); this.log = logger ? logger : function () {}; } _createClass(DeliveryStream, [{ key: 'validateRecord', value: function validateRecord(record) { return schemaValidator.validate(record, this.schema); } }, { key: 'validateRecords', value: function validateRecords(records) { var _this = this; if (!this.schema) { return [records, []]; } var validRecords = []; var invalidRecords = []; _.forEach(records, function (record) { var validationErrors = _this.validateRecord(record); if (_.isEmpty(validationErrors)) { validRecords.push(record); } else { var ve = validationErrors[0]; invalidRecords.push({ type: "schema", originalRecord: record, description: buildSchemaErrorDescription(ve), details: ve }); } }); return [validRecords, invalidRecords]; } }, { key: 'formatRecord', value: function formatRecord(record) { return { Data: record + '\n' }; } }, { key: 'putRecord', value: function putRecord(record) { return this.putRecords([record]); } }, { key: 'putRecords', value: function putRecords(records) { var _this2 = this; this.log('DeliveryStream.putRecords() called with ' + records.length + ' records.'); return new Promise(function (resolve, reject) { // Validate records against a schema, if necessary. var _validateRecords = _this2.validateRecords(records); var _validateRecords2 = _slicedToArray(_validateRecords, 2); var validRecords = _validateRecords2[0]; var invalidRecords = _validateRecords2[1]; // Split the records into reasonably-sized chunks. records = _.map(validRecords, _this2.formatRecord); var chunks = _.chunk(records, _this2.maxIngestion); var tasks = []; for (var i = 0; i < chunks.length; i++) { tasks.push(_this2.drain.bind(_this2, chunks[i])); } // Schedule the chunks all at the same time. _this2.log('Kicking off ' + tasks.length + ' calls to drain() for ' + records.length + ' records.'); async.parallelLimit(tasks, _this2.maxDrains, function (err, results) { var allErrors = invalidRecords.concat(_.flatten(results)); if (err || !_.isEmpty(allErrors)) { return reject(allErrors); } return resolve(); }); }); } }, { key: 'drain', value: function drain(records, cb) { var numRetries = arguments.length <= 2 || arguments[2] === undefined ? 0 : arguments[2]; var leftovers = []; this.log('Draining ' + records.length + ' records. Pass #' + (numRetries + 1)); this.firehose.putRecordBatch({ Records: records }, function (firehoseErr, resp) { // Stuff broke! if (firehoseErr) { return cb(null, { type: "firehose", description: "Internal aws-sdk error.", details: firehoseErr, originalRecord: null }); } // Not all records make it in, but firehose keeps on chugging! if (resp.FailedPutCount > 0) {} // Push errored records back into the next list. var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = _.zip(records, resp.RequestResponses)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var _step$value = _slicedToArray(_step.value, 2); var orig = _step$value[0]; var result = _step$value[1]; if (!_.isUndefined(result.ErrorCode)) { this.log('Got ErrorCode ' + result.ErrorCode + ' for record ' + orig); leftovers.push({ type: "firehose", description: result.ErrorMessage, details: { ErrorCode: result.ErrorCode, ErrorMessage: result.ErrorMessage }, originalRecord: orig }); } } // Recurse! } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } if (leftovers.length && numRetries < this.maxRetries) { // We're about to recurse, let the child handle storing error details. leftovers = _.map(leftovers, function (leftover) { return _.pick(leftover, ['originalRecord']); }); return setTimeout(function () { this.drain.bind(this, leftovers, cb, numRetries + 1); }, this.retryInterval); } else { return cb(null, leftovers); } }); } }]); return DeliveryStream; }(); var JSONDeliveryStream = function (_DeliveryStream) { _inherits(JSONDeliveryStream, _DeliveryStream); function JSONDeliveryStream() { _classCallCheck(this, JSONDeliveryStream); return _possibleConstructorReturn(this, Object.getPrototypeOf(JSONDeliveryStream).apply(this, arguments)); } _createClass(JSONDeliveryStream, [{ key: 'formatRecord', value: function formatRecord(record) { return _get(Object.getPrototypeOf(JSONDeliveryStream.prototype), 'formatRecord', this).call(this, JSON.stringify(record)); } }]); return JSONDeliveryStream; }(DeliveryStream); var QueuableDeliveryStream = function (_DeliveryStream2) { _inherits(QueuableDeliveryStream, _DeliveryStream2); function QueuableDeliveryStream(name) { var _Object$getPrototypeO; var maxTime = arguments.length <= 1 || arguments[1] === undefined ? 30000 : arguments[1]; var maxSize = arguments.length <= 2 || arguments[2] === undefined ? 500 : arguments[2]; _classCallCheck(this, QueuableDeliveryStream); for (var _len = arguments.length, args = Array(_len > 3 ? _len - 3 : 0), _key = 3; _key < _len; _key++) { args[_key - 3] = arguments[_key]; } var _this4 = _possibleConstructorReturn(this, (_Object$getPrototypeO = Object.getPrototypeOf(QueuableDeliveryStream)).call.apply(_Object$getPrototypeO, [this, name].concat(args))); _this4.queue = []; _this4.timeout = null; _this4.maxTime = maxTime; _this4.maxSize = maxSize; _this4.promise = null; setInterval(_this4.drainQueue.bind(_this4), _this4.maxTime); return _this4; } _createClass(QueuableDeliveryStream, [{ key: 'putRecords', value: function putRecords(records) { var _queue, _this5 = this; this.log('QueuableDeliveryStream.putRecords() called with ' + records.length + ' records.'); (_queue = this.queue).push.apply(_queue, _toConsumableArray(records)); if (this.promise === null) { this.promise = new Promise(function (resolve, reject) { _this5.resolver = resolve; _this5.rejecter = reject; }); } this.log('queue size is: ' + this.queue.length + ', maxSize is: ' + this.maxSize + '.'); if (this.queue.length >= this.maxSize) { // Queue's full! this.log('queue is full, draining immediately.'); setImmediate(this.drainQueue.bind(this)); } return this.promise; } }, { key: 'drainQueue', value: function drainQueue() { var _this6 = this; this.log('Countdown timer expired or queue limit reached.'); this.log('Time to drain the queue of ' + this.queue.length + ' records.'); var toQueue = this.queue.splice(0, this.queue.length); if (!toQueue.length) { this.log('No records in queue, not draining anything.'); return; } _get(Object.getPrototypeOf(QueuableDeliveryStream.prototype), 'putRecords', this).call(this, toQueue).then(this.resolver, this.rejecter).then(function () { _this6.promise = null; _this6.rejecter = null; _this6.resolver = null; }); } }]); return QueuableDeliveryStream; }(DeliveryStream); var QueuableJSONDeliveryStream = function (_QueuableDeliveryStre) { _inherits(QueuableJSONDeliveryStream, _QueuableDeliveryStre); function QueuableJSONDeliveryStream() { _classCallCheck(this, QueuableJSONDeliveryStream); return _possibleConstructorReturn(this, Object.getPrototypeOf(QueuableJSONDeliveryStream).apply(this, arguments)); } _createClass(QueuableJSONDeliveryStream, [{ key: 'formatRecord', value: function formatRecord(record) { return _get(Object.getPrototypeOf(QueuableJSONDeliveryStream.prototype), 'formatRecord', this).call(this, JSON.stringify(record)); } }]); return QueuableJSONDeliveryStream; }(QueuableDeliveryStream); function buildSchemaErrorDescription(ve) { if (ve.desc) { return ve.desc; } var field = ve.instanceContext.replace(/(#\/)|(#)/ig, "").replace(/\//g, "."); return (ve.kind || 'Error') + ' on \'' + field + '\'. Expected ' + ve.constraintName + ' to be ' + ve.constraintValue + ', actual value was ' + ve.testedValue + '.'; } function makeRedshiftTimestamp(input) { return moment(input).utc().format('YYYY-MM-DD HH:mm:ss'); } module.exports = { DeliveryStream: DeliveryStream, JSONDeliveryStream: JSONDeliveryStream, QueuableDeliveryStream: QueuableDeliveryStream, QueuableJSONDeliveryStream: QueuableJSONDeliveryStream, makeRedshiftTimestamp: makeRedshiftTimestamp };