taler-typescript-core

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

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:
Mpackages/taler-wallet-core/src/crypto/workers/crypto-dispatcher.ts | 62+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
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`, );