taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 7d327bd9df668b1b4148695cb53a73151958b8f3
parent 4fbd3635e411d0d0a85a754ba688138f03121da8
Author: Florian Dold <dold@taler.net>
Date:   Sun,  9 Aug 2026 23:09:12 +0200

wallet-core: report database health on resume

Diffstat:
Mpackages/taler-wallet-core/src/db-common.ts | 2++
Apackages/taler-wallet-core/src/requests.test.ts | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/requests.ts | 33+++++++++++++++++++++++++++++++--
Mpackages/taler-wallet-core/src/wallet-api-types.ts | 7++++++-
4 files changed, 134 insertions(+), 3 deletions(-)

diff --git a/packages/taler-wallet-core/src/db-common.ts b/packages/taler-wallet-core/src/db-common.ts @@ -2059,6 +2059,7 @@ export enum ConfigRecordKey { // Only for testing, do not use! TestLoopTx = "testTxLoop", LastInitInfo = "lastInitInfo", + LastResumed = "lastResumed", MaterializedTransactionsVersion = "materializedTransactionsVersion", DonauConfig = "donauConfig", } @@ -2106,6 +2107,7 @@ export type ConfigRecord = | { key: ConfigRecordKey.CurrencyDefaultsApplied; value: boolean | number } | { key: ConfigRecordKey.TestLoopTx; value: number } | { key: ConfigRecordKey.LastInitInfo; value: DbProtocolTimestamp } + | { key: ConfigRecordKey.LastResumed; value: DbProtocolTimestamp } | { key: ConfigRecordKey.MaterializedTransactionsVersion; value: number } | { key: ConfigRecordKey.DonauConfig; value: DonauConfig }; diff --git a/packages/taler-wallet-core/src/requests.test.ts b/packages/taler-wallet-core/src/requests.test.ts @@ -0,0 +1,95 @@ +/* + This file is part of GNU Taler + (C) 2026 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/> + */ + +import assert from "node:assert"; +import { test } from "node:test"; + +import { ConfigRecord, ConfigRecordKey } from "./db-common.js"; +import { WalletDbTransaction } from "./dbtx.js"; +import { handleHintApplicationResumed } from "./requests.js"; +import { WalletExecutionContext } from "./wallet.js"; + +interface TestContext { + wex: WalletExecutionContext; + getStoredRecord(): ConfigRecord | undefined; + getReloadCount(): number; +} + +function makeTestContext(options?: { + failWrite?: boolean; + failRead?: boolean; +}): TestContext { + let storedRecord: ConfigRecord | undefined; + let reloadCount = 0; + const tx = { + async upsertConfig(record: ConfigRecord): Promise<void> { + if (options?.failWrite) { + throw Error("test write failure"); + } + storedRecord = record; + }, + async getConfig(key: ConfigRecordKey): Promise<ConfigRecord | undefined> { + if (options?.failRead) { + throw Error("test read failure"); + } + return storedRecord?.key === key ? storedRecord : undefined; + }, + } as WalletDbTransaction; + const wex = { + async runWalletDbTx<T>( + f: (tx: WalletDbTransaction) => Promise<T>, + ): Promise<T> { + return await f(tx); + }, + taskScheduler: { + async reload(): Promise<void> { + reloadCount++; + }, + }, + } as WalletExecutionContext; + return { + wex, + getStoredRecord: () => storedRecord, + getReloadCount: () => reloadCount, + }; +} + +test("application-resumed hint reports healthy DB operations", async () => { + const ctx = makeTestContext(); + + const result = await handleHintApplicationResumed(ctx.wex, {}); + + assert.deepStrictEqual(result, { + dbWriteHealthy: true, + dbReadHealthy: true, + }); + assert.strictEqual(ctx.getStoredRecord()?.key, ConfigRecordKey.LastResumed); + assert.strictEqual(ctx.getReloadCount(), 1); +}); + +test("application-resumed hint reports DB failures independently", async () => { + const writeFailure = makeTestContext({ failWrite: true }); + assert.deepStrictEqual( + await handleHintApplicationResumed(writeFailure.wex, {}), + { dbWriteHealthy: false, dbReadHealthy: true }, + ); + + const readFailure = makeTestContext({ failRead: true }); + assert.deepStrictEqual( + await handleHintApplicationResumed(readFailure.wex, {}), + { dbWriteHealthy: true, dbReadHealthy: false }, + ); +}); diff --git a/packages/taler-wallet-core/src/requests.ts b/packages/taler-wallet-core/src/requests.ts @@ -421,6 +421,7 @@ import { WALLET_MERCHANT_PROTOCOL_VERSION, } from "./versions.js"; import { + HintApplicationResumedResponse, WalletApiOperation, WalletCoreRequestType, WalletCoreResponseType, @@ -2071,10 +2072,38 @@ async function handleGetCurrencySpecification( export async function handleHintApplicationResumed( wex: WalletExecutionContext, req: EmptyObject, -): Promise<EmptyObject> { +): Promise<HintApplicationResumedResponse> { logger.info("handling hintApplicationResumed"); + + let dbWriteHealthy = false; + try { + await wex.runWalletDbTx(async (tx) => { + await tx.upsertConfig({ + key: ConfigRecordKey.LastResumed, + value: timestampProtocolToDb(TalerProtocolTimestamp.now()), + }); + }); + dbWriteHealthy = true; + } catch (e) { + logger.error( + `database write health check failed: ${j2s(getErrorDetailFromException(e))}`, + ); + } + + let dbReadHealthy = false; + try { + await wex.runWalletDbTx(async (tx) => { + await tx.getConfig(ConfigRecordKey.LastResumed); + }); + dbReadHealthy = true; + } catch (e) { + logger.error( + `database read health check failed: ${j2s(getErrorDetailFromException(e))}`, + ); + } + await restartAllRunningTasks(wex); - return {}; + return { dbWriteHealthy, dbReadHealthy }; } export async function handleTestingRunFixup( diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -441,6 +441,11 @@ export type ShutdownOp = { response: EmptyObject; }; +export interface HintApplicationResumedResponse { + dbWriteHealthy: boolean; + dbReadHealthy: boolean; +} + /** * Give wallet-core a kick and restart all pending tasks. * Useful when the host application gets suspended and resumed, @@ -449,7 +454,7 @@ export type ShutdownOp = { export type HintApplicationResumedOp = { op: WalletApiOperation.HintApplicationResumed; request: EmptyObject; - response: EmptyObject; + response: HintApplicationResumedResponse; }; /**