Skip to content
This repository was archived by the owner on Sep 25, 2020. It is now read-only.

Commit 058e678

Browse files
committed
Merge pull request #22 from uber/feature/discovery
Feature/discovery
2 parents 51243b6 + 6936ad4 commit 058e678

27 files changed

+398
-117
lines changed

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,38 @@ Command-line tools for Ringpop
2828
2929
-h, --help output usage information
3030
-V, --version output the version number
31+
32+
Discovery:
33+
34+
Most of the commands can discover the ring via
35+
a discoverUri like this: 'ringpop://127.0.0.1:3000'.
36+
If no protocol is specified 'ringpop://' will be
37+
used.
38+
39+
Supported protocols are:
40+
41+
- ringpop://
42+
Discover the ring by connecting to a host of
43+
the ring.
44+
45+
Example: ringpop://127.0.0.1:3000
46+
47+
- file://
48+
Discover the ring by reading a json file
49+
containing an array of host:port combinations
50+
51+
Example: file:///absolute/path/to/file
52+
Example: file://./relative/path
53+
File content: ["127.0.0.1:3000"]
54+
55+
- hyperbahn://
56+
Discover the ring by querying hyperbahn for
57+
the members of a service. When no hyperbahn
58+
ip and port are given 127.0.0.1:21300 will be
59+
used.
60+
61+
Example: hyperbahn:///ringpop
62+
Example: hyperbahn://hyperbahn-ip:port/ringpop
3163
```
3264

3365
## Tests

checksums.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,19 @@ function main() {
2929
program
3030
.description('Prints membership checksums')
3131
.option('--tchannel-v1')
32-
.usage('[options] <hostport or bootstrapfile>');
32+
.usage('[options] <discoveryUri>');
3333
program.parse(process.argv);
3434

35-
var address = program.args[0];
35+
var discoveryUri = program.args[0];
3636

37-
if (!address) {
38-
console.error('Error: hostport or bootstrapfile is required');
37+
if (!discoveryUri) {
38+
console.error('Error: discoveryUri is required');
3939
process.exit(1);
4040
}
4141

4242
var clusterManager = new ClusterManager({
4343
useTChannelV1: program.useTChannelV1,
44-
coordAddr: address
44+
discoveryUri: discoveryUri
4545
});
4646
clusterManager.fetchStats(function onStats(err) {
4747
if (err) {

commands.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,22 @@
1919
// THE SOFTWARE.
2020
'use strict';
2121

22-
function ReuseCommand(tchannelV1, coordinator, member, limit) {
22+
function ReuseCommand(tchannelV1, discoveryUri, member, limit) {
2323
this.useTChannelV1 = tchannelV1;
24-
this.coordinator = coordinator;
24+
this.discoveryUri = discoveryUri;
2525
this.member = member;
2626
this.limit = limit;
2727
}
2828

29-
function StatusCommand(tchannelV1, coordinator, quiet) {
29+
function StatusCommand(tchannelV1, discoveryUri, quiet) {
3030
this.useTChannelV1 = tchannelV1;
31-
this.coordinator = coordinator;
31+
this.discoveryUri = discoveryUri;
3232
this.quiet = quiet;
3333
}
3434

35-
function PartitionCommand(tchannelV1, coordinatorOrFile, quiet) {
35+
function PartitionCommand(tchannelV1, discoveryUri, quiet) {
3636
this.useTChannelV1 = tchannelV1;
37-
this.coordinatorOrFile = coordinatorOrFile;
37+
this.discoveryUri = discoveryUri;
3838
this.quiet = quiet;
3939
}
4040

count.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,13 @@ function main() {
3131
.option('-m --members', 'Count of members')
3232
.option('-p --partitions', 'Count of partitions')
3333
.option('--tchannel-v1')
34-
.usage('[options] <hostport or bootstrapfile>');
34+
.usage('[options] <discoveryUri>');
3535
program.parse(process.argv);
3636

37-
var coord = program.args[0];
37+
var discoveryUri = program.args[0];
3838

39-
if (!coord) {
40-
console.error('Error: hostport or path to bootstrap file is required');
39+
if (!discoveryUri) {
40+
console.error('Error: discoveryUri is required');
4141
process.exit(1);
4242
}
4343

@@ -48,7 +48,7 @@ function main() {
4848

4949
var clusterManager = new ClusterManager({
5050
useTChannelV1: program.tchannelV1,
51-
coordAddr: coord
51+
discoveryUri: discoveryUri
5252
});
5353
clusterManager.fetchStats(function onStats(err) {
5454
if (err) {

dump.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,13 @@ function main() {
3030
.description('Dumps membership information to file')
3131
.option('-f --file <file>', 'File to dump to')
3232
.option('--tchannel-v1')
33-
.usage('[options] <hostport or bootstrapfile>');
33+
.usage('[options] <discoveryUri>');
3434
program.parse(process.argv);
3535

36-
var coord = program.args[0];
36+
var discoveryUri = program.args[0];
3737

38-
if (!coord) {
39-
console.error('Error: hostport or path to bootstrap file is required');
38+
if (!discoveryUri) {
39+
console.error('Error: discovery discoveryUri is required');
4040
process.exit(1);
4141
}
4242

@@ -47,7 +47,7 @@ function main() {
4747

4848
var clusterManager = new ClusterManager({
4949
useTChannelV1: program.tchannelV1,
50-
coordAddr: coord
50+
discoveryUri: discoveryUri
5151
});
5252
clusterManager.fetchStats(onStats);
5353

lib/cluster.js

Lines changed: 11 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,15 @@ var Stats = require('./stats.js');
2929
var format = require('util').format;
3030
var startsWith = require('./util.js').startsWith;
3131

32+
var discover = require('./discover').discover;
33+
3234
function Cluster(opts) {
3335
opts = opts || {
3436
dumpTo: 'ringpop-admin-stats.dump'
3537
};
3638

3739
this.useTChannelV1 = opts.useTChannelV1;
38-
this.coordAddr = opts.coordAddr;
40+
this.discoveryUri = opts.discoveryUri;
3941
this.adminClient = new AdminClient({
4042
useTChannelV1: this.useTChannelV1
4143
});
@@ -84,29 +86,8 @@ Cluster.prototype.getPartitionCount = function getPartitionCount() {
8486
};
8587

8688
Cluster.prototype.getSeedList = function getSeedList(callback) {
87-
var self = this;
88-
// figure out if the coordinator is a file
89-
fs.exists(self.coordAddr, function (exists) {
90-
if (exists) {
91-
// treat the coordinator as a bootstrap file
92-
fs.readFile(self.coordAddr, function (err, body) {
93-
if (err) {
94-
return callback(err);
95-
}
96-
97-
try {
98-
body = JSON.parse(body);
99-
} catch (e) {
100-
return callback(e);
101-
}
102-
103-
// seed the list with the hosts in the bootstrap file
104-
return callback(null, body);
105-
});
106-
} else {
107-
return callback(null, [self.coordAddr]);
108-
}
109-
});
89+
// use the discover library to fetch the seedlist
90+
discover(this.discoveryUri, callback);
11091
};
11192

11293
Cluster.prototype.fetchStats = function fetchStats(callback) {
@@ -128,7 +109,8 @@ Cluster.prototype.fetchStats = function fetchStats(callback) {
128109

129110
this.getSeedList(function (err, seeds) {
130111
if (err) {
131-
return callback(err);
112+
callback(err);
113+
return;
132114
}
133115
queueMembers(seeds);
134116
});
@@ -179,7 +161,7 @@ Cluster.prototype.fetchStats = function fetchStats(callback) {
179161

180162
function onComplete(err, allStats) {
181163
if (allStats.length === 0) {
182-
var addr = self.coordAddr;
164+
var addr = self.discoveryUri;
183165
var msg = format('Failed to connect to ringpop listening on %s.', addr);
184166

185167
// Check if user tries to connect to localhost or 127.0.0.1.
@@ -221,13 +203,13 @@ Cluster.prototype.fetchStats = function fetchStats(callback) {
221203

222204
Cluster.prototype.lookup = function lookup(key, callback) {
223205
var self = this;
224-
this.fetchStats(function onStats(err) {
206+
207+
this.getSeedList(function (err, seeds) {
225208
if (err) {
226209
callback(err);
227210
return;
228211
}
229-
230-
self.adminClient.lookup(self.coordAddr, key, callback);
212+
self.adminClient.lookup(seeds[0], key, callback);
231213
});
232214
};
233215

lib/discover/file.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
var fs = require('fs');
2+
3+
module.exports.discover = function fileDiscover(urlObj, uri, callback) {
4+
var filename = urlObj.host + urlObj.pathname;
5+
fs.readFile(filename, function (err, data) {
6+
if (err) {
7+
callback(err);
8+
return;
9+
}
10+
11+
// assume the file is a json list of hosts
12+
try {
13+
data = JSON.parse(data);
14+
} catch (e) {
15+
callback(e);
16+
return;
17+
}
18+
19+
// TODO add type tests for the data read from the file
20+
callback(null, data);
21+
return;
22+
});
23+
};

lib/discover/hyperbahn.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
var TChannelAsThrift = require('tchannel/as/thrift');
2+
var TChannel = require('tchannel');
3+
var path = require('path');
4+
5+
module.exports.discover = function hyperbahnDiscover(urlObj, uri, callback) {
6+
var host;
7+
if (urlObj.host) {
8+
host = urlObj.host;
9+
} else {
10+
host = '127.0.0.1:21300';
11+
}
12+
13+
var client = TChannel();
14+
var hChannel = client.makeSubChannel({
15+
serviceName: 'hyperbahn',
16+
peers: [host]
17+
});
18+
var tchannelAsThrift = TChannelAsThrift({
19+
channel: hChannel,
20+
entryPoint: path.join(__dirname, 'hyperbahn.thrift')
21+
});
22+
23+
tchannelAsThrift.waitForIdentified({
24+
host: host
25+
}, function onIdentified(err) {
26+
if (err) {
27+
callback(err);
28+
return;
29+
}
30+
31+
tchannelAsThrift.request({
32+
host: host,
33+
timeout: 5000,
34+
35+
serviceName: 'hyperbahn',
36+
headers: {
37+
cn: 'hyperbahn'
38+
},
39+
hasNoParent: true
40+
}).send('Hyperbahn::discover', {}, {
41+
query: {
42+
serviceName: urlObj.pathname.substr(1) // remove first slash
43+
}
44+
}, function (err, resp) {
45+
if (err) {
46+
callback(err);
47+
return;
48+
}
49+
50+
// parse the hyperbahn response
51+
var seeds;
52+
try {
53+
seeds = resp.body.peers.map(function(peer) {
54+
switch (peer.ip.type) {
55+
case 'ipv4':
56+
return [intToIP(peer.ip.ipv4), peer.port].join(':');
57+
default:
58+
throw new Error('Hyperbahn returned peers with an invalid IP type: \'' + peer.ip.type + '\'');
59+
}
60+
});
61+
} catch (e) {
62+
callback(e);
63+
return;
64+
}
65+
66+
callback(null, seeds);
67+
});
68+
});
69+
};
70+
71+
function intToIP(int) {
72+
var part1 = int & 255;
73+
var part2 = ((int >> 8) & 255);
74+
var part3 = ((int >> 16) & 255);
75+
var part4 = ((int >> 24) & 255);
76+
77+
return part4 + "." + part3 + "." + part2 + "." + part1;
78+
}

lib/discover/hyperbahn.thrift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
exception NoPeersAvailable {
2+
1: required string message
3+
2: required string serviceName
4+
}
5+
6+
exception InvalidServiceName {
7+
1: required string message
8+
2: required string serviceName
9+
}
10+
11+
struct DiscoveryQuery {
12+
1: required string serviceName
13+
}
14+
15+
union IpAddress {
16+
1: i32 ipv4
17+
}
18+
19+
struct ServicePeer {
20+
1: required IpAddress ip
21+
2: required i32 port
22+
}
23+
24+
struct DiscoveryResult {
25+
1: required list<ServicePeer> peers
26+
}
27+
28+
service Hyperbahn {
29+
DiscoveryResult discover(
30+
1: required DiscoveryQuery query
31+
) throws (
32+
1: NoPeersAvailable noPeersAvailable
33+
2: InvalidServiceName invalidServiceName
34+
)
35+
}

lib/discover/index.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
var url = require('url');
2+
3+
module.exports.protocols = {
4+
"hyperbahn:": require('./hyperbahn').discover,
5+
"file:": require('./file').discover,
6+
"ringpop:": require('./ringpop').discover,
7+
// by default fallback on ringpop discovery
8+
default: require('./ringpop').discover
9+
};
10+
11+
// find a list of initial nodes to connect to based on the discover string
12+
module.exports.discover = function discover(uri, callback) {
13+
var urlObj = url.parse(uri);
14+
var handler = module.exports.protocols[urlObj.protocol];
15+
if (typeof handler === 'undefined') {
16+
handler = module.exports.protocols.default;
17+
}
18+
19+
handler(urlObj, uri, callback);
20+
};

0 commit comments

Comments
 (0)