commit 63854330be05f77652955fe4654713081855c7bf
parent 516ae509b978eb0b6fb1647c094d37d012e498a6
Author: Florian Dold <florian@dold.me>
Date: Wed, 1 Jul 2026 15:29:09 +0200
wallet-core: implement traffic control dev experiment
Diffstat:
2 files changed, 95 insertions(+), 5 deletions(-)
diff --git a/packages/taler-wallet-core/src/dev-experiments.ts b/packages/taler-wallet-core/src/dev-experiments.ts
@@ -31,7 +31,6 @@ import {
Amounts,
ContractTermsUtil,
DenomLossEventType,
- TalerDevExperimentUri,
Duration,
Logger,
MerchantContractTermsV0,
@@ -39,6 +38,7 @@ import {
PeerContractTerms,
RefreshReason,
Result,
+ TalerDevExperimentUri,
TalerError,
TalerErrorCode,
TalerPreciseTimestamp,
@@ -173,6 +173,29 @@ export async function applyDevExperiment(
wex.ws.devExperimentState.merchantDepositInsufficient = true;
return;
}
+ case "start-tc": {
+ const maybeFloatParam = (name: string) => {
+ const val = parsedUri.query?.get(name);
+ if (val == null) {
+ return undefined;
+ }
+ if (typeof val === "string") {
+ return Number.parseFloat(val);
+ }
+ throw Error("unsupported parameter");
+ };
+ wex.ws.devExperimentState.tc = {
+ delayResponseMs: maybeFloatParam("delay_resp"),
+ dropRequestProbability: maybeFloatParam("drop_req"),
+ dropResponseProbability: maybeFloatParam("drop_resp"),
+ fake500Probability: maybeFloatParam("fake_500"),
+ };
+ return;
+ }
+ case "stop-tc": {
+ wex.ws.devExperimentState.tc = undefined;
+ return;
+ }
case "start-fakeprotover": {
const baseUrl = parsedUri.query?.get("base_url");
if (!baseUrl) {
@@ -734,7 +757,10 @@ async function addFakeTx(
}
}
-function mockResponseJson(resp: HttpResponse, respJson: any): HttpResponse {
+function mockResponseReplacementJson(
+ resp: HttpResponse,
+ respJson: any,
+): HttpResponse {
const textEncoder = new TextEncoder();
return {
requestMethod: resp.requestMethod,
@@ -777,6 +803,19 @@ function mockInternalServerError(resp: HttpResponse): HttpResponse {
};
}
+function waitMs(tMs: number): Promise<void> {
+ return new Promise<void>((resolve) => {
+ setTimeout(() => resolve(), tMs);
+ });
+}
+
+function evalFailProbability(p: number | undefined): boolean {
+ if (p == null) {
+ return true;
+ }
+ return Math.random() >= p;
+}
+
export class DevExperimentHttpLib implements HttpRequestLibrary {
_isDevExperimentLib = true;
underlyingLib: HttpRequestLibrary;
@@ -815,7 +854,7 @@ export class DevExperimentHttpLib implements HttpRequestLibrary {
logger.info(`replacing proto version with ${fakeSt.fakeVer}`);
const respJson = await resp.json();
respJson.version = fakeSt.fakeVer;
- return mockResponseJson(resp, respJson);
+ return mockResponseReplacementJson(resp, respJson);
}
}
} else if (this.devExperimentState.blockPayResponse) {
@@ -827,6 +866,46 @@ export class DevExperimentHttpLib implements HttpRequestLibrary {
return mockInternalServerError(realResp);
}
}
- return this.underlyingLib.fetch(url, opt);
+ if (
+ evalFailProbability(this.devExperimentState.tc?.dropRequestProbability)
+ ) {
+ await waitMs(5000);
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+ {
+ requestUrl: url,
+ requestMethod: method,
+ httpStatusCode: 0,
+ },
+ `Request timed out after ${5000} ms`,
+ );
+ }
+ const resp = await this.underlyingLib.fetch(url, opt);
+ if (evalFailProbability(this.devExperimentState.tc?.fake500Probability)) {
+ return mockInternalServerError(resp);
+ }
+ if (
+ evalFailProbability(this.devExperimentState.tc?.dropResponseProbability)
+ ) {
+ await waitMs(5000);
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+ {
+ requestUrl: url,
+ requestMethod: method,
+ httpStatusCode: 0,
+ },
+ `Request timed out after ${5000} ms`,
+ );
+ }
+ // Delay response if requested
+ {
+ const delayMs = this.devExperimentState.tc?.delayResponseMs;
+ if (delayMs != null) {
+ logger.info(`delaying response by ${delayMs}ms`);
+ await waitMs(delayMs);
+ }
+ }
+ return resp;
}
}
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
@@ -2124,7 +2124,7 @@ export async function handleTestingWaitExchangeReady(
await waitReadyExchange(wex, req.exchangeBaseUrl, {
noBail: req.noBail,
forceUpdate: req.forceUpdate,
- waitAutoRefresh: req.waitAutoRefresh
+ waitAutoRefresh: req.waitAutoRefresh,
});
return {};
}
@@ -3197,6 +3197,17 @@ export interface DevExperimentState {
/** Migration test for confirmPay */
flagConfirmPayNoWait?: boolean;
+
+ tc?: {
+ dropRequestProbability?: number;
+ dropResponseProbability?: number;
+ /**
+ * Fake a 500 response *after* the request has been sent
+ * and the real response has been received.
+ */
+ fake500Probability?: number;
+ delayResponseMs?: number;
+ };
}
export class Cache<T> {