wallet-core/packages/taler-wallet-core/src/util/retries.ts

354 lines
11 KiB
TypeScript
Raw Normal View History

2020-09-08 17:00:03 +02:00
/*
This file is part of GNU Taler
(C) 2020 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Helpers for dealing with retry timeouts.
*/
/**
* Imports.
*/
2022-09-05 18:12:30 +02:00
import {
AbsoluteTime,
Duration,
Logger,
2022-09-05 18:12:30 +02:00
TalerErrorDetail,
} from "@gnu-taler/taler-util";
import {
BackupProviderRecord,
DepositGroupRecord,
ExchangeRecord,
2023-02-19 23:13:44 +01:00
PeerPullPaymentIncomingRecord,
PeerPullPaymentInitiationRecord,
PeerPushPaymentIncomingRecord,
PeerPushPaymentInitiationRecord,
2022-09-05 18:12:30 +02:00
PurchaseRecord,
RecoupGroupRecord,
RefreshGroupRecord,
TipRecord,
WalletStoresV1,
WithdrawalGroupRecord,
} from "../db.js";
2023-02-15 23:32:42 +01:00
import { TalerError } from "@gnu-taler/taler-util";
2022-09-05 18:12:30 +02:00
import { InternalWalletState } from "../internal-wallet-state.js";
import { PendingTaskType } from "../pending-types.js";
import { GetReadWriteAccess } from "./query.js";
import { assertUnreachable } from "./assertUnreachable.js";
2020-09-08 17:00:03 +02:00
const logger = new Logger("util/retries.ts");
2022-09-16 17:21:54 +02:00
export enum OperationAttemptResultType {
Finished = "finished",
Pending = "pending",
Error = "error",
Longpoll = "longpoll",
}
export type OperationAttemptResult<TSuccess = unknown, TPending = unknown> =
| OperationAttemptFinishedResult<TSuccess>
| OperationAttemptErrorResult
| OperationAttemptLongpollResult
| OperationAttemptPendingResult<TPending>;
export namespace OperationAttemptResult {
export function finishedEmpty(): OperationAttemptResult<unknown, unknown> {
return {
type: OperationAttemptResultType.Finished,
result: undefined,
};
}
2023-04-22 14:17:49 +02:00
export function pendingEmpty(): OperationAttemptResult<unknown, unknown> {
return {
type: OperationAttemptResultType.Pending,
result: undefined,
};
}
2022-09-16 17:21:54 +02:00
}
export interface OperationAttemptFinishedResult<T> {
type: OperationAttemptResultType.Finished;
result: T;
}
export interface OperationAttemptPendingResult<T> {
type: OperationAttemptResultType.Pending;
result: T;
}
export interface OperationAttemptErrorResult {
type: OperationAttemptResultType.Error;
errorDetail: TalerErrorDetail;
}
export interface OperationAttemptLongpollResult {
type: OperationAttemptResultType.Longpoll;
}
2020-09-08 17:00:03 +02:00
export interface RetryInfo {
2022-03-18 15:32:41 +01:00
firstTry: AbsoluteTime;
nextRetry: AbsoluteTime;
2020-09-08 17:00:03 +02:00
retryCounter: number;
}
export interface RetryPolicy {
readonly backoffDelta: Duration;
readonly backoffBase: number;
readonly maxTimeout: Duration;
2020-09-08 17:00:03 +02:00
}
const defaultRetryPolicy: RetryPolicy = {
backoffBase: 1.5,
backoffDelta: Duration.fromSpec({ seconds: 1 }),
2022-06-08 10:58:54 +02:00
maxTimeout: Duration.fromSpec({ minutes: 2 }),
2020-09-08 17:00:03 +02:00
};
function updateTimeout(
2020-09-08 17:00:03 +02:00
r: RetryInfo,
p: RetryPolicy = defaultRetryPolicy,
): void {
2022-03-18 15:32:41 +01:00
const now = AbsoluteTime.now();
2020-09-08 17:00:03 +02:00
if (now.t_ms === "never") {
throw Error("assertion failed");
}
if (p.backoffDelta.d_ms === "forever") {
r.nextRetry = { t_ms: "never" };
return;
}
2022-03-18 15:32:41 +01:00
const nextIncrement =
p.backoffDelta.d_ms * Math.pow(p.backoffBase, r.retryCounter);
2020-09-08 17:00:03 +02:00
const t =
2022-03-18 15:32:41 +01:00
now.t_ms +
(p.maxTimeout.d_ms === "forever"
? nextIncrement
: Math.min(p.maxTimeout.d_ms, nextIncrement));
2020-09-08 17:00:03 +02:00
r.nextRetry = { t_ms: t };
}
export namespace RetryInfo {
export function getDuration(
r: RetryInfo | undefined,
p: RetryPolicy = defaultRetryPolicy,
): Duration {
if (!r) {
// If we don't have any retry info, run immediately.
return { d_ms: 0 };
}
if (p.backoffDelta.d_ms === "forever") {
return { d_ms: "forever" };
}
const t = p.backoffDelta.d_ms * Math.pow(p.backoffBase, r.retryCounter);
return {
2022-05-19 11:01:07 +02:00
d_ms:
p.maxTimeout.d_ms === "forever" ? t : Math.min(p.maxTimeout.d_ms, t),
};
2020-09-08 17:00:03 +02:00
}
export function reset(p: RetryPolicy = defaultRetryPolicy): RetryInfo {
const now = AbsoluteTime.now();
const info = {
firstTry: now,
nextRetry: now,
retryCounter: 0,
};
updateTimeout(info, p);
return info;
}
export function increment(
r: RetryInfo | undefined,
p: RetryPolicy = defaultRetryPolicy,
): RetryInfo {
if (!r) {
return reset(p);
}
const r2 = { ...r };
r2.retryCounter++;
updateTimeout(r2, p);
return r2;
}
}
2022-09-05 18:12:30 +02:00
/**
* Parsed representation of task identifiers.
*/
export type ParsedTaskIdentifier =
| {
tag: PendingTaskType.Withdraw;
withdrawalGroupId: string;
}
| { tag: PendingTaskType.ExchangeUpdate; exchangeBaseUrl: string }
| { tag: PendingTaskType.Backup; backupProviderBaseUrl: string }
| { tag: PendingTaskType.Deposit; depositGroupId: string }
| { tag: PendingTaskType.ExchangeCheckRefresh; exchangeBaseUrl: string }
| { tag: PendingTaskType.ExchangeUpdate; exchangeBaseUrl: string }
| { tag: PendingTaskType.PeerPullDebit; peerPullPaymentIncomingId: string }
| { tag: PendingTaskType.PeerPullInitiation; pursePub: string }
| { tag: PendingTaskType.PeerPushCredit; peerPushPaymentIncomingId: string }
| { tag: PendingTaskType.PeerPushInitiation; pursePub: string }
| { tag: PendingTaskType.Purchase; proposalId: string }
| { tag: PendingTaskType.Recoup; recoupGroupId: string }
| { tag: PendingTaskType.TipPickup; walletTipId: string }
| { tag: PendingTaskType.Refresh; refreshGroupId: string };
export function parseTaskIdentifier(x: string): ParsedTaskIdentifier {
throw Error("not yet implemented");
}
export function constructTaskIdentifier(p: ParsedTaskIdentifier): string {
switch (p.tag) {
case PendingTaskType.Backup:
return `${p.tag}:${p.backupProviderBaseUrl}`;
case PendingTaskType.Deposit:
return `${p.tag}:${p.depositGroupId}`;
case PendingTaskType.ExchangeCheckRefresh:
return `${p.tag}:${p.exchangeBaseUrl}`;
case PendingTaskType.ExchangeUpdate:
return `${p.tag}:${p.exchangeBaseUrl}`;
case PendingTaskType.PeerPullDebit:
return `${p.tag}:${p.peerPullPaymentIncomingId}`;
case PendingTaskType.PeerPushCredit:
return `${p.tag}:${p.peerPushPaymentIncomingId}`;
case PendingTaskType.PeerPullInitiation:
return `${p.tag}:${p.pursePub}`;
case PendingTaskType.PeerPushInitiation:
return `${p.tag}:${p.pursePub}`;
case PendingTaskType.Purchase:
return `${p.tag}:${p.proposalId}`;
case PendingTaskType.Recoup:
return `${p.tag}:${p.recoupGroupId}`;
case PendingTaskType.Refresh:
return `${p.tag}:${p.refreshGroupId}`;
case PendingTaskType.TipPickup:
return `${p.tag}:${p.walletTipId}`;
case PendingTaskType.Withdraw:
return `${p.tag}:${p.withdrawalGroupId}`;
default:
assertUnreachable(p);
}
}
export namespace TaskIdentifiers {
2022-09-05 18:12:30 +02:00
export function forWithdrawal(wg: WithdrawalGroupRecord): string {
return `${PendingTaskType.Withdraw}:${wg.withdrawalGroupId}`;
}
export function forExchangeUpdate(exch: ExchangeRecord): string {
return `${PendingTaskType.ExchangeUpdate}:${exch.baseUrl}`;
}
export function forExchangeUpdateFromUrl(exchBaseUrl: string): string {
return `${PendingTaskType.ExchangeUpdate}:${exchBaseUrl}`;
}
2022-09-05 18:12:30 +02:00
export function forExchangeCheckRefresh(exch: ExchangeRecord): string {
return `${PendingTaskType.ExchangeCheckRefresh}:${exch.baseUrl}`;
}
export function forTipPickup(tipRecord: TipRecord): string {
return `${PendingTaskType.TipPickup}:${tipRecord.walletTipId}`;
}
export function forRefresh(refreshGroupRecord: RefreshGroupRecord): string {
2022-11-02 12:50:34 +01:00
return `${PendingTaskType.Refresh}:${refreshGroupRecord.refreshGroupId}`;
2022-09-05 18:12:30 +02:00
}
export function forPay(purchaseRecord: PurchaseRecord): string {
return `${PendingTaskType.Purchase}:${purchaseRecord.proposalId}`;
2022-09-05 18:12:30 +02:00
}
export function forRecoup(recoupRecord: RecoupGroupRecord): string {
return `${PendingTaskType.Recoup}:${recoupRecord.recoupGroupId}`;
}
export function forDeposit(depositRecord: DepositGroupRecord): string {
return `${PendingTaskType.Deposit}:${depositRecord.depositGroupId}`;
}
export function forBackup(backupRecord: BackupProviderRecord): string {
return `${PendingTaskType.Backup}:${backupRecord.baseUrl}`;
}
export function forPeerPushPaymentInitiation(
ppi: PeerPushPaymentInitiationRecord,
): string {
return `${PendingTaskType.PeerPushInitiation}:${ppi.pursePub}`;
}
export function forPeerPullPaymentInitiation(
ppi: PeerPullPaymentInitiationRecord,
): string {
return `${PendingTaskType.PeerPullInitiation}:${ppi.pursePub}`;
}
2023-02-19 23:13:44 +01:00
export function forPeerPullPaymentDebit(
ppi: PeerPullPaymentIncomingRecord,
): string {
return `${PendingTaskType.PeerPullDebit}:${ppi.peerPullPaymentIncomingId}`;
2023-02-19 23:13:44 +01:00
}
export function forPeerPushCredit(
ppi: PeerPushPaymentIncomingRecord,
): string {
return `${PendingTaskType.PeerPushCredit}:${ppi.peerPushPaymentIncomingId}`;
}
2022-09-05 18:12:30 +02:00
}
export async function scheduleRetryInTx(
ws: InternalWalletState,
tx: GetReadWriteAccess<{
operationRetries: typeof WalletStoresV1.operationRetries;
}>,
opId: string,
errorDetail?: TalerErrorDetail,
): Promise<void> {
let retryRecord = await tx.operationRetries.get(opId);
if (!retryRecord) {
retryRecord = {
id: opId,
retryInfo: RetryInfo.reset(),
};
if (errorDetail) {
retryRecord.lastError = errorDetail;
}
} else {
retryRecord.retryInfo = RetryInfo.increment(retryRecord.retryInfo);
if (errorDetail) {
retryRecord.lastError = errorDetail;
} else {
delete retryRecord.lastError;
}
}
await tx.operationRetries.put(retryRecord);
}
export async function scheduleRetry(
ws: InternalWalletState,
opId: string,
errorDetail?: TalerErrorDetail,
): Promise<void> {
return await ws.db
.mktx((x) => [x.operationRetries])
2022-09-05 18:12:30 +02:00
.runReadWrite(async (tx) => {
2022-09-16 19:27:24 +02:00
tx.operationRetries;
2022-09-05 18:12:30 +02:00
scheduleRetryInTx(ws, tx, opId, errorDetail);
});
}
/**
* Run an operation handler, expect a success result and extract the success value.
*/
export async function unwrapOperationHandlerResultOrThrow<T>(
2022-09-05 18:12:30 +02:00
res: OperationAttemptResult<T>,
): Promise<T> {
switch (res.type) {
case OperationAttemptResultType.Finished:
return res.result;
case OperationAttemptResultType.Error:
throw TalerError.fromUncheckedDetail(res.errorDetail);
default:
throw Error(`unexpected operation result (${res.type})`);
}
}