You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(zero-cache): improve IVM time-slicing to avoid I/O starvation (#4989)
Improve IVM time-slicing to run slices in separate iterations of the
Node JS event loop, thus allowing I/O processing to happen between
slices and preventing connection failures due to unresponsive pings.
The included simulator illustrates the behavior of the two time-slicing
implementations based on a single simulated "pipeline" which:
* fetches ~250K rows from an SQLite db
* yields every 50K rows (roughly 500ms)
* measures the delay between a per-second ping and its received
response:
<img width="362" height="267" alt="Screenshot 2025-10-09 at 11 10 04"
src="https://github.com/user-attachments/assets/215051e2-417f-4c53-8b08-a87c957815e0"
/>
### Previous implementation
The previous `setTimeout()`-based implementation, while achieving
fairness across pipelines, was problematic in that the slices of all
pipelines were run in a single iteration of the event loop
(specifically, the [pending callbacks
phase](https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick#pending-callbacks)),
thereby introducing an I/O processing delay proportional to the number
of pipelines.
For example, with 20 pipelines (e.g. ViewSyncers) in the process, the
ping delay goes up to 20 x 500ms = 10sec.
<img width="376" height="898" alt="Screenshot 2025-10-09 at 11 09 23"
src="https://github.com/user-attachments/assets/03178883-d382-4470-afbd-29abe7c24fb0"
/>
### New implementation
The new time slicing implementation leverages a queue (implemented with
a `Lock`) to:
* run a single time-slice per iteration of the event loop
* using `setImmediate()` to run logic after I/O, and
* scheduling the next slice by running `setImmediate()` in that phase.
```ts
const queue = new Lock();
function yieldProcess() {
// previously ...
// return new Promise(resolve => setTimeout(resolve, 0));
// now ...
return queue.withLock(() => new Promise(setImmediate));
}
```
As explained in the [specification of
setImmediate()](https://nodejs.org/api/timers.html#setimmediatecallback-args):
> If an immediate timer is queued from inside an executing callback,
that timer will not be triggered until the next event loop iteration.
The resulting behavior achieves the same fairness and overall completion
time, but allows I/O to be processed between slices, capping the ping
delay to the duration of a single time slice:
<img width="379" height="1120" alt="Screenshot 2025-10-09 at 11 13 52"
src="https://github.com/user-attachments/assets/ba0d9097-5797-487d-b4f4-9878b2e5c146"
/>
### Miscellaneous
Also consolidated the timer and yielding code, and made it such that we
yield before the first time slice (e.g. so that we don't run into the
same problem when a bunch of ViewSyncers start processing a long
advancement).
0 commit comments