mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
fix(insight): invalidate snapshots on session deletion
Remove persisted annual summary snapshots whenever a session is deleted through shared or desktop cleanup paths. Drop cached in-memory snapshots when their backing files disappear so stale responses cannot expose deleted session data.
This commit is contained in:
1 parent
b15f38c4a5
commit
e1288c3cc7
7 files changed
+59
-2
No files matched your search
@@ -176,6 +176,9 @@ export function deleteSession(sessionId: string): boolean {
|
||||
sessionId,
|
||||
globalInsightService.getGlobalInsightFactsCacheDir(getPathProvider().getUserDataDir())
|
||||
)
|
||||
globalInsightService.deleteAnnualSummarySnapshots(
|
||||
globalInsightService.getGlobalInsightDir(getPathProvider().getUserDataDir())
|
||||
)
|
||||
deleteSessionCache(
|
||||
sessionId,
|
||||
peopleRelationshipsService.getPeopleRelationshipsFactsCacheDir(getPathProvider().getUserDataDir())
|
||||
|
||||
@@ -28,7 +28,8 @@ import {
|
||||
} from './migrations'
|
||||
import { tokenizeForFts } from './nlp/fts-tokenizer'
|
||||
import { getContactsFactsCacheDir } from './services/contacts/paths'
|
||||
import { getGlobalInsightFactsCacheDir } from './services/global-insight/paths'
|
||||
import { getGlobalInsightDir, getGlobalInsightFactsCacheDir } from './services/global-insight/paths'
|
||||
import { deleteAnnualSummarySnapshots } from './services/global-insight/snapshot'
|
||||
import { getPeopleRelationshipsFactsCacheDir } from './services/people/relationships/paths'
|
||||
|
||||
function createMigrationDeps(overrides?: MigrationDeps): MigrationDeps {
|
||||
@@ -251,6 +252,7 @@ export class DatabaseManager {
|
||||
deleteSessionCache(sessionId, path.join(cacheDir, 'query'))
|
||||
deleteSessionCache(sessionId, getContactsFactsCacheDir(this.pathProvider.getUserDataDir()))
|
||||
deleteSessionCache(sessionId, getGlobalInsightFactsCacheDir(this.pathProvider.getUserDataDir()))
|
||||
deleteAnnualSummarySnapshots(getGlobalInsightDir(this.pathProvider.getUserDataDir()))
|
||||
deleteSessionCache(sessionId, getPeopleRelationshipsFactsCacheDir(this.pathProvider.getUserDataDir()))
|
||||
return existed
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { PathProvider } from '@openchatlab/core'
|
||||
import { DatabaseManager } from '../../database-manager'
|
||||
import { createDatabaseManagerAdapter } from './database-manager-adapter'
|
||||
import { getContactsFactsCacheDir } from '../contacts/paths'
|
||||
import { getGlobalInsightFactsCacheDir } from '../global-insight/paths'
|
||||
import { getGlobalInsightDir, getGlobalInsightFactsCacheDir } from '../global-insight/paths'
|
||||
|
||||
function makeTempDir(): string {
|
||||
const baseDir = fs.existsSync('/private/tmp') ? '/private/tmp' : os.tmpdir()
|
||||
@@ -52,14 +52,25 @@ test('DatabaseManager adapter deletes session database sidecar files and caches'
|
||||
getGlobalInsightFactsCacheDir(pathProvider.getUserDataDir()),
|
||||
`${sessionId}.cache.json`
|
||||
)
|
||||
const annualSummaryYearSnapshotPath = path.join(
|
||||
getGlobalInsightDir(pathProvider.getUserDataDir()),
|
||||
'annual-summary-year-2026.json'
|
||||
)
|
||||
const annualSummaryRecentSnapshotPath = path.join(
|
||||
getGlobalInsightDir(pathProvider.getUserDataDir()),
|
||||
'annual-summary-recent-365.json'
|
||||
)
|
||||
fs.mkdirSync(path.dirname(cachePath), { recursive: true })
|
||||
fs.mkdirSync(path.dirname(queryCachePath), { recursive: true })
|
||||
fs.mkdirSync(path.dirname(contactsFactsCachePath), { recursive: true })
|
||||
fs.mkdirSync(path.dirname(globalInsightFactsCachePath), { recursive: true })
|
||||
fs.mkdirSync(path.dirname(annualSummaryYearSnapshotPath), { recursive: true })
|
||||
fs.writeFileSync(cachePath, '{}', 'utf-8')
|
||||
fs.writeFileSync(queryCachePath, '{}', 'utf-8')
|
||||
fs.writeFileSync(contactsFactsCachePath, '{}', 'utf-8')
|
||||
fs.writeFileSync(globalInsightFactsCachePath, '{}', 'utf-8')
|
||||
fs.writeFileSync(annualSummaryYearSnapshotPath, '{}', 'utf-8')
|
||||
fs.writeFileSync(annualSummaryRecentSnapshotPath, '{}', 'utf-8')
|
||||
|
||||
assert.equal(adapter.deleteSessionFile(sessionId), true)
|
||||
assert.equal(fs.existsSync(dbPath), false)
|
||||
@@ -69,4 +80,6 @@ test('DatabaseManager adapter deletes session database sidecar files and caches'
|
||||
assert.equal(fs.existsSync(queryCachePath), false)
|
||||
assert.equal(fs.existsSync(contactsFactsCachePath), false)
|
||||
assert.equal(fs.existsSync(globalInsightFactsCachePath), false)
|
||||
assert.equal(fs.existsSync(annualSummaryYearSnapshotPath), false)
|
||||
assert.equal(fs.existsSync(annualSummaryRecentSnapshotPath), false)
|
||||
})
|
||||
@@ -3,3 +3,4 @@ export type { GlobalInsightService, GlobalInsightServiceDeps, GlobalInsightServi
|
||||
export { ANNUAL_SUMMARY_ALGORITHM_VERSION } from './types'
|
||||
export type { AnnualSummaryComputeRunner, AnnualSummaryRunnerOptions } from './worker-runner'
|
||||
export { getGlobalInsightDir, getGlobalInsightFactsCacheDir } from './paths'
|
||||
export { deleteAnnualSummarySnapshots } from './snapshot'
|
||||
@@ -6,6 +6,7 @@ import test from 'node:test'
|
||||
import type { AnnualSummaryRange } from '@openchatlab/shared-types'
|
||||
import type { SessionRuntimeAdapter } from '../adapters'
|
||||
import { createGlobalInsightService } from './service'
|
||||
import { getAnnualSummarySnapshotPath, writeAnnualSummarySnapshot } from './snapshot'
|
||||
import type { AnnualSummarySnapshot } from './types'
|
||||
import type { AnnualSummaryComputeRunner } from './worker-runner'
|
||||
|
||||
@@ -101,6 +102,25 @@ test('serves stale data while recomputing after the DB signature changes', async
|
||||
resolveRunner?.(makeSnapshot(range, stale.cache.signature ?? '', 9))
|
||||
})
|
||||
|
||||
test('drops an in-memory stale snapshot after its persisted file is deleted', async (t) => {
|
||||
const env = createEnv(async ({ signature, range }) => makeSnapshot(range, signature, 9))
|
||||
t.after(async () => {
|
||||
await env.service.close()
|
||||
fs.rmSync(env.dir, { recursive: true, force: true })
|
||||
})
|
||||
const range = env.service.normalizeRange({ mode: 'year', year: 2026 })
|
||||
const snapshotDir = path.join(env.dir, 'insight', 'annual-summary')
|
||||
const staleSnapshot = makeSnapshot(range, 'old-signature')
|
||||
writeAnnualSummarySnapshot(snapshotDir, staleSnapshot)
|
||||
env.service.replaceSnapshotForTests(staleSnapshot)
|
||||
fs.rmSync(getAnnualSummarySnapshotPath(snapshotDir, range))
|
||||
|
||||
const response = env.service.getAnnualSummary({ mode: 'year', year: 2026, acceptStale: true })
|
||||
|
||||
assert.equal(response.cache.status, 'missing')
|
||||
assert.equal(response.metrics, null)
|
||||
})
|
||||
|
||||
test('preserves failed state until explicit recompute', async (t) => {
|
||||
let calls = 0
|
||||
const env = createEnv(async ({ signature, range }) => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import fs from 'node:fs'
|
||||
import type { PathProvider } from '@openchatlab/core'
|
||||
import type { AnnualSummaryRange, AnnualSummaryResponse, AnnualSummaryTaskState } from '@openchatlab/shared-types'
|
||||
import type { RuntimeIdentity } from '../../data-dir-compat'
|
||||
@@ -7,6 +8,7 @@ import { getGlobalInsightDir } from './paths'
|
||||
import { buildAnnualSummarySignature } from './signature'
|
||||
import {
|
||||
cleanupAnnualSummarySnapshotTempFiles,
|
||||
getAnnualSummarySnapshotPath,
|
||||
readAnnualSummarySnapshot,
|
||||
writeAnnualSummarySnapshot,
|
||||
} from './snapshot'
|
||||
@@ -96,6 +98,7 @@ class DefaultGlobalInsightService implements GlobalInsightService {
|
||||
}
|
||||
|
||||
replaceSnapshotForTests(snapshot: AnnualSummarySnapshot): void {
|
||||
writeAnnualSummarySnapshot(this.snapshotDir, snapshot)
|
||||
this.snapshots.set(toAnnualSummaryRangeKey(snapshot.range), snapshot)
|
||||
}
|
||||
|
||||
@@ -224,6 +227,9 @@ class DefaultGlobalInsightService implements GlobalInsightService {
|
||||
|
||||
private getSnapshot(range: AnnualSummaryRange): AnnualSummarySnapshot | null {
|
||||
const key = toAnnualSummaryRangeKey(range)
|
||||
if (this.snapshots.has(key) && !fs.existsSync(getAnnualSummarySnapshotPath(this.snapshotDir, range))) {
|
||||
this.snapshots.delete(key)
|
||||
}
|
||||
if (!this.snapshots.has(key)) {
|
||||
this.snapshots.set(key, readAnnualSummarySnapshot(this.snapshotDir, range, { now: this.deps.now }))
|
||||
}
|
||||
|
||||
@@ -50,3 +50,15 @@ export function cleanupAnnualSummarySnapshotTempFiles(snapshotDir: string): void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteAnnualSummarySnapshots(snapshotDir: string): void {
|
||||
if (!fs.existsSync(snapshotDir)) return
|
||||
for (const name of fs.readdirSync(snapshotDir)) {
|
||||
if (!name.startsWith('annual-summary-') && !name.startsWith('annual-summary.')) continue
|
||||
try {
|
||||
fs.rmSync(path.join(snapshotDir, name), { force: true })
|
||||
} catch (error) {
|
||||
appLogger.warn('global-insight', 'failed to remove annual summary snapshot', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user