wallet-core: fix exchange management test case, surface exchange update error info in list

This commit is contained in:
Florian Dold 2022-11-02 14:23:26 +01:00
parent fe011321a4
commit 1e6e1a22cd
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
8 changed files with 84 additions and 22 deletions

View File

@ -904,6 +904,10 @@ export enum ExchangeEntryStatus {
Ok = "ok", Ok = "ok",
} }
export interface OperationErrorInfo {
error: TalerErrorDetail;
}
// FIXME: This should probably include some error status. // FIXME: This should probably include some error status.
export interface ExchangeListItem { export interface ExchangeListItem {
exchangeBaseUrl: string; exchangeBaseUrl: string;
@ -917,6 +921,12 @@ export interface ExchangeListItem {
* temporarily queried. * temporarily queried.
*/ */
permanent: boolean; permanent: boolean;
/**
* Information about the last error that occured when trying
* to update the exchange info.
*/
lastUpdateErrorInfo?: OperationErrorInfo;
} }
const codecForAuditorDenomSig = (): Codec<AuditorDenomSig> => const codecForAuditorDenomSig = (): Codec<AuditorDenomSig> =>

View File

@ -194,12 +194,16 @@ export async function runExchangeManagementTest(
t.assertTrue( t.assertTrue(
err1.errorDetail.code === TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, err1.errorDetail.code === TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
); );
exchangesList = await wallet.client.call( exchangesList = await wallet.client.call(
WalletApiOperation.ListExchanges, WalletApiOperation.ListExchanges,
{}, {},
); );
t.assertTrue(exchangesList.exchanges.length === 0); console.log("exchanges list", j2s(exchangesList));
t.assertTrue(exchangesList.exchanges.length === 1);
t.assertTrue(
exchangesList.exchanges[0].lastUpdateErrorInfo?.error.code ===
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
);
/* /*
* ========================================================================= * =========================================================================
@ -240,7 +244,11 @@ export async function runExchangeManagementTest(
WalletApiOperation.ListExchanges, WalletApiOperation.ListExchanges,
{}, {},
); );
t.assertTrue(exchangesList.exchanges.length === 0); t.assertTrue(exchangesList.exchanges.length === 1);
t.assertTrue(
exchangesList.exchanges[0].lastUpdateErrorInfo?.error.code ===
TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE,
);
/* /*
* ========================================================================= * =========================================================================

View File

@ -28,6 +28,7 @@ import {
ExchangeTosStatus, ExchangeTosStatus,
j2s, j2s,
Logger, Logger,
OperationErrorInfo,
RefreshReason, RefreshReason,
TalerErrorCode, TalerErrorCode,
TalerErrorDetail, TalerErrorDetail,
@ -224,30 +225,37 @@ export async function storeOperationPending(
}); });
} }
export async function runOperationWithErrorReporting( export async function runOperationWithErrorReporting<T1, T2>(
ws: InternalWalletState, ws: InternalWalletState,
opId: string, opId: string,
f: () => Promise<OperationAttemptResult>, f: () => Promise<OperationAttemptResult<T1, T2>>,
): Promise<void> { ): Promise<OperationAttemptResult<T1, T2>> {
let maybeError: TalerErrorDetail | undefined; let maybeError: TalerErrorDetail | undefined;
try { try {
const resp = await f(); const resp = await f();
switch (resp.type) { switch (resp.type) {
case OperationAttemptResultType.Error: case OperationAttemptResultType.Error:
return await storeOperationError(ws, opId, resp.errorDetail); await storeOperationError(ws, opId, resp.errorDetail);
return resp;
case OperationAttemptResultType.Finished: case OperationAttemptResultType.Finished:
return await storeOperationFinished(ws, opId); await storeOperationFinished(ws, opId);
return resp;
case OperationAttemptResultType.Pending: case OperationAttemptResultType.Pending:
return await storeOperationPending(ws, opId); await storeOperationPending(ws, opId);
return resp;
case OperationAttemptResultType.Longpoll: case OperationAttemptResultType.Longpoll:
break; return resp;
} }
} catch (e) { } catch (e) {
if (e instanceof TalerError) { if (e instanceof TalerError) {
logger.warn("operation processed resulted in error"); logger.warn("operation processed resulted in error");
logger.warn(`error was: ${j2s(e.errorDetail)}`); logger.warn(`error was: ${j2s(e.errorDetail)}`);
maybeError = e.errorDetail; maybeError = e.errorDetail;
return await storeOperationError(ws, opId, maybeError!); await storeOperationError(ws, opId, maybeError!);
return {
type: OperationAttemptResultType.Error,
errorDetail: e.errorDetail,
};
} else if (e instanceof Error) { } else if (e instanceof Error) {
// This is a bug, as we expect pending operations to always // This is a bug, as we expect pending operations to always
// do their own error handling and only throw WALLET_PENDING_OPERATION_FAILED // do their own error handling and only throw WALLET_PENDING_OPERATION_FAILED
@ -261,7 +269,11 @@ export async function runOperationWithErrorReporting(
}, },
`unexpected exception (message: ${e.message})`, `unexpected exception (message: ${e.message})`,
); );
return await storeOperationError(ws, opId, maybeError); await storeOperationError(ws, opId, maybeError);
return {
type: OperationAttemptResultType.Error,
errorDetail: maybeError,
};
} else { } else {
logger.error("Uncaught exception, value is not even an error."); logger.error("Uncaught exception, value is not even an error.");
maybeError = makeErrorDetail( maybeError = makeErrorDetail(
@ -269,7 +281,11 @@ export async function runOperationWithErrorReporting(
{}, {},
`unexpected exception (not even an error)`, `unexpected exception (not even an error)`,
); );
return await storeOperationError(ws, opId, maybeError); await storeOperationError(ws, opId, maybeError);
return {
type: OperationAttemptResultType.Error,
errorDetail: maybeError,
};
} }
} }
} }
@ -357,7 +373,13 @@ export function getExchangeTosStatus(
export function makeExchangeListItem( export function makeExchangeListItem(
r: ExchangeRecord, r: ExchangeRecord,
exchangeDetails: ExchangeDetailsRecord | undefined, exchangeDetails: ExchangeDetailsRecord | undefined,
lastError: TalerErrorDetail | undefined,
): ExchangeListItem { ): ExchangeListItem {
const lastUpdateErrorInfo: OperationErrorInfo | undefined = lastError
? {
error: lastError,
}
: undefined;
if (!exchangeDetails) { if (!exchangeDetails) {
return { return {
exchangeBaseUrl: r.baseUrl, exchangeBaseUrl: r.baseUrl,
@ -367,6 +389,7 @@ export function makeExchangeListItem(
exchangeStatus: ExchangeEntryStatus.Unknown, exchangeStatus: ExchangeEntryStatus.Unknown,
permanent: r.permanent, permanent: r.permanent,
ageRestrictionOptions: [], ageRestrictionOptions: [],
lastUpdateErrorInfo,
}; };
} }
let exchangeStatus; let exchangeStatus;
@ -381,5 +404,6 @@ export function makeExchangeListItem(
ageRestrictionOptions: exchangeDetails.ageMask ageRestrictionOptions: exchangeDetails.ageMask
? AgeRestriction.getAgeGroupsFromMask(exchangeDetails.ageMask) ? AgeRestriction.getAgeGroupsFromMask(exchangeDetails.ageMask)
: [], : [],
lastUpdateErrorInfo,
}; };
} }

View File

@ -73,9 +73,11 @@ import {
import { import {
OperationAttemptResult, OperationAttemptResult,
OperationAttemptResultType, OperationAttemptResultType,
runOperationHandlerForResult, RetryTags,
unwrapOperationHandlerResultOrThrow,
} from "../util/retries.js"; } from "../util/retries.js";
import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "../versions.js"; import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "../versions.js";
import { runOperationWithErrorReporting } from "./common.js";
import { isWithdrawableDenom } from "./withdraw.js"; import { isWithdrawableDenom } from "./withdraw.js";
const logger = new Logger("exchanges.ts"); const logger = new Logger("exchanges.ts");
@ -546,8 +548,13 @@ export async function updateExchangeFromUrl(
exchange: ExchangeRecord; exchange: ExchangeRecord;
exchangeDetails: ExchangeDetailsRecord; exchangeDetails: ExchangeDetailsRecord;
}> { }> {
return runOperationHandlerForResult( const canonUrl = canonicalizeBaseUrl(baseUrl);
await updateExchangeFromUrlHandler(ws, baseUrl, options), return unwrapOperationHandlerResultOrThrow(
await runOperationWithErrorReporting(
ws,
RetryTags.forExchangeUpdateFromUrl(canonUrl),
() => updateExchangeFromUrlHandler(ws, canonUrl, options),
),
); );
} }

View File

@ -54,7 +54,7 @@ import { checkDbInvariant } from "../util/invariants.js";
import { GetReadWriteAccess } from "../util/query.js"; import { GetReadWriteAccess } from "../util/query.js";
import { import {
OperationAttemptResult, OperationAttemptResult,
runOperationHandlerForResult, unwrapOperationHandlerResultOrThrow,
} from "../util/retries.js"; } from "../util/retries.js";
import { createRefreshGroup, processRefreshGroup } from "./refresh.js"; import { createRefreshGroup, processRefreshGroup } from "./refresh.js";
import { internalCreateWithdrawalGroup } from "./withdraw.js"; import { internalCreateWithdrawalGroup } from "./withdraw.js";
@ -307,7 +307,7 @@ export async function processRecoupGroup(
forceNow?: boolean; forceNow?: boolean;
} = {}, } = {},
): Promise<void> { ): Promise<void> {
await runOperationHandlerForResult( await unwrapOperationHandlerResultOrThrow(
await processRecoupGroupHandler(ws, recoupGroupId, options), await processRecoupGroupHandler(ws, recoupGroupId, options),
); );
return; return;

View File

@ -1385,6 +1385,7 @@ export async function getWithdrawalDetailsForUri(
x.exchangeDetails, x.exchangeDetails,
x.exchangeTos, x.exchangeTos,
x.denominations, x.denominations,
x.operationRetries,
]) ])
.runReadOnly(async (tx) => { .runReadOnly(async (tx) => {
const exchangeRecords = await tx.exchanges.iter().toArray(); const exchangeRecords = await tx.exchanges.iter().toArray();
@ -1396,8 +1397,13 @@ export async function getWithdrawalDetailsForUri(
const denominations = await tx.denominations.indexes.byExchangeBaseUrl const denominations = await tx.denominations.indexes.byExchangeBaseUrl
.iter(r.baseUrl) .iter(r.baseUrl)
.toArray(); .toArray();
const retryRecord = await tx.operationRetries.get(
RetryTags.forExchangeUpdate(r),
);
if (exchangeDetails && denominations) { if (exchangeDetails && denominations) {
exchanges.push(makeExchangeListItem(r, exchangeDetails)); exchanges.push(
makeExchangeListItem(r, exchangeDetails, retryRecord?.lastError),
);
} }
} }
}); });

View File

@ -176,6 +176,9 @@ export namespace RetryTags {
export function forExchangeUpdate(exch: ExchangeRecord): string { export function forExchangeUpdate(exch: ExchangeRecord): string {
return `${PendingTaskType.ExchangeUpdate}:${exch.baseUrl}`; return `${PendingTaskType.ExchangeUpdate}:${exch.baseUrl}`;
} }
export function forExchangeUpdateFromUrl(exchBaseUrl: string): string {
return `${PendingTaskType.ExchangeUpdate}:${exchBaseUrl}`;
}
export function forExchangeCheckRefresh(exch: ExchangeRecord): string { export function forExchangeCheckRefresh(exch: ExchangeRecord): string {
return `${PendingTaskType.ExchangeCheckRefresh}:${exch.baseUrl}`; return `${PendingTaskType.ExchangeCheckRefresh}:${exch.baseUrl}`;
} }
@ -246,7 +249,7 @@ export async function scheduleRetry(
/** /**
* Run an operation handler, expect a success result and extract the success value. * Run an operation handler, expect a success result and extract the success value.
*/ */
export async function runOperationHandlerForResult<T>( export async function unwrapOperationHandlerResultOrThrow<T>(
res: OperationAttemptResult<T>, res: OperationAttemptResult<T>,
): Promise<T> { ): Promise<T> {
switch (res.type) { switch (res.type) {

View File

@ -239,7 +239,7 @@ import {
GetReadOnlyAccess, GetReadOnlyAccess,
GetReadWriteAccess, GetReadWriteAccess,
} from "./util/query.js"; } from "./util/query.js";
import { OperationAttemptResult } from "./util/retries.js"; import { OperationAttemptResult, RetryTags } from "./util/retries.js";
import { TimerAPI, TimerGroup } from "./util/timer.js"; import { TimerAPI, TimerGroup } from "./util/timer.js";
import { import {
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION, WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
@ -650,12 +650,16 @@ async function getExchanges(
x.exchangeDetails, x.exchangeDetails,
x.exchangeTos, x.exchangeTos,
x.denominations, x.denominations,
x.operationRetries,
]) ])
.runReadOnly(async (tx) => { .runReadOnly(async (tx) => {
const exchangeRecords = await tx.exchanges.iter().toArray(); const exchangeRecords = await tx.exchanges.iter().toArray();
for (const r of exchangeRecords) { for (const r of exchangeRecords) {
const exchangeDetails = await getExchangeDetails(tx, r.baseUrl); const exchangeDetails = await getExchangeDetails(tx, r.baseUrl);
exchanges.push(makeExchangeListItem(r, exchangeDetails)); const opRetryRecord = await tx.operationRetries.get(
RetryTags.forExchangeUpdate(r),
);
exchanges.push(makeExchangeListItem(r, exchangeDetails, opRetryRecord?.lastError));
} }
}); });
return { exchanges }; return { exchanges };