mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
fix(database): batch database saves for bulk host writes (#1554)
* fix(database): yield to the event loop between database saves Each SQLite save serializes and encrypts the whole database into new buffers, which V8 only releases once the event loop turns. Boot migrations and bulk host import write plugin settings per host and per key in an awaited loop, so hundreds of full copies piled up and the container ran out of memory. * fix(database): save once per bulk host write instead of once per row On SQLite every repository write force-saves the whole database. Boot plugin data migrations, host defaults materialization and the bulk host routes write one row per host per setting, so they serialized and encrypted the full database hundreds of times in a row. DatabaseSaveTrigger.batched wraps a function so force saves made while it runs collapse into a single save when it finishes. Nested scopes fold into the outer one, and work that outlives its scope saves normally.
This commit is contained in:
1 parent
59083aac00
commit
94a9f815c9
6 files changed
+225
-92
No files matched your search
@@ -37,6 +37,18 @@ describe("DatabaseSaveTrigger", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("lets the event loop turn before a force save resolves", async () => {
|
||||
DatabaseSaveTrigger.initialize(vi.fn().mockResolvedValue(undefined));
|
||||
let turned = false;
|
||||
setImmediate(() => {
|
||||
turned = true;
|
||||
});
|
||||
|
||||
await DatabaseSaveTrigger.forceSave("bulk_write");
|
||||
|
||||
expect(turned).toBe(true);
|
||||
});
|
||||
|
||||
it("debounces dirty saves and marks the database clean after saving", async () => {
|
||||
vi.useFakeTimers();
|
||||
const save = vi.fn().mockResolvedValue(undefined);
|
||||
@@ -49,6 +61,7 @@ describe("DatabaseSaveTrigger", () => {
|
||||
expect(DatabaseSaveTrigger.getStatus().hasPendingTimeout).toBe(true);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(2000);
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
|
||||
expect(save).toHaveBeenCalledTimes(1);
|
||||
expect(DatabaseSaveTrigger.isDirty).toBe(false);
|
||||
@@ -78,4 +91,77 @@ describe("DatabaseSaveTrigger", () => {
|
||||
expect(save).toHaveBeenCalledTimes(2);
|
||||
expect(DatabaseSaveTrigger.getStatus().pendingSave).toBe(false);
|
||||
});
|
||||
|
||||
it("collapses force saves inside a batch into one save", async () => {
|
||||
const save = vi.fn().mockResolvedValue(undefined);
|
||||
DatabaseSaveTrigger.initialize(save);
|
||||
|
||||
const result = await DatabaseSaveTrigger.batched(async (rows: number) => {
|
||||
for (let i = 0; i < rows; i++) {
|
||||
await DatabaseSaveTrigger.forceSave("row_write");
|
||||
}
|
||||
expect(save).not.toHaveBeenCalled();
|
||||
return rows;
|
||||
})(50);
|
||||
|
||||
expect(result).toBe(50);
|
||||
expect(save).toHaveBeenCalledTimes(1);
|
||||
expect(DatabaseSaveTrigger.isDirty).toBe(false);
|
||||
});
|
||||
|
||||
it("folds a nested batch into the outer one", async () => {
|
||||
const save = vi.fn().mockResolvedValue(undefined);
|
||||
DatabaseSaveTrigger.initialize(save);
|
||||
const inner = DatabaseSaveTrigger.batched(() =>
|
||||
DatabaseSaveTrigger.forceSave("inner_write"),
|
||||
);
|
||||
|
||||
await DatabaseSaveTrigger.batched(async () => {
|
||||
await inner();
|
||||
await inner();
|
||||
})();
|
||||
|
||||
expect(save).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("skips the save when a batch wrote nothing", async () => {
|
||||
const save = vi.fn().mockResolvedValue(undefined);
|
||||
DatabaseSaveTrigger.initialize(save);
|
||||
|
||||
await DatabaseSaveTrigger.batched(async () => {})();
|
||||
|
||||
expect(save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("still saves what a failed batch wrote", async () => {
|
||||
const save = vi.fn().mockResolvedValue(undefined);
|
||||
DatabaseSaveTrigger.initialize(save);
|
||||
|
||||
await expect(
|
||||
DatabaseSaveTrigger.batched(async () => {
|
||||
await DatabaseSaveTrigger.forceSave("row_write");
|
||||
throw new Error("boom");
|
||||
})(),
|
||||
).rejects.toThrow("boom");
|
||||
|
||||
expect(save).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("saves immediately for work that outlives its batch", async () => {
|
||||
const save = vi.fn().mockResolvedValue(undefined);
|
||||
DatabaseSaveTrigger.initialize(save);
|
||||
let release: (() => void) | undefined;
|
||||
const gate = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
let detached: Promise<void> | undefined;
|
||||
|
||||
await DatabaseSaveTrigger.batched(async () => {
|
||||
detached = gate.then(() => DatabaseSaveTrigger.forceSave("late_write"));
|
||||
})();
|
||||
release?.();
|
||||
await detached;
|
||||
|
||||
expect(save).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user