feat(metadata): share table metadata cache
This commit is contained in:
1 parent
81f787e8d1
commit
3da3849953
5 files changed
+269
-39
No files matched your search
@@ -2,8 +2,8 @@ import type { ColumnInfo, DatabaseType, IndexInfo, QueryTab } from "@/types/data
|
||||
import * as api from "@/lib/backend/api";
|
||||
import { editableRowIdentifierColumns } from "@/lib/table/tableEditing";
|
||||
import { createMetadataLoadTrace, logMetadataLoadTrace, MetadataLoadCoordinator, type MetadataLoadCacheStatus, type MetadataLoadTraceLogger } from "./metadataLoadCoordinator";
|
||||
import type { MetadataScopeInput } from "./metadataLoadScope";
|
||||
import { MetadataResultCache, type MetadataCacheInvalidation } from "./metadataResultCache";
|
||||
import { metadataScopeKey, metadataScopeParts, type MetadataScopeInput } from "./metadataLoadScope";
|
||||
import { metadataCacheInvalidationMatcher, MetadataResultCache, type MetadataCacheInvalidation } from "./metadataResultCache";
|
||||
|
||||
export const TABLE_METADATA_CACHE_TTL_MS = 30_000;
|
||||
const TABLE_METADATA_CACHE_MAX_ENTRIES = 120;
|
||||
@@ -48,6 +48,41 @@ const tableMetadataCoordinator = new MetadataLoadCoordinator((event) => {
|
||||
console.debug("[DBX][metadata-load:table-coordinator]", event);
|
||||
});
|
||||
|
||||
// 失效代数(按 scope key 隔离):跨越失效边界的旧加载完成后不得写缓存——
|
||||
// 结构变更后 force 拉到的新值可能被保存前启动、最后返回的在途加载回填覆盖。
|
||||
// 只登记在途 scope 并只对匹配失效条件的 key 递增代数,避免失效表 A 时
|
||||
// 波及无关表 B 的在途去重与缓存写入
|
||||
interface InFlightTableMetadataScope {
|
||||
parts: ReturnType<typeof metadataScopeParts>;
|
||||
count: number;
|
||||
}
|
||||
const inFlightTableMetadataScopes = new Map<string, InFlightTableMetadataScope>();
|
||||
const tableMetadataInvalidationStamps = new Map<string, number>();
|
||||
|
||||
function registerInFlightTableMetadataScope(scopeKey: string, scope: MetadataScopeInput): void {
|
||||
const entry = inFlightTableMetadataScopes.get(scopeKey);
|
||||
if (entry) {
|
||||
entry.count++;
|
||||
} else {
|
||||
inFlightTableMetadataScopes.set(scopeKey, { parts: metadataScopeParts(scope), count: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
function unregisterInFlightTableMetadataScope(scopeKey: string): void {
|
||||
const entry = inFlightTableMetadataScopes.get(scopeKey);
|
||||
if (!entry) return;
|
||||
entry.count--;
|
||||
if (entry.count > 0) return;
|
||||
inFlightTableMetadataScopes.delete(scopeKey);
|
||||
// 代数只在加载的 start→end 窗口内比较,且失效只 bump 在途 key;
|
||||
// 无在途加载时清掉代数,两张 Map 都随在途集合有界
|
||||
tableMetadataInvalidationStamps.delete(scopeKey);
|
||||
}
|
||||
|
||||
function bumpTableMetadataInvalidationStamp(scopeKey: string): void {
|
||||
tableMetadataInvalidationStamps.set(scopeKey, (tableMetadataInvalidationStamps.get(scopeKey) ?? 0) + 1);
|
||||
}
|
||||
|
||||
export function tableMetadataScope(request: Pick<TableMetadataRequest, "connectionId" | "database" | "schema" | "tableName" | "tableType" | "driverProfile" | "databaseType" | "catalog">): MetadataScopeInput {
|
||||
return {
|
||||
kind: "table-metadata",
|
||||
@@ -95,28 +130,39 @@ export async function loadTableMetadata(request: TableMetadataRequest): Promise<
|
||||
}
|
||||
|
||||
logMetadataLoadTrace(request.traceLogger, trace, "cache-miss", { cacheStatus: request.force ? "refresh" : "miss", force: request.force === true });
|
||||
const metadata = await tableMetadataCoordinator.run(
|
||||
scope,
|
||||
async () => {
|
||||
const columns = await api.getColumns(request.connectionId, request.database, request.schema ?? "", request.tableName, request.catalog);
|
||||
const indexes = await api.listIndexes(request.connectionId, request.database, request.schema ?? "", request.tableName, request.catalog).catch((): IndexInfo[] => []);
|
||||
const primaryKeys = editableRowIdentifierColumns(request.databaseType as DatabaseType, columns, indexes, request.tableType);
|
||||
return {
|
||||
schema: request.schema || undefined,
|
||||
tableName: request.tableName,
|
||||
tableType: request.tableType,
|
||||
catalog: request.catalog,
|
||||
database: request.database,
|
||||
columns,
|
||||
indexes,
|
||||
primaryKeys,
|
||||
cachedAt: Date.now(),
|
||||
};
|
||||
},
|
||||
{ force: request.force, kind: scope.kind },
|
||||
);
|
||||
const scopeKey = metadataScopeKey(scope);
|
||||
const invalidationStampAtStart = tableMetadataInvalidationStamps.get(scopeKey) ?? 0;
|
||||
registerInFlightTableMetadataScope(scopeKey, scope);
|
||||
let metadata: TableMetadata;
|
||||
try {
|
||||
metadata = await tableMetadataCoordinator.run(
|
||||
scope,
|
||||
async () => {
|
||||
const columns = await api.getColumns(request.connectionId, request.database, request.schema ?? "", request.tableName, request.catalog);
|
||||
const indexes = await api.listIndexes(request.connectionId, request.database, request.schema ?? "", request.tableName, request.catalog).catch((): IndexInfo[] => []);
|
||||
const primaryKeys = editableRowIdentifierColumns(request.databaseType as DatabaseType, columns, indexes, request.tableType);
|
||||
return {
|
||||
schema: request.schema || undefined,
|
||||
tableName: request.tableName,
|
||||
tableType: request.tableType,
|
||||
catalog: request.catalog,
|
||||
database: request.database,
|
||||
columns,
|
||||
indexes,
|
||||
primaryKeys,
|
||||
cachedAt: Date.now(),
|
||||
};
|
||||
},
|
||||
{ force: request.force, kind: scope.kind },
|
||||
);
|
||||
|
||||
tableMetadataCache.set(scope, metadata);
|
||||
// 必须在 unregister 前比较:最后一个在途加载注销时会顺带清掉代数记录
|
||||
if (invalidationStampAtStart === (tableMetadataInvalidationStamps.get(scopeKey) ?? 0)) {
|
||||
tableMetadataCache.set(scope, metadata);
|
||||
}
|
||||
} finally {
|
||||
unregisterInFlightTableMetadataScope(scopeKey);
|
||||
}
|
||||
logMetadataLoadTrace(request.traceLogger, trace, "done", {
|
||||
cacheStatus: request.force ? "refresh" : "miss",
|
||||
resultCount: metadata.columns.length,
|
||||
@@ -126,9 +172,23 @@ export async function loadTableMetadata(request: TableMetadataRequest): Promise<
|
||||
}
|
||||
|
||||
export function invalidateTableMetadataCache(match: MetadataCacheInvalidation): number {
|
||||
// 只处理匹配失效条件的在途 scope,不波及其他表/连接的在途去重与缓存写入:
|
||||
// 1) bump 该 scope 的失效代数——跨边界的旧加载完成后不得写缓存;
|
||||
// 2) 甩掉该 scope 的在途登记——否则失效后启动的 non-force 调用会加入
|
||||
// 失效前的旧加载,且其失效代数取自失效之后,完成时把旧结果写回缓存
|
||||
const matches = metadataCacheInvalidationMatcher(match);
|
||||
for (const [scopeKey, entry] of inFlightTableMetadataScopes) {
|
||||
if (!matches(entry.parts)) continue;
|
||||
bumpTableMetadataInvalidationStamp(scopeKey);
|
||||
tableMetadataCoordinator.clear(scopeKey);
|
||||
}
|
||||
return tableMetadataCache.invalidate(match);
|
||||
}
|
||||
|
||||
export function clearTableMetadataCache(): void {
|
||||
for (const scopeKey of inFlightTableMetadataScopes.keys()) {
|
||||
bumpTableMetadataInvalidationStamp(scopeKey);
|
||||
}
|
||||
tableMetadataCoordinator.clear();
|
||||
tableMetadataCache.clear();
|
||||
}
|
||||
Reference in new issue
Block a user