UNPKG

nitrogen-core

Version:

Core services used across ingestion, registry, and consumption servers.

370 lines (292 loc) 13.3 kB
var async = require('async') , core = require('../../lib') , moment = require('moment') , mongoose = require('mongoose') , RedisStore = require('socket.io/lib/stores/redis') , redis = require('socket.io/node_modules/redis'); var io; var attach = function(server) { if (!core.config.pubsub_provider) return core.log.warn('pubsub provider not configured: subscription endpoint not started.'); io = require('socket.io').listen(server); this.pub = createRedisClient(); this.sub = createRedisClient(); this.client = createRedisClient(); io.set('store', new RedisStore({ redisPub: this.pub, redisSub: this.sub, redisClient: this.client })); io.set('log level', 1); attachAuthFilter(); attachSubscriptionsEndpoint(); core.log.info('listening for realtime connections on ' + core.config.subscriptions_path); }; var attachAuthFilter = function() { io.configure(function () { io.set('authorization', function (handshakeData, callback) { if (!handshakeData.query.auth) return callback(null, false); core.services.accessTokens.verify(handshakeData.query.auth, function(err, principal) { var success = !err && principal; handshakeData.principal = principal; callback(null, success); }); }); }); }; var attachSubscriptionsEndpoint = function() { io.sockets.on('connection', function(socket) { if (!socket.handshake.principal) return core.log.error('subscription request without type and/or principal.'); socket.subscriptions = {}; socket.on('start', function(spec) { core.log.info('subscriptions: starting subscription with spec: ' + JSON.stringify(spec)); start(socket, spec); }); socket.on('disconnect', function() { var subscriptionKeys = Object.keys(socket.subscriptions); core.log.info('subscriptions: socket: ' + socket.id + ' disconnected. stopping ' + subscriptionKeys.length + ' subscriptions on this socket.'); async.each(subscriptionKeys, function(clientId, callback) { stop(socket.subscriptions[clientId], function(err) { delete socket.subscriptions[clientId]; return callback(err); }); }); }); socket.on('stop', function(spec) { core.log.info('subscriptions: stopping subscription with spec: ' + JSON.stringify(spec)); stop(socket.subscriptions[spec.id], function(err) { if (err) log.error(err); delete socket.subscriptions[spec.id]; }); }); // Expose message endpoint through socket connection. socket.on('messages', function(messageBundle) { core.services.messages.createMany(socket.handshake.principal, messageBundle.messages, function(err, messages) { socket.emit(messageBundle.uniqueId, { error: err, messages: messages }); }); }); }); }; var cacheKeySubscriptionsForPrincipal = function(principalId) { return "subscriptions.principal." + principalId.toString(); }; var clearPrincipalSubscriptionsCacheEntry = function(principalId, callback) { var cacheKey = cacheKeySubscriptionsForPrincipal(principalId); core.log.debug('subscriptions: clearing cache entry ' + cacheKey); core.config.cache_provider.del('subscriptions', cacheKey, callback); }; var count = function(callback) { core.models.Subscription.count(callback); }; var create = function(subscription, callback) { core.config.pubsub_provider.createSubscription(subscription, function(err) { if (err) callback(err); save(subscription, callback); }); }; var createRedisClient = function() { var firstRedisServerKey = Object.keys(core.config.redis_servers)[0]; var firstRedisServer = core.config.redis_servers[firstRedisServerKey]; var redisClient = redis.createClient(firstRedisServer.port, firstRedisServer.host); if (firstRedisServer.password) { redisClient.auth(firstRedisServer.password, function(err) { if (err) core.log.error('redis auth error: ' + err); }); } return redisClient; }; var find = function(authPrincipal, filter, options, callback) { core.models.Subscription.find(filter, null, options, callback); }; var findByPrincipalCached = function(authPrincipal, principalId, options, callback) { var cacheKey = cacheKeySubscriptionsForPrincipal(principalId); core.config.cache_provider.get('subscriptions', cacheKey, function(err, subscriptionObjs) { if (err) return callback(err); if (subscriptionObjs) { core.log.debug("subscriptions: " + cacheKey + ": cache hit: " + subscriptionObjs.length); var subscriptions = subscriptionObjs.map(function(obj) { var subscription = new core.models.Subscription(obj); // Mongoose by default will override the passed id with a new unique one. Set it back. subscription._id = mongoose.Types.ObjectId(obj.id); return subscription; }); return callback(null, subscriptions); } core.log.debug("subscriptions: " + cacheKey + ": cache miss."); // find and cache result return findByPrincipal(authPrincipal, principalId, options, callback); }); }; var findByPrincipal = function(authPrincipal, principalId, options, callback) { var cacheKey = cacheKeySubscriptionsForPrincipal(principalId); core.models.Subscription.find({ principal: principalId }, null, options, function(err, subscriptions) { if (err) return callback(err); core.log.debug("subscriptions: setting cache entry for " + cacheKey + ": " + subscriptions.length); core.config.cache_provider.set('subscriptions', cacheKey, subscriptions, moment().add(1, 'days').toDate(), function(err) { return callback(err, subscriptions); }); }); }; var findOne = function(subscription, callback) { var filter = { principal: subscription.principal, type: subscription.type, name: subscription.name }; core.models.Subscription.findOne(filter, callback); }; var findOrCreate = function(subscription, callback) { findOne(subscription, function(err, existingSubscription) { if (err) return callback(err); if (existingSubscription) return callback(null, existingSubscription); create(subscription, callback); }); }; var initialize = function(callback) { core.config.pubsub_provider.services = core.services; return callback(); } var janitor = function(callback) { var cutoffTime = core.config.pubsub_provider.staleSubscriptionCutoff(); find(core.services.principals.servicePrincipal, { last_receive: { $lt: cutoffTime }, permanent: false }, function(err, subscriptions) { core.log.info('subscriptions: janitoring ' + subscriptions.length + ' abandoned session subscriptions from before: ' + cutoffTime.toString()); async.each(subscriptions, remove, callback); }); }; var publish = function(type, item, callback) { if (!core.config.pubsub_provider) return callback(new Error("subscription service: can't publish without pubsub_provider")); core.config.pubsub_provider.publish(type, item, callback); }; var receive = function(subscription, callback) { if (!core.config.pubsub_provider) return callback(new Error("subscription service: can't receive without pubsub_provider")); // fire and forget an update to tag this subscription with the last attempted receive. // used for janitorial purposes for non-permanent subscriptions. core.log.debug('subscriptions: updating last_receive for subscription: ' + subscription.id + ': ' + subscription.name + ': ' + subscription.filter_string); core.config.pubsub_provider.receive(subscription, callback); subscription.last_receive = new Date(); subscription.save(); //update(subscription, { last_receive: new Date() }); }; var remove = function(subscription, callback) { if (!subscription) return log.error('undefined subscription passed to services.subscription.remove.'); core.log.debug('subscriptions: removing subscription: ' + subscription.id + ': ' + subscription.name + ': filter: ' + JSON.stringify(subscription.filter) + ' last_receive: ' + subscription.last_receive); core.config.pubsub_provider.removeSubscription(subscription, function(err) { if (err) core.log.error('subscriptions: remove failed in provider with error: ' + err); subscription.remove(function(err, removedCount) { if (err) return callback(err); if (subscription.socket) delete subscription.socket.subscriptions[subscription.clientId]; clearPrincipalSubscriptionsCacheEntry(subscription.principal, function(err) { return callback(err, removedCount); }); }); }); }; var save = function(subscription, callback) { subscription.save(function(err, subscription) { if (err) return callback(err); clearPrincipalSubscriptionsCacheEntry(subscription.principal, function(err) { return callback(err, subscription); }); }); }; var start = function(socket, spec, callback) { var subscription = new core.models.Subscription({ clientId: spec.id, filter: spec.filter || {}, name: spec.name, principal: socket.handshake.principal.id, socket: socket, type: spec.type }); subscription.permanent = !!subscription.name; if (!subscription.permanent) { // assign the subscription a uuid as a name if this is session subscription subscription.name = core.utils.uuid(); } findOrCreate(subscription, function(err, subscription) { if (err) { var msg = 'subscriptions: failed to create: ' + err; core.log.error(msg); if (callback) callback(new Error(msg)); return; } core.log.debug('subscriptions: connecting subscription: ' + subscription.id + ' with clientId: ' + spec.id); subscription.clientId = spec.id; socket.subscriptions[subscription.clientId] = subscription; stream(socket, subscription); if (callback) return callback(null, subscription); }); }; // stop is invoked when an active subscription is closed. // for permanent subscriptions this is a noop. // for session subscriptions this removes them. var stop = function(subscription, callback) { if (!subscription) { core.log.warn('subscriptions: stop: passed null subscription.'); } if (subscription && !subscription.permanent) { remove(subscription, callback); } else { return callback(); } }; var stream = function(socket, subscription) { async.whilst( function() { return socket.subscriptions[subscription.clientId] !== undefined; }, function(callback) { receive(subscription, function(err, item, ref) { if (err) return callback(err); // if the socket has disconnected in the meantime, reject the message. if (socket.subscriptions[subscription.clientId] === undefined) { core.log.info('subscription service: subscription is closed, rejecting message.'); core.config.pubsub_provider.ackReceive(ref, false); } else { // there might not be an item when the provider timed out waiting for an item. if (item) { core.log.info('subscription service: new message from subscription: ' + subscription.clientId + ' with name: ' + subscription.name + ' of type: ' + subscription.type + ": " + JSON.stringify(item)); socket.emit(subscription.clientId, item); } core.config.pubsub_provider.ackReceive(ref, true); } callback(); }); }, function(err) { if (err) core.log.error("subscription service: receive loop error: " + err); core.log.info("subscription service: stream for " + subscription.clientId + " disconnected."); } ); }; var update = function(subscription, updates, callback) { core.models.Subscription.update({ _id: subscription.id }, { $set: updates }, function(err, updateCount) { if (err) return callback(err); clearPrincipalSubscriptionsCacheEntry(subscription.principal, function(err) { return callback(err, updateCount); }); }); }; module.exports = { attach: attach, count: count, create: create, find: find, findByPrincipal: findByPrincipal, findByPrincipalCached: findByPrincipalCached, findOne: findOne, findOrCreate: findOrCreate, initialize: initialize, janitor: janitor, publish: publish, receive: receive, remove: remove, start: start, stop: stop };