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:
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;
};
/**