UNPKG

postchain-client

Version:

Client library for accessing a Postchain node through REST.

247 lines 12.3 kB
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