From 5056da6548d5880211abd3e1cdacd92134e40dab Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 1 Sep 2020 18:00:46 +0530 Subject: test error handling --- packages/taler-wallet-core/src/TalerErrorCode.ts | 62 +++++++++++++++++++-- .../taler-wallet-core/src/operations/errors.ts | 4 +- .../taler-wallet-core/src/operations/pending.ts | 1 + .../src/operations/transactions.ts | 1 + .../taler-wallet-core/src/operations/withdraw.ts | 32 ++++++----- packages/taler-wallet-core/src/types/pending.ts | 7 +++ .../taler-wallet-core/src/types/transactions.ts | 21 +------- .../taler-wallet-core/src/types/walletTypes.ts | 3 +- packages/taler-wallet-core/src/wallet.ts | 63 ++++++++++++++-------- 9 files changed, 132 insertions(+), 62 deletions(-) (limited to 'packages/taler-wallet-core') diff --git a/packages/taler-wallet-core/src/TalerErrorCode.ts b/packages/taler-wallet-core/src/TalerErrorCode.ts index 7285a0fbe..a4c4b5a62 100644 --- a/packages/taler-wallet-core/src/TalerErrorCode.ts +++ b/packages/taler-wallet-core/src/TalerErrorCode.ts @@ -969,6 +969,13 @@ export enum TalerErrorCode { */ REFUND_INVALID_FAILURE_PROOF_BY_EXCHANGE = 1516, + /** + * The exchange failed to lookup information for the refund from its database. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + REFUND_DATABASE_LOOKUP_ERROR = 1517, + /** * The wire format specified in the "sender_account_details" is not understood or not supported by this exchange. * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). @@ -1571,6 +1578,20 @@ export enum TalerErrorCode { */ FORGET_PATH_NOT_FORGETTABLE = 2182, + /** + * The merchant backend cannot forget part of an order because it failed to start the database transaction. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + FORGET_ORDER_DB_START_ERROR = 2183, + + /** + * The merchant backend cannot forget part of an order because it failed to commit the database transaction. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + FORGET_ORDER_DB_COMMIT_ERROR = 2184, + /** * Integer overflow with specified timestamp argument detected. * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). @@ -1991,6 +2012,13 @@ export enum TalerErrorCode { */ ORDERS_ALREADY_CLAIMED = 2521, + /** + * The merchant backend couldn't find a product with the specified id. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + GET_PRODUCTS_NOT_FOUND = 2549, + /** * The merchant backend failed to lookup the products. * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). @@ -2983,7 +3011,7 @@ export enum TalerErrorCode { * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). * (A value of 0 indicates that the error is generated client-side). */ - SYNC_DB_FETCH_ERROR = 6000, + SYNC_DB_HARD_FETCH_ERROR = 6000, /** * The sync service failed find the record in its database. @@ -3028,11 +3056,11 @@ export enum TalerErrorCode { SYNC_INVALID_SIGNATURE = 6007, /** - * The "Content-length" field for the upload is either not a number, or too big, or missing. + * The "Content-length" field for the upload is either not a number, or too big. * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). * (A value of 0 indicates that the error is generated client-side). */ - SYNC_BAD_CONTENT_LENGTH = 6008, + SYNC_MALFORMED_CONTENT_LENGTH = 6008, /** * The "Content-length" field for the upload is too big based on the server's terms of service. @@ -3111,6 +3139,27 @@ export enum TalerErrorCode { */ SYNC_PREVIOUS_BACKUP_UNKNOWN = 6019, + /** + * The sync service had a serialization failure when accessing its database. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + SYNC_DB_SOFT_FETCH_ERROR = 6020, + + /** + * The sync service first found information, and then later not. This could happen if a backup was garbage collected just when it was being accessed. Trying again may give a different answer. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + SYNC_DB_INCONSISTENT_FETCH_ERROR = 6021, + + /** + * The "Content-length" field for the upload is missing. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + SYNC_MISSING_CONTENT_LENGTH = 6022, + /** * The wallet does not implement a version of the exchange protocol that is compatible with the protocol version of the exchange. * Returned with an HTTP status code of #MHD_HTTP_NOT_IMPLEMENTED (501). @@ -3216,6 +3265,13 @@ export enum TalerErrorCode { */ WALLET_ORDER_ALREADY_CLAIMED = 7014, + /** + * A group of withdrawal operations (typically for the same reserve at the same exchange) has errors and will be tried again later. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_WITHDRAWAL_GROUP_INCOMPLETE = 7015, + /** * End of error code range. * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). diff --git a/packages/taler-wallet-core/src/operations/errors.ts b/packages/taler-wallet-core/src/operations/errors.ts index 76f640344..6d9f44e03 100644 --- a/packages/taler-wallet-core/src/operations/errors.ts +++ b/packages/taler-wallet-core/src/operations/errors.ts @@ -66,8 +66,8 @@ export function makeErrorDetails( details: Record, ): OperationErrorDetails { return { - talerErrorCode: ec, - talerErrorHint: `Error: ${TalerErrorCode[ec]}`, + code: ec, + hint: `Error: ${TalerErrorCode[ec]}`, details: details, message, }; diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts index acad5e634..881961627 100644 --- a/packages/taler-wallet-core/src/operations/pending.ts +++ b/packages/taler-wallet-core/src/operations/pending.ts @@ -262,6 +262,7 @@ async function gatherWithdrawalPending( source: wsr.source, withdrawalGroupId: wsr.withdrawalGroupId, lastError: wsr.lastError, + retryInfo: wsr.retryInfo, }); }); } diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index d869ed770..3115b9506 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -165,6 +165,7 @@ export async function getTransactions( TransactionType.Withdrawal, wsr.withdrawalGroupId, ), + ...(wsr.lastError ? { error: wsr.lastError} : {}), }); } break; diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 270735fcb..3977ba121 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -59,6 +59,7 @@ import { import { readSuccessResponseJsonOrThrow } from "../util/http"; import { URL } from "../util/url"; import { TalerErrorCode } from "../TalerErrorCode"; +import { encodeCrock } from "../crypto/talerCrypto"; const logger = new Logger("withdraw.ts"); @@ -558,9 +559,6 @@ async function incrementWithdrawalRetry( if (!wsr) { return; } - if (!wsr.retryInfo) { - return; - } wsr.retryInfo.retryCounter++; updateRetryInfoTimeout(wsr.retryInfo); wsr.lastError = err; @@ -647,12 +645,13 @@ async function processWithdrawGroupImpl( let numFinished = 0; let finishedForFirstTime = false; + let errorsPerCoin: Record = {}; await ws.db.runWithWriteTransaction( [Stores.coins, Stores.withdrawalGroups, Stores.reserves, Stores.planchets], async (tx) => { - const ws = await tx.get(Stores.withdrawalGroups, withdrawalGroupId); - if (!ws) { + const wg = await tx.get(Stores.withdrawalGroups, withdrawalGroupId); + if (!wg) { return; } @@ -662,22 +661,29 @@ async function processWithdrawGroupImpl( if (x.withdrawalDone) { numFinished++; } + if (x.lastError) { + errorsPerCoin[x.coinIdx] = x.lastError; + } }); - - if (ws.timestampFinish === undefined && numFinished == numTotalCoins) { + logger.trace(`now withdrawn ${numFinished} of ${numTotalCoins} coins`); + if (wg.timestampFinish === undefined && numFinished === numTotalCoins) { finishedForFirstTime = true; - ws.timestampFinish = getTimestampNow(); - ws.lastError = undefined; - ws.retryInfo = initRetryInfo(false); + wg.timestampFinish = getTimestampNow(); + wg.lastError = undefined; + wg.retryInfo = initRetryInfo(false); } - await tx.put(Stores.withdrawalGroups, ws); + + await tx.put(Stores.withdrawalGroups, wg); }, ); if (numFinished != numTotalCoins) { - // FIXME: aggregate individual problems into the big error message here. - throw Error( + throw OperationFailedError.fromCode( + TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE, `withdrawal did not finish (${numFinished} / ${numTotalCoins} coins withdrawn)`, + { + errorsPerCoin, + }, ); } diff --git a/packages/taler-wallet-core/src/types/pending.ts b/packages/taler-wallet-core/src/types/pending.ts index 85f7585c5..67d243a37 100644 --- a/packages/taler-wallet-core/src/types/pending.ts +++ b/packages/taler-wallet-core/src/types/pending.ts @@ -210,6 +210,7 @@ export interface PendingWithdrawOperation { type: PendingOperationType.Withdraw; source: WithdrawalSource; lastError: OperationErrorDetails | undefined; + retryInfo: RetryInfo; withdrawalGroupId: string; numCoinsWithdrawn: number; numCoinsTotal: number; @@ -229,6 +230,12 @@ export interface PendingOperationInfoCommon { * as opposed to some regular scheduled operation or a permanent failure. */ givesLifeness: boolean; + + /** + * Retry info, not available on all pending operations. + * If it is available, it must have the same name. + */ + retryInfo?: RetryInfo; } /** diff --git a/packages/taler-wallet-core/src/types/transactions.ts b/packages/taler-wallet-core/src/types/transactions.ts index 061ce28f4..400439548 100644 --- a/packages/taler-wallet-core/src/types/transactions.ts +++ b/packages/taler-wallet-core/src/types/transactions.ts @@ -42,6 +42,7 @@ import { codecForList, codecForAny, } from "../util/codec"; +import { OperationErrorDetails } from "./walletTypes"; export interface TransactionsRequest { /** @@ -63,24 +64,6 @@ export interface TransactionsResponse { transactions: Transaction[]; } -export interface TransactionError { - /** - * TALER_EC_* unique error code. - * The action(s) offered and message displayed on the transaction item depend on this code. - */ - ec: number; - - /** - * English-only error hint, if available. - */ - hint?: string; - - /** - * Error details specific to "ec", if applicable/available - */ - details?: any; -} - export interface TransactionCommon { // opaque unique ID for the transaction, used as a starting point for paginating queries // and for invoking actions on the transaction (e.g. deleting/hiding it from the history) @@ -103,7 +86,7 @@ export interface TransactionCommon { // Amount added or removed from the wallet's balance (including all fees and other costs) amountEffective: AmountString; - error?: TransactionError; + error?: OperationErrorDetails; } export type Transaction = diff --git a/packages/taler-wallet-core/src/types/walletTypes.ts b/packages/taler-wallet-core/src/types/walletTypes.ts index 2cf3c7fbc..eb7d878fa 100644 --- a/packages/taler-wallet-core/src/types/walletTypes.ts +++ b/packages/taler-wallet-core/src/types/walletTypes.ts @@ -51,7 +51,6 @@ import { buildCodecForUnion, } from "../util/codec"; import { AmountString, codecForContractTerms, ContractTerms } from "./talerTypes"; -import { TransactionError, OrderShortInfo, codecForOrderShortInfo } from "./transactions"; /** * Response for the create reserve request to the wallet. @@ -215,7 +214,7 @@ export interface ConfirmPayResultDone { export interface ConfirmPayResultPending { type: ConfirmPayResultType.Pending; - lastError: TransactionError; + lastError: OperationErrorDetails; } export type ConfirmPayResult = ConfirmPayResultDone | ConfirmPayResultPending; diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 352cb29ef..845c6d71d 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -299,10 +299,15 @@ export class Wallet { * liveness left. The wallet will be in a stopped state when this function * returns without resolving to an exception. */ - public async runUntilDone(): Promise { + public async runUntilDone( + req: { + maxRetries?: number; + } = {}, + ): Promise { let done = false; const p = new Promise((resolve, reject) => { - // Run this asynchronously + // Monitor for conditions that means we're done or we + // should quit with an error (due to exceeded retries). this.addNotificationListener((n) => { if (done) { return; @@ -315,7 +320,29 @@ export class Wallet { logger.trace("no liveness-giving operations left"); resolve(); } + const maxRetries = req.maxRetries; + if (!maxRetries) { + return; + } + this.getPendingOperations({ onlyDue: false }) + .then((pending) => { + for (const p of pending.pendingOperations) { + if (p.retryInfo && p.retryInfo.retryCounter > maxRetries) { + console.warn( + `stopping, as ${maxRetries} retries are exceeded in an operation of type ${p.type}`, + ); + this.stop(); + done = true; + resolve(); + } + } + }) + .catch((e) => { + logger.error(e); + reject(e); + }); }); + // Run this asynchronously this.runRetryLoop().catch((e) => { logger.error("exception in wallet retry loop"); reject(e); @@ -324,16 +351,6 @@ export class Wallet { await p; } - /** - * Run the wallet until there are no more pending operations that give - * liveness left. The wallet will be in a stopped state when this function - * returns without resolving to an exception. - */ - public async runUntilDoneAndStop(): Promise { - await this.runUntilDone(); - logger.trace("stopping after liveness-giving operations done"); - this.stop(); - } /** * Process pending operations and wait for scheduled operations in @@ -392,7 +409,7 @@ export class Wallet { if (e instanceof OperationFailedAndReportedError) { logger.warn("operation processed resulted in reported error"); } else { - console.error("Uncaught exception", e); + logger.error("Uncaught exception", e); this.ws.notify({ type: NotificationType.InternalError, message: "uncaught exception", @@ -902,10 +919,13 @@ export class Wallet { return getTransactions(this.ws, request); } - async withdrawTestBalance( - req: WithdrawTestBalanceRequest, - ): Promise { - await withdrawTestBalance(this.ws, req.amount, req.bankBaseUrl, req.exchangeBaseUrl); + async withdrawTestBalance(req: WithdrawTestBalanceRequest): Promise { + await withdrawTestBalance( + this.ws, + req.amount, + req.bankBaseUrl, + req.exchangeBaseUrl, + ); } async runIntegrationtest(args: IntegrationTestArgs): Promise { @@ -940,12 +960,12 @@ export class Wallet { case "runIntegrationTest": { const req = codecForIntegrationTestArgs().decode(payload); await this.runIntegrationtest(req); - return {} + return {}; } case "testPay": { const req = codecForTestPayArgs().decode(payload); await this.testPay(req); - return {} + return {}; } case "getTransactions": { const req = codecForTransactionsRequest().decode(payload); @@ -988,10 +1008,7 @@ export class Wallet { } case "setExchangeTosAccepted": { const req = codecForAcceptExchangeTosRequest().decode(payload); - await this.acceptExchangeTermsOfService( - req.exchangeBaseUrl, - req.etag, - ); + await this.acceptExchangeTermsOfService(req.exchangeBaseUrl, req.etag); return {}; } case "applyRefund": { -- cgit v1.2.3