commit 4cf0b13414deb1579453bd72a4c785a045c74c1d
parent c33029b2ecba72420f0500ea25228c59ab6f1d2f
Author: Florian Dold <dold@taler.net>
Date: Wed, 22 Jul 2026 16:23:04 +0200
wallet: reset a crypto worker that stops answering
Diffstat:
1 file changed, 59 insertions(+), 3 deletions(-)
diff --git a/packages/taler-wallet-core/src/crypto/workers/crypto-dispatcher.ts b/packages/taler-wallet-core/src/crypto/workers/crypto-dispatcher.ts
@@ -26,7 +26,9 @@
import {
j2s,
Logger,
+ makeErrorDetail,
openPromise,
+ performanceDelta,
performanceNow,
TalerError,
TalerErrorCode,
@@ -84,6 +86,18 @@ interface WorkItem {
const NUM_PRIO = 5;
/**
+ * How long a request may be running on a worker before that worker counts as
+ * hung.
+ *
+ * Chosen well above any real operation rather than close to it: the heaviest
+ * single request derives a refresh session, which blinds kappa times the
+ * number of fresh coins, and the wallet also runs on slow devices. A worker
+ * that crashes is already handled through onerror; this covers the one that
+ * stops answering without crashing, where the alternative is waiting forever.
+ */
+const WORKER_HANG_TIMEOUT_MS = 2 * 60 * 1000;
+
+/**
* A crypto worker factory is responsible for creating new
* crypto workers on-demand.
*/
@@ -134,6 +148,12 @@ export class CryptoDispatcher {
private stopped = false;
/**
+ * See {@link WORKER_HANG_TIMEOUT_MS}. Overridable so that tests do not have
+ * to wait out the real one.
+ */
+ private readonly workerHangTimeoutMs: number;
+
+ /**
* Terminate all worker threads.
*/
terminateWorkers(): void {
@@ -291,7 +311,12 @@ export class CryptoDispatcher {
cryptoApi: TalerCryptoInterface;
- constructor(workerFactory: CryptoWorkerFactory) {
+ constructor(
+ workerFactory: CryptoWorkerFactory,
+ opts: { workerHangTimeoutMs?: number } = {},
+ ) {
+ this.workerHangTimeoutMs =
+ opts.workerHangTimeoutMs ?? WORKER_HANG_TIMEOUT_MS;
const fns: any = {};
for (const name of Object.keys(nullCrypto)) {
fns[name] = (x: any) => this.doRpc(name, 0, x);
@@ -366,8 +391,39 @@ export class CryptoDispatcher {
let timeoutHandle: TimerHandle | undefined = undefined;
const timeoutMs = 5000;
const onTimeout = () => {
- // FIXME: Maybe destroy and re-init worker if request is in processing
- // state and really taking too long?
+ // Only time spent on a worker counts. An item still waiting for a
+ // free worker is not hung, however long it waits.
+ if (workItem.state === WorkItemState.Running) {
+ const runningMs = performanceDelta(
+ workItem.startTime as bigint,
+ performanceNow(),
+ );
+ if (runningMs > this.workerHangTimeoutMs) {
+ const ws = this.workers.find((w) => w.currentWorkItem === workItem);
+ if (ws) {
+ logger.error(
+ `crypto worker did not answer '${operation}' within ${runningMs} ms, resetting it`,
+ );
+ // Terminates the worker, rejects this caller and hands the
+ // worker's queue on, the same way a crashed worker is handled.
+ this.resetWorker(
+ ws,
+ TalerError.fromDetail(
+ TalerErrorCode.WALLET_CRYPTO_WORKER_ERROR,
+ {
+ innerError: makeErrorDetail(
+ TalerErrorCode.WALLET_CRYPTO_WORKER_ERROR,
+ {} as any,
+ `no answer for '${operation}' within ${runningMs} ms`,
+ ),
+ },
+ "crypto worker timed out",
+ ),
+ );
+ return;
+ }
+ }
+ }
logger.warn(
`crypto RPC call ('${operation}') has been queued for a long time`,
);