diff options
| author | Florian Dold <florian@dold.me> | 2021-08-24 14:25:46 +0200 | 
|---|---|---|
| committer | Florian Dold <florian@dold.me> | 2021-08-24 14:30:33 +0200 | 
| commit | 408d8e9fc896193fbcff1afd12aa04ab6d513798 (patch) | |
| tree | a117a3c5d9130ea9b18c4198d3978f38dbd2f101 /packages/taler-wallet-core/src | |
| parent | 7553ae7c74bc04c268b77d010fb2f5b5eacad460 (diff) | |
towards handling frozen refreshes
Diffstat (limited to 'packages/taler-wallet-core/src')
6 files changed, 134 insertions, 39 deletions
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 093332e84..ef6b45c11 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -915,6 +915,17 @@ export interface TipRecord {    retryInfo: RetryInfo;  } +export enum RefreshCoinStatus { +  Pending = "pending", +  Finished = "finished", + +  /** +   * The refresh for this coin has been frozen, because of a permanent error. +   * More info in lastErrorPerCoin. +   */ +  Frozen = "frozen", +} +  export interface RefreshGroupRecord {    /**     * Retry info, even present when the operation isn't active to allow indexing @@ -926,8 +937,15 @@ export interface RefreshGroupRecord {    lastErrorPerCoin: { [coinIndex: number]: TalerErrorDetails }; +  /** +   * Unique, randomly generated identifier for this group of +   * refresh operations. +   */    refreshGroupId: string; +  /** +   * Reason why this refresh group has been created. +   */    reason: RefreshReason;    oldCoinPubs: string[]; @@ -946,7 +964,7 @@ export interface RefreshGroupRecord {     * it will be marked as finished, but no refresh session will     * be created.     */ -  finishedPerCoin: boolean[]; +  statusPerCoin: RefreshCoinStatus[];    timestampCreated: Timestamp; @@ -954,6 +972,11 @@ export interface RefreshGroupRecord {     * Timestamp when the refresh session finished.     */    timestampFinished: Timestamp | undefined; + +  /** +   * No coins are pending, but at least one is frozen. +   */ +  frozen?: boolean;  }  /** @@ -1162,6 +1185,9 @@ export interface PurchaseRecord {    /**     * Downloaded and parsed proposal data. +   * +   * FIXME:  Move this into another object store, +   * to improve read/write perf on purchases.     */    download: ProposalDownload; diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts b/packages/taler-wallet-core/src/operations/backup/export.ts index 4d9ca6697..0410ab3af 100644 --- a/packages/taler-wallet-core/src/operations/backup/export.ts +++ b/packages/taler-wallet-core/src/operations/backup/export.ts @@ -66,6 +66,7 @@ import {    CoinSourceType,    CoinStatus,    ProposalStatus, +  RefreshCoinStatus,    RefundState,    WALLET_BACKUP_STATE_KEY,  } from "../../db.js"; @@ -440,7 +441,7 @@ export async function exportBackup(              estimated_output_amount: Amounts.stringify(                rg.estimatedOutputPerCoin[i],              ), -            finished: rg.finishedPerCoin[i], +            finished: rg.statusPerCoin[i] === RefreshCoinStatus.Finished,              input_amount: Amounts.stringify(rg.inputPerCoin[i]),              refresh_session: refreshSession,            }); diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts index 8ba4e4db3..a694d9f4d 100644 --- a/packages/taler-wallet-core/src/operations/backup/import.ts +++ b/packages/taler-wallet-core/src/operations/backup/import.ts @@ -45,6 +45,7 @@ import {    RefreshSessionRecord,    WireInfo,    WalletStoresV1, +  RefreshCoinStatus,  } from "../../db.js";  import { PayCoinSelection } from "../../util/coinSelection.js";  import { j2s } from "@gnu-taler/taler-util"; @@ -831,8 +832,10 @@ export async function importBackup(              lastError: undefined,              lastErrorPerCoin: {},              oldCoinPubs: backupRefreshGroup.old_coins.map((x) => x.coin_pub), -            finishedPerCoin: backupRefreshGroup.old_coins.map( -              (x) => x.finished, +            statusPerCoin: backupRefreshGroup.old_coins.map((x) => +              x.finished +                ? RefreshCoinStatus.Finished +                : RefreshCoinStatus.Pending,              ),              inputPerCoin: backupRefreshGroup.old_coins.map((x) =>                Amounts.parseOrThrow(x.input_amount), diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts index 200e6ccbd..a4ca972a7 100644 --- a/packages/taler-wallet-core/src/operations/pending.ts +++ b/packages/taler-wallet-core/src/operations/pending.ts @@ -27,6 +27,7 @@ import {    AbortStatus,    WalletStoresV1,    BackupProviderStateTag, +  RefreshCoinStatus,  } from "../db.js";  import {    PendingOperationsResponse, @@ -111,12 +112,17 @@ async function gatherRefreshPending(      if (r.timestampFinished) {        return;      } +    if (r.frozen) { +      return; +    }      resp.pendingOperations.push({        type: PendingTaskType.Refresh,        givesLifeness: true,        timestampDue: r.retryInfo.nextRetry,        refreshGroupId: r.refreshGroupId, -      finishedPerCoin: r.finishedPerCoin, +      finishedPerCoin: r.statusPerCoin.map( +        (x) => x === RefreshCoinStatus.Finished, +      ),        retryInfo: r.retryInfo,      });    }); diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index 5c4ed4f70..8926559e3 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -20,6 +20,7 @@ import {    CoinSourceType,    CoinStatus,    DenominationRecord, +  RefreshCoinStatus,    RefreshGroupRecord,    RefreshPlanchet,    WalletStoresV1, @@ -28,6 +29,7 @@ import {    codecForExchangeMeltResponse,    codecForExchangeRevealResponse,    CoinPublicKey, +  fnutil,    NotificationType,    RefreshGroupId,    RefreshReason, @@ -37,7 +39,11 @@ import {  } from "@gnu-taler/taler-util";  import { AmountJson, Amounts } from "@gnu-taler/taler-util";  import { amountToPretty } from "@gnu-taler/taler-util"; -import { readSuccessResponseJsonOrThrow } from "../util/http.js"; +import { +  HttpResponseStatus, +  readSuccessResponseJsonOrThrow, +  readUnexpectedResponseDetails, +} from "../util/http.js";  import { checkDbInvariant } from "../util/invariants.js";  import { Logger } from "@gnu-taler/taler-util";  import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js"; @@ -99,6 +105,26 @@ export function getTotalRefreshCost(    return totalCost;  } +function updateGroupStatus(rg: RefreshGroupRecord): void { +  let allDone = fnutil.all( +    rg.statusPerCoin, +    (x) => x === RefreshCoinStatus.Finished || x === RefreshCoinStatus.Frozen, +  ); +  let anyFrozen = fnutil.any( +    rg.statusPerCoin, +    (x) => x === RefreshCoinStatus.Frozen, +  ); +  if (allDone) { +    if (anyFrozen) { +      rg.frozen = true; +      rg.retryInfo = initRetryInfo(); +    } else { +      rg.timestampFinished = getTimestampNow(); +      rg.retryInfo = initRetryInfo(); +    } +  } +} +  /**   * Create a refresh session for one particular coin inside a refresh group.   */ @@ -121,7 +147,9 @@ async function refreshCreateSession(        if (!refreshGroup) {          return;        } -      if (refreshGroup.finishedPerCoin[coinIndex]) { +      if ( +        refreshGroup.statusPerCoin[coinIndex] === RefreshCoinStatus.Finished +      ) {          return;        }        const existingRefreshSession = @@ -211,18 +239,9 @@ async function refreshCreateSession(          if (!rg) {            return;          } -        rg.finishedPerCoin[coinIndex] = true; -        let allDone = true; -        for (const f of rg.finishedPerCoin) { -          if (!f) { -            allDone = false; -            break; -          } -        } -        if (allDone) { -          rg.timestampFinished = getTimestampNow(); -          rg.retryInfo = initRetryInfo(); -        } +        rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Finished; +        updateGroupStatus(rg); +          await tx.refreshGroups.put(rg);        });      ws.notify({ type: NotificationType.RefreshUnwarranted }); @@ -358,6 +377,31 @@ async function refreshMelt(      });    }); +  if (resp.status === HttpResponseStatus.NotFound) { +    const errDetails = await readUnexpectedResponseDetails(resp); +    await ws.db +      .mktx((x) => ({ +        refreshGroups: x.refreshGroups, +      })) +      .runReadWrite(async (tx) => { +        const rg = await tx.refreshGroups.get(refreshGroupId); +        if (!rg) { +          return; +        } +        if (rg.timestampFinished) { +          return; +        } +        if (rg.statusPerCoin[coinIndex] !== RefreshCoinStatus.Pending) { +          return; +        } +        rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Frozen; +        rg.lastErrorPerCoin[coinIndex] = errDetails; +        updateGroupStatus(rg); +        await tx.refreshGroups.put(rg); +      }); +    return; +  } +    const meltResponse = await readSuccessResponseJsonOrThrow(      resp,      codecForExchangeMeltResponse(), @@ -598,18 +642,8 @@ async function refreshReveal(        if (!rs) {          return;        } -      rg.finishedPerCoin[coinIndex] = true; -      let allDone = true; -      for (const f of rg.finishedPerCoin) { -        if (!f) { -          allDone = false; -          break; -        } -      } -      if (allDone) { -        rg.timestampFinished = getTimestampNow(); -        rg.retryInfo = initRetryInfo(); -      } +      rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Finished; +      updateGroupStatus(rg);        for (const coin of coins) {          await tx.coins.put(coin);        } @@ -728,7 +762,7 @@ async function processRefreshSession(    if (!refreshGroup) {      return;    } -  if (refreshGroup.finishedPerCoin[coinIndex]) { +  if (refreshGroup.statusPerCoin[coinIndex] === RefreshCoinStatus.Finished) {      return;    }    if (!refreshGroup.refreshSessionPerCoin[coinIndex]) { @@ -744,7 +778,7 @@ async function processRefreshSession(    }    const refreshSession = refreshGroup.refreshSessionPerCoin[coinIndex];    if (!refreshSession) { -    if (!refreshGroup.finishedPerCoin[coinIndex]) { +    if (refreshGroup.statusPerCoin[coinIndex] !== RefreshCoinStatus.Finished) {        throw Error(          "BUG: refresh session was not created and coin not marked as finished",        ); @@ -826,13 +860,13 @@ export async function createRefreshGroup(    const refreshGroup: RefreshGroupRecord = {      timestampFinished: undefined, -    finishedPerCoin: oldCoinPubs.map((x) => false), +    statusPerCoin: oldCoinPubs.map(() => RefreshCoinStatus.Pending),      lastError: undefined,      lastErrorPerCoin: {},      oldCoinPubs: oldCoinPubs.map((x) => x.coinPub),      reason,      refreshGroupId, -    refreshSessionPerCoin: oldCoinPubs.map((x) => undefined), +    refreshSessionPerCoin: oldCoinPubs.map(() => undefined),      retryInfo: initRetryInfo(),      inputPerCoin,      estimatedOutputPerCoin, diff --git a/packages/taler-wallet-core/src/util/http.ts b/packages/taler-wallet-core/src/util/http.ts index 68a63e124..ce507465a 100644 --- a/packages/taler-wallet-core/src/util/http.ts +++ b/packages/taler-wallet-core/src/util/http.ts @@ -24,10 +24,7 @@  /**   * Imports   */ -import { -  OperationFailedError, -  makeErrorDetails, -} from "../errors.js"; +import { OperationFailedError, makeErrorDetails } from "../errors.js";  import {    Logger,    Duration, @@ -68,6 +65,7 @@ export enum HttpResponseStatus {    Gone = 210,    NotModified = 304,    PaymentRequired = 402, +  NotFound = 404,    Conflict = 409,  } @@ -158,6 +156,33 @@ export async function readTalerErrorResponse(    return errJson;  } +export async function readUnexpectedResponseDetails( +  httpResponse: HttpResponse, +): Promise<TalerErrorDetails> { +  const errJson = await httpResponse.json(); +  const talerErrorCode = errJson.code; +  if (typeof talerErrorCode !== "number") { +    return makeErrorDetails( +      TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, +      "Error response did not contain error code", +      { +        requestUrl: httpResponse.requestUrl, +        requestMethod: httpResponse.requestMethod, +        httpStatusCode: httpResponse.status, +      }, +    ); +  } +  return makeErrorDetails( +    TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, +    "Unexpected error code in response", +    { +      requestUrl: httpResponse.requestUrl, +      httpStatusCode: httpResponse.status, +      errorResponse: errJson, +    }, +  ); +} +  export async function readSuccessResponseJsonOrErrorCode<T>(    httpResponse: HttpResponse,    codec: Codec<T>,  | 
