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

View File

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

View File

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

View File

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

View File

@ -92,3 +92,18 @@ export function initRetryInfo(p: RetryPolicy = defaultRetryPolicy): RetryInfo {
updateRetryInfoTimeout(info, p); updateRetryInfoTimeout(info, p);
return info; 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 { DbAccess, GetReadWriteAccess } from "./util/query.js";
import { TimerGroup } from "./util/timer.js"; import { TimerGroup } from "./util/timer.js";
import { WalletCoreApiClient } from "./wallet-api-types.js"; import { WalletCoreApiClient } from "./wallet-api-types.js";
import { import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
TalerCryptoInterface,
TalerCryptoInterfaceR,
} from "./crypto/cryptoImplementation.js";
const builtinAuditors: AuditorTrustRecord[] = [ const builtinAuditors: AuditorTrustRecord[] = [
{ {

View File

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

View File

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