Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V3 Release #60

Merged
merged 11 commits into from
May 9, 2016
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## 3.0.0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be called CHANGELOG.md

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kesne PRs are gladly accepted. 💯 🎱

- Added better exception handling around malformed YAML configuration files.
- Added support for the following events: `started`, `registered`, `deregistered`, `heartbeat`, and `registryUpdated`.
- Improved the stability of the client when it encounters downstream DNS errors, as a side-effect the callback for `fetchRegistries()` now returns errors when they are encountered.
- Populate registry cache with instances that have a status of `UP`, `filterUpInstances` can be set to `false` to disable.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,31 @@ If your have multiple availability zones and your DNS entries set up according t

This will cause the client to perform a DNS lookup using `config.eureka.host` and `config.eureka.ec2Region`. The naming convention for the DNS TXT records required for this to function is also described in the Wiki article above.

## Configuration Options
option | default value | description
---- | --- | ---
`logger` | console logging | logger implementation for the client to use
`eureka.heartbeatInterval` | `30000` | milliseconds to wait between heartbeats
`eureka.registryFetchInterval` | `30000` | milliseconds to wait between registry fetches
`eureka.fetchRegistry` | `true` | enable/disable registry fetching
`eureka.filterUpInstances` | `true` | enable/disable filtering of instances with status === `UP`
`eureka.servicePath` | `/eureka/v2/apps/` | path to eureka REST service
`eureka.ssl` | `false` | enable SSL communication with Eureka server
`eureka.useDns` | `false` | look up Eureka server using DNS, see [Looking up Eureka Servers in AWS using DNS](#looking-up-eureka-servers-in-aws-using-dns)
`eureka.fetchMetadata` | `true` | fetch AWS metadata when in AWS environment, see [Configuring for AWS environments](#configuring-for-aws-environments)
`eureka.useLocalMetadata` | `false` | use local IP and local hostname from metadata when in an AWS environment.

## Events

Eureka client is an instance of `EventEmitter` and provides the following events for consumption:

event | data provided | description
---- | --- | ---
`started` | N/A | Fired when eureka client is fully registered and all registries have been updated.
`registered` | N/A | Fired when the eureka client is registered with eureka.
`deregistered` | N/A | Fired when the eureka client is deregistered with eureka.
`heartbeat` | N/A | Fired when the eureka client has successfully renewed it's lease with eureka.
`registryUpdated` | N/A | Fired when the eureka client has successfully update it's registries.

## Debugging

Expand Down
92 changes: 68 additions & 24 deletions src/EurekaClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import merge from 'deepmerge';
import path from 'path';
import dns from 'dns';
import { series } from 'async';
import { EventEmitter } from 'events';

import AwsMetadata from './AwsMetadata';
import Logger from './Logger';
Expand All @@ -18,19 +19,32 @@ function noop() {}
for reporting instance health.
*/

function fileExists(file) {
try {
return fs.statSync(file);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would !! this to ensure consistent return types.

} catch (e) {
return false;
}
}

function getYaml(file) {
let yml = {};
if (!fileExists(file)) {
return yml; // no configuration file
}
try {
yml = yaml.safeLoad(fs.readFileSync(file, 'utf8'));
} catch (e) {
// Ignore YAML load error.
// configuration file exists but was malformed
throw new Error(`Error loading YAML configuration file: ${file} ${e}`);
}
return yml;
}

export default class Eureka {
export default class Eureka extends EventEmitter {

constructor(config = {}) {
super();
// Allow passing in a custom logger:
this.logger = config.logger || new Logger();

Expand Down Expand Up @@ -91,10 +105,11 @@ export default class Eureka {
Build the base Eureka server URL + path
*/
buildEurekaUrl(callback = noop) {
this.lookupCurrentEurekaHost(eurekaHost => {
this.lookupCurrentEurekaHost((err, eurekaHost) => {
if (err) return callback(err);
const { port, servicePath, ssl } = this.config.eureka;
const host = ssl ? 'https' : 'http';
callback(`${host}://${eurekaHost}:${port}${servicePath}`);
callback(null, `${host}://${eurekaHost}:${port}${servicePath}`);
});
}

Expand Down Expand Up @@ -131,7 +146,11 @@ export default class Eureka {
done();
}
},
], callback);
], (err, ...rest) => {
if (err) this.logger.warn('Error starting the Eureka Client', err);
this.emit('started');
callback(err, ...rest);
});
}

/*
Expand Down Expand Up @@ -168,10 +187,11 @@ export default class Eureka {
this.config.instance.status = 'UP';
const connectionTimeout = setTimeout(() => {
this.logger.warn('It looks like it\'s taking a while to register with ' +
'Eureka. This usually means there is an issue connecting to the host ' +
'specified. Start application with NODE_DEBUG=request for more logging.');
'Eureka. This usually means there is an issue connecting to the host ' +
'specified. Start application with NODE_DEBUG=request for more logging.');
}, 10000);
this.buildEurekaUrl(eurekaUrl => {
this.buildEurekaUrl((err, eurekaUrl) => {
if (err) return callback(err);
request.post({
url: eurekaUrl + this.config.instance.app,
json: true,
Expand All @@ -184,8 +204,10 @@ export default class Eureka {
'registered with eureka: ',
`${this.config.instance.app}/${this.instanceId}`
);
this.emit('registered');
return callback(null);
} else if (error) {
this.logger.warn('Error registering with eureka client.', error);
return callback(error);
}
return callback(
Expand All @@ -199,7 +221,8 @@ export default class Eureka {
De-registers with the Eureka server and stops heartbeats.
*/
deregister(callback = noop) {
this.buildEurekaUrl(eurekaUrl => {
this.buildEurekaUrl((err, eurekaUrl) => {
if (err) return callback(err);
request.del({
url: `${eurekaUrl}${this.config.instance.app}/${this.instanceId}`,
gzip: true,
Expand All @@ -209,8 +232,10 @@ export default class Eureka {
'de-registered with eureka: ',
`${this.config.instance.app}/${this.instanceId}`
);
this.emit('deregistered');
return callback(null);
} else if (error) {
this.logger.warn('Error deregistering with eureka', error);
return callback(error);
}
return callback(
Expand All @@ -231,13 +256,18 @@ export default class Eureka {
}

renew() {
this.buildEurekaUrl(eurekaUrl => {
this.buildEurekaUrl((err, eurekaUrl) => {
if (err) {
this.logger.warn('eureka heartbeat FAILED, will retry', err);
return;
}
request.put({
url: `${eurekaUrl}${this.config.instance.app}/${this.instanceId}`,
gzip: true,
}, (error, response, body) => {
if (!error && response.statusCode === 200) {
this.logger.debug('eureka heartbeat success');
this.emit('heartbeat');
} else if (!error && response.statusCode === 404) {
this.logger.warn('eureka heartbeat FAILED, Re-registering app');
this.register();
Expand All @@ -260,7 +290,9 @@ export default class Eureka {
*/
startRegistryFetches() {
this.registryFetch = setInterval(() => {
this.fetchRegistry();
this.fetchRegistry(err => {
if (err) this.logger.warn('Error fetching registries', err);
});
}, this.config.eureka.registryFetchInterval);
}

Expand Down Expand Up @@ -296,7 +328,8 @@ export default class Eureka {
Retrieves all applications registered with the Eureka server
*/
fetchRegistry(callback = noop) {
this.buildEurekaUrl(eurekaUrl => {
this.buildEurekaUrl((err, eurekaUrl) => {
if (err) return callback(err);
request.get({
url: eurekaUrl,
headers: {
Expand All @@ -307,8 +340,10 @@ export default class Eureka {
if (!error && response.statusCode === 200) {
this.logger.debug('retrieved registry successfully');
this.transformRegistry(JSON.parse(body));
this.emit('registryUpdated');
return callback(null);
} else if (error) {
this.logger.warn('Error fetching registry', error);
return callback(error);
}
callback(new Error('Unable to retrieve registry from Eureka server'));
Expand Down Expand Up @@ -340,19 +375,27 @@ export default class Eureka {

/*
Transforms the given application and places in client cache. If an application
has a single instance, the instance is placed into the cache as an array of one
has a single instance, the instance is placed into the cache as an array of one
*/
transformApp(app, cache) {
if (app.instance.length) {
cache.app[app.name.toUpperCase()] = app.instance;
cache.vip[app.instance[0].vipAddress] = app.instance;
} else {
const instances = app.instance.filter((instance) => (this.validateInstance(instance)));
cache.app[app.name.toUpperCase()] = instances;
cache.vip[app.instance[0].vipAddress] = instances;
} else if (this.validateInstance(app.instance)) {
const instances = [app.instance];
cache.vip[app.instance.vipAddress] = instances;
cache.app[app.name.toUpperCase()] = instances;
}
}

/*
Returns true if instance filtering is disabled, or if the instance is UP
*/
validateInstance(instance) {
return (!this.config.eureka.filterUpInstances || instance.status === 'UP');
}

/*
Fetches the metadata using the built-in client and updates the instance
configuration with the hostname and IP address. If the value of the config
Expand Down Expand Up @@ -401,9 +444,9 @@ export default class Eureka {
*/
lookupCurrentEurekaHost(callback = noop) {
if (this.amazonDataCenter && this.config.eureka.useDns) {
this.locateEurekaHostUsingDns(resolvedHost => callback(resolvedHost));
this.locateEurekaHostUsingDns((err, resolvedHost) => callback(err, resolvedHost));
} else {
return callback(this.config.eureka.host);
return callback(null, this.config.eureka.host);
}
}

Expand All @@ -417,24 +460,25 @@ export default class Eureka {
locateEurekaHostUsingDns(callback = noop) {
const { ec2Region, host } = this.config.eureka;
if (!ec2Region) {
throw new Error(
return callback(new Error(
'EC2 region was undefined. ' +
'config.eureka.ec2Region must be set to resolve Eureka using DNS records.'
);
));
}
dns.resolveTxt(`txt.${ec2Region}.${host}`, (err, addresses) => {
if (err) {
throw new Error(
return callback(new Error(
`Error resolving eureka server list for region [${ec2Region}] using DNS: [${err}]`
);
));
}
const random = Math.floor(Math.random() * addresses[0].length);
dns.resolveTxt(`txt.${addresses[0][random]}`, (resolveErr, results) => {
if (resolveErr) {
throw new Error(`Error locating eureka server using DNS: [${resolveErr}]`);
this.logger.warn('Failed to locate DNS record for Eureka', resolveErr);
callback(new Error(`Error locating eureka server using DNS: [${resolveErr}]`));
}
this.logger.debug('Found Eureka Server @ ', results);
callback([].concat(...results).shift());
callback(null, [].concat(...results).shift());
});
});
}
Expand Down
1 change: 1 addition & 0 deletions src/defaultConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export default {
heartbeatInterval: 30000,
registryFetchInterval: 30000,
fetchRegistry: true,
filterUpInstances: true,
servicePath: '/eureka/v2/apps/',
ssl: false,
useDns: false,
Expand Down
Loading