From c265e7d019d445add2d2cfb7cfcbdeee059684d3 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 29 Mar 2022 13:47:32 +0200 Subject: wallet: make retries more robust and consistent --- .../taler-wallet-core/src/operations/README.md | 2 +- .../src/operations/backup/import.ts | 18 ++-- .../src/operations/backup/index.ts | 6 +- .../taler-wallet-core/src/operations/common.ts | 2 +- .../taler-wallet-core/src/operations/deposits.ts | 85 ++++++--------- .../taler-wallet-core/src/operations/exchanges.ts | 58 ++++------ packages/taler-wallet-core/src/operations/pay.ts | 117 +++++++++------------ .../taler-wallet-core/src/operations/pending.ts | 11 +- .../taler-wallet-core/src/operations/recoup.ts | 88 +++++++++------- .../taler-wallet-core/src/operations/refresh.ts | 85 +++++++++------ .../taler-wallet-core/src/operations/refund.ts | 84 +++++++++------ .../taler-wallet-core/src/operations/reserves.ts | 83 ++++++--------- packages/taler-wallet-core/src/operations/tip.ts | 62 ++++++++--- .../taler-wallet-core/src/operations/withdraw.ts | 81 ++++++++------ 14 files changed, 404 insertions(+), 378 deletions(-) (limited to 'packages/taler-wallet-core/src/operations') diff --git a/packages/taler-wallet-core/src/operations/README.md b/packages/taler-wallet-core/src/operations/README.md index 426f2c553..9a2937c0c 100644 --- a/packages/taler-wallet-core/src/operations/README.md +++ b/packages/taler-wallet-core/src/operations/README.md @@ -17,7 +17,7 @@ Generally, the code to process a pending operation should first increment the retryInfo (and reset the lastError) and then process the operation. This way, it is impossble to forget incrementing the retryInfo. -For each retriable operation, there are usually `resetRetry`, `incrementRetry` and +For each retriable operation, there are usually `setupRetry`, `incrementRetry` and `reportError` operations. Note that this means that _during_ some operation, lastError will be cleared. The UI diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts index ece0781a8..37e97fbc8 100644 --- a/packages/taler-wallet-core/src/operations/backup/import.ts +++ b/packages/taler-wallet-core/src/operations/backup/import.ts @@ -57,7 +57,7 @@ import { checkLogicInvariant, } from "../../util/invariants.js"; import { Logger } from "@gnu-taler/taler-util"; -import { initRetryInfo } from "../../util/retries.js"; +import { resetRetryInfo } from "../../util/retries.js"; import { InternalWalletState } from "../../internal-wallet-state.js"; import { provideBackupState } from "./state.js"; import { makeEventId, TombstoneTag } from "../transactions.js"; @@ -276,7 +276,7 @@ export async function importBackup( protocolVersionRange: backupExchange.protocol_version_range, }, permanent: true, - retryInfo: initRetryInfo(), + retryInfo: resetRetryInfo(), lastUpdate: undefined, nextUpdate: TalerProtocolTimestamp.now(), nextRefreshCheck: TalerProtocolTimestamp.now(), @@ -464,7 +464,7 @@ export async function importBackup( timestampReserveInfoPosted: backupReserve.bank_info?.timestamp_reserve_info_posted, senderWire: backupReserve.sender_wire, - retryInfo: initRetryInfo(), + retryInfo: resetRetryInfo(), lastError: undefined, initialWithdrawalGroupId: backupReserve.initial_withdrawal_group_id, @@ -505,7 +505,7 @@ export async function importBackup( backupWg.raw_withdrawal_amount, ), reservePub, - retryInfo: initRetryInfo(), + retryInfo: resetRetryInfo(), secretSeed: backupWg.secret_seed, timestampStart: backupWg.timestamp_created, timestampFinish: backupWg.timestamp_finish, @@ -618,7 +618,7 @@ export async function importBackup( cryptoComp.proposalNoncePrivToPub[backupProposal.nonce_priv], proposalId: backupProposal.proposal_id, repurchaseProposalId: backupProposal.repurchase_proposal_id, - retryInfo: initRetryInfo(), + retryInfo: resetRetryInfo(), download, proposalStatus, }); @@ -753,7 +753,7 @@ export async function importBackup( cryptoComp.proposalNoncePrivToPub[backupPurchase.nonce_priv], lastPayError: undefined, autoRefundDeadline: TalerProtocolTimestamp.never(), - refundStatusRetryInfo: initRetryInfo(), + refundStatusRetryInfo: resetRetryInfo(), lastRefundStatusError: undefined, timestampAccept: backupPurchase.timestamp_accept, timestampFirstSuccessfulPay: @@ -763,7 +763,7 @@ export async function importBackup( lastSessionId: undefined, abortStatus, // FIXME! - payRetryInfo: initRetryInfo(), + payRetryInfo: resetRetryInfo(), download, paymentSubmitPending: !backupPurchase.timestamp_first_successful_pay, @@ -864,7 +864,7 @@ export async function importBackup( Amounts.parseOrThrow(x.estimated_output_amount), ), refreshSessionPerCoin, - retryInfo: initRetryInfo(), + retryInfo: resetRetryInfo(), }); } } @@ -890,7 +890,7 @@ export async function importBackup( merchantBaseUrl: backupTip.exchange_base_url, merchantTipId: backupTip.merchant_tip_id, pickedUpTimestamp: backupTip.timestamp_finished, - retryInfo: initRetryInfo(), + retryInfo: resetRetryInfo(), secretSeed: backupTip.secret_seed, tipAmountEffective: denomsSel.totalCoinValue, tipAmountRaw: Amounts.parseOrThrow(backupTip.tip_amount_raw), diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts b/packages/taler-wallet-core/src/operations/backup/index.ts index 5013b9032..0b2bd7b80 100644 --- a/packages/taler-wallet-core/src/operations/backup/index.ts +++ b/packages/taler-wallet-core/src/operations/backup/index.ts @@ -89,7 +89,7 @@ import { checkLogicInvariant, } from "../../util/invariants.js"; import { GetReadWriteAccess } from "../../util/query.js"; -import { initRetryInfo, updateRetryInfoTimeout } from "../../util/retries.js"; +import { resetRetryInfo, updateRetryInfoTimeout } from "../../util/retries.js"; import { checkPaymentByProposalId, confirmPay, @@ -434,7 +434,7 @@ async function runBackupCycleForProvider( // FIXME: Allocate error code for this situation? prov.state = { tag: BackupProviderStateTag.Retrying, - retryInfo: initRetryInfo(), + retryInfo: resetRetryInfo(), }; await tx.backupProvider.put(prov); }); @@ -478,7 +478,7 @@ async function incrementBackupRetryInTx( } else if (pr.state.tag === BackupProviderStateTag.Ready) { pr.state = { tag: BackupProviderStateTag.Retrying, - retryInfo: initRetryInfo(), + retryInfo: resetRetryInfo(), lastError: err, }; } diff --git a/packages/taler-wallet-core/src/operations/common.ts b/packages/taler-wallet-core/src/operations/common.ts index 0fdde9dca..6d54503a1 100644 --- a/packages/taler-wallet-core/src/operations/common.ts +++ b/packages/taler-wallet-core/src/operations/common.ts @@ -47,7 +47,7 @@ export async function guardOperationException( throw TalerError.fromDetail( TalerErrorCode.WALLET_PENDING_OPERATION_FAILED, { - innerError: e.errorDetail, + innerError: opErr, }, ); } diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts index c11c45dc6..075d9d738 100644 --- a/packages/taler-wallet-core/src/operations/deposits.ts +++ b/packages/taler-wallet-core/src/operations/deposits.ts @@ -41,11 +41,11 @@ import { TrackDepositGroupResponse, URL, } from "@gnu-taler/taler-util"; -import { DepositGroupRecord, OperationStatus } from "../db.js"; +import { DepositGroupRecord, OperationStatus, WireFee } 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, RetryInfo } from "../util/retries.js"; +import { resetRetryInfo, RetryInfo } from "../util/retries.js"; import { guardOperationException } from "./common.js"; import { getExchangeDetails } from "./exchanges.js"; import { @@ -63,9 +63,15 @@ import { getTotalRefreshCost } from "./refresh.js"; */ const logger = new Logger("deposits.ts"); -async function resetDepositGroupRetry( +/** + * Set up the retry timeout for a deposit group. + */ +async function setupDepositGroupRetry( ws: InternalWalletState, depositGroupId: string, + options: { + resetRetry: boolean; + }, ): Promise { await ws.db .mktx((x) => ({ @@ -76,29 +82,19 @@ async function resetDepositGroupRetry( if (!x) { return; } - x.retryInfo = initRetryInfo(); + if (options.resetRetry) { + x.retryInfo = resetRetryInfo(); + } else { + x.retryInfo = RetryInfo.increment(x.retryInfo); + } delete x.lastError; await tx.depositGroups.put(x); }); } -async function incrementDepositGroupRetry( - ws: InternalWalletState, - depositGroupId: string, -): Promise { - 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); - }); -} - +/** + * Report an error that occurred while processing the deposit group. + */ async function reportDepositGroupError( ws: InternalWalletState, depositGroupId: string, @@ -131,9 +127,6 @@ export async function processDepositGroup( cancellationToken?: CancellationToken; } = {}, ): Promise { - if (ws.taskCancellationSourceForDeposit) { - ws.taskCancellationSourceForDeposit.cancel(); - } const onOpErr = (err: TalerErrorDetail): Promise => reportDepositGroupError(ws, depositGroupId, err); return await guardOperationException( @@ -170,11 +163,7 @@ async function processDepositGroupImpl( return; } - if (forceNow) { - await resetDepositGroupRetry(ws, depositGroupId); - } else { - await incrementDepositGroupRetry(ws, depositGroupId); - } + await setupDepositGroupRetry(ws, depositGroupId, { resetRetry: forceNow }); const contractData = extractContractData( depositGroup.contractTermsRaw, @@ -315,7 +304,7 @@ export async function trackDepositGroup( export async function getFeeForDeposit( ws: InternalWalletState, req: GetFeeForDepositRequest, -): Promise { +): Promise { const p = parsePaytoUri(req.depositPaytoUri); if (!p) { throw Error("invalid payto URI"); @@ -370,7 +359,7 @@ export async function getFeeForDeposit( throw Error("insufficient funds"); } - return await getTotalFeeForDepositAmount( + return await getTotalFeesForDepositAmount( ws, p.targetType, amount, @@ -429,14 +418,12 @@ export async function createDepositGroup( nonce: noncePair.pub, wire_transfer_deadline: nowRounded, order_id: "", - // This is always the v2 wire hash, as we're the "merchant" and support v2. h_wire: wireHash, - // Required for older exchanges. pay_deadline: AbsoluteTime.toTimestamp( AbsoluteTime.addDuration(now, durationFromSpec({ hours: 1 })), ), merchant: { - name: "", + name: "(wallet)", }, merchant_pub: merchantPair.pub, refund_deadline: TalerProtocolTimestamp.zero(), @@ -505,7 +492,7 @@ export async function createDepositGroup( payto_uri: req.depositPaytoUri, salt: wireSalt, }, - retryInfo: initRetryInfo(), + retryInfo: resetRetryInfo(), operationStatus: OperationStatus.Pending, lastError: undefined, }; @@ -594,8 +581,7 @@ export async function getEffectiveDepositAmount( return Amounts.sub(Amounts.sum(amt).amount, Amounts.sum(fees).amount).amount; } -// FIXME: rename to DepositGroupFee -export interface DepositFee { +export interface DepositGroupFees { coin: AmountJson; wire: AmountJson; refresh: AmountJson; @@ -605,12 +591,12 @@ export interface DepositFee { * Get the fee amount that will be charged when trying to deposit the * specified amount using the selected coins and the wire method. */ -export async function getTotalFeeForDepositAmount( +export async function getTotalFeesForDepositAmount( ws: InternalWalletState, wireType: string, total: AmountJson, pcs: PayCoinSelection, -): Promise { +): Promise { const wireFee: AmountJson[] = []; const coinFee: AmountJson[] = []; const refreshFee: AmountJson[] = []; @@ -638,8 +624,6 @@ export async function getTotalFeeForDepositAmount( if (!denom) { throw Error("can't find denomination to calculate deposit amount"); } - // const cc = pcs.coinContributions[i] - // acc = Amounts.add(acc, cc).amount coinFee.push(denom.feeDeposit); exchangeSet.add(coin.exchangeBaseUrl); @@ -661,16 +645,15 @@ export async function getTotalFeeForDepositAmount( if (!exchangeDetails) { continue; } - // FIXME/NOTE: the line below _likely_ throws exception - // about "find method not found on undefined" when the wireType - // is not supported by the Exchange. - const fee = exchangeDetails.wireInfo.feesForType[wireType].find((x) => { - return AbsoluteTime.isBetween( - AbsoluteTime.now(), - AbsoluteTime.fromTimestamp(x.startStamp), - AbsoluteTime.fromTimestamp(x.endStamp), - ); - })?.wireFee; + const fee = exchangeDetails.wireInfo.feesForType[wireType]?.find( + (x) => { + return AbsoluteTime.isBetween( + AbsoluteTime.now(), + AbsoluteTime.fromTimestamp(x.startStamp), + AbsoluteTime.fromTimestamp(x.endStamp), + ); + }, + )?.wireFee; if (fee) { wireFee.push(fee); } diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts index fe1c9ef35..26bca8c14 100644 --- a/packages/taler-wallet-core/src/operations/exchanges.ts +++ b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -20,6 +20,7 @@ import { AbsoluteTime, Amounts, + CancellationToken, canonicalizeBaseUrl, codecForExchangeKeysJson, codecForExchangeWireJson, @@ -61,11 +62,7 @@ import { readSuccessResponseTextOrThrow, } from "../util/http.js"; import { DbAccess, GetReadOnlyAccess } from "../util/query.js"; -import { - initRetryInfo, - RetryInfo, - updateRetryInfoTimeout, -} from "../util/retries.js"; +import { resetRetryInfo, RetryInfo } from "../util/retries.js"; import { WALLET_CACHE_BREAKER_CLIENT_VERSION, WALLET_EXCHANGE_PROTOCOL_VERSION, @@ -124,9 +121,12 @@ async function reportExchangeUpdateError( ws.notify({ type: NotificationType.ExchangeOperationError, error: err }); } -async function resetExchangeUpdateRetry( +async function setupExchangeUpdateRetry( ws: InternalWalletState, baseUrl: string, + options: { + reset: boolean; + }, ): Promise { await ws.db .mktx((x) => ({ exchanges: x.exchanges })) @@ -135,25 +135,12 @@ async function resetExchangeUpdateRetry( if (!exchange) { return; } - delete exchange.lastError; - exchange.retryInfo = initRetryInfo(); - await tx.exchanges.put(exchange); - }); -} - -async function incrementExchangeUpdateRetry( - ws: InternalWalletState, - baseUrl: string, -): Promise { - await ws.db - .mktx((x) => ({ exchanges: x.exchanges })) - .runReadWrite(async (tx) => { - const exchange = await tx.exchanges.get(baseUrl); - if (!exchange) { - return; + if (options.reset) { + exchange.retryInfo = resetRetryInfo(); + } else { + exchange.retryInfo = RetryInfo.increment(exchange.retryInfo); } delete exchange.lastError; - exchange.retryInfo = RetryInfo.increment(exchange.retryInfo); await tx.exchanges.put(exchange); }); } @@ -376,8 +363,10 @@ async function downloadExchangeWireInfo( export async function updateExchangeFromUrl( ws: InternalWalletState, baseUrl: string, - acceptedFormat?: string[], - forceNow = false, + options: { + forceNow?: boolean; + cancellationToken?: CancellationToken; + } = {}, ): Promise<{ exchange: ExchangeRecord; exchangeDetails: ExchangeDetailsRecord; @@ -385,7 +374,7 @@ export async function updateExchangeFromUrl( const onOpErr = (e: TalerErrorDetail): Promise => reportExchangeUpdateError(ws, baseUrl, e); return await guardOperationException( - () => updateExchangeFromUrlImpl(ws, baseUrl, acceptedFormat, forceNow), + () => updateExchangeFromUrlImpl(ws, baseUrl, options), onOpErr, ); } @@ -409,7 +398,7 @@ async function provideExchangeRecord( const r: ExchangeRecord = { permanent: true, baseUrl: baseUrl, - retryInfo: initRetryInfo(), + retryInfo: resetRetryInfo(), detailsPointer: undefined, lastUpdate: undefined, nextUpdate: AbsoluteTime.toTimestamp(now), @@ -552,12 +541,15 @@ export async function downloadTosFromAcceptedFormat( async function updateExchangeFromUrlImpl( ws: InternalWalletState, baseUrl: string, - acceptedFormat?: string[], - forceNow = false, + options: { + forceNow?: boolean; + cancellationToken?: CancellationToken; + } = {}, ): Promise<{ exchange: ExchangeRecord; exchangeDetails: ExchangeDetailsRecord; }> { + const forceNow = options.forceNow ?? false; logger.info(`updating exchange info for ${baseUrl}, forced: ${forceNow}`); const now = AbsoluteTime.now(); baseUrl = canonicalizeBaseUrl(baseUrl); @@ -577,11 +569,7 @@ async function updateExchangeFromUrlImpl( return { exchange, exchangeDetails }; } - if (forceNow) { - await resetExchangeUpdateRetry(ws, baseUrl); - } else { - await incrementExchangeUpdateRetry(ws, baseUrl); - } + await setupExchangeUpdateRetry(ws, baseUrl, { reset: forceNow }); logger.info("updating exchange /keys info"); @@ -617,7 +605,7 @@ async function updateExchangeFromUrlImpl( ws, baseUrl, timeout, - acceptedFormat, + ["text/plain"], ); const tosHasBeenAccepted = exchangeDetails?.termsOfServiceAcceptedEtag === tosDownload.tosEtag; diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts index b761367fb..fa36c724f 100644 --- a/packages/taler-wallet-core/src/operations/pay.ts +++ b/packages/taler-wallet-core/src/operations/pay.ts @@ -97,7 +97,7 @@ import { import { GetReadWriteAccess } from "../util/query.js"; import { getRetryDuration, - initRetryInfo, + resetRetryInfo, RetryInfo, updateRetryInfoTimeout, } from "../util/retries.js"; @@ -428,8 +428,8 @@ async function recordConfirmPay( proposalId: proposal.proposalId, lastPayError: undefined, lastRefundStatusError: undefined, - payRetryInfo: initRetryInfo(), - refundStatusRetryInfo: initRetryInfo(), + payRetryInfo: resetRetryInfo(), + refundStatusRetryInfo: resetRetryInfo(), refundQueryRequested: false, timestampFirstSuccessfulPay: undefined, autoRefundDeadline: undefined, @@ -453,7 +453,7 @@ async function recordConfirmPay( if (p) { p.proposalStatus = ProposalStatus.Accepted; delete p.lastError; - p.retryInfo = initRetryInfo(); + delete p.retryInfo; await tx.proposals.put(p); } await tx.purchases.put(t); @@ -491,9 +491,12 @@ async function reportProposalError( ws.notify({ type: NotificationType.ProposalOperationError, error: err }); } -async function incrementProposalRetry( +async function setupProposalRetry( ws: InternalWalletState, proposalId: string, + options: { + reset: boolean; + }, ): Promise { await ws.db .mktx((x) => ({ proposals: x.proposals })) @@ -502,47 +505,37 @@ async function incrementProposalRetry( if (!pr) { return; } - if (!pr.retryInfo) { - return; + if (options.reset) { + pr.retryInfo = resetRetryInfo(); } else { - pr.retryInfo.retryCounter++; - updateRetryInfoTimeout(pr.retryInfo); + pr.retryInfo = RetryInfo.increment(pr.retryInfo); } delete pr.lastError; await tx.proposals.put(pr); }); } -async function resetPurchasePayRetry( +async function setupPurchasePayRetry( ws: InternalWalletState, proposalId: string, + options: { + reset: boolean; + }, ): Promise { await ws.db .mktx((x) => ({ purchases: x.purchases })) .runReadWrite(async (tx) => { const p = await tx.purchases.get(proposalId); - if (p) { - p.payRetryInfo = initRetryInfo(); - delete p.lastPayError; - await tx.purchases.put(p); - } - }); -} - -async function incrementPurchasePayRetry( - ws: InternalWalletState, - proposalId: string, -): Promise { - await ws.db - .mktx((x) => ({ purchases: x.purchases })) - .runReadWrite(async (tx) => { - const pr = await tx.purchases.get(proposalId); - if (!pr) { + if (!p) { return; } - pr.payRetryInfo = RetryInfo.increment(pr.payRetryInfo); - delete pr.lastPayError; - await tx.purchases.put(pr); + if (options.reset) { + p.payRetryInfo = resetRetryInfo(); + } else { + p.payRetryInfo = RetryInfo.increment(p.payRetryInfo); + } + delete p.lastPayError; + await tx.purchases.put(p); }); } @@ -572,32 +565,18 @@ async function reportPurchasePayError( export async function processDownloadProposal( ws: InternalWalletState, proposalId: string, - forceNow = false, + options: { + forceNow?: boolean; + } = {}, ): Promise { const onOpErr = (err: TalerErrorDetail): Promise => reportProposalError(ws, proposalId, err); await guardOperationException( - () => processDownloadProposalImpl(ws, proposalId, forceNow), + () => processDownloadProposalImpl(ws, proposalId, options), onOpErr, ); } -async function resetDownloadProposalRetry( - ws: InternalWalletState, - proposalId: string, -): Promise { - await ws.db - .mktx((x) => ({ proposals: x.proposals })) - .runReadWrite(async (tx) => { - const p = await tx.proposals.get(proposalId); - if (p) { - p.retryInfo = initRetryInfo(); - delete p.lastError; - await tx.proposals.put(p); - } - }); -} - async function failProposalPermanently( ws: InternalWalletState, proposalId: string, @@ -678,8 +657,11 @@ export function extractContractData( async function processDownloadProposalImpl( ws: InternalWalletState, proposalId: string, - forceNow: boolean, + options: { + forceNow?: boolean; + } = {}, ): Promise { + const forceNow = options.forceNow ?? false; const proposal = await ws.db .mktx((x) => ({ proposals: x.proposals })) .runReadOnly(async (tx) => { @@ -694,11 +676,7 @@ async function processDownloadProposalImpl( return; } - if (forceNow) { - await resetDownloadProposalRetry(ws, proposalId); - } else { - await incrementProposalRetry(ws, proposalId); - } + await setupProposalRetry(ws, proposalId, { reset: forceNow }); const orderClaimUrl = new URL( `orders/${proposal.orderId}/claim`, @@ -946,7 +924,7 @@ async function startDownloadProposal( proposalId: proposalId, proposalStatus: ProposalStatus.Downloading, repurchaseProposalId: undefined, - retryInfo: initRetryInfo(), + retryInfo: resetRetryInfo(), lastError: undefined, downloadSessionId: sessionId, }; @@ -994,7 +972,7 @@ async function storeFirstPaySuccess( purchase.paymentSubmitPending = false; purchase.lastPayError = undefined; purchase.lastSessionId = sessionId; - purchase.payRetryInfo = initRetryInfo(); + purchase.payRetryInfo = resetRetryInfo(); purchase.merchantPaySig = paySig; if (isFirst) { const protoAr = purchase.download.contractData.autoRefund; @@ -1002,7 +980,7 @@ async function storeFirstPaySuccess( const ar = Duration.fromTalerProtocolDuration(protoAr); logger.info("auto_refund present"); purchase.refundQueryRequested = true; - purchase.refundStatusRetryInfo = initRetryInfo(); + purchase.refundStatusRetryInfo = resetRetryInfo(); purchase.lastRefundStatusError = undefined; purchase.autoRefundDeadline = AbsoluteTime.toTimestamp( AbsoluteTime.addDuration(AbsoluteTime.now(), ar), @@ -1033,7 +1011,7 @@ async function storePayReplaySuccess( } purchase.paymentSubmitPending = false; purchase.lastPayError = undefined; - purchase.payRetryInfo = initRetryInfo(); + purchase.payRetryInfo = resetRetryInfo(); purchase.lastSessionId = sessionId; await tx.purchases.put(purchase); }); @@ -1289,7 +1267,7 @@ export async function checkPaymentByProposalId( p.paymentSubmitPending = true; await tx.purchases.put(p); }); - const r = await processPurchasePay(ws, proposalId, true); + const r = await processPurchasePay(ws, proposalId, { forceNow: true }); if (r.type !== ConfirmPayResultType.Done) { throw Error("submitting pay failed"); } @@ -1466,7 +1444,7 @@ export async function confirmPay( if (existingPurchase) { logger.trace("confirmPay: submitting payment for existing purchase"); - return await processPurchasePay(ws, proposalId, true); + return await processPurchasePay(ws, proposalId, { forceNow: true }); } logger.trace("confirmPay: purchase record does not exist yet"); @@ -1516,18 +1494,20 @@ export async function confirmPay( sessionIdOverride, ); - return await processPurchasePay(ws, proposalId, true); + return await processPurchasePay(ws, proposalId, { forceNow: true }); } export async function processPurchasePay( ws: InternalWalletState, proposalId: string, - forceNow = false, + options: { + forceNow?: boolean; + } = {}, ): Promise { const onOpErr = (e: TalerErrorDetail): Promise => reportPurchasePayError(ws, proposalId, e); return await guardOperationException( - () => processPurchasePayImpl(ws, proposalId, forceNow), + () => processPurchasePayImpl(ws, proposalId, options), onOpErr, ); } @@ -1535,8 +1515,11 @@ export async function processPurchasePay( async function processPurchasePayImpl( ws: InternalWalletState, proposalId: string, - forceNow: boolean, + options: { + forceNow?: boolean; + } = {}, ): Promise { + const forceNow = options.forceNow ?? false; const purchase = await ws.db .mktx((x) => ({ purchases: x.purchases })) .runReadOnly(async (tx) => { @@ -1559,11 +1542,7 @@ async function processPurchasePayImpl( lastError: purchase.lastPayError, }; } - if (forceNow) { - await resetPurchasePayRetry(ws, proposalId); - } else { - await incrementPurchasePayRetry(ws, proposalId); - } + await setupPurchasePayRetry(ws, proposalId, { reset: forceNow }); logger.trace(`processing purchase pay ${proposalId}`); const sessionId = purchase.lastSessionId; diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts index 71b8b34f4..b89c7c640 100644 --- a/packages/taler-wallet-core/src/operations/pending.ts +++ b/packages/taler-wallet-core/src/operations/pending.ts @@ -51,18 +51,17 @@ async function gatherExchangePending( resp.pendingOperations.push({ type: PendingTaskType.ExchangeUpdate, givesLifeness: false, - timestampDue: e.lastError - ? e.retryInfo.nextRetry - : AbsoluteTime.fromTimestamp(e.nextUpdate), + timestampDue: + e.retryInfo?.nextRetry ?? AbsoluteTime.fromTimestamp(e.nextUpdate), exchangeBaseUrl: e.baseUrl, lastError: e.lastError, }); resp.pendingOperations.push({ type: PendingTaskType.ExchangeCheckRefresh, - timestampDue: e.lastError - ? e.retryInfo.nextRetry - : AbsoluteTime.fromTimestamp(e.nextRefreshCheck), + timestampDue: + e.retryInfo?.nextRetry ?? + AbsoluteTime.fromTimestamp(e.nextRefreshCheck), givesLifeness: false, exchangeBaseUrl: e.baseUrl, }); diff --git a/packages/taler-wallet-core/src/operations/recoup.ts b/packages/taler-wallet-core/src/operations/recoup.ts index e30f09808..4fac2ec16 100644 --- a/packages/taler-wallet-core/src/operations/recoup.ts +++ b/packages/taler-wallet-core/src/operations/recoup.ts @@ -48,7 +48,11 @@ import { import { readSuccessResponseJsonOrThrow } from "../util/http.js"; import { Logger, URL } from "@gnu-taler/taler-util"; -import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js"; +import { + resetRetryInfo, + RetryInfo, + updateRetryInfoTimeout, +} from "../util/retries.js"; import { createRefreshGroup, processRefreshGroup } from "./refresh.js"; import { getReserveRequestTimeout, processReserve } from "./reserves.js"; import { InternalWalletState } from "../internal-wallet-state.js"; @@ -57,10 +61,12 @@ import { guardOperationException } from "./common.js"; const logger = new Logger("operations/recoup.ts"); -async function incrementRecoupRetry( +async function setupRecoupRetry( ws: InternalWalletState, recoupGroupId: string, - err: TalerErrorDetail | undefined, + options: { + reset: boolean; + }, ): Promise { await ws.db .mktx((x) => ({ @@ -71,17 +77,39 @@ async function incrementRecoupRetry( if (!r) { return; } - if (!r.retryInfo) { + if (options.reset) { + r.retryInfo = resetRetryInfo(); + } else { + r.retryInfo = RetryInfo.increment(r.retryInfo); + } + delete r.lastError; + await tx.recoupGroups.put(r); + }); +} + +async function reportRecoupError( + ws: InternalWalletState, + recoupGroupId: string, + err: TalerErrorDetail, +): Promise { + await ws.db + .mktx((x) => ({ + recoupGroups: x.recoupGroups, + })) + .runReadWrite(async (tx) => { + const r = await tx.recoupGroups.get(recoupGroupId); + if (!r) { return; } - r.retryInfo.retryCounter++; - updateRetryInfoTimeout(r.retryInfo); + if (!r.retryInfo) { + logger.error( + "reporting error for inactive recoup group (no retry info)", + ); + } r.lastError = err; await tx.recoupGroups.put(r); }); - if (err) { - ws.notify({ type: NotificationType.RecoupOperationError, error: err }); - } + ws.notify({ type: NotificationType.RecoupOperationError, error: err }); } async function putGroupAsFinished( @@ -111,7 +139,7 @@ async function putGroupAsFinished( if (allFinished) { logger.info("all recoups of recoup group are finished"); recoupGroup.timestampFinished = TalerProtocolTimestamp.now(); - recoupGroup.retryInfo = initRetryInfo(); + recoupGroup.retryInfo = resetRetryInfo(); recoupGroup.lastError = undefined; if (recoupGroup.scheduleRefreshCoins.length > 0) { const refreshGroupId = await createRefreshGroup( @@ -250,7 +278,7 @@ async function recoupWithdrawCoin( const currency = updatedCoin.currentAmount.currency; updatedCoin.currentAmount = Amounts.getZero(currency); updatedReserve.reserveStatus = ReserveRecordStatus.QueryingStatus; - updatedReserve.retryInfo = initRetryInfo(); + updatedReserve.retryInfo = resetRetryInfo(); updatedReserve.operationStatus = OperationStatus.Pending; await tx.coins.put(updatedCoin); await tx.reserves.put(updatedReserve); @@ -361,33 +389,18 @@ async function recoupRefreshCoin( }); } -async function resetRecoupGroupRetry( - ws: InternalWalletState, - recoupGroupId: string, -): Promise { - await ws.db - .mktx((x) => ({ - recoupGroups: x.recoupGroups, - })) - .runReadWrite(async (tx) => { - const x = await tx.recoupGroups.get(recoupGroupId); - if (x) { - x.retryInfo = initRetryInfo(); - await tx.recoupGroups.put(x); - } - }); -} - export async function processRecoupGroup( ws: InternalWalletState, recoupGroupId: string, - forceNow = false, + options: { + forceNow?: boolean; + } = {}, ): Promise { await ws.memoProcessRecoup.memo(recoupGroupId, async () => { const onOpErr = (e: TalerErrorDetail): Promise => - incrementRecoupRetry(ws, recoupGroupId, e); + reportRecoupError(ws, recoupGroupId, e); return await guardOperationException( - async () => await processRecoupGroupImpl(ws, recoupGroupId, forceNow), + async () => await processRecoupGroupImpl(ws, recoupGroupId, options), onOpErr, ); }); @@ -396,11 +409,12 @@ export async function processRecoupGroup( async function processRecoupGroupImpl( ws: InternalWalletState, recoupGroupId: string, - forceNow = false, + options: { + forceNow?: boolean; + } = {}, ): Promise { - if (forceNow) { - await resetRecoupGroupRetry(ws, recoupGroupId); - } + const forceNow = options.forceNow ?? false; + await setupRecoupRetry(ws, recoupGroupId, { reset: forceNow }); const recoupGroup = await ws.db .mktx((x) => ({ recoupGroups: x.recoupGroups, @@ -444,7 +458,7 @@ async function processRecoupGroupImpl( } for (const r of reserveSet.values()) { - processReserve(ws, r, true).catch((e) => { + processReserve(ws, r, { forceNow: true }).catch((e) => { logger.error(`processing reserve ${r} after recoup failed`); }); } @@ -468,7 +482,7 @@ export async function createRecoupGroup( lastError: undefined, timestampFinished: undefined, timestampStarted: TalerProtocolTimestamp.now(), - retryInfo: initRetryInfo(), + retryInfo: resetRetryInfo(), recoupFinishedPerCoin: coinPubs.map(() => false), // Will be populated later oldAmountPerCoin: [], diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index 2ab06abae..762023d2e 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -53,7 +53,11 @@ import { } from "../util/http.js"; import { checkDbInvariant } from "../util/invariants.js"; import { Logger } from "@gnu-taler/taler-util"; -import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js"; +import { + resetRetryInfo, + RetryInfo, + updateRetryInfoTimeout, +} from "../util/retries.js"; import { Duration, durationFromSpec, @@ -130,11 +134,11 @@ function updateGroupStatus(rg: RefreshGroupRecord): void { if (allDone) { if (anyFrozen) { rg.frozen = true; - rg.retryInfo = initRetryInfo(); + rg.retryInfo = resetRetryInfo(); } else { rg.timestampFinished = AbsoluteTime.toTimestamp(AbsoluteTime.now()); rg.operationStatus = OperationStatus.Finished; - rg.retryInfo = initRetryInfo(); + rg.retryInfo = resetRetryInfo(); } } } @@ -712,7 +716,33 @@ async function refreshReveal( }); } -async function incrementRefreshRetry( +async function setupRefreshRetry( + ws: InternalWalletState, + refreshGroupId: string, + options: { + reset: boolean; + }, +): Promise { + await ws.db + .mktx((x) => ({ + refreshGroups: x.refreshGroups, + })) + .runReadWrite(async (tx) => { + const r = await tx.refreshGroups.get(refreshGroupId); + if (!r) { + return; + } + if (options.reset) { + r.retryInfo = resetRetryInfo(); + } else { + r.retryInfo = RetryInfo.increment(r.retryInfo); + } + delete r.lastError; + await tx.refreshGroups.put(r); + }); +} + +async function reportRefreshError( ws: InternalWalletState, refreshGroupId: string, err: TalerErrorDetail | undefined, @@ -727,10 +757,10 @@ async function incrementRefreshRetry( return; } if (!r.retryInfo) { - return; + logger.error( + "reported error for inactive refresh group (no retry info)", + ); } - r.retryInfo.retryCounter++; - updateRetryInfoTimeout(r.retryInfo); r.lastError = err; await tx.refreshGroups.put(r); }); @@ -745,44 +775,31 @@ async function incrementRefreshRetry( export async function processRefreshGroup( ws: InternalWalletState, refreshGroupId: string, - forceNow = false, + options: { + forceNow?: boolean; + } = {}, ): Promise { await ws.memoProcessRefresh.memo(refreshGroupId, async () => { const onOpErr = (e: TalerErrorDetail): Promise => - incrementRefreshRetry(ws, refreshGroupId, e); + reportRefreshError(ws, refreshGroupId, e); return await guardOperationException( - async () => await processRefreshGroupImpl(ws, refreshGroupId, forceNow), + async () => await processRefreshGroupImpl(ws, refreshGroupId, options), onOpErr, ); }); } -async function resetRefreshGroupRetry( - ws: InternalWalletState, - refreshGroupId: string, -): Promise { - await ws.db - .mktx((x) => ({ - refreshGroups: x.refreshGroups, - })) - .runReadWrite(async (tx) => { - const x = await tx.refreshGroups.get(refreshGroupId); - if (x) { - x.retryInfo = initRetryInfo(); - await tx.refreshGroups.put(x); - } - }); -} - async function processRefreshGroupImpl( ws: InternalWalletState, refreshGroupId: string, - forceNow: boolean, + options: { + forceNow?: boolean; + } = {}, ): Promise { + const forceNow = options.forceNow ?? false; logger.info(`processing refresh group ${refreshGroupId}`); - if (forceNow) { - await resetRefreshGroupRetry(ws, refreshGroupId); - } + await setupRefreshRetry(ws, refreshGroupId, { reset: forceNow }); + const refreshGroup = await ws.db .mktx((x) => ({ refreshGroups: x.refreshGroups, @@ -939,7 +956,7 @@ export async function createRefreshGroup( reason, refreshGroupId, refreshSessionPerCoin: oldCoinPubs.map(() => undefined), - retryInfo: initRetryInfo(), + retryInfo: resetRetryInfo(), inputPerCoin, estimatedOutputPerCoin, timestampCreated: TalerProtocolTimestamp.now(), @@ -994,7 +1011,9 @@ export async function autoRefresh( exchangeBaseUrl: string, ): Promise { logger.info(`doing auto-refresh check for '${exchangeBaseUrl}'`); - await updateExchangeFromUrl(ws, exchangeBaseUrl, undefined, true); + await updateExchangeFromUrl(ws, exchangeBaseUrl, { + forceNow: true, + }); let minCheckThreshold = AbsoluteTime.addDuration( AbsoluteTime.now(), durationFromSpec({ days: 1 }), diff --git a/packages/taler-wallet-core/src/operations/refund.ts b/packages/taler-wallet-core/src/operations/refund.ts index 4190bee7f..7ef8076f0 100644 --- a/packages/taler-wallet-core/src/operations/refund.ts +++ b/packages/taler-wallet-core/src/operations/refund.ts @@ -58,37 +58,54 @@ import { import { readSuccessResponseJsonOrThrow } from "../util/http.js"; import { checkDbInvariant } from "../util/invariants.js"; import { GetReadWriteAccess } from "../util/query.js"; -import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js"; +import { + resetRetryInfo, + RetryInfo, + updateRetryInfoTimeout, +} from "../util/retries.js"; import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js"; import { InternalWalletState } from "../internal-wallet-state.js"; import { guardOperationException } from "./common.js"; const logger = new Logger("refund.ts"); -async function resetPurchaseQueryRefundRetry( +/** + * Retry querying and applying refunds for an order later. + */ +async function setupPurchaseQueryRefundRetry( ws: InternalWalletState, proposalId: string, + options: { + reset: boolean; + }, ): Promise { await ws.db .mktx((x) => ({ purchases: x.purchases, })) .runReadWrite(async (tx) => { - const x = await tx.purchases.get(proposalId); - if (x) { - x.refundStatusRetryInfo = initRetryInfo(); - await tx.purchases.put(x); + const pr = await tx.purchases.get(proposalId); + if (!pr) { + return; + } + if (options.reset) { + pr.refundStatusRetryInfo = resetRetryInfo(); + } else { + pr.refundStatusRetryInfo = RetryInfo.increment( + pr.refundStatusRetryInfo, + ); } + await tx.purchases.put(pr); }); } /** - * Retry querying and applying refunds for an order later. + * Report an error that happending when querying for a purchase's refund. */ -async function incrementPurchaseQueryRefundRetry( +async function reportPurchaseQueryRefundError( ws: InternalWalletState, proposalId: string, - err: TalerErrorDetail | undefined, + err: TalerErrorDetail, ): Promise { await ws.db .mktx((x) => ({ @@ -100,10 +117,10 @@ async function incrementPurchaseQueryRefundRetry( return; } if (!pr.refundStatusRetryInfo) { - return; + logger.error( + "reported error on an inactive purchase (no refund status retry info)", + ); } - pr.refundStatusRetryInfo.retryCounter++; - updateRetryInfoTimeout(pr.refundStatusRetryInfo); pr.lastRefundStatusError = err; await tx.purchases.put(pr); }); @@ -425,7 +442,7 @@ async function acceptRefunds( if (queryDone) { p.timestampLastRefundStatus = now; p.lastRefundStatusError = undefined; - p.refundStatusRetryInfo = initRetryInfo(); + p.refundStatusRetryInfo = resetRetryInfo(); p.refundQueryRequested = false; if (p.abortStatus === AbortStatus.AbortRefund) { p.abortStatus = AbortStatus.AbortFinished; @@ -506,7 +523,7 @@ export async function applyRefund( } p.refundQueryRequested = true; p.lastRefundStatusError = undefined; - p.refundStatusRetryInfo = initRetryInfo(); + p.refundStatusRetryInfo = resetRetryInfo(); await tx.purchases.put(p); return true; }); @@ -515,7 +532,10 @@ export async function applyRefund( ws.notify({ type: NotificationType.RefundStarted, }); - await processPurchaseQueryRefundImpl(ws, proposalId, true, false); + await processPurchaseQueryRefundImpl(ws, proposalId, { + forceNow: true, + waitForAutoRefund: false, + }); } purchase = await ws.db @@ -590,12 +610,15 @@ export async function applyRefund( export async function processPurchaseQueryRefund( ws: InternalWalletState, proposalId: string, - forceNow = false, + options: { + forceNow?: boolean; + waitForAutoRefund?: boolean; + } = {}, ): Promise { const onOpErr = (e: TalerErrorDetail): Promise => - incrementPurchaseQueryRefundRetry(ws, proposalId, e); + reportPurchaseQueryRefundError(ws, proposalId, e); await guardOperationException( - () => processPurchaseQueryRefundImpl(ws, proposalId, forceNow, true), + () => processPurchaseQueryRefundImpl(ws, proposalId, options), onOpErr, ); } @@ -603,12 +626,14 @@ export async function processPurchaseQueryRefund( async function processPurchaseQueryRefundImpl( ws: InternalWalletState, proposalId: string, - forceNow: boolean, - waitForAutoRefund: boolean, + options: { + forceNow?: boolean; + waitForAutoRefund?: boolean; + } = {}, ): Promise { - if (forceNow) { - await resetPurchaseQueryRefundRetry(ws, proposalId); - } + const forceNow = options.forceNow ?? false; + const waitForAutoRefund = options.waitForAutoRefund ?? false; + await setupPurchaseQueryRefundRetry(ws, proposalId, { reset: forceNow }); const purchase = await ws.db .mktx((x) => ({ purchases: x.purchases, @@ -650,7 +675,7 @@ async function processPurchaseQueryRefundImpl( codecForMerchantOrderStatusPaid(), ); if (!orderStatus.refunded) { - incrementPurchaseQueryRefundRetry(ws, proposalId, undefined); + // Wait for retry ... return; } } @@ -666,11 +691,6 @@ async function processPurchaseQueryRefundImpl( h_contract: purchase.download.contractData.contractTermsHash, }); - logger.trace( - "got json", - JSON.stringify(await request.json(), undefined, 2), - ); - const refundResponse = await readSuccessResponseJsonOrThrow( request, codecForMerchantOrderRefundPickupResponse(), @@ -777,10 +797,12 @@ export async function abortFailedPayWithRefund( purchase.paymentSubmitPending = false; purchase.abortStatus = AbortStatus.AbortRefund; purchase.lastPayError = undefined; - purchase.payRetryInfo = initRetryInfo(); + purchase.payRetryInfo = resetRetryInfo(); await tx.purchases.put(purchase); }); - processPurchaseQueryRefund(ws, proposalId, true).catch((e) => { + processPurchaseQueryRefund(ws, proposalId, { + forceNow: true, + }).catch((e) => { logger.trace(`error during refund processing after abort pay: ${e}`); }); } diff --git a/packages/taler-wallet-core/src/operations/reserves.ts b/packages/taler-wallet-core/src/operations/reserves.ts index 9cbd63c45..38a7386b2 100644 --- a/packages/taler-wallet-core/src/operations/reserves.ts +++ b/packages/taler-wallet-core/src/operations/reserves.ts @@ -57,7 +57,8 @@ import { import { GetReadOnlyAccess } from "../util/query.js"; import { getRetryDuration, - initRetryInfo, + resetRetryInfo, + RetryInfo, updateRetryInfoTimeout, } from "../util/retries.js"; import { @@ -79,34 +80,15 @@ import { guardOperationException } from "./common.js"; const logger = new Logger("taler-wallet-core:reserves.ts"); /** - * Reset the retry counter for the reserve - * and reset the last error. + * Set up the reserve's retry timeout in preparation for + * processing the reserve. */ -async function resetReserveRetry( - ws: InternalWalletState, - reservePub: string, -): Promise { - await ws.db - .mktx((x) => ({ - reserves: x.reserves, - })) - .runReadWrite(async (tx) => { - const x = await tx.reserves.get(reservePub); - if (x) { - x.retryInfo = initRetryInfo(); - delete x.lastError; - await tx.reserves.put(x); - } - }); -} - -/** - * Increment the retry counter for the reserve and - * reset the last eror. - */ -async function incrementReserveRetry( +async function setupReserveRetry( ws: InternalWalletState, reservePub: string, + options: { + reset: boolean; + }, ): Promise { await ws.db .mktx((x) => ({ @@ -117,11 +99,10 @@ async function incrementReserveRetry( if (!r) { return; } - if (!r.retryInfo) { - r.retryInfo = initRetryInfo(); + if (options.reset) { + r.retryInfo = resetRetryInfo(); } else { - r.retryInfo.retryCounter++; - updateRetryInfoTimeout(r.retryInfo); + r.retryInfo = RetryInfo.increment(r.retryInfo); } delete r.lastError; await tx.reserves.put(r); @@ -216,7 +197,7 @@ export async function createReserve( timestampReserveInfoPosted: undefined, bankInfo, reserveStatus, - retryInfo: initRetryInfo(), + retryInfo: resetRetryInfo(), lastError: undefined, currency: req.amount.currency, operationStatus: OperationStatus.Pending, @@ -288,7 +269,7 @@ export async function createReserve( // Asynchronously process the reserve, but return // to the caller already. - processReserve(ws, resp.reservePub, true).catch((e) => { + processReserve(ws, resp.reservePub, { forceNow: true }).catch((e) => { logger.error("Processing reserve (after createReserve) failed:", e); }); @@ -316,14 +297,14 @@ export async function forceQueryReserve( case ReserveRecordStatus.Dormant: reserve.reserveStatus = ReserveRecordStatus.QueryingStatus; reserve.operationStatus = OperationStatus.Pending; - reserve.retryInfo = initRetryInfo(); + reserve.retryInfo = resetRetryInfo(); break; default: break; } await tx.reserves.put(reserve); }); - await processReserve(ws, reservePub, true); + await processReserve(ws, reservePub, { forceNow: true }); } /** @@ -336,13 +317,15 @@ export async function forceQueryReserve( export async function processReserve( ws: InternalWalletState, reservePub: string, - forceNow = false, + options: { + forceNow?: boolean; + } = {}, ): Promise { return ws.memoProcessReserve.memo(reservePub, async () => { const onOpError = (err: TalerErrorDetail): Promise => reportReserveError(ws, reservePub, err); await guardOperationException( - () => processReserveImpl(ws, reservePub, forceNow), + () => processReserveImpl(ws, reservePub, options), onOpError, ); }); @@ -409,7 +392,7 @@ async function registerReserveWithBank( if (!r.bankInfo) { throw Error("invariant failed"); } - r.retryInfo = initRetryInfo(); + r.retryInfo = resetRetryInfo(); await tx.reserves.put(r); }); ws.notify({ type: NotificationType.ReserveRegisteredWithBank }); @@ -476,7 +459,7 @@ async function processReserveBankStatus( r.timestampBankConfirmed = now; r.reserveStatus = ReserveRecordStatus.BankAborted; r.operationStatus = OperationStatus.Finished; - r.retryInfo = initRetryInfo(); + r.retryInfo = resetRetryInfo(); await tx.reserves.put(r); }); return; @@ -513,7 +496,7 @@ async function processReserveBankStatus( r.timestampBankConfirmed = now; r.reserveStatus = ReserveRecordStatus.QueryingStatus; r.operationStatus = OperationStatus.Pending; - r.retryInfo = initRetryInfo(); + r.retryInfo = resetRetryInfo(); } else { switch (r.reserveStatus) { case ReserveRecordStatus.WaitConfirmBank: @@ -684,7 +667,7 @@ async function updateReserve( reservePub: reserve.reservePub, rawWithdrawalAmount: remainingAmount, timestampStart: AbsoluteTime.toTimestamp(AbsoluteTime.now()), - retryInfo: initRetryInfo(), + retryInfo: resetRetryInfo(), lastError: undefined, denomsSel: denomSelectionInfoToState(denomSelInfo), secretSeed: encodeCrock(getRandomBytes(64)), @@ -717,8 +700,12 @@ async function updateReserve( async function processReserveImpl( ws: InternalWalletState, reservePub: string, - forceNow = false, + options: { + forceNow?: boolean; + } = {}, ): Promise { + const forceNow = options.forceNow ?? false; + await setupReserveRetry(ws, reservePub, { reset: forceNow }); const reserve = await ws.db .mktx((x) => ({ reserves: x.reserves, @@ -732,27 +719,17 @@ async function processReserveImpl( ); return; } - if (forceNow) { - await resetReserveRetry(ws, reservePub); - } else if ( - reserve.retryInfo && - !AbsoluteTime.isExpired(reserve.retryInfo.nextRetry) - ) { - logger.trace("processReserve retry not due yet"); - return; - } - await incrementReserveRetry(ws, reservePub); logger.trace( `Processing reserve ${reservePub} with status ${reserve.reserveStatus}`, ); switch (reserve.reserveStatus) { case ReserveRecordStatus.RegisteringBank: await processReserveBankStatus(ws, reservePub); - return await processReserveImpl(ws, reservePub, true); + return await processReserveImpl(ws, reservePub, { forceNow: true }); case ReserveRecordStatus.QueryingStatus: const res = await updateReserve(ws, reservePub); if (res.ready) { - return await processReserveImpl(ws, reservePub, true); + return await processReserveImpl(ws, reservePub, { forceNow: true }); } break; case ReserveRecordStatus.Dormant: diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts index 9582f18e8..8bf85fe99 100644 --- a/packages/taler-wallet-core/src/operations/tip.ts +++ b/packages/taler-wallet-core/src/operations/tip.ts @@ -43,7 +43,11 @@ import { } from "../db.js"; import { j2s } from "@gnu-taler/taler-util"; import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; -import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js"; +import { + resetRetryInfo, + RetryInfo, + updateRetryInfoTimeout, +} from "../util/retries.js"; import { makeErrorDetail } from "../errors.js"; import { updateExchangeFromUrl } from "./exchanges.js"; import { InternalWalletState } from "../internal-wallet-state.js"; @@ -127,7 +131,7 @@ export async function prepareTip( createdTimestamp: TalerProtocolTimestamp.now(), merchantTipId: res.merchantTipId, tipAmountEffective: selectedDenoms.totalCoinValue, - retryInfo: initRetryInfo(), + retryInfo: resetRetryInfo(), lastError: undefined, denomsSel: denomSelectionInfoToState(selectedDenoms), pickedUpTimestamp: undefined, @@ -157,10 +161,10 @@ export async function prepareTip( return tipStatus; } -async function incrementTipRetry( +async function reportTipError( ws: InternalWalletState, walletTipId: string, - err: TalerErrorDetail | undefined, + err: TalerErrorDetail, ): Promise { await ws.db .mktx((x) => ({ @@ -172,10 +176,8 @@ async function incrementTipRetry( return; } if (!t.retryInfo) { - return; + logger.reportBreak(); } - t.retryInfo.retryCounter++; - updateRetryInfoTimeout(t.retryInfo); t.lastError = err; await tx.tips.put(t); }); @@ -184,15 +186,43 @@ async function incrementTipRetry( } } +async function setupTipRetry( + ws: InternalWalletState, + walletTipId: string, + options: { + reset: boolean; + }, +): Promise { + await ws.db + .mktx((x) => ({ + tips: x.tips, + })) + .runReadWrite(async (tx) => { + const t = await tx.tips.get(walletTipId); + if (!t) { + return; + } + if (options.reset) { + t.retryInfo = resetRetryInfo(); + } else { + t.retryInfo = RetryInfo.increment(t.retryInfo); + } + delete t.lastError; + await tx.tips.put(t); + }); +} + export async function processTip( ws: InternalWalletState, tipId: string, - forceNow = false, + options: { + forceNow?: boolean; + } = {}, ): Promise { const onOpErr = (e: TalerErrorDetail): Promise => - incrementTipRetry(ws, tipId, e); + reportTipError(ws, tipId, e); await guardOperationException( - () => processTipImpl(ws, tipId, forceNow), + () => processTipImpl(ws, tipId, options), onOpErr, ); } @@ -208,7 +238,7 @@ async function resetTipRetry( .runReadWrite(async (tx) => { const x = await tx.tips.get(tipId); if (x) { - x.retryInfo = initRetryInfo(); + x.retryInfo = resetRetryInfo(); await tx.tips.put(x); } }); @@ -217,8 +247,11 @@ async function resetTipRetry( async function processTipImpl( ws: InternalWalletState, walletTipId: string, - forceNow: boolean, + options: { + forceNow?: boolean; + } = {}, ): Promise { + const forceNow = options.forceNow ?? false; if (forceNow) { await resetTipRetry(ws, walletTipId); } @@ -293,12 +326,13 @@ async function processTipImpl( merchantResp.status === 424) ) { logger.trace(`got transient tip error`); + // FIXME: wrap in another error code that indicates a transient error const err = makeErrorDetail( TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, getHttpResponseErrorDetails(merchantResp), "tip pickup failed (transient)", ); - await incrementTipRetry(ws, tipRecord.walletTipId, err); + await reportTipError(ws, tipRecord.walletTipId, err); // FIXME: Maybe we want to signal to the caller that the transient error happened? return; } @@ -397,7 +431,7 @@ async function processTipImpl( } tr.pickedUpTimestamp = TalerProtocolTimestamp.now(); tr.lastError = undefined; - tr.retryInfo = initRetryInfo(); + tr.retryInfo = resetRetryInfo(); await tx.tips.put(tr); for (const cr of newCoinRecords) { await tx.coins.put(cr); diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 4a7adbb9c..e7dcd0784 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -68,7 +68,11 @@ import { HttpRequestLibrary, readSuccessResponseJsonOrThrow, } from "../util/http.js"; -import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js"; +import { + resetRetryInfo, + RetryInfo, + updateRetryInfoTimeout, +} from "../util/retries.js"; import { WALLET_BANK_INTEGRATION_PROTOCOL_VERSION, WALLET_EXCHANGE_PROTOCOL_VERSION, @@ -792,10 +796,33 @@ export async function updateWithdrawalDenoms( } } -async function incrementWithdrawalRetry( +async function setupWithdrawalRetry( + ws: InternalWalletState, + withdrawalGroupId: string, + options: { + reset: boolean; + }, +): Promise { + await ws.db + .mktx((x) => ({ withdrawalGroups: x.withdrawalGroups })) + .runReadWrite(async (tx) => { + const wsr = await tx.withdrawalGroups.get(withdrawalGroupId); + if (!wsr) { + return; + } + if (options.reset) { + wsr.retryInfo = resetRetryInfo(); + } else { + wsr.retryInfo = RetryInfo.increment(wsr.retryInfo); + } + await tx.withdrawalGroups.put(wsr); + }); +} + +async function reportWithdrawalError( ws: InternalWalletState, withdrawalGroupId: string, - err: TalerErrorDetail | undefined, + err: TalerErrorDetail, ): Promise { await ws.db .mktx((x) => ({ withdrawalGroups: x.withdrawalGroups })) @@ -804,56 +831,40 @@ async function incrementWithdrawalRetry( if (!wsr) { return; } - wsr.retryInfo.retryCounter++; - updateRetryInfoTimeout(wsr.retryInfo); + if (!wsr.retryInfo) { + logger.reportBreak(); + } wsr.lastError = err; await tx.withdrawalGroups.put(wsr); }); - if (err) { - ws.notify({ type: NotificationType.WithdrawOperationError, error: err }); - } + ws.notify({ type: NotificationType.WithdrawOperationError, error: err }); } export async function processWithdrawGroup( ws: InternalWalletState, withdrawalGroupId: string, - forceNow = false, + options: { + forceNow?: boolean; + } = {}, ): Promise { const onOpErr = (e: TalerErrorDetail): Promise => - incrementWithdrawalRetry(ws, withdrawalGroupId, e); + reportWithdrawalError(ws, withdrawalGroupId, e); await guardOperationException( - () => processWithdrawGroupImpl(ws, withdrawalGroupId, forceNow), + () => processWithdrawGroupImpl(ws, withdrawalGroupId, options), onOpErr, ); } -async function resetWithdrawalGroupRetry( - ws: InternalWalletState, - withdrawalGroupId: string, -): Promise { - await ws.db - .mktx((x) => ({ - withdrawalGroups: x.withdrawalGroups, - reserves: x.reserves, - })) - .runReadWrite(async (tx) => { - const x = await tx.withdrawalGroups.get(withdrawalGroupId); - if (x) { - x.retryInfo = initRetryInfo(); - await tx.withdrawalGroups.put(x); - } - }); -} - async function processWithdrawGroupImpl( ws: InternalWalletState, withdrawalGroupId: string, - forceNow: boolean, + options: { + forceNow?: boolean; + } = {}, ): Promise { + const forceNow = options.forceNow ?? false; logger.trace("processing withdraw group", withdrawalGroupId); - if (forceNow) { - await resetWithdrawalGroupRetry(ws, withdrawalGroupId); - } + await setupWithdrawalRetry(ws, withdrawalGroupId, { reset: forceNow }); const withdrawalGroup = await ws.db .mktx((x) => ({ withdrawalGroups: x.withdrawalGroups })) .runReadOnly(async (tx) => { @@ -876,7 +887,7 @@ async function processWithdrawGroupImpl( ); return; } - return await ws.reserveOps.processReserve(ws, reservePub, forceNow); + return await ws.reserveOps.processReserve(ws, reservePub, { forceNow }); } await ws.exchangeOps.updateExchangeFromUrl( @@ -948,7 +959,7 @@ async function processWithdrawGroupImpl( wg.timestampFinish = TalerProtocolTimestamp.now(); wg.operationStatus = OperationStatus.Finished; delete wg.lastError; - wg.retryInfo = initRetryInfo(); + wg.retryInfo = resetRetryInfo(); } await tx.withdrawalGroups.put(wg); -- cgit v1.2.3