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,
|
2023-01-19 21:05:34 +01:00
|
|
|
Logger,
|
2022-09-05 18:12:30 +02:00
|
|
|
TalerErrorDetail,
|
|
|
|
} from "@gnu-taler/taler-util";
|
|
|
|
import {
|
|
|
|
BackupProviderRecord,
|
|
|
|
DepositGroupRecord,
|
|
|
|
ExchangeRecord,
|
2023-01-12 16:57:51 +01:00
|
|
|
PeerPullPaymentInitiationRecord,
|
2023-01-12 15:11:32 +01:00
|
|
|
PeerPushPaymentInitiationRecord,
|
2022-09-05 18:12:30 +02:00
|
|
|
PurchaseRecord,
|
|
|
|
RecoupGroupRecord,
|
|
|
|
RefreshGroupRecord,
|
|
|
|
TipRecord,
|
|
|
|
WalletStoresV1,
|
|
|
|
WithdrawalGroupRecord,
|
|
|
|
} from "../db.js";
|
|
|
|
import { TalerError } from "../errors.js";
|
|
|
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
|
|
|
import { PendingTaskType } from "../pending-types.js";
|
|
|
|
import { GetReadWriteAccess } from "./query.js";
|
2020-09-08 17:00:03 +02:00
|
|
|
|
2023-01-19 21:05:34 +01: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,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
2022-01-13 05:33:03 +01:00
|
|
|
readonly maxTimeout: Duration;
|
2020-09-08 17:00:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const defaultRetryPolicy: RetryPolicy = {
|
|
|
|
backoffBase: 1.5,
|
2022-06-10 13:03:47 +02:00
|
|
|
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
|
|
|
};
|
|
|
|
|
2022-05-18 19:41:51 +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-01-13 05:33:03 +01:00
|
|
|
|
2022-03-18 15:32:41 +01:00
|
|
|
const nextIncrement =
|
|
|
|
p.backoffDelta.d_ms * Math.pow(p.backoffBase, r.retryCounter);
|
2022-01-13 05:33:03 +01:00
|
|
|
|
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 };
|
|
|
|
}
|
|
|
|
|
2022-05-18 19:41:51 +02:00
|
|
|
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),
|
2022-05-18 19:41:51 +02:00
|
|
|
};
|
2020-09-08 17:00:03 +02:00
|
|
|
}
|
|
|
|
|
2022-05-18 19:41:51 +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;
|
|
|
|
}
|
2022-03-28 23:21:49 +02:00
|
|
|
|
|
|
|
export function increment(
|
|
|
|
r: RetryInfo | undefined,
|
|
|
|
p: RetryPolicy = defaultRetryPolicy,
|
2022-05-18 19:41:51 +02:00
|
|
|
): RetryInfo {
|
2022-03-28 23:21:49 +02:00
|
|
|
if (!r) {
|
2022-05-18 19:41:51 +02:00
|
|
|
return reset(p);
|
2022-03-28 23:21:49 +02:00
|
|
|
}
|
|
|
|
const r2 = { ...r };
|
|
|
|
r2.retryCounter++;
|
2022-05-18 19:41:51 +02:00
|
|
|
updateTimeout(r2, p);
|
2022-03-28 23:21:49 +02:00
|
|
|
return r2;
|
|
|
|
}
|
|
|
|
}
|
2022-09-05 18:12:30 +02:00
|
|
|
|
|
|
|
export namespace RetryTags {
|
|
|
|
export function forWithdrawal(wg: WithdrawalGroupRecord): string {
|
|
|
|
return `${PendingTaskType.Withdraw}:${wg.withdrawalGroupId}`;
|
|
|
|
}
|
|
|
|
export function forExchangeUpdate(exch: ExchangeRecord): string {
|
|
|
|
return `${PendingTaskType.ExchangeUpdate}:${exch.baseUrl}`;
|
|
|
|
}
|
2022-11-02 14:23:26 +01:00
|
|
|
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 {
|
2022-10-08 20:56:57 +02:00
|
|
|
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}`;
|
|
|
|
}
|
2023-01-12 15:11:32 +01:00
|
|
|
export function forPeerPushPaymentInitiation(
|
|
|
|
ppi: PeerPushPaymentInitiationRecord,
|
|
|
|
): string {
|
2023-01-12 16:57:51 +01:00
|
|
|
return `${PendingTaskType.PeerPushInitiation}:${ppi.pursePub}`;
|
|
|
|
}
|
|
|
|
export function forPeerPullPaymentInitiation(
|
|
|
|
ppi: PeerPullPaymentInitiationRecord,
|
|
|
|
): string {
|
|
|
|
return `${PendingTaskType.PeerPullInitiation}:${ppi.pursePub}`;
|
2023-01-12 15:11:32 +01:00
|
|
|
}
|
2022-09-19 12:13:31 +02:00
|
|
|
export function byPaymentProposalId(proposalId: string): string {
|
2022-10-08 20:56:57 +02:00
|
|
|
return `${PendingTaskType.Purchase}:${proposalId}`;
|
2022-09-19 12:13:31 +02:00
|
|
|
}
|
2023-01-13 20:05:17 +01:00
|
|
|
export function byPeerPushPaymentInitiationPursePub(
|
|
|
|
pursePub: string,
|
|
|
|
): string {
|
2023-01-12 16:57:51 +01:00
|
|
|
return `${PendingTaskType.PeerPushInitiation}:${pursePub}`;
|
|
|
|
}
|
2023-01-13 20:05:17 +01:00
|
|
|
export function byPeerPullPaymentInitiationPursePub(
|
|
|
|
pursePub: string,
|
|
|
|
): string {
|
2023-01-12 16:57:51 +01:00
|
|
|
return `${PendingTaskType.PeerPullInitiation}:${pursePub}`;
|
2023-01-12 15:11:32 +01:00
|
|
|
}
|
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
|
2022-09-13 13:25:41 +02:00
|
|
|
.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.
|
|
|
|
*/
|
2022-11-02 14:23:26 +01:00
|
|
|
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})`);
|
|
|
|
}
|
|
|
|
}
|