mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-28 05:26:16 +00:00
fix(diffs): sweep expired sqlite artifacts
This commit is contained in:
@@ -2,21 +2,25 @@ import fs from "node:fs/promises";
|
||||
import type { IncomingMessage } from "node:http";
|
||||
import path from "node:path";
|
||||
import { resetPluginBlobStoreForTests } from "openclaw/plugin-sdk/plugin-state-runtime";
|
||||
import type { PluginBlobStore } from "openclaw/plugin-sdk/plugin-state-runtime";
|
||||
import { createMockServerResponse } from "openclaw/plugin-sdk/test-env";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createDiffsHttpHandler } from "./http.js";
|
||||
import { DiffArtifactStore } from "./store.js";
|
||||
import type { DiffBlobMetadata } from "./store.js";
|
||||
import { createDiffStoreHarness } from "./test-helpers.js";
|
||||
|
||||
describe("DiffArtifactStore", () => {
|
||||
let rootDir: string;
|
||||
let store: DiffArtifactStore;
|
||||
let blobStore: PluginBlobStore<DiffBlobMetadata>;
|
||||
let cleanupRootDir: () => Promise<void>;
|
||||
|
||||
beforeEach(async () => {
|
||||
({
|
||||
rootDir,
|
||||
store,
|
||||
blobStore,
|
||||
cleanup: cleanupRootDir,
|
||||
} = await createDiffStoreHarness("openclaw-diffs-store-"));
|
||||
});
|
||||
@@ -92,6 +96,26 @@ describe("DiffArtifactStore", () => {
|
||||
expect(loaded).toBeNull();
|
||||
});
|
||||
|
||||
it("sweeps expired SQLite-only viewer artifacts during cleanup", async () => {
|
||||
vi.useFakeTimers();
|
||||
const now = new Date("2026-02-27T16:00:00Z");
|
||||
vi.setSystemTime(now);
|
||||
|
||||
const artifact = await store.createArtifact({
|
||||
html: "<html>sqlite</html>",
|
||||
title: "SQLite",
|
||||
inputKind: "patch",
|
||||
fileCount: 1,
|
||||
ttlMs: 1_000,
|
||||
});
|
||||
|
||||
vi.setSystemTime(new Date(now.getTime() + 2_000));
|
||||
await store.cleanupExpired();
|
||||
|
||||
expect(await blobStore.deleteExpired()).toBe(0);
|
||||
await expect(blobStore.lookup(`view:${artifact.id}`)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("updates the stored file path", async () => {
|
||||
const artifact = await store.createArtifact({
|
||||
html: "<html>demo</html>",
|
||||
|
||||
@@ -188,6 +188,8 @@ export class DiffArtifactStore {
|
||||
}
|
||||
|
||||
async cleanupExpired(): Promise<void> {
|
||||
await this.blobStore.deleteExpired();
|
||||
|
||||
const root = await this.artifactRoot();
|
||||
const entries = await root.list("", { withFileTypes: true }).catch(() => []);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import {
|
||||
createPluginBlobStore,
|
||||
type PluginBlobStore,
|
||||
resetPluginBlobStoreForTests,
|
||||
} from "openclaw/plugin-sdk/plugin-state-runtime";
|
||||
import { resolvePreferredOpenClawTmpDir } from "../api.js";
|
||||
@@ -28,22 +29,25 @@ export async function createDiffStoreHarness(
|
||||
): Promise<{
|
||||
rootDir: string;
|
||||
store: DiffArtifactStore;
|
||||
blobStore: PluginBlobStore<DiffBlobMetadata>;
|
||||
cleanup: () => Promise<void>;
|
||||
}> {
|
||||
const { rootDir, cleanup } = await createTempDiffRoot(prefix);
|
||||
const originalStateDir = process.env.OPENCLAW_STATE_DIR;
|
||||
process.env.OPENCLAW_STATE_DIR = await fs.mkdtemp(path.join(rootDir, "state-"));
|
||||
resetPluginBlobStoreForTests();
|
||||
const blobStore = createPluginBlobStore<DiffBlobMetadata>("diffs", {
|
||||
namespace: "artifacts",
|
||||
maxEntries: MAX_TEST_DIFF_ARTIFACT_BLOBS,
|
||||
});
|
||||
return {
|
||||
rootDir,
|
||||
store: new DiffArtifactStore({
|
||||
rootDir,
|
||||
cleanupIntervalMs: options.cleanupIntervalMs,
|
||||
blobStore: createPluginBlobStore<DiffBlobMetadata>("diffs", {
|
||||
namespace: "artifacts",
|
||||
maxEntries: MAX_TEST_DIFF_ARTIFACT_BLOBS,
|
||||
}),
|
||||
blobStore,
|
||||
}),
|
||||
blobStore,
|
||||
cleanup: async () => {
|
||||
if (originalStateDir === undefined) {
|
||||
delete process.env.OPENCLAW_STATE_DIR;
|
||||
|
||||
@@ -30,4 +30,35 @@ describe("plugin blob store", () => {
|
||||
await expect(store.entries()).resolves.toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it("deletes expired entries for the current namespace", async () => {
|
||||
await withOpenClawTestState({ label: "plugin-blob-store-expired" }, async () => {
|
||||
const store = createPluginBlobStore<{ contentType: string }>("zalo", {
|
||||
namespace: "media",
|
||||
maxEntries: 10,
|
||||
});
|
||||
const otherNamespace = createPluginBlobStore<{ contentType: string }>("zalo", {
|
||||
namespace: "other-media",
|
||||
maxEntries: 10,
|
||||
});
|
||||
|
||||
await store.register("live", { contentType: "image/jpeg" }, Buffer.from("live"));
|
||||
await store.register("expired", { contentType: "image/png" }, Buffer.from("expired"), {
|
||||
ttlMs: 1,
|
||||
});
|
||||
await otherNamespace.register("expired", { contentType: "image/gif" }, Buffer.from("other"), {
|
||||
ttlMs: 1,
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
|
||||
await expect(store.deleteExpired()).resolves.toBe(1);
|
||||
await expect(store.entries()).resolves.toMatchObject([
|
||||
{
|
||||
key: "live",
|
||||
metadata: { contentType: "image/jpeg" },
|
||||
},
|
||||
]);
|
||||
await expect(otherNamespace.deleteExpired()).resolves.toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,6 +30,7 @@ export type PluginBlobStore<TMetadata = Record<string, unknown>> = {
|
||||
lookup(key: string): Promise<PluginBlobEntry<TMetadata> | undefined>;
|
||||
consume(key: string): Promise<PluginBlobEntry<TMetadata> | undefined>;
|
||||
delete(key: string): Promise<boolean>;
|
||||
deleteExpired(): Promise<number>;
|
||||
entries(): Promise<PluginBlobEntry<TMetadata>[]>;
|
||||
clear(): Promise<void>;
|
||||
};
|
||||
@@ -39,6 +40,7 @@ export type PluginBlobSyncStore<TMetadata = Record<string, unknown>> = {
|
||||
lookup(key: string): PluginBlobEntry<TMetadata> | undefined;
|
||||
consume(key: string): PluginBlobEntry<TMetadata> | undefined;
|
||||
delete(key: string): boolean;
|
||||
deleteExpired(): number;
|
||||
entries(): PluginBlobEntry<TMetadata>[];
|
||||
clear(): void;
|
||||
};
|
||||
@@ -173,6 +175,9 @@ export function createPluginBlobStore<TMetadata = Record<string, unknown>>(
|
||||
async delete(key) {
|
||||
return syncStore.delete(key);
|
||||
},
|
||||
async deleteExpired() {
|
||||
return syncStore.deleteExpired();
|
||||
},
|
||||
async entries() {
|
||||
return syncStore.entries();
|
||||
},
|
||||
@@ -341,6 +346,23 @@ export function createPluginBlobSyncStore<TMetadata = Record<string, unknown>>(
|
||||
);
|
||||
return Number(result.numAffectedRows ?? 0) > 0;
|
||||
},
|
||||
deleteExpired() {
|
||||
const expiredAt = now();
|
||||
const result = runOpenClawStateWriteTransaction(
|
||||
(database) =>
|
||||
executeSqliteQuerySync(
|
||||
database.db,
|
||||
getPluginBlobKysely(database.db)
|
||||
.deleteFrom("plugin_blob_entries")
|
||||
.where("plugin_id", "=", pluginId)
|
||||
.where("namespace", "=", namespace)
|
||||
.where("expires_at", "is not", null)
|
||||
.where("expires_at", "<=", expiredAt),
|
||||
),
|
||||
databaseOptions,
|
||||
);
|
||||
return Number(result.numAffectedRows ?? 0);
|
||||
},
|
||||
entries() {
|
||||
const database = openOpenClawStateDatabase(databaseOptions);
|
||||
const rows = executeSqliteQuerySync(
|
||||
|
||||
Reference in New Issue
Block a user