@globalworldwide/grpc-resolvers
Version:
Custom resolvers for @grpc/grpc-js
109 lines • 4.33 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.K8SResolver = void 0;
const tslib_1 = require("tslib");
const grpc = tslib_1.__importStar(require("@grpc/grpc-js"));
const uri_parser_js_1 = require("@grpc/grpc-js/build/src/uri-parser.js");
const k8sWatch = tslib_1.__importStar(require("./internal/k8s-watch.js"));
const logging_js_1 = require("./internal/logging.js");
const well_known_channel_options_js_1 = require("./well-known-channel-options.js");
const logger = (0, logging_js_1.makeLogger)('k8s-resolver');
class K8SResolver {
static getDefaultAuthority(target) {
return K8SResolver.parseHostPort(target)[0];
}
static parseHostPort(target) {
const [host, port] = target.path.split(':');
return [host, port];
}
#target;
#listener;
#watch;
#defaultResolutionError;
#serviceName;
#portName;
// readonly #channelOptions: grpc.ChannelOptions
#lastNotifyTimeMS = 0;
#timeoutId = undefined;
#hasEndpoints = false;
#watchListenerRegistered = false;
constructor(target, listener, channelOptions) {
this.#target = target;
this.#listener = listener;
this.#watch = this.onWatchChange.bind(this);
this.#defaultResolutionError = {
code: grpc.status.UNAVAILABLE,
details: `No endpoints available for target ${(0, uri_parser_js_1.uriToString)(this.#target)}`,
metadata: new grpc.Metadata(),
};
this.#serviceName = K8SResolver.getDefaultAuthority(this.#target);
// MSED - I would like this to use the port from the target first and foremost,
// but that doesn't work for us yet. Will switch once we drop the dns resolver.
this.#portName = channelOptions[well_known_channel_options_js_1.WellKnownChannelOptions.portName] ?? 'grpc';
// this.#channelOptions = channelOptions
}
updateResolution() {
if (!this.#watchListenerRegistered) {
k8sWatch.addListener(this.#serviceName, this.#watch);
this.#watchListenerRegistered = true;
this.#hasEndpoints = this.hasEndpoints();
}
// do not notify until we've seen an update from the k8s watch at least once
if (!this.#hasEndpoints) {
return;
}
// If the service is not actually running, then grpc-js starts calling updateResolution
// as fast as we answer. In order to stop that from turning into a busy loop, we never
// resolve more than once every 250ms
if (!this.#timeoutId) {
const nowMS = Date.now();
const delayMS = Math.max(0, 250 - (nowMS - this.#lastNotifyTimeMS));
this.#timeoutId = setTimeout(this.onTimeout.bind(this), delayMS);
}
}
destroy() {
clearTimeout(this.#timeoutId);
this.#timeoutId = undefined;
this.#lastNotifyTimeMS = 0;
this.#watchListenerRegistered = false;
k8sWatch.removeListener(this.#serviceName, this.#watch);
}
onWatchChange() {
this.#hasEndpoints = true;
if (this.#timeoutId) {
clearTimeout(this.#timeoutId);
this.#timeoutId = undefined;
}
this.notify();
}
onTimeout() {
this.#timeoutId = undefined;
this.notify();
}
notify() {
this.#lastNotifyTimeMS = Date.now();
const endpoints = this.getEndpoints();
logger.debug?.(`mapped ${this.#serviceName} ${this.#portName}`, endpoints);
if (endpoints.length > 0) {
this.#listener.onSuccessfulResolution(endpoints, { loadBalancingConfig: [{ round_robin: {} }], methodConfig: [] }, null, null, {});
}
else {
this.#listener.onError(this.#defaultResolutionError);
}
}
hasEndpoints() {
return k8sWatch
.getEndpoints(this.#serviceName)
.some((e) => e.addresses.some((a) => a.portName === this.#portName));
}
getEndpoints() {
return k8sWatch
.getEndpoints(this.#serviceName)
.map((e) => ({
addresses: e.addresses.filter((a) => a.portName === this.#portName),
}))
.filter((e) => e.addresses.length > 0);
}
}
exports.K8SResolver = K8SResolver;
//# sourceMappingURL=k8s-resolver.js.map