mirror of
https://gh-proxy.org/https://github.com/klinsmaya/open-connector.git
synced 2026-11-08 01:16:52 +08:00
Runtime token authentication prepares the same SQLite SELECT and last-use UPDATE on every successful request. Reuse those two statements per `SqliteRuntimeTokenStore` instance while continuing to read current database rows on every call. Preparation is lazy: custom migration sources can omit `runtime_tokens`, so eagerly preparing in the constructor would break existing database initialization behavior. SQL, parameter binding, row decoding, error propagation, and public interfaces are unchanged. No token records or authorization decisions are cached. ### Validation - `npm run fix-check` passed (lint, formatting, and src/scripts/examples typechecks). Unrelated formatter changes were excluded from the commit. - SQLite store and runtime token service suites: **36 tests passed**, including alternating token bindings, all policy fields, revocation, independent last-use updates, cross-connection changes, reopening, custom migrations, and best-effort last-use failure handling. - `npm test` on Windows: 3,132 passed, 9 failed, 4 skipped. Six failures were source-scan/catalog timeouts under the default parallelism. - `npm test -- --maxWorkers=2 --testTimeout=30000`: **3,138 passed, 3 failed, 4 skipped**. Remaining failures are in unchanged provider source guards: two clone-baseline assertions (Windows backslash paths do not match the committed slash-separated baseline) and one auth-status error-code assertion. No provider sources or guard tests are changed by this PR. The upstream Linux workflow remains the CI check for this change. ### Benchmark Run `node scripts/benchmark-sqlite-token-statements.ts` after `npm ci`. It compares the pre-change store method implementations with the actual updated store, including row decoding and async method calls. Both use the same migrated schema, 100 tokens, precomputed inputs, warmup, and seven alternating measurement rounds. Database creation and seeding are outside the timed loops; there is no timing assertion in CI. Windows, Node **24.14.1**, SQLite **3.51.2**, `synchronous=2` (FULL): | Storage | Lookup/update pairs per round | Baseline median | Reused median | Baseline / reused per pair | Speedup | | --- | ---: | ---: | ---: | ---: | ---: | | In-memory | 30,000 | 1,205.28 ms | 247.22 ms | 40.18 / 8.24 us | 4.88x | | File, WAL | 1,000 | 652.60 ms | 591.26 ms | 652.60 / 591.26 us | 1.10x | The file-backed case reduces elapsed time by about **9.4%** on this machine. These are isolated store-operation measurements, not end-to-end HTTP throughput claims; filesystem synchronization and upstream API latency affect real workloads. Co-authored-by: machaoxin0407 <[email protected]> Co-authored-by: l1shen <[email protected]>
135 lines
5.1 KiB
TypeScript
135 lines
5.1 KiB
TypeScript
/** Run with `node scripts/benchmark-sqlite-token-statements.ts`; timings are not CI assertions. */
|
|
import type { RuntimeTokenRecord } from "../src/server/storage/runtime-token-service.ts";
|
|
|
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { performance } from "node:perf_hooks";
|
|
import { DatabaseSync } from "node:sqlite";
|
|
import { readRuntimeTokenRow, runtimeTokenColumns } from "../src/server/storage/runtime-sql.ts";
|
|
import { SqliteRuntimeDatabase, SqliteRuntimeTokenStore } from "../src/server/storage/sqlite/runtime-store.ts";
|
|
|
|
/** The two store methods before statement reuse, with the same decoding and async boundary. */
|
|
class PreparePerCallTokenStore extends SqliteRuntimeTokenStore {
|
|
private readonly connection: DatabaseSync;
|
|
|
|
constructor(database: DatabaseSync) {
|
|
super(database);
|
|
this.connection = database;
|
|
}
|
|
|
|
override async findByHash(tokenHash: string): Promise<RuntimeTokenRecord | undefined> {
|
|
const row = this.connection
|
|
.prepare(`select ${runtimeTokenColumns} from runtime_tokens where token_hash = ?`)
|
|
.get(tokenHash);
|
|
return row ? readRuntimeTokenRow(row) : undefined;
|
|
}
|
|
|
|
override async markUsed(id: string, usedAt: string): Promise<void> {
|
|
this.connection.prepare("update runtime_tokens set last_used_at = ? where id = ?").run(usedAt, id);
|
|
}
|
|
}
|
|
|
|
const tokenCount = 100;
|
|
const rounds = 7;
|
|
const hashes = Array.from({ length: tokenCount }, (_, index) => `hash-${index}`);
|
|
const timestamps = Array.from({ length: 30_000 }, (_, index) => new Date(1_790_726_400_000 + index).toISOString());
|
|
|
|
async function measure(store: SqliteRuntimeTokenStore, iterations: number): Promise<number> {
|
|
const start = performance.now();
|
|
for (let index = 0; index < iterations; index++) {
|
|
const token = await store.findByHash(hashes[index % tokenCount]);
|
|
if (!token) throw new Error("Benchmark token is missing.");
|
|
await store.markUsed(token.id, timestamps[index]);
|
|
}
|
|
return performance.now() - start;
|
|
}
|
|
|
|
function median(values: number[]): number {
|
|
return [...values].sort((left, right) => left - right)[Math.floor(values.length / 2)];
|
|
}
|
|
|
|
async function benchmark(filename: string, iterations: number, schema?: string): Promise<void> {
|
|
const database = new DatabaseSync(filename);
|
|
try {
|
|
if (schema) database.exec(schema);
|
|
// Match the runtime's WAL setting; in-memory SQLite reports "memory" instead.
|
|
database.exec("pragma journal_mode = wal;");
|
|
const baseline = new PreparePerCallTokenStore(database);
|
|
const reused = new SqliteRuntimeTokenStore(database);
|
|
for (let index = 0; index < tokenCount; index++) {
|
|
await reused.add({
|
|
id: `token-${index}`,
|
|
name: `Token ${index}`,
|
|
tokenHash: hashes[index],
|
|
allowedActions: ["github.*"],
|
|
blockedActions: ["github.delete_repository"],
|
|
allowedProxies: ["github"],
|
|
allowedConnections: ["example:work"],
|
|
createdAt: timestamps[0],
|
|
});
|
|
}
|
|
await measure(baseline, 500);
|
|
await measure(reused, 500);
|
|
const baselineMs: number[] = [];
|
|
const reusedMs: number[] = [];
|
|
for (let round = 0; round < rounds; round++) {
|
|
if (round % 2 === 0) {
|
|
baselineMs.push(await measure(baseline, iterations));
|
|
reusedMs.push(await measure(reused, iterations));
|
|
} else {
|
|
reusedMs.push(await measure(reused, iterations));
|
|
baselineMs.push(await measure(baseline, iterations));
|
|
}
|
|
}
|
|
const before = median(baselineMs);
|
|
const after = median(reusedMs);
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
storage: filename === ":memory:" ? "memory" : "file",
|
|
node: process.version,
|
|
sqlite: database.prepare("select sqlite_version() as version").get()?.version,
|
|
journalMode: database.prepare("pragma journal_mode").get()?.journal_mode,
|
|
synchronous: database.prepare("pragma synchronous").get()?.synchronous,
|
|
tokenCount,
|
|
iterations,
|
|
rounds,
|
|
baselineMs,
|
|
reusedMs,
|
|
baselineMedianMs: before,
|
|
reusedMedianMs: after,
|
|
baselineMicrosecondsPerPair: (before * 1000) / iterations,
|
|
reusedMicrosecondsPerPair: (after * 1000) / iterations,
|
|
speedup: before / after,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
} finally {
|
|
database.close();
|
|
}
|
|
}
|
|
|
|
const directory = await mkdtemp(join(tmpdir(), "sqlite-token-benchmark-"));
|
|
try {
|
|
const filename = join(directory, "runtime.sqlite");
|
|
// Apply actual runtime migrations once, outside the measured loops.
|
|
const runtime = new SqliteRuntimeDatabase(filename);
|
|
runtime.close();
|
|
// Copy the migrated empty schema so both modes use the same table and indexes.
|
|
const source = new DatabaseSync(filename);
|
|
const schema = source
|
|
.prepare("select sql from sqlite_master where sql is not null and name not like 'sqlite_%'")
|
|
.all()
|
|
.map((row) => String(row.sql))
|
|
.join(";\n");
|
|
source.close();
|
|
// An in-memory database needs its schema on the same connection as the benchmark.
|
|
await benchmark(":memory:", 30_000, schema);
|
|
await benchmark(filename, 1000);
|
|
} finally {
|
|
await rm(directory, { recursive: true, force: true });
|
|
}
|