taler-typescript-core

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

commit dfce0e58c5fa57e2f865d345de6d9a0174241b33
parent 0e4807845fc06f7c705711974650d67ef30e7919
Author: Florian Dold <florian@dold.me>
Date:   Wed, 15 Jul 2026 17:47:05 +0200

util: fix longpoll permit management issue

Diffstat:
Mpackages/taler-util/src/index.ts | 2+-
Apackages/taler-util/src/longpoll-queue.ts | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dpackages/taler-util/src/longpool-queue.ts | 85-------------------------------------------------------------------------------
3 files changed, 98 insertions(+), 86 deletions(-)

diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts @@ -39,7 +39,7 @@ export { sha256, HashSha256 } from "./sha256.js"; export * from "./libtool-version.js"; export * from "./logging.js"; -export * from "./longpool-queue.js"; +export * from "./longpoll-queue.js"; export { crypto_sign_keyPair_fromSeed, randomBytes, diff --git a/packages/taler-util/src/longpoll-queue.ts b/packages/taler-util/src/longpoll-queue.ts @@ -0,0 +1,97 @@ +/* + This file is part of GNU Taler + (C) 2025 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/> + + SPDX-License-Identifier: AGPL3.0-or-later +*/ + +import { CancellationToken } from "./CancellationToken.js"; +import { Logger } from "./logging.js"; +import { openPromise } from "./promises.js"; + +const logger = new Logger("longpoll-queue.ts"); + +const PERMITS: number = 20; +type LongpollRunFn<T> = (timeoutMs: number) => Promise<T>; + +export class LongpollQueue { + private idCounter: number = 0; + private queue: (() => void)[] = []; + private permits: number = PERMITS; + + async run<T>( + url: URL, + cancellationToken: CancellationToken, + f: LongpollRunFn<T>, + ): Promise<T> { + const hostname = url.hostname; + const rid = this.idCounter++; + + const triggerNextLongpoll = () => { + logger.trace(`cleaning up after long-poll ${rid} to ${hostname}`); + // Run pending task + const next = this.queue.shift(); + if (next != null) { + next(); + } else { + // Else release permit + this.permits++; + } + }; + + const doRunLongpoll: () => Promise<T> = async () => { + const numWaiting = this.queue.length; + const numConcurrent = PERMITS - this.permits; + logger.info( + `running long-poll ${rid} to ${hostname} with ${numWaiting} waiting and ${numConcurrent} running`, + ); + try { + const timeoutMs = Math.round(Math.max(10000, 30000 / (numWaiting + 1))); + return await f(timeoutMs); + } finally { + triggerNextLongpoll(); + } + }; + + if (this.permits > 0) { + this.permits--; + return doRunLongpoll(); + } else { + logger.info(`long-poll ${rid} to ${hostname} queued`); + const promcap = openPromise<void>(); + const entry = promcap.resolve; + this.queue.push(entry); + try { + await cancellationToken.racePromise(promcap.promise); + } catch (e) { + // The wait was interrupted (cancelled/errored) rather than woken + // normally. If our entry is still queued we were never handed a + // slot, so just remove ourselves without releasing a permit. If it + // was already dequeued (a departing task handed us a slot before the + // cancellation won the race), release that slot so it isn't leaked. + const idx = this.queue.indexOf(entry); + if (idx >= 0) { + this.queue.splice(idx, 1); + } else { + triggerNextLongpoll(); + } + logger.info(`long-poll ${rid} to ${hostname} cancelled while queued`); + throw e; + } + // Woken normally: a departing task dequeued our entry and handed its + // slot to us. doRunLongpoll() is the sole releaser of that slot. + return doRunLongpoll(); + } + } +} diff --git a/packages/taler-util/src/longpool-queue.ts b/packages/taler-util/src/longpool-queue.ts @@ -1,85 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2025 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/> - - SPDX-License-Identifier: AGPL3.0-or-later -*/ - -import { CancellationToken } from "./CancellationToken.js"; -import { Logger } from "./logging.js"; -import { openPromise } from "./promises.js"; - -const logger = new Logger("longpoll-queue.ts"); - -const PERMITS: number = 20; -type LongpollRunFn<T> = (timeoutMs: number) => Promise<T>; - -export class LongpollQueue { - private idCounter: number = 0; - private queue: (() => void)[] = []; - private permits: number = PERMITS; - - constructor() {} - - async run<T>( - url: URL, - cancellationToken: CancellationToken, - f: LongpollRunFn<T>, - ): Promise<T> { - const hostname = url.hostname; - const rid = this.idCounter++; - - const triggerNextLongpoll = () => { - logger.trace(`cleaning up after long-poll ${rid} to ${hostname}`); - // Run pending task - const next = this.queue.shift(); - if (next != null) { - next(); - } else { - // Else release permit - this.permits++; - } - }; - - const doRunLongpoll: () => Promise<T> = async () => { - const numWaiting = this.queue.length; - const numConcurrent = PERMITS - this.permits; - logger.info( - `running long-poll ${rid} to ${hostname} with ${numWaiting} waiting and ${numConcurrent} running`, - ); - try { - const timeoutMs = Math.round(Math.max(10000, 30000 / (numWaiting + 1))); - return await f(timeoutMs); - } finally { - triggerNextLongpoll(); - } - }; - - if (this.permits > 0) { - this.permits--; - return doRunLongpoll(); - } else { - logger.info(`long-poll ${rid} to ${hostname} queued`); - const promcap = openPromise<void>(); - this.queue.push(promcap.resolve); - try { - await cancellationToken.racePromise(promcap.promise); - } finally { - logger.info(`long-poll ${rid} to ${hostname} cancelled while queued`); - triggerNextLongpoll(); - } - return doRunLongpoll(); - } - } -}