taler-typescript-core

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

commit 246905ec79c4432095c07413a26e58b7588aafd6
parent d2e770e0ff271cc0477fa317b5572d726a51d6cb
Author: Florian Dold <dold@taler.net>
Date:   Wed, 22 Jul 2026 17:10:16 +0200

wallet: deliver a wakeup whatever the shepherd is doing

Diffstat:
Mpackages/taler-wallet-core/src/shepherd.ts | 90+++++++++++++++++++++++++++++++++----------------------------------------------
1 file changed, 37 insertions(+), 53 deletions(-)

diff --git a/packages/taler-wallet-core/src/shepherd.ts b/packages/taler-wallet-core/src/shepherd.ts @@ -115,14 +115,6 @@ import { WalletDbTransaction } from "./dbtx.js"; const logger = new Logger("shepherd.ts"); -enum TaskState { - StopRequested = 0, - Running = 1, - WaitingBackoff = 2, - WaitingScheduled = 3, - Done = 4, -} - /** * Info about one task being shepherded. */ @@ -134,16 +126,25 @@ interface ShepherdInfo { */ latch?: Promise<void>; + /** + * Cut the current wait short. Only installed while the loop is actually + * waiting; a no-op otherwise, so it is always safe to call. + */ interruptWait: () => void; - taskState: TaskState; + /** + * Set when this shepherd has been asked to stop. It then no longer accepts + * wakeups and is replaced rather than reused. + */ + stopRequested: boolean; /** - * Set when a wakeup (startShepherdTask) arrives while this shepherd is - * already Running and can therefore not be delivered via interruptWait(). - * The shepherd loop re-runs the handler instead of terminating so that a - * transition back into a working state right as the task finishes is not - * lost. + * Set by a wakeup (startShepherdTask) for a shepherd that already exists. + * + * This is what makes a wakeup survive until the loop can act on it, whatever + * the loop happens to be doing at the time: the loop clears it before each + * handler run, skips waiting while it is set, and re-runs instead of + * terminating when the handler reports that it is finished. */ rerunRequested: boolean; @@ -328,26 +329,16 @@ export class TaskSchedulerImpl implements TaskScheduler { logger.trace(`Starting to shepherd task ${taskId}`); const oldShep = this.sheps.get(taskId); if (oldShep) { - switch (oldShep.taskState) { - case TaskState.WaitingBackoff: - case TaskState.Running: - // We can't interrupt a running/backing-off shepherd, so record that - // another run is required. The loop (and the finally below) will - // honor this so the wakeup is not silently dropped. - oldShep.rerunRequested = true; - logger.trace(`Already have a shepherd for ${taskId}`); - return; - case TaskState.WaitingScheduled: - logger.trace( - `Already have a shepherd (waiting) for ${taskId}, kicking shepherd`, - ); - oldShep.interruptWait(); - return; - case TaskState.StopRequested: - case TaskState.Done: - break; - default: - assertUnreachable(oldShep.taskState); + if (!oldShep.stopRequested) { + // Both of these, always. Recording the wakeup is what makes it + // survive; interrupting the wait only shortens a sleep that happens + // to be in progress and is a no-op otherwise. Deciding between them + // would mean knowing where the loop currently is, and getting that + // wrong drops the wakeup. + oldShep.rerunRequested = true; + oldShep.interruptWait(); + logger.trace(`Already have a shepherd for ${taskId}`); + return; } logger.trace( `Waiting for old task to complete the loop in cancel mode ${taskId}`, @@ -356,7 +347,7 @@ export class TaskSchedulerImpl implements TaskScheduler { } const newShep: ShepherdInfo = { cts: CancellationToken.create(), - taskState: TaskState.Running, + stopRequested: false, interruptWait: () => undefined, rerunRequested: false, shepId: this.shepCounter++, @@ -388,7 +379,7 @@ export class TaskSchedulerImpl implements TaskScheduler { if ( deleted && newShep.rerunRequested && - newShep.taskState !== TaskState.StopRequested && + !newShep.stopRequested && !this.ws.stopped ) { logger.trace( @@ -404,18 +395,9 @@ export class TaskSchedulerImpl implements TaskScheduler { const oldShep = this.sheps.get(taskId); if (oldShep) { logger.trace(`Cancelling old shepherd for ${taskId}`); - switch (oldShep.taskState) { - case TaskState.Running: - case TaskState.WaitingBackoff: - case TaskState.WaitingScheduled: - oldShep.cts.cancel(`stopping task ${taskId}`); - oldShep.taskState = TaskState.StopRequested; - break; - case TaskState.Done: - case TaskState.StopRequested: - break; - default: - assertUnreachable(oldShep.taskState); + if (!oldShep.stopRequested) { + oldShep.cts.cancel(`stopping task ${taskId}`); + oldShep.stopRequested = true; } this.iterCond.trigger(); } @@ -444,6 +426,13 @@ export class TaskSchedulerImpl implements TaskScheduler { info: ShepherdInfo, delay: Duration, ): Promise<void> { + if (info.rerunRequested) { + // A wakeup arrived while the handler was running. Waiting it out would + // mean answering it only after the delay that the wakeup was meant to + // cut short. + logger.trace(`Not waiting on ${taskId}, a rerun is already requested`); + return; + } // Limit how long we wait. // The NodeJS runtime doesn't like very long timeouts. delay = Duration.min(delay, Duration.fromSpec({ hours: 1 })); @@ -537,7 +526,6 @@ export class TaskSchedulerImpl implements TaskScheduler { const t = timestampAbsoluteFromDb(retryRecord.retryInfo.nextRetry); const delay = AbsoluteTime.remaining(t); logger.trace(`Stored error for ${taskId},`); - info.taskState = TaskState.WaitingBackoff; await this.wait(taskId, info, delay); break; } @@ -546,7 +534,6 @@ export class TaskSchedulerImpl implements TaskScheduler { const t = timestampAbsoluteFromDb(retryRecord.retryInfo.nextRetry); const delay = AbsoluteTime.remaining(t); logger.trace(`Waiting on ${taskId} (backoff)`); - info.taskState = TaskState.WaitingBackoff; await this.wait(taskId, info, delay); break; } @@ -563,7 +550,6 @@ export class TaskSchedulerImpl implements TaskScheduler { const t = timestampAbsoluteFromDb(retryRecord.retryInfo.nextRetry); const delay = AbsoluteTime.remaining(t); logger.trace(`Waiting on ${taskId} (scheduled later)`); - info.taskState = TaskState.WaitingScheduled; await this.wait(taskId, info, delay); break; } @@ -588,7 +574,6 @@ export class TaskSchedulerImpl implements TaskScheduler { logger.info( `long-poller for ${taskId} returned unexpectedly early (${taskDuration.d_ms} ms), waiting 10 seconds`, ); - info.taskState = TaskState.WaitingScheduled; await this.wait(taskId, info, Duration.fromSpec({ seconds: 10 })); } else { logger.info(`task ${taskId} will long-poll again`); @@ -599,7 +584,6 @@ export class TaskSchedulerImpl implements TaskScheduler { await storePendingTaskPending(this.ws, taskId); const delay = Duration.getForever(); logger.trace(`Not retrying task until network is restored.`); - info.taskState = TaskState.WaitingScheduled; await this.wait(taskId, info, delay); break; }