Skip to content

Instantly share code, notes, and snippets.

@SlootSantos
Created August 23, 2018 15:34
Show Gist options
  • Save SlootSantos/10de92d97d1b0b0596b9489519afc186 to your computer and use it in GitHub Desktop.
Save SlootSantos/10de92d97d1b0b0596b9489519afc186 to your computer and use it in GitHub Desktop.
const server = require('express')();
const axios = require('axios');
const createClient = require('../client');
// very same thing as in databaseService.js
const serviceDefinition = {
name: 'anyService',
ipv4: '127.0.0.1',
port: 3001
};
// create a new map to store the locations we care about
const serviceLocations = new Map();
// first start the server and
// then register at the discovery service
// then fetch the database service and store it!
server
.get('/', async (_, res) => {
const dbService = serviceLocations.get('databaseService');
const response = await axios.get(`http://${dbService.ipv4}:${dbService.port}/db`);
res.send(response.data);
})
.listen(serviceDefinition.port, () => console.log(`AnyService listens on port ${serviceDefinition.port}`));
(async () => {
const client = await createClient();
await client.register(serviceDefinition);
const { registrations: [dbService] } = await client.fetchServiceLocation({
registrations: [{
name: 'databaseService'
}]
});
serviceLocations.set('databaseService', dbService);
})();
const REGISTRY_PROTO_PATH = `${__dirname}/protos/registry.proto`;
const { promisify } = require('util');
const grpc = require('grpc');
const protoLoader = require('@grpc/proto-loader');
const packageDefinition = protoLoader.loadSync(REGISTRY_PROTO_PATH);
const { registry: registryProto } = grpc.loadPackageDefinition(packageDefinition);
module.exports = async () => {
const client = new registryProto.Registry('localhost:3333', grpc.credentials.createInsecure());
const promisableFns = ['register', 'unregister', 'fetchServiceLocation'];
// I wrapped the client instance in a proxy because...
// I can.
// I wanted to use promise, and because I didn't want to
// write an astonishing number of 4 lines to promisify all of those functions
// Looks kinda sexy to use a proxy.. doesn't it?
// WARNING: though it looks sexy, please be aware that we are creating a new
// function instance on every property look up.
// "promisify(target[property])" creates a new function everytime!
const clientProxy = new Proxy(client, {
get: (target, property) => {
return promisableFns.includes(property)
? promisify(target[property])
: target[property];
}
});
return clientProxy;
};
const server = require('express')();
const createClient = require('../client');
// well.. this is on our localmachine
// hence we know for sure where what location this process has.
// in production you'd want to come up w/ something more flexible
const serviceDefinition = {
name: 'databaseService',
ipv4: '127.0.0.1',
port: 3000
};
// just a example response that our db could return
const exampleResponse = {
entries: [{
name: 'what',
value: 'ever you like'
},
{
name: 'what',
value: 'ever you like'
}]
};
// first start the server and
// then register at the discovery service
server
.get('/db', (_, res) => {
res.send(exampleResponse);
})
.listen(serviceDefinition.port, () => console.log(`DB service listens on ${serviceDefinition.port}`));
(async () => {
const client = await createClient();
const response = await client.register(serviceDefinition);
})();
// we need to tell the compiler which version we're using
syntax = "proto3";
package registry;
// here we define the service and methods we're going to use later on
// to do the registration process
service Registry {
rpc Register (Registration) returns (RegisterResponse);
rpc Unregister (Registration) returns (RegisterResponse);
rpc fetchServiceLocation (RegistrationFetchRequest) returns (RegistrationList);
}
// we define what a registration needs to have
// all optional and self explanatory
message Registration {
optional string name = 1;
optional string ipv4 = 2;
optional string port = 3;
optional string domain = 4;
}
// a list/array of registrations
message RegistrationList {
repeated Registration registrations = 1;
}
// w/ this message we define what the server needs
// to know what you want to have
message RegistrationFetchRequest {
repeated Registration registrations = 1;
optional bool fetchAll = 2;
}
// simple response when you sign up
message RegisterResponse {
required string message = 1;
}
// let's create a variable for the path of the .proto file
const REGISTRY_PROTO_PATH = `${__dirname}/protos/registry.proto`;
// import grpc and the loader
const grpc = require('grpc');
const protoLoader = require('@grpc/proto-loader');
// here we actually load the proto content
const packageDefinition = protoLoader.loadSync(REGISTRY_PROTO_PATH);
// and then we create the definition of it
// so that we can go ahead and use it
const { registry: registryProto } = grpc.loadPackageDefinition(packageDefinition);
// we use Map because.. well why not. It's a perfect use case for a Map
const registryStore = new Map();
// to save a bunch of boilerplate code
// we create a "higher order function"
// that takes a function which will define what to do w/ the request
// aka put it into the map, remove it from there, return all entries, etc.
const registryFunction = mapOperatorFn => ({ request }, rpcCallback) => {
let error;
let payload;
try {
payload = mapOperatorFn(request);
} catch (e) {
error = e;
}
rpcCallback(error, payload);
};
// here we simply create the operator function and pass it
// into our registryFunction wrapper
// we set the properties for every given service and identify them by their name
// note: even if there is an entry already we simply override it
// since we believe that the latest registration is always the correct one
const register = registryFunction(({ name, ipv4, port }) => {
registryStore.set(name, { ipv4, port });
return {
message: `${name} was registered under ${ipv4}:${port}`
};
});
// this function simply deletes the registration from the map
const unregister = registryFunction(({ name }) => {
registryStore.delete(name);
return {
message: `${name} was unregistered`
};
});
// this function let's you fetch one or more registered services
// e.g.: if you care about the database-services location
// you'd just send an array of locations you're interested in: ['databaseService']
// BE AWARE that looping through ALL given services in the Map AND then looking
// in the array whether it's what you requested could give you
// a performance hit w/ big datasets.
// Map look ups are very efficient => O(1)
// vs. the looping through that we're doing => O(n) + O(n) = O(n^2)
const fetchServiceLocation = registryFunction(({ registrations: requestedRegistrations = [], fetchAll }) => {
const reqRegistrationNames = [...requestedRegistrations].map(({ name }) => name);
const registrations = [];
for ([name, properties] of registryStore.entries()) {
if (reqRegistrationNames.includes(name) || fetchAll) {
registrations.push({
name,
...properties
})
}
}
return { registrations };
});
// in this iife we simply start the grpc server and define the services
// to match our .proto definition
(() => {
const server = new grpc.Server();
server.addService(registryProto.Registry.service, {
register,
unregister,
fetchServiceLocation
});
server.bind('0.0.0.0:3333', grpc.ServerCredentials.createInsecure());
server.start();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment