Files
browser/src/browser/js/Scheduler.zig
T
Karl Seguin 64d162af16 idb: allow re-entrant and interleaved transactions
Because of the single-connection nature of our Engine (in-memory SQLite), the
IDB Engine now keeps a list of "gated" transactions, processing one at time.
When one transaction completes, waiting transactions are signaled so that they
can processes their queue.

This also adds a double-queue so that any requests that comes in while
processing requests goes into a new queue and is only drained on the next tick.

Note: this introduces a UAF, where Engine lives on Session and referneces
IDBTransactions which are tied to the page. The next bit work on IDB is the
memory re-work, so I'm punting that there.
2026-07-03 07:10:03 +08:00

174 lines
5.2 KiB
Zig

// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program 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 Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const builtin = @import("builtin");
const milliTimestamp = @import("../../datetime.zig").milliTimestamp;
const log = lp.log;
const IS_DEBUG = builtin.mode == .Debug;
const Queue = std.PriorityQueue(Task, void, struct {
fn compare(_: void, a: Task, b: Task) std.math.Order {
const time_order = std.math.order(a.run_at, b.run_at);
if (time_order != .eq) return time_order;
// Break ties with sequence number to maintain FIFO order
return std.math.order(a.sequence, b.sequence);
}
}.compare);
const Scheduler = @This();
_sequence: u64,
// Some things (e.g. IndexedDB) can have operations that are only valid for a
// specific task boundary. So every time we start a task, we increment the
// scheduler's generation. Code can snapshot this version and then compare it
// later to see if we're still in the same task.
generation: u64,
low_priority: Queue,
high_priority: Queue,
pub fn init(allocator: std.mem.Allocator) Scheduler {
return .{
._sequence = 0,
.generation = 0,
.low_priority = Queue.init(allocator, {}),
.high_priority = Queue.init(allocator, {}),
};
}
pub fn deinit(self: *Scheduler) void {
finalizeTasks(&self.low_priority);
finalizeTasks(&self.high_priority);
}
pub fn reset(self: *Scheduler) void {
finalizeTasks(&self.low_priority);
finalizeTasks(&self.high_priority);
self.low_priority.clearRetainingCapacity();
self.high_priority.clearRetainingCapacity();
}
const AddOpts = struct {
name: []const u8 = "",
low_priority: bool = false,
finalizer: ?Finalizer = null,
};
pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts: AddOpts) !void {
if (comptime IS_DEBUG) {
log.debug(.scheduler, "scheduler.add", .{ .name = opts.name, .run_in_ms = run_in_ms, .low_priority = opts.low_priority });
}
var queue = if (opts.low_priority) &self.low_priority else &self.high_priority;
const seq = self._sequence + 1;
self._sequence = seq;
return queue.add(.{
.ctx = ctx,
.callback = cb,
.sequence = seq,
.name = opts.name,
.finalizer = opts.finalizer,
.run_at = milliTimestamp(.monotonic) + run_in_ms,
});
}
pub fn run(self: *Scheduler) !void {
try self.runQueue(&self.low_priority);
try self.runQueue(&self.high_priority);
}
pub fn hasReadyTasks(self: *Scheduler) bool {
const now = milliTimestamp(.monotonic);
return queueHasReadyTask(&self.low_priority, now) or queueHasReadyTask(&self.high_priority, now);
}
pub fn msToNextHigh(self: *Scheduler) ?u64 {
const task = self.high_priority.peek() orelse return null;
const now = milliTimestamp(.monotonic);
if (task.run_at <= now) {
return 0;
}
return @intCast(task.run_at - now);
}
fn runQueue(self: *Scheduler, queue: *Queue) !void {
if (queue.count() == 0) {
return;
}
const start = milliTimestamp(.monotonic);
var now = start;
while (queue.peek()) |*task_| {
if (task_.run_at > now) {
return;
}
var task = queue.remove();
if (comptime IS_DEBUG) {
log.debug(.scheduler, "scheduler.runTask", .{ .name = task.name });
}
self.generation +%= 1;
const repeat_in_ms = task.callback(task.ctx) catch |err| {
log.warn(.scheduler, "task.callback", .{ .name = task.name, .err = err });
continue;
};
if (repeat_in_ms) |ms| {
// Task cannot be repeated immediately, and they should know that
if (comptime IS_DEBUG) {
std.debug.assert(ms != 0);
}
task.run_at = now + ms;
try self.low_priority.add(task);
}
now = milliTimestamp(.monotonic);
if (now - start > 500) {
return;
}
}
return;
}
fn queueHasReadyTask(queue: *Queue, now: u64) bool {
const task = queue.peek() orelse return false;
return task.run_at <= now;
}
fn finalizeTasks(queue: *Queue) void {
var it = queue.iterator();
while (it.next()) |t| {
if (t.finalizer) |func| {
func(t.ctx);
}
}
}
const Task = struct {
run_at: u64,
sequence: u64,
ctx: *anyopaque,
name: []const u8,
callback: Callback,
finalizer: ?Finalizer,
};
const Callback = *const fn (ctx: *anyopaque) anyerror!?u32;
const Finalizer = *const fn (ctx: *anyopaque) void;