postchain-client
Version:
Client library for accessing a Postchain node through REST.
247 lines • 12.3 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { sleep } from "./utils";
import { handleRequest } from "./httpUtil";
import * as logger from "../logger";
const isSuccessfullRequest = (statusCode) => statusCode >= 200 && statusCode < 300;
const hasClientError = (statusCode) => statusCode >= 400 && statusCode < 500;
const hasServerError = (statusCode) => statusCode >= 500 && statusCode < 600;
export function abortOnError({ method, path, config, postObject, }) {
return __awaiter(this, void 0, void 0, function* () {
return yield retryRequest({
method,
path,
config,
postObject,
validateStatusCode: statuscode => !hasServerError(statuscode),
});
});
}
export function tryNextOnError({ method, path, config, postObject, }) {
return __awaiter(this, void 0, void 0, function* () {
return yield retryRequest({
method,
path,
config,
postObject,
validateStatusCode: statusCode => !hasClientError(statusCode) && !hasServerError(statusCode),
});
});
}
export const errorMessages = {
NO_CONSENSUS: "Nodes were not able to reach a consensus",
};
function calculateBftMajorityThreshold(endpointPoolLength) {
return endpointPoolLength - (endpointPoolLength - 1) / 3;
}
export function queryMajority({ method, path, config, postObject, }) {
return __awaiter(this, void 0, void 0, function* () {
// Set up constants
const bftMajorityThreshold = calculateBftMajorityThreshold(config.endpointPool.length);
const failureThreshold = config.endpointPool.length - bftMajorityThreshold + 1;
const { nodeManager } = config;
const timeout = 15000;
const availableNodes = nodeManager.getAvailableNodes();
// If the available nodes are less than bfthMajorityThreshold, set all nodes as reachable again.
if (availableNodes.length < bftMajorityThreshold) {
nodeManager.makeAllNodesAvailable();
}
const outcomes = [];
const promises = nodeManager.getAvailableNodes().map(node => {
return handleRequest(method, path, node.url, postObject)
.then(response => {
const { statusCode } = response;
if (statusCode && isSuccessfullRequest(statusCode)) {
outcomes.push({ type: "SUCCESS", result: response });
}
else {
if (statusCode && hasServerError(statusCode)) {
nodeManager.makeNodeUnavailable(node.url);
}
outcomes.push({ type: "FAILURE", result: response });
}
return response;
})
.catch((error) => {
outcomes.push({ type: "ERROR", result: error });
return error;
});
});
let remainingPromises = promises;
// Wait for the necessary majority of outcomes to come back or for the timeout to be reached, whichever comes first.
for (let i = 0; i < bftMajorityThreshold; i++) {
remainingPromises = yield racePromisesWithTimeout(remainingPromises, timeout);
}
// Evaluate the outcomes
const successfullOutcomes = outcomes.filter(outcome => outcome.type === "SUCCESS");
const failureOutcomes = outcomes.filter(outcome => outcome.type === "FAILURE");
const errorOutcomes = outcomes.filter(outcome => outcome.type === "ERROR");
// group all of the same responses together into groups and count each groups size
const groupedSuccessfullResponses = groupResponses(successfullOutcomes);
// validate the responses
// eslint-disable-next-line no-constant-condition
while (true) {
// Successfull majority response was found
if (groupedSuccessfullResponses.maxNumberOfEqualResponses() >= bftMajorityThreshold) {
if (groupedSuccessfullResponses.numberOfDistinctResponses() > 1) {
logger.warning(`Got disagreeing responses, but could still reach BFT majority`);
}
return groupedSuccessfullResponses.majorityResponse();
}
// Too many failures or errors to be able to reach a consensus
// If the number of failures and errors is greater than the failure threshold, return first failure, if any, otherwise throw the first error.
if ([...failureOutcomes, ...errorOutcomes].length >= failureThreshold) {
if (failureOutcomes.length > 0) {
return failureOutcomes[0].result;
}
else {
throw errorOutcomes[0].result;
}
}
// If all nodes have responded without a majority, throw an error.
if (outcomes.length >= config.endpointPool.length) {
throw new Error(errorMessages.NO_CONSENSUS);
}
// Wait for more one more response to come back. ( we need to set a timeout here)
remainingPromises = yield racePromisesWithTimeout(remainingPromises, timeout);
}
});
}
export function singleEndpoint({ method, path, config, postObject, }) {
return __awaiter(this, void 0, void 0, function* () {
let statusCode = null;
let rspBody = null;
let error = null;
let transactionTimestamp = undefined;
const endpoint = config.nodeManager.getNode();
if (!endpoint) {
throw new Error("Cannot get endpoint. Node not found!");
}
for (let attempt = 0; attempt < config.attemptsPerEndpoint; attempt++) {
const response = yield handleRequest(method, path, endpoint.url, postObject);
if (response) {
({ error, statusCode, rspBody, transactionTimestamp } = response);
}
const isError = statusCode ? hasServerError(statusCode) || hasClientError(statusCode) : false;
if (!isError && !error) {
return { error, statusCode, rspBody, transactionTimestamp };
}
logger.info(`${method} request failed on ${endpoint.url}. Attempt: ${attempt + 1} / ${config.attemptsPerEndpoint}`);
yield sleep(config.attemptInterval);
}
return { error, statusCode, rspBody, transactionTimestamp };
});
}
export function retryRequest({ method, path, config, postObject, validateStatusCode, }) {
var _a, _b, _c;
return __awaiter(this, void 0, void 0, function* () {
let statusCode = null;
let rspBody = null;
let error = null;
let transactionTimestamp = undefined;
const { nodeManager } = config;
for (const endpoint of nodeManager.getAvailableNodes()) {
for (let attempt = 0; attempt < config.attemptsPerEndpoint; attempt++) {
const response = yield handleRequest(method, path, endpoint.url, postObject);
error = (_a = response === null || response === void 0 ? void 0 : response.error) !== null && _a !== void 0 ? _a : null;
statusCode = (_b = response === null || response === void 0 ? void 0 : response.statusCode) !== null && _b !== void 0 ? _b : null;
rspBody = (_c = response === null || response === void 0 ? void 0 : response.rspBody) !== null && _c !== void 0 ? _c : null;
transactionTimestamp = response === null || response === void 0 ? void 0 : response.transactionTimestamp;
const isStatusCodeValid = statusCode ? validateStatusCode(statusCode) : false;
const isServerError = statusCode ? hasServerError(statusCode) : false;
if (isStatusCodeValid && !error) {
// Find a way to have this handled more elegantly in the node manager.
if (nodeManager.stickedNode !== endpoint) {
nodeManager.setStickyNode(endpoint);
}
return { error, statusCode, rspBody, transactionTimestamp };
}
if (isServerError) {
nodeManager.makeNodeUnavailable(endpoint.url);
}
logger.info(`${method} request failed on ${endpoint.url}. Attempt: ${attempt + 1} / ${config.attemptsPerEndpoint}`);
yield sleep(config.attemptInterval);
}
}
return { error, statusCode, rspBody, transactionTimestamp };
});
}
export function racePromisesWithTimeout(promises, timeout) {
return __awaiter(this, void 0, void 0, function* () {
let remainingPromises = [];
try {
const resolvedPromise = yield Promise.race([createTimeoutPromise(timeout), ...promises]);
const promisesWithState = yield checkStateOfPromises(promises);
const indexOfpromiseToRemove = promisesWithState.findIndex(p => JSON.stringify(p.value) === JSON.stringify(resolvedPromise));
// Remove the promise that resolved from the list of promises
promisesWithState.splice(indexOfpromiseToRemove, 1);
// Remove the state before returning the promises.
remainingPromises = promisesWithState.map(p => p.value);
return remainingPromises;
}
catch (error) {
if (error instanceof Error && error.message === "Timeout exceeded") {
throw error;
}
return remainingPromises;
}
});
}
export function createTimeoutPromise(timeoutMs) {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((_, reject) => {
setTimeout(() => {
reject(new Error("Timeout exceeded"));
}, timeoutMs);
});
});
}
export const checkStateOfPromises = (promises) => __awaiter(void 0, void 0, void 0, function* () {
const promiseList = [];
const pendingState = { status: "pending" };
for (const p of promises) {
promiseList.push(yield Promise.race([p, pendingState]).then(value => value === pendingState ? Object.assign(Object.assign({}, pendingState), { value: p }) : { status: "fulfilled", value }, reason => ({ status: "rejected", value: reason })));
}
return promiseList;
});
const stableStringify = (obj) => {
if (obj && typeof obj === "object" && !Array.isArray(obj)) {
return JSON.stringify(obj, Object.keys(obj).sort());
}
return JSON.stringify(obj);
};
export const groupResponses = (responses) => {
const responseMap = responses.reduce((acc, response) => {
const key = stableStringify(response.result);
acc[key] = acc[key]
? { response: acc[key].response, count: acc[key].count + 1 }
: { response, count: 1 };
return acc;
}, {});
const distinctResponses = Object.values(responseMap).sort((a, b) => b.count - a.count);
// Returns the count of the most common response
const maxNumberOfEqualResponses = () => {
return distinctResponses.length > 0 ? distinctResponses[0].count : 0;
};
// Returns the number of distinct responses
const numberOfDistinctResponses = () => {
return distinctResponses.length;
};
// Returns the most common response
const majorityResponse = () => {
return distinctResponses[0].response.result;
};
return {
maxNumberOfEqualResponses,
numberOfDistinctResponses,
majorityResponse,
};
};
//# sourceMappingURL=failoverStrategies.js.map