throttling diagnostics and request timeouts
This commit is contained in:
parent
ddf9171c5b
commit
421e613f92
@ -3202,6 +3202,13 @@ export enum TalerErrorCode {
|
|||||||
*/
|
*/
|
||||||
WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK = 7012,
|
WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK = 7012,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An HTTP request made by the wallet timed out.
|
||||||
|
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
|
||||||
|
* (A value of 0 indicates that the error is generated client-side).
|
||||||
|
*/
|
||||||
|
WALLET_HTTP_REQUEST_TIMEOUT = 7013,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* End of error code range.
|
* End of error code range.
|
||||||
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
|
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
|
||||||
|
@ -29,6 +29,7 @@ import { RequestThrottler } from "../util/RequestThrottler";
|
|||||||
import Axios from "axios";
|
import Axios from "axios";
|
||||||
import { OperationFailedError, makeErrorDetails } from "../operations/errors";
|
import { OperationFailedError, makeErrorDetails } from "../operations/errors";
|
||||||
import { TalerErrorCode } from "../TalerErrorCode";
|
import { TalerErrorCode } from "../TalerErrorCode";
|
||||||
|
import { URL } from "../util/url";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation of the HTTP request library interface for node.
|
* Implementation of the HTTP request library interface for node.
|
||||||
@ -50,8 +51,20 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
|||||||
body: any,
|
body: any,
|
||||||
opt?: HttpRequestOptions,
|
opt?: HttpRequestOptions,
|
||||||
): Promise<HttpResponse> {
|
): Promise<HttpResponse> {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
if (this.throttlingEnabled && this.throttle.applyThrottle(url)) {
|
if (this.throttlingEnabled && this.throttle.applyThrottle(url)) {
|
||||||
throw Error("request throttled");
|
throw OperationFailedError.fromCode(
|
||||||
|
TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
|
||||||
|
`request to origin ${parsedUrl.origin} was throttled`,
|
||||||
|
{
|
||||||
|
requestMethod: method,
|
||||||
|
requestUrl: url,
|
||||||
|
throttleStats: this.throttle.getThrottleStats(url),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let timeout: number | undefined;
|
||||||
|
if (typeof opt?.timeout?.d_ms === "number") {
|
||||||
|
timeout = opt.timeout.d_ms;
|
||||||
}
|
}
|
||||||
const resp = await Axios({
|
const resp = await Axios({
|
||||||
method,
|
method,
|
||||||
@ -61,6 +74,7 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
|||||||
validateStatus: () => true,
|
validateStatus: () => true,
|
||||||
transformResponse: (x) => x,
|
transformResponse: (x) => x,
|
||||||
data: body,
|
data: body,
|
||||||
|
timeout,
|
||||||
});
|
});
|
||||||
|
|
||||||
const respText = resp.data;
|
const respText = resp.data;
|
||||||
|
@ -43,7 +43,7 @@ import {
|
|||||||
WALLET_CACHE_BREAKER_CLIENT_VERSION,
|
WALLET_CACHE_BREAKER_CLIENT_VERSION,
|
||||||
WALLET_EXCHANGE_PROTOCOL_VERSION,
|
WALLET_EXCHANGE_PROTOCOL_VERSION,
|
||||||
} from "./versions";
|
} from "./versions";
|
||||||
import { getTimestampNow } from "../util/time";
|
import { getTimestampNow, Duration } from "../util/time";
|
||||||
import { compare } from "../util/libtoolVersion";
|
import { compare } from "../util/libtoolVersion";
|
||||||
import { createRecoupGroup, processRecoupGroup } from "./recoup";
|
import { createRecoupGroup, processRecoupGroup } from "./recoup";
|
||||||
import { TalerErrorCode } from "../TalerErrorCode";
|
import { TalerErrorCode } from "../TalerErrorCode";
|
||||||
@ -96,6 +96,10 @@ async function setExchangeError(
|
|||||||
await ws.db.mutate(Stores.exchanges, baseUrl, mut);
|
await ws.db.mutate(Stores.exchanges, baseUrl, mut);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getExchangeRequestTimeout(e: ExchangeRecord): Duration {
|
||||||
|
return { d_ms: 5000 };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch the exchange's /keys and update our database accordingly.
|
* Fetch the exchange's /keys and update our database accordingly.
|
||||||
*
|
*
|
||||||
@ -117,7 +121,9 @@ async function updateExchangeWithKeys(
|
|||||||
const keysUrl = new URL("keys", baseUrl);
|
const keysUrl = new URL("keys", baseUrl);
|
||||||
keysUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
|
keysUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
|
||||||
|
|
||||||
const resp = await ws.http.get(keysUrl.href);
|
const resp = await ws.http.get(keysUrl.href, {
|
||||||
|
timeout: getExchangeRequestTimeout(existingExchangeRecord),
|
||||||
|
});
|
||||||
const exchangeKeysJson = await readSuccessResponseJsonOrThrow(
|
const exchangeKeysJson = await readSuccessResponseJsonOrThrow(
|
||||||
resp,
|
resp,
|
||||||
codecForExchangeKeysJson(),
|
codecForExchangeKeysJson(),
|
||||||
@ -303,7 +309,10 @@ async function updateExchangeWithTermsOfService(
|
|||||||
Accept: "text/plain",
|
Accept: "text/plain",
|
||||||
};
|
};
|
||||||
|
|
||||||
const resp = await ws.http.get(reqUrl.href, { headers });
|
const resp = await ws.http.get(reqUrl.href, {
|
||||||
|
headers,
|
||||||
|
timeout: getExchangeRequestTimeout(exchange),
|
||||||
|
});
|
||||||
const tosText = await readSuccessResponseTextOrThrow(resp);
|
const tosText = await readSuccessResponseTextOrThrow(resp);
|
||||||
const tosEtag = resp.headers.get("etag") || undefined;
|
const tosEtag = resp.headers.get("etag") || undefined;
|
||||||
|
|
||||||
@ -361,7 +370,9 @@ async function updateExchangeWithWireInfo(
|
|||||||
const reqUrl = new URL("wire", exchangeBaseUrl);
|
const reqUrl = new URL("wire", exchangeBaseUrl);
|
||||||
reqUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
|
reqUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
|
||||||
|
|
||||||
const resp = await ws.http.get(reqUrl.href);
|
const resp = await ws.http.get(reqUrl.href, {
|
||||||
|
timeout: getExchangeRequestTimeout(exchange),
|
||||||
|
});
|
||||||
const wireInfo = await readSuccessResponseJsonOrThrow(
|
const wireInfo = await readSuccessResponseJsonOrThrow(
|
||||||
resp,
|
resp,
|
||||||
codecForExchangeWireJson(),
|
codecForExchangeWireJson(),
|
||||||
|
@ -35,6 +35,7 @@ import {
|
|||||||
updateRetryInfoTimeout,
|
updateRetryInfoTimeout,
|
||||||
PayEventRecord,
|
PayEventRecord,
|
||||||
WalletContractData,
|
WalletContractData,
|
||||||
|
getRetryDuration,
|
||||||
} from "../types/dbTypes";
|
} from "../types/dbTypes";
|
||||||
import { NotificationType } from "../types/notifications";
|
import { NotificationType } from "../types/notifications";
|
||||||
import {
|
import {
|
||||||
@ -58,7 +59,13 @@ import { parsePayUri } from "../util/taleruri";
|
|||||||
import { guardOperationException, OperationFailedError } from "./errors";
|
import { guardOperationException, OperationFailedError } from "./errors";
|
||||||
import { createRefreshGroup, getTotalRefreshCost } from "./refresh";
|
import { createRefreshGroup, getTotalRefreshCost } from "./refresh";
|
||||||
import { InternalWalletState, EXCHANGE_COINS_LOCK } from "./state";
|
import { InternalWalletState, EXCHANGE_COINS_LOCK } from "./state";
|
||||||
import { getTimestampNow, timestampAddDuration } from "../util/time";
|
import {
|
||||||
|
getTimestampNow,
|
||||||
|
timestampAddDuration,
|
||||||
|
Duration,
|
||||||
|
durationMax,
|
||||||
|
durationMin,
|
||||||
|
} from "../util/time";
|
||||||
import { strcmp, canonicalJson } from "../util/helpers";
|
import { strcmp, canonicalJson } from "../util/helpers";
|
||||||
import {
|
import {
|
||||||
readSuccessResponseJsonOrThrow,
|
readSuccessResponseJsonOrThrow,
|
||||||
@ -588,6 +595,17 @@ async function resetDownloadProposalRetry(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getProposalRequestTimeout(proposal: ProposalRecord): Duration {
|
||||||
|
return durationMax(
|
||||||
|
{ d_ms: 60000 },
|
||||||
|
durationMin({ d_ms: 5000 }, getRetryDuration(proposal.retryInfo)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPurchaseRequestTimeout(purchase: PurchaseRecord): Duration {
|
||||||
|
return { d_ms: 5000 };
|
||||||
|
}
|
||||||
|
|
||||||
async function processDownloadProposalImpl(
|
async function processDownloadProposalImpl(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
proposalId: string,
|
proposalId: string,
|
||||||
@ -620,7 +638,9 @@ async function processDownloadProposalImpl(
|
|||||||
requestBody.token = proposal.claimToken;
|
requestBody.token = proposal.claimToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resp = await ws.http.postJson(orderClaimUrl, requestBody);
|
const resp = await ws.http.postJson(orderClaimUrl, requestBody, {
|
||||||
|
timeout: getProposalRequestTimeout(proposal),
|
||||||
|
});
|
||||||
const proposalResp = await readSuccessResponseJsonOrThrow(
|
const proposalResp = await readSuccessResponseJsonOrThrow(
|
||||||
resp,
|
resp,
|
||||||
codecForProposal(),
|
codecForProposal(),
|
||||||
@ -886,7 +906,9 @@ export async function submitPay(
|
|||||||
logger.trace("making pay request", JSON.stringify(reqBody, undefined, 2));
|
logger.trace("making pay request", JSON.stringify(reqBody, undefined, 2));
|
||||||
|
|
||||||
const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], () =>
|
const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], () =>
|
||||||
ws.http.postJson(payUrl, reqBody),
|
ws.http.postJson(payUrl, reqBody, {
|
||||||
|
timeout: getPurchaseRequestTimeout(purchase),
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const merchantResp = await readSuccessResponseJsonOrThrow(
|
const merchantResp = await readSuccessResponseJsonOrThrow(
|
||||||
|
@ -40,7 +40,7 @@ import {
|
|||||||
|
|
||||||
import { codecForRecoupConfirmation } from "../types/talerTypes";
|
import { codecForRecoupConfirmation } from "../types/talerTypes";
|
||||||
import { NotificationType } from "../types/notifications";
|
import { NotificationType } from "../types/notifications";
|
||||||
import { forceQueryReserve } from "./reserves";
|
import { forceQueryReserve, getReserveRequestTimeout } from "./reserves";
|
||||||
|
|
||||||
import { Amounts } from "../util/amounts";
|
import { Amounts } from "../util/amounts";
|
||||||
import { createRefreshGroup, processRefreshGroup } from "./refresh";
|
import { createRefreshGroup, processRefreshGroup } from "./refresh";
|
||||||
@ -154,7 +154,9 @@ async function recoupWithdrawCoin(
|
|||||||
|
|
||||||
const recoupRequest = await ws.cryptoApi.createRecoupRequest(coin);
|
const recoupRequest = await ws.cryptoApi.createRecoupRequest(coin);
|
||||||
const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl);
|
const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl);
|
||||||
const resp = await ws.http.postJson(reqUrl.href, recoupRequest);
|
const resp = await ws.http.postJson(reqUrl.href, recoupRequest, {
|
||||||
|
timeout: getReserveRequestTimeout(reserve),
|
||||||
|
});
|
||||||
const recoupConfirmation = await readSuccessResponseJsonOrThrow(
|
const recoupConfirmation = await readSuccessResponseJsonOrThrow(
|
||||||
resp,
|
resp,
|
||||||
codecForRecoupConfirmation(),
|
codecForRecoupConfirmation(),
|
||||||
|
@ -42,7 +42,7 @@ import {
|
|||||||
import { guardOperationException } from "./errors";
|
import { guardOperationException } from "./errors";
|
||||||
import { NotificationType } from "../types/notifications";
|
import { NotificationType } from "../types/notifications";
|
||||||
import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
|
import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
|
||||||
import { getTimestampNow } from "../util/time";
|
import { getTimestampNow, Duration } from "../util/time";
|
||||||
import { readSuccessResponseJsonOrThrow, HttpResponse } from "../util/http";
|
import { readSuccessResponseJsonOrThrow, HttpResponse } from "../util/http";
|
||||||
import {
|
import {
|
||||||
codecForExchangeMeltResponse,
|
codecForExchangeMeltResponse,
|
||||||
@ -211,6 +211,10 @@ async function refreshCreateSession(
|
|||||||
ws.notify({ type: NotificationType.RefreshStarted });
|
ws.notify({ type: NotificationType.RefreshStarted });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRefreshRequestTimeout(rg: RefreshGroupRecord): Duration {
|
||||||
|
return { d_ms: 5000 };
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshMelt(
|
async function refreshMelt(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
refreshGroupId: string,
|
refreshGroupId: string,
|
||||||
@ -249,12 +253,11 @@ async function refreshMelt(
|
|||||||
};
|
};
|
||||||
logger.trace(`melt request for coin:`, meltReq);
|
logger.trace(`melt request for coin:`, meltReq);
|
||||||
|
|
||||||
const resp = await ws.runSequentialized(
|
const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], async () => {
|
||||||
[EXCHANGE_COINS_LOCK],
|
return await ws.http.postJson(reqUrl.href, meltReq, {
|
||||||
async () => {
|
timeout: getRefreshRequestTimeout(refreshGroup),
|
||||||
return await ws.http.postJson(reqUrl.href, meltReq);
|
});
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const meltResponse = await readSuccessResponseJsonOrThrow(
|
const meltResponse = await readSuccessResponseJsonOrThrow(
|
||||||
resp,
|
resp,
|
||||||
@ -346,12 +349,11 @@ async function refreshReveal(
|
|||||||
refreshSession.exchangeBaseUrl,
|
refreshSession.exchangeBaseUrl,
|
||||||
);
|
);
|
||||||
|
|
||||||
const resp = await ws.runSequentialized(
|
const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], async () => {
|
||||||
[EXCHANGE_COINS_LOCK],
|
return await ws.http.postJson(reqUrl.href, req, {
|
||||||
async () => {
|
timeout: getRefreshRequestTimeout(refreshGroup),
|
||||||
return await ws.http.postJson(reqUrl.href, req);
|
});
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const reveal = await readSuccessResponseJsonOrThrow(
|
const reveal = await readSuccessResponseJsonOrThrow(
|
||||||
resp,
|
resp,
|
||||||
|
@ -35,6 +35,7 @@ import {
|
|||||||
WithdrawalSourceType,
|
WithdrawalSourceType,
|
||||||
ReserveHistoryRecord,
|
ReserveHistoryRecord,
|
||||||
ReserveBankInfo,
|
ReserveBankInfo,
|
||||||
|
getRetryDuration,
|
||||||
} from "../types/dbTypes";
|
} from "../types/dbTypes";
|
||||||
import { Logger } from "../util/logging";
|
import { Logger } from "../util/logging";
|
||||||
import { Amounts } from "../util/amounts";
|
import { Amounts } from "../util/amounts";
|
||||||
@ -64,7 +65,12 @@ import {
|
|||||||
} from "./errors";
|
} from "./errors";
|
||||||
import { NotificationType } from "../types/notifications";
|
import { NotificationType } from "../types/notifications";
|
||||||
import { codecForReserveStatus } from "../types/ReserveStatus";
|
import { codecForReserveStatus } from "../types/ReserveStatus";
|
||||||
import { getTimestampNow } from "../util/time";
|
import {
|
||||||
|
getTimestampNow,
|
||||||
|
Duration,
|
||||||
|
durationMin,
|
||||||
|
durationMax,
|
||||||
|
} from "../util/time";
|
||||||
import {
|
import {
|
||||||
reconcileReserveHistory,
|
reconcileReserveHistory,
|
||||||
summarizeReserveHistory,
|
summarizeReserveHistory,
|
||||||
@ -331,10 +337,16 @@ async function registerReserveWithBank(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const bankStatusUrl = bankInfo.statusUrl;
|
const bankStatusUrl = bankInfo.statusUrl;
|
||||||
const httpResp = await ws.http.postJson(bankStatusUrl, {
|
const httpResp = await ws.http.postJson(
|
||||||
reserve_pub: reservePub,
|
bankStatusUrl,
|
||||||
selected_exchange: bankInfo.exchangePaytoUri,
|
{
|
||||||
});
|
reserve_pub: reservePub,
|
||||||
|
selected_exchange: bankInfo.exchangePaytoUri,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timeout: getReserveRequestTimeout(reserve),
|
||||||
|
},
|
||||||
|
);
|
||||||
await readSuccessResponseJsonOrThrow(
|
await readSuccessResponseJsonOrThrow(
|
||||||
httpResp,
|
httpResp,
|
||||||
codecForBankWithdrawalOperationPostResponse(),
|
codecForBankWithdrawalOperationPostResponse(),
|
||||||
@ -371,6 +383,13 @@ async function processReserveBankStatus(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getReserveRequestTimeout(r: ReserveRecord): Duration {
|
||||||
|
return durationMax(
|
||||||
|
{ d_ms: 60000 },
|
||||||
|
durationMin({ d_ms: 5000 }, getRetryDuration(r.retryInfo)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function processReserveBankStatusImpl(
|
async function processReserveBankStatusImpl(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
reservePub: string,
|
reservePub: string,
|
||||||
@ -388,7 +407,9 @@ async function processReserveBankStatusImpl(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusResp = await ws.http.get(bankStatusUrl);
|
const statusResp = await ws.http.get(bankStatusUrl, {
|
||||||
|
timeout: getReserveRequestTimeout(reserve),
|
||||||
|
});
|
||||||
const status = await readSuccessResponseJsonOrThrow(
|
const status = await readSuccessResponseJsonOrThrow(
|
||||||
statusResp,
|
statusResp,
|
||||||
codecForWithdrawOperationStatusResponse(),
|
codecForWithdrawOperationStatusResponse(),
|
||||||
@ -501,6 +522,9 @@ async function updateReserve(
|
|||||||
|
|
||||||
const resp = await ws.http.get(
|
const resp = await ws.http.get(
|
||||||
new URL(`reserves/${reservePub}`, reserve.exchangeBaseUrl).href,
|
new URL(`reserves/${reservePub}`, reserve.exchangeBaseUrl).href,
|
||||||
|
{
|
||||||
|
timeout: getReserveRequestTimeout(reserve),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await readSuccessResponseJsonOrErrorCode(
|
const result = await readSuccessResponseJsonOrErrorCode(
|
||||||
|
@ -117,6 +117,17 @@ export function updateRetryInfoTimeout(
|
|||||||
r.nextRetry = { t_ms: t };
|
r.nextRetry = { t_ms: t };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getRetryDuration(
|
||||||
|
r: RetryInfo,
|
||||||
|
p: RetryPolicy = defaultRetryPolicy,
|
||||||
|
): Duration {
|
||||||
|
if (p.backoffDelta.d_ms === "forever") {
|
||||||
|
return { d_ms: "forever" };
|
||||||
|
}
|
||||||
|
const t = p.backoffDelta.d_ms * Math.pow(p.backoffBase, r.retryCounter);
|
||||||
|
return { d_ms: t };
|
||||||
|
}
|
||||||
|
|
||||||
export function initRetryInfo(
|
export function initRetryInfo(
|
||||||
active = true,
|
active = true,
|
||||||
p: RetryPolicy = defaultRetryPolicy,
|
p: RetryPolicy = defaultRetryPolicy,
|
||||||
|
@ -30,25 +30,25 @@ const logger = new Logger("RequestThrottler.ts");
|
|||||||
/**
|
/**
|
||||||
* Maximum request per second, per origin.
|
* Maximum request per second, per origin.
|
||||||
*/
|
*/
|
||||||
const MAX_PER_SECOND = 50;
|
const MAX_PER_SECOND = 100;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maximum request per minute, per origin.
|
* Maximum request per minute, per origin.
|
||||||
*/
|
*/
|
||||||
const MAX_PER_MINUTE = 100;
|
const MAX_PER_MINUTE = 500;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maximum request per hour, per origin.
|
* Maximum request per hour, per origin.
|
||||||
*/
|
*/
|
||||||
const MAX_PER_HOUR = 1000;
|
const MAX_PER_HOUR = 2000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Throttling state for one origin.
|
* Throttling state for one origin.
|
||||||
*/
|
*/
|
||||||
class OriginState {
|
class OriginState {
|
||||||
private tokensSecond: number = MAX_PER_SECOND;
|
tokensSecond: number = MAX_PER_SECOND;
|
||||||
private tokensMinute: number = MAX_PER_MINUTE;
|
tokensMinute: number = MAX_PER_MINUTE;
|
||||||
private tokensHour: number = MAX_PER_HOUR;
|
tokensHour: number = MAX_PER_HOUR;
|
||||||
private lastUpdate = getTimestampNow();
|
private lastUpdate = getTimestampNow();
|
||||||
|
|
||||||
private refill(): void {
|
private refill(): void {
|
||||||
@ -57,6 +57,9 @@ class OriginState {
|
|||||||
if (d.d_ms === "forever") {
|
if (d.d_ms === "forever") {
|
||||||
throw Error("assertion failed");
|
throw Error("assertion failed");
|
||||||
}
|
}
|
||||||
|
if (d.d_ms < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const d_s = d.d_ms / 1000;
|
const d_s = d.d_ms / 1000;
|
||||||
this.tokensSecond = Math.min(
|
this.tokensSecond = Math.min(
|
||||||
MAX_PER_SECOND,
|
MAX_PER_SECOND,
|
||||||
@ -129,4 +132,20 @@ export class RequestThrottler {
|
|||||||
const origin = new URL(requestUrl).origin;
|
const origin = new URL(requestUrl).origin;
|
||||||
return this.getState(origin).applyThrottle();
|
return this.getState(origin).applyThrottle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the throttle statistics for a particular URL.
|
||||||
|
*/
|
||||||
|
getThrottleStats(requestUrl: string): Record<string, unknown> {
|
||||||
|
const origin = new URL(requestUrl).origin;
|
||||||
|
const state = this.getState(origin);
|
||||||
|
return {
|
||||||
|
tokensHour: state.tokensHour,
|
||||||
|
tokensMinute: state.tokensMinute,
|
||||||
|
tokensSecond: state.tokensSecond,
|
||||||
|
maxTokensHour: MAX_PER_HOUR,
|
||||||
|
maxTokensMinute: MAX_PER_MINUTE,
|
||||||
|
maxTokensSecond: MAX_PER_SECOND,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ import { Codec } from "./codec";
|
|||||||
import { OperationFailedError, makeErrorDetails } from "../operations/errors";
|
import { OperationFailedError, makeErrorDetails } from "../operations/errors";
|
||||||
import { TalerErrorCode } from "../TalerErrorCode";
|
import { TalerErrorCode } from "../TalerErrorCode";
|
||||||
import { Logger } from "./logging";
|
import { Logger } from "./logging";
|
||||||
|
import { Duration } from "./time";
|
||||||
|
|
||||||
const logger = new Logger("http.ts");
|
const logger = new Logger("http.ts");
|
||||||
|
|
||||||
@ -43,6 +44,7 @@ export interface HttpResponse {
|
|||||||
|
|
||||||
export interface HttpRequestOptions {
|
export interface HttpRequestOptions {
|
||||||
headers?: { [name: string]: string };
|
headers?: { [name: string]: string };
|
||||||
|
timeout?: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum HttpResponseStatus {
|
export enum HttpResponseStatus {
|
||||||
|
@ -95,6 +95,16 @@ export function durationMin(d1: Duration, d2: Duration): Duration {
|
|||||||
return { d_ms: Math.min(d1.d_ms, d2.d_ms) };
|
return { d_ms: Math.min(d1.d_ms, d2.d_ms) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function durationMax(d1: Duration, d2: Duration): Duration {
|
||||||
|
if (d1.d_ms === "forever") {
|
||||||
|
return { d_ms: "forever" };
|
||||||
|
}
|
||||||
|
if (d2.d_ms === "forever") {
|
||||||
|
return { d_ms: "forever" };
|
||||||
|
}
|
||||||
|
return { d_ms: Math.max(d1.d_ms, d2.d_ms) };
|
||||||
|
}
|
||||||
|
|
||||||
export function timestampCmp(t1: Timestamp, t2: Timestamp): number {
|
export function timestampCmp(t1: Timestamp, t2: Timestamp): number {
|
||||||
if (t1.t_ms === "never") {
|
if (t1.t_ms === "never") {
|
||||||
if (t2.t_ms === "never") {
|
if (t2.t_ms === "never") {
|
||||||
|
Loading…
Reference in New Issue
Block a user