UNPKG

ctrip-apollo-ex

Version:

The most delightful and handy Node.js client for ctrip apollo configuration service

211 lines (169 loc) 4.7 kB
// Ref: // https://github.com/ctripcorp/apollo/wiki/%E5%85%B6%E5%AE%83%E8%AF%AD%E8%A8%80%E5%AE%A2%E6%88%B7%E7%AB%AF%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97#14-%E5%BA%94%E7%94%A8%E6%84%9F%E7%9F%A5%E9%85%8D%E7%BD%AE%E6%9B%B4%E6%96%B0 const EventEmitter = require('events') const log = require('util').debuglog('ctrip-apollo') const request = require('request') const {queryUpdate} = require('./url') const {DEFAULT_NAMESPACE_TYPE} = require('./options') const {error} = require('./error') // TIMEOUT SHOULD LONGER THAN 60s const POLLING_TIMEOUT = 70 * 1000 // All namespaces of the same cluster use a single Polling class Polling extends EventEmitter { constructor (options, enabled) { super() this._options = options this._ns = new Set() this._nsTypes = Object.create(null) this._notificationIds = Object.create(null) this._abandoned = false this._started = false this.enable(enabled) } enable (enable) { this._enabled = enable if (enable) { this._check() } } _check () { if ( this._enabled && this._ns.size > 0 && !this._abandoned && !this._started ) { this.start() } } addNamespace (namespace, type) { this._ns.add(namespace) this._nsTypes[namespace] = type this._check() } _getNotifications () { const notifications = [] for (const namespace of this._ns.values()) { const item = { namespaceName: this._nsTypes[namespace] === DEFAULT_NAMESPACE_TYPE ? namespace : `${namespace}.json`, notificationId: (namespace in this._notificationIds) ? this._notificationIds[namespace] : 0 } notifications.push(item) } return notifications } start () { this._start(0) } _start (retries) { /* istanbul ignore if */ if (!this._enabled) { // Stop polling when it is disabled this._started = false return } this._started = true const url = queryUpdate({ ...this._options, notifications: this._getNotifications() }) log('polling: request %s', decodeURIComponent(url)) const start = Date.now() request(url, { timeout: POLLING_TIMEOUT }, (err, response) => { // Do nothing is polling is disabled if (!this._enabled) { this._started = false return } log('polling: responses cost: %s ms', Date.now() - start) if (err) { return this._handleError( error('POLLING_ERROR', err), retries ) } const { statusCode: status, body } = response // There is no changes if (status === 304) { log('polling: no changes, start polling again') this._start(0) return } if (status !== 200) { log('polling: response error, status: %s, body: %s', status, body) return this._handleError( error('POLLING_STATUS_ERROR', status), retries ) } try { this._diff(JSON.parse(body)) } catch (parseError) { this._handleError( error('POLLING_JSON_PARSE_ERROR', parseError), retries ) } }) } _handleError (err, retries) { log('polling: error, code: %s, stack: %s', err.code, err.stack) const { delay = 0, reset, abandon } = this._options.pollingRetryPolicy(retries) if (abandon) { this._abandoned = true // Mark as stopped when abandon this._started = false this.emit('abandon') return } const nextRetries = reset ? 0 : retries + 1 log('polling: retry %s in %s ms', nextRetries, delay) setTimeout(() => { this._start(nextRetries) }, delay) } _diff (notifications) { notifications.forEach(({ namespaceName, notificationId }) => { const oldNotificationId = this._notificationIds[namespaceName] if (!oldNotificationId) { // Update the current notification id this._notificationIds[namespaceName] = notificationId // Do not emit update for the first time return } // Actually, it will never happen // which is handled by apollo config service // but we still do this for fault tolerance /* istanbul ignore next */ if (oldNotificationId === notificationId) { /* istanbul ignore next */ return } this._notificationIds[namespaceName] = notificationId this.emit('update', namespaceName) }) log('polling: emit update, start polling again') this.start(0) } } module.exports = { Polling }