View Issue Details

IDProjectCategoryView StatusLast Update
0011951Talerqtartpublic2026-11-05 08:59
Reporterbot-codex-florian-dold [agent] Assigned ToResolution deadline
ImplementerReviewer 
Release signed byDeployed by 
VerifierCommunicatorCM_7  
Data protection authority notification requiredNo  
CSIRT notification requiredNo  
Estimated effort (hours)Actual effort (hours)0 
Fast-trackNo
Work packagenone
PrioritynormalSeveritymajorReproducibilityhave not tried
Status newResolutionopen 
Summary0011951: qtart: zero-delay timer chains starve HTTP response processing
DescriptionA background computation that cooperatively yields with await new Promise(resolve => setTimeout(resolve, 0)) can prevent Qtart from delivering an HTTP response that has already arrived. This can stall an in-progress payment while ordinary host API requests remain responsive.

In quickjs/quickjs-libc.c, js_std_loop drains pending promise jobs before calling js_os_poll. js_os_poll checks host messages, then runs an expired timer and returns immediately, before poll() and handle_http_message(). The timer resolves the computation's promise, whose continuation runs another chunk and schedules another already-expired timer. A sustained chain can therefore bypass HTTP and worker I/O indefinitely. The event-source fairness rotation below poll() cannot help when that code is never reached. The early timer return also bypasses the poll_iteration_count increment.

The prelude maps setTimeout directly to os.setTimeout, with no positive minimum delay. Native HTTP callbacks enqueue responses on http_pipe, separate from host_pipe. The explicit pre-timer host-message check explains why API calls can remain fast while HTTP completion is starved.

Observed while investigating Android wallet-core 1.6.49 (9b08843c54a93d83b2df70cad9d25093b85720e1). Source inspection confirmed the same event-loop ordering in quickjs-tart 3f58047d99674e7376680d18f4475be0f2687543, which bumps the Android package to 1.6.49. The local runtime reproducer below demonstrates the scheduling defect independently of the wallet database; it was not run on the affected Android device.

In the wallet log, native request 28 starts at 12:24:25.439 and its response is ready at 25.564. Wallet-core records the temporally corresponding /pay completion at 27.429, about 1.87 seconds later, amid background balance calculations. Native and wallet-core request IDs are not explicitly mapped in the capture, so this correspondence is inferred from timing.
Steps To ReproduceRun this as an ES module with qtart. It makes a pipe readable before starting 100 two-millisecond computation chunks:

import * as os from "os";
const [rd, wr] = os.pipe();
const buf = new Uint8Array([1]);
os.write(wr, buf.buffer, 0, 1);
const start = Date.now();
let count = 0;
os.setReadHandler(rd, () => {
  os.read(rd, buf.buffer, 0, 1);
  console.log("I/O serviced after", Date.now() - start, "ms; chunks", count);
  os.setReadHandler(rd, null);
  os.close(rd);
  os.close(wr);
});
(async () => {
  for (let i = 0; i < 100; i++) {
    const end = Date.now() + 2;
    while (Date.now() < end) {}
    count++;
    await new Promise(resolve => os.setTimeout(resolve, 0));
  }
})();

Observed locally: the readable pipe is serviced after all 100 chunks, at 200 ms. Changing the timer delay to 1 ms services it after the first chunk, at 2 ms. The positive-delay variant is a diagnostic control, not a proposed fairness guarantee.
Additional InformationExpected fix and acceptance criteria:
- Include ready timers, host messages, HTTP responses, worker messages, and other ready I/O in a bounded fair dispatch policy. Preserve prompt host-message and shutdown handling without merely moving starvation to another event source.
- Add a regression test in which HTTP or worker completion must be delivered while a sustained zero-delay timer chain is still running, not only after the chain ends. Use deterministic synchronization where possible rather than fragile timing thresholds.
- Check both timer-heavy and message-heavy workloads for starvation.
- Do not rely only on changing wallet yields from 0 to 1 ms: multiple ready timers or other work can eliminate that accidental polling opportunity.

Relevant code: quickjs/quickjs-libc.c js_os_poll (around lines 3355-3470), js_std_loop (around 5361), handle_http_resp (around 2373); prelude.js setTimeout mapping.

This issue addresses runtime fairness. Separate wallet-core issues cover excessive computation and interactive-work prioritization.
TagsNo tags attached.

Required notifications

Address or linkRemarkAdded byNotifiedActions

Activities

There are no notes attached to this issue.

Issue History

Date ModifiedUsernameFieldChange
2026-11-05 08:59bot-codex-florian-doldNew Issue