wallet: timeout handling refactoring WIP

This commit is contained in:
Florian Dold 2022-03-28 23:21:49 +02:00
parent c194bd539a
commit 80e43db2ca
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
10 changed files with 121 additions and 118 deletions

View File

@ -129,6 +129,17 @@ export class Amounts {
return Amounts.add(jsonAmounts[0], ...jsonAmounts.slice(1));
}
static sumOrZero(currency: string, amounts: AmountLike[]): Result {
if (amounts.length <= 0) {
return {
amount: Amounts.getZero(currency),
saturated: false,
};
}
const jsonAmounts = amounts.map((x) => Amounts.jsonifyAmount(x));
return Amounts.add(jsonAmounts[0], ...jsonAmounts.slice(1));
}
/**
* Add two amounts. Return the result and whether
* the addition overflowed. The overflow is always handled

View File

@ -14,6 +14,9 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Imports.
*/
import { TalerErrorDetail, TalerErrorCode } from "@gnu-taler/taler-util";
import { CryptoApiStoppedError } from "../crypto/workers/cryptoDispatcher.js";
import { TalerError, getErrorDetailFromException } from "../errors.js";

View File

@ -14,17 +14,15 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Imports.
*/
import {
AbsoluteTime,
AmountJson,
Amounts,
buildCodecForObject,
canonicalJson,
Codec,
codecForDepositSuccess,
codecForString,
codecForTimestamp,
codecOptional,
ContractTerms,
CreateDepositGroupRequest,
CreateDepositGroupResponse,
@ -42,21 +40,22 @@ import {
TrackDepositGroupResponse,
URL,
} from "@gnu-taler/taler-util";
import { InternalWalletState } from "../internal-wallet-state.js";
import { DepositGroupRecord, OperationStatus } from "../db.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { PayCoinSelection, selectPayCoins } from "../util/coinSelection.js";
import { readSuccessResponseJsonOrThrow } from "../util/http.js";
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js";
import { initRetryInfo, RetryInfo } from "../util/retries.js";
import { guardOperationException } from "./common.js";
import { getExchangeDetails } from "./exchanges.js";
import {
applyCoinSpend,
CoinSelectionRequest,
extractContractData,
generateDepositPermissions,
getCandidatePayCoins,
getTotalPaymentCost,
} from "./pay.js";
import { getTotalRefreshCost } from "./refresh.js";
import { guardOperationException } from "./common.js";
/**
* Logger.
@ -73,17 +72,36 @@ async function resetDepositGroupRetry(
}))
.runReadWrite(async (tx) => {
const x = await tx.depositGroups.get(depositGroupId);
if (x) {
x.retryInfo = initRetryInfo();
await tx.depositGroups.put(x);
if (!x) {
return;
}
x.retryInfo = initRetryInfo();
delete x.lastError;
await tx.depositGroups.put(x);
});
}
async function incrementDepositRetry(
async function incrementDepositGroupRetry(
ws: InternalWalletState,
depositGroupId: string,
err: TalerErrorDetail | undefined,
): Promise<void> {
await ws.db
.mktx((x) => ({ depositGroups: x.depositGroups }))
.runReadWrite(async (tx) => {
const r = await tx.depositGroups.get(depositGroupId);
if (!r) {
return;
}
r.retryInfo = RetryInfo.increment(r.retryInfo);
delete r.lastError;
await tx.depositGroups.put(r);
});
}
async function reportDepositGroupError(
ws: InternalWalletState,
depositGroupId: string,
err: TalerErrorDetail,
): Promise<void> {
await ws.db
.mktx((x) => ({ depositGroups: x.depositGroups }))
@ -93,16 +111,15 @@ async function incrementDepositRetry(
return;
}
if (!r.retryInfo) {
logger.error(
`deposit group record (${depositGroupId}) reports error, but no retry active`,
);
return;
}
r.retryInfo.retryCounter++;
updateRetryInfoTimeout(r.retryInfo);
r.lastError = err;
await tx.depositGroups.put(r);
});
if (err) {
ws.notify({ type: NotificationType.DepositOperationError, error: err });
}
ws.notify({ type: NotificationType.DepositOperationError, error: err });
}
export async function processDepositGroup(
@ -111,8 +128,8 @@ export async function processDepositGroup(
forceNow = false,
): Promise<void> {
await ws.memoProcessDeposit.memo(depositGroupId, async () => {
const onOpErr = (e: TalerErrorDetail): Promise<void> =>
incrementDepositRetry(ws, depositGroupId, e);
const onOpErr = (err: TalerErrorDetail): Promise<void> =>
reportDepositGroupError(ws, depositGroupId, err);
return await guardOperationException(
async () => await processDepositGroupImpl(ws, depositGroupId, forceNow),
onOpErr,
@ -125,9 +142,6 @@ async function processDepositGroupImpl(
depositGroupId: string,
forceNow = false,
): Promise<void> {
if (forceNow) {
await resetDepositGroupRetry(ws, depositGroupId);
}
const depositGroup = await ws.db
.mktx((x) => ({
depositGroups: x.depositGroups,
@ -144,6 +158,12 @@ async function processDepositGroupImpl(
return;
}
if (forceNow) {
await resetDepositGroupRetry(ws, depositGroupId);
} else {
await incrementDepositGroupRetry(ws, depositGroupId);
}
const contractData = extractContractData(
depositGroup.contractTermsRaw,
depositGroup.contractTermsHash,
@ -306,42 +326,25 @@ export async function getFeeForDeposit(
}
});
const timestamp = AbsoluteTime.now();
const timestampRound = AbsoluteTime.toTimestamp(timestamp);
const contractTerms: ContractTerms = {
auditors: [],
exchanges: exchangeInfos,
amount: req.amount,
max_fee: Amounts.stringify(amount),
max_wire_fee: Amounts.stringify(amount),
wire_method: p.targetType,
timestamp: timestampRound,
merchant_base_url: "",
summary: "",
nonce: "",
wire_transfer_deadline: timestampRound,
order_id: "",
h_wire: "",
pay_deadline: AbsoluteTime.toTimestamp(
AbsoluteTime.addDuration(timestamp, durationFromSpec({ hours: 1 })),
),
merchant: {
name: "",
},
merchant_pub: "",
refund_deadline: TalerProtocolTimestamp.zero(),
const csr: CoinSelectionRequest = {
allowedAuditors: [],
allowedExchanges: [],
amount: Amounts.parseOrThrow(req.amount),
maxDepositFee: Amounts.parseOrThrow(req.amount),
maxWireFee: Amounts.parseOrThrow(req.amount),
timestamp: TalerProtocolTimestamp.now(),
wireFeeAmortization: 1,
wireMethod: p.targetType,
};
const contractData = extractContractData(contractTerms, "", "");
const candidates = await getCandidatePayCoins(ws, contractData);
const candidates = await getCandidatePayCoins(ws, csr);
const payCoinSel = selectPayCoins({
candidates,
contractTermsAmount: contractData.amount,
depositFeeLimit: contractData.maxDepositFee,
wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
wireFeeLimit: contractData.maxWireFee,
contractTermsAmount: csr.amount,
depositFeeLimit: csr.maxDepositFee,
wireFeeAmortization: csr.wireFeeAmortization,
wireFeeLimit: csr.maxWireFee,
prevPayCoins: [],
});
@ -573,6 +576,7 @@ export async function getEffectiveDepositAmount(
return Amounts.sub(Amounts.sum(amt).amount, Amounts.sum(fees).amount).amount;
}
// FIXME: rename to DepositGroupFee
export interface DepositFee {
coin: AmountJson;
wire: AmountJson;
@ -594,8 +598,6 @@ export async function getTotalFeeForDepositAmount(
const refreshFee: AmountJson[] = [];
const exchangeSet: Set<string> = new Set();
// let acc: AmountJson = Amounts.getZero(total.currency);
await ws.db
.mktx((x) => ({
coins: x.coins,
@ -658,17 +660,8 @@ export async function getTotalFeeForDepositAmount(
});
return {
coin:
coinFee.length === 0
? Amounts.getZero(total.currency)
: Amounts.sum(coinFee).amount,
wire:
wireFee.length === 0
? Amounts.getZero(total.currency)
: Amounts.sum(wireFee).amount,
refresh:
refreshFee.length === 0
? Amounts.getZero(total.currency)
: Amounts.sum(refreshFee).amount,
coin: Amounts.sumOrZero(total.currency, coinFee).amount,
wire: Amounts.sumOrZero(total.currency, wireFee).amount,
refresh: Amounts.sumOrZero(total.currency, refreshFee).amount,
};
}

View File

@ -18,35 +18,31 @@
* Imports.
*/
import {
AbsoluteTime,
Amounts,
ExchangeAuditor,
canonicalizeBaseUrl,
codecForExchangeKeysJson,
codecForExchangeWireJson,
ExchangeDenomination,
DenominationPubKey,
Duration,
durationFromSpec,
encodeCrock,
ExchangeAuditor,
ExchangeDenomination,
ExchangeSignKeyJson,
ExchangeWireJson,
hashDenomPub,
LibtoolVersion,
Logger,
NotificationType,
parsePaytoUri,
Recoup,
TalerErrorCode,
URL,
TalerErrorDetail,
AbsoluteTime,
hashDenomPub,
LibtoolVersion,
codecForAny,
DenominationPubKey,
DenomKeyType,
ExchangeKeysJson,
TalerProtocolTimestamp,
TalerProtocolDuration,
TalerProtocolTimestamp,
URL,
} from "@gnu-taler/taler-util";
import { decodeCrock, encodeCrock, hash } from "@gnu-taler/taler-util";
import { CryptoDispatcher } from "../crypto/workers/cryptoDispatcher.js";
import {
DenominationRecord,
DenominationVerificationStatus,
@ -56,6 +52,8 @@ import {
WireFee,
WireInfo,
} from "../db.js";
import { TalerError } from "../errors.js";
import { InternalWalletState, TrustInfo } from "../internal-wallet-state.js";
import {
getExpiry,
HttpRequestLibrary,
@ -64,8 +62,6 @@ import {
} from "../util/http.js";
import { DbAccess, GetReadOnlyAccess } from "../util/query.js";
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js";
import { TalerError } from "../errors.js";
import { InternalWalletState, TrustInfo } from "../internal-wallet-state.js";
import {
WALLET_CACHE_BREAKER_CLIENT_VERSION,
WALLET_EXCHANGE_PROTOCOL_VERSION,

View File

@ -98,6 +98,7 @@ import { GetReadWriteAccess } from "../util/query.js";
import {
getRetryDuration,
initRetryInfo,
RetryInfo,
updateRetryInfoTimeout,
} from "../util/retries.js";
import { getExchangeDetails } from "./exchanges.js";
@ -539,11 +540,7 @@ async function incrementPurchasePayRetry(
if (!pr) {
return;
}
if (!pr.payRetryInfo) {
pr.payRetryInfo = initRetryInfo();
}
pr.payRetryInfo.retryCounter++;
updateRetryInfoTimeout(pr.payRetryInfo);
pr.payRetryInfo = RetryInfo.increment(pr.payRetryInfo);
delete pr.lastPayError;
await tx.purchases.put(pr);
});

View File

@ -18,32 +18,31 @@
* Imports.
*/
import {
AbsoluteTime,
AmountJson,
Amounts,
AmountString,
BankWithdrawDetails,
codecForTalerConfigResponse,
codecForWithdrawOperationStatusResponse,
codecForWithdrawResponse,
DenomKeyType,
Duration,
durationFromSpec,
ExchangeListItem,
ExchangeWithdrawRequest,
LibtoolVersion,
Logger,
NotificationType,
parseWithdrawUri,
TalerErrorCode,
TalerErrorDetail,
AbsoluteTime,
WithdrawResponse,
URL,
WithdrawUriInfoResponse,
VersionMatchResult,
DenomKeyType,
LibtoolVersion,
UnblindedSignature,
ExchangeWithdrawRequest,
Duration,
TalerProtocolTimestamp,
TransactionType,
AmountString,
UnblindedSignature,
URL,
VersionMatchResult,
WithdrawResponse,
WithdrawUriInfoResponse,
} from "@gnu-taler/taler-util";
import {
CoinRecord,
@ -58,18 +57,18 @@ import {
PlanchetRecord,
WithdrawalGroupRecord,
} from "../db.js";
import { walletCoreDebugFlags } from "../util/debugFlags.js";
import {
HttpRequestLibrary,
readSuccessResponseJsonOrThrow,
} from "../util/http.js";
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js";
import {
getErrorDetailFromException,
makeErrorDetail,
TalerError,
} from "../errors.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { walletCoreDebugFlags } from "../util/debugFlags.js";
import {
HttpRequestLibrary,
readSuccessResponseJsonOrThrow,
} from "../util/http.js";
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js";
import {
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
WALLET_EXCHANGE_PROTOCOL_VERSION,

View File

@ -92,3 +92,18 @@ export function initRetryInfo(p: RetryPolicy = defaultRetryPolicy): RetryInfo {
updateRetryInfoTimeout(info, p);
return info;
}
export namespace RetryInfo {
export function increment(
r: RetryInfo | undefined,
p: RetryPolicy = defaultRetryPolicy,
) {
if (!r) {
return initRetryInfo(p);
}
const r2 = { ...r };
r2.retryCounter++;
updateRetryInfoTimeout(r2, p);
return r2;
}
}

View File

@ -193,10 +193,7 @@ import {
import { DbAccess, GetReadWriteAccess } from "./util/query.js";
import { TimerGroup } from "./util/timer.js";
import { WalletCoreApiClient } from "./wallet-api-types.js";
import {
TalerCryptoInterface,
TalerCryptoInterfaceR,
} from "./crypto/cryptoImplementation.js";
import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
const builtinAuditors: AuditorTrustRecord[] = [
{

View File

@ -22,7 +22,6 @@ import {
PaytoUri,
} from "@gnu-taler/taler-util";
import { DepositFee } from "@gnu-taler/taler-wallet-core/src/operations/deposits";
import { saturate } from "polished";
import { Fragment, h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
import { Loading } from "../components/Loading";
@ -30,7 +29,6 @@ import { LoadingError } from "../components/LoadingError";
import { SelectList } from "../components/SelectList";
import {
Button,
ButtonBoxWarning,
ButtonPrimary,
ErrorText,
Input,

View File

@ -193,7 +193,6 @@ importers:
'@rollup/plugin-replace': ^3.0.1
'@types/node': ^17.0.17
axios: ^0.25.0
cancellationtoken: ^2.2.0
prettier: ^2.5.1
rimraf: ^3.0.2
rollup: ^2.67.2
@ -207,7 +206,6 @@ importers:
'@gnu-taler/taler-util': link:../taler-util
'@gnu-taler/taler-wallet-core': link:../taler-wallet-core
axios: 0.25.0
cancellationtoken: 2.2.0
source-map-support: 0.5.21
tslib: 2.3.1
devDependencies:
@ -8726,10 +8724,6 @@ packages:
engines: {node: '>=10'}
dev: true
/cancellationtoken/2.2.0:
resolution: {integrity: sha512-uF4sHE5uh2VdEZtIRJKGoXAD9jm7bFY0tDRCzH4iLp262TOJ2lrtNHjMG2zc8H+GICOpELIpM7CGW5JeWnb3Hg==}
dev: false
/caniuse-api/3.0.0:
resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==}
dependencies: