brainy/src/utils/rebuildCounts.ts

155 lines
4.5 KiB
TypeScript
Raw Normal View History

fix(storage): resolve count synchronization race condition across all storage adapters Fixed critical bug where entity and relationship counts were not being tracked correctly during add(), relate(), and import() operations. The root cause was a race condition where count increment code tried to read metadata before it was saved to storage. Core Fixes: - Modified baseStorage.saveNounMetadata_internal to increment counts AFTER metadata is saved - Modified baseStorage.saveVerbMetadata_internal to increment verb counts AFTER metadata is saved - Added verb type to VerbMetadata to avoid circular dependency during count tracking - Refactored verb count methods to prevent mutex deadlocks (synchronous base + async Safe wrapper) Storage Adapter Cleanup: - Removed broken count increment code from FileSystemStorage, GcsStorage, R2Storage, AzureBlobStorage - Updated MemoryStorage comments to reflect centralized fix - All count tracking now centralized in baseStorage (fixes ALL adapters automatically) New Utilities: - Added rebuildCounts utility to repair corrupted counts.json from actual storage data - Added comprehensive integration tests for count synchronization across all operations Verification: - All 8 storage adapters verified (FileSystem, GCS, Memory, S3Compatible, R2, Azure, OPFS, TypeAware) - All code paths verified (add, relate, import, batch, update, delete) - 599 tests passing (no regressions) - No deadlocks (tests complete in 6s vs 150s+) Fixes #1 and #2 reported by Workshop team
2025-10-21 10:58:44 -07:00
/**
* Rebuild Counts Utility
*
* Scans storage and rebuilds counts.json from actual data
* Use this to fix databases affected by the v4.1.1 count synchronization bug
*
* NO MOCKS - Production-ready implementation
*/
import type { BaseStorage } from '../storage/baseStorage.js'
export interface RebuildCountsResult {
/** Total number of entities (nouns) found */
nounCount: number
/** Total number of relationships (verbs) found */
verbCount: number
/** Entity counts by type */
entityCounts: Map<string, number>
/** Verb counts by type */
verbCounts: Map<string, number>
/** Processing time in milliseconds */
duration: number
}
/**
* Rebuild counts.json from actual storage data
*
* This scans all entities and relationships in storage and reconstructs
* the counts index from scratch. Use this to fix count desynchronization.
*
* @param storage - The storage adapter to rebuild counts for
* @returns Promise that resolves to rebuild statistics
*
* @example
* ```typescript
* const brain = new Brainy({ storage: { type: 'filesystem', path: './brainy-data' } })
* await brain.init()
*
* const result = await rebuildCounts(brain.storage)
* console.log(`Rebuilt counts: ${result.nounCount} nouns, ${result.verbCount} verbs`)
* ```
*/
export async function rebuildCounts(storage: BaseStorage): Promise<RebuildCountsResult> {
const startTime = Date.now()
console.log('🔧 Rebuilding counts from storage...')
const entityCounts = new Map<string, number>()
const verbCounts = new Map<string, number>()
let totalNouns = 0
let totalVerbs = 0
// Scan all nouns using pagination
console.log('📊 Scanning entities...')
// Check if pagination method exists
const storageWithPagination = storage as any
if (typeof storageWithPagination.getNounsWithPagination !== 'function') {
throw new Error('Storage adapter does not support getNounsWithPagination')
}
let hasMore = true
let cursor: string | undefined
while (hasMore) {
const result: any = await storageWithPagination.getNounsWithPagination({ limit: 100, cursor })
for (const noun of result.items) {
const metadata = await storage.getNounMetadata(noun.id)
if (metadata?.noun) {
const entityType = metadata.noun
entityCounts.set(entityType, (entityCounts.get(entityType) || 0) + 1)
totalNouns++
}
}
hasMore = result.hasMore
cursor = result.nextCursor
}
console.log(` Found ${totalNouns} entities across ${entityCounts.size} types`)
// Scan all verbs using pagination
console.log('🔗 Scanning relationships...')
if (typeof storageWithPagination.getVerbsWithPagination !== 'function') {
throw new Error('Storage adapter does not support getVerbsWithPagination')
}
hasMore = true
cursor = undefined
while (hasMore) {
const result: any = await storageWithPagination.getVerbsWithPagination({ limit: 100, cursor })
for (const verb of result.items) {
if (verb.verb) {
const verbType = verb.verb
verbCounts.set(verbType, (verbCounts.get(verbType) || 0) + 1)
totalVerbs++
}
}
hasMore = result.hasMore
cursor = result.nextCursor
}
console.log(` Found ${totalVerbs} relationships across ${verbCounts.size} types`)
// Update storage adapter's in-memory counts FIRST
storageWithPagination.totalNounCount = totalNouns
storageWithPagination.totalVerbCount = totalVerbs
storageWithPagination.entityCounts = entityCounts
storageWithPagination.verbCounts = verbCounts
// Mark counts as pending persist (required for flushCounts to actually persist)
storageWithPagination.pendingCountPersist = true
storageWithPagination.pendingCountOperations = 1
// Persist counts using storage adapter's own persist method
// This ensures counts.json is written correctly (compressed or uncompressed)
await storageWithPagination.flushCounts()
const duration = Date.now() - startTime
console.log(`✅ Counts rebuilt successfully in ${duration}ms`)
console.log(` Entities: ${totalNouns}`)
console.log(` Relationships: ${totalVerbs}`)
console.log('')
console.log('Entity breakdown:')
entityCounts.forEach((count, entityType) => {
console.log(` ${entityType}: ${count}`)
})
if (verbCounts.size > 0) {
console.log('')
console.log('Relationship breakdown:')
verbCounts.forEach((count, verbType) => {
console.log(` ${verbType}: ${count}`)
})
}
return {
nounCount: totalNouns,
verbCount: totalVerbs,
entityCounts,
verbCounts,
duration
}
}