all files / src/ DnsClusterResolver.js

100% Statements 106/106
92.98% Branches 53/57
95.65% Functions 22/23
100% Lines 75/75
1 statement, 1 branch Ignored     
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131                   20× 20× 20× 20×           18×                                 10× 10× 10×                       21× 20×     18×   16×                 20× 20×   19× 19×        
import dns from 'dns';
import async from 'async';
import shuffle from 'lodash/shuffle';
import xor from 'lodash/xor';
import Logger from './Logger';
 
function noop() {}
 
/*
  Locates a Eureka host using DNS lookups. The DNS records are looked up by a naming
  convention and TXT records must be created according to the Eureka Wiki here:
  https://github.com/Netflix/eureka/wiki/Configuring-Eureka-in-AWS-Cloud
 
  Naming convention: txt.<REGION>.<HOST>
 */
export default class DnsClusterResolver {
  constructor(config, logger) {
    this.logger = logger || new Logger();
    this.serverList = undefined;
    this.config = config;
    if (!this.config.eureka.ec2Region) {
      throw new Error(
        'EC2 region was undefined. ' +
        'config.eureka.ec2Region must be set to resolve Eureka using DNS records.'
      );
    }
 
    if (this.config.eureka.clusterRefreshInterval) {
      this.startClusterRefresh();
    }
  }
 
  resolveEurekaUrl(callback, retryAttempt = 0) {
    this.getCurrentCluster((err) => {
      if (err) return callback(err);
 
      if (retryAttempt > 0) {
        this.serverList.push(this.serverList.shift());
      }
      const { port, servicePath, ssl } = this.config.eureka;
      const protocol = ssl ? 'https' : 'http';
      callback(null, `${protocol}://${this.serverList[0]}:${port}${servicePath}`);
    });
  }
 
  getCurrentCluster(callback) {
    if (this.serverList) {
      return callback(null, this.serverList);
    }
    this.refreshCurrentCluster((err) => {
      if (err) return callback(err);
      return callback(null, this.serverList);
    });
  }
 
  startClusterRefresh() {
    const refreshTimer = setInterval(() => {
      this.refreshCurrentCluster((err) => {
        if (Eerr) this.logger.warn(err.message);
      });
    }, this.config.eureka.clusterRefreshInterval);
    refreshTimer.unref();
  }
 
  refreshCurrentCluster(callback = noop) {
    this.resolveClusterHosts((err, hosts) => {
      if (err) return callback(err);
      // if the cluster is the same (aside from order), we want to maintain our order
      if (xor(this.serverList, hosts).length) {
        this.serverList = hosts;
        this.logger.info('Eureka cluster located, hosts will be used in the following order',
          this.serverList);
      } else {
        this.logger.debug('Eureka cluster hosts unchanged, maintaining current server list.');
      }
      callback();
    });
  }
 
  resolveClusterHosts(callback = noop) {
    const { ec2Region, host, preferSameZone } = this.config.eureka;
    const { dataCenterInfo } = this.config.instance;
    const metadata = dataCenterInfo ? dataCenterInfo.metadata : undefined;
    const availabilityZone = metadata ? metadata['availability-zone'] : undefined;
    const dnsHost = `txt.${ec2Region}.${host}`;
    dns.resolveTxt(dnsHost, (err, addresses) => {
      if (err) {
        return callback(new Error(
          `Error resolving eureka cluster for region [${ec2Region}] using DNS: [${err}]`
        ));
      }
      const zoneRecords = [].concat(...addresses);
      const dnsTasks = {};
      zoneRecords.forEach((zoneRecord) => {
        dnsTasks[zoneRecord] = (cb) => {
          this.resolveZoneHosts(`txt.${zoneRecord}`, cb);
        };
      });
      async.parallel(dnsTasks, (error, results) => {
        if (error) return callback(error);
        const hosts = [];
        const myZoneHosts = [];
        Object.keys(results).forEach((zone) => {
          if (preferSameZone && availabilityZone && zone.lastIndexOf(availabilityZone, 0) === 0) {
            myZoneHosts.push(...results[zone]);
          } else {
            hosts.push(...results[zone]);
          }
        });
        const combinedHosts = [].concat(shuffle(myZoneHosts), shuffle(hosts));
        if (!combinedHosts.length) {
          return callback(
            new Error(`Unable to locate any Eureka hosts in any zone via DNS @ ${dnsHost}`));
        }
        callback(null, combinedHosts);
      });
    });
  }
 
  resolveZoneHosts(zoneRecord, callback) {
    dns.resolveTxt(zoneRecord, (err, results) => {
      if (err) {
        this.logger.warn(`Failed to resolve cluster zone ${zoneRecord}`, err.message);
        return callback(new Error(`Error resolving cluster zone ${zoneRecord}: [${err}]`));
      }
      this.logger.debug(`Found Eureka Servers @ ${zoneRecord}`, results);
      callback(null, ([].concat(...results)).filter((value) => (!!value)));
    });
  }
}