fix: distribute metadata index keys across sub-prefixes to avoid cloud rate limits
All metadata index files were stored under a single _system/ prefix, causing per-prefix rate limiting on GCS/S3/R2/Azure during cold starts and bulk imports. Distributes high-volume system keys across 256 sub-prefixes using FNV-1a hash. Backward-compatible with legacy path fallback.
This commit is contained in:
parent
66d7aa736c
commit
23e1c56ae0
1 changed files with 71 additions and 7 deletions
|
|
@ -79,6 +79,39 @@ export const VERBS_METADATA_DIR = 'entities/verbs/metadata'
|
||||||
export const SYSTEM_DIR = '_system'
|
export const SYSTEM_DIR = '_system'
|
||||||
export const STATISTICS_KEY = 'statistics'
|
export const STATISTICS_KEY = 'statistics'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FNV-1a hash returning a 2-char hex bucket (00-ff).
|
||||||
|
* Distributes system keys across 256 sub-prefixes to avoid
|
||||||
|
* cloud storage per-prefix rate limits.
|
||||||
|
*/
|
||||||
|
function systemKeyBucket(key: string): string {
|
||||||
|
let hash = 2166136261
|
||||||
|
for (let i = 0; i < key.length; i++) {
|
||||||
|
hash ^= key.charCodeAt(i)
|
||||||
|
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24)
|
||||||
|
}
|
||||||
|
return ((hash >>> 0) & 0xff).toString(16).padStart(2, '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
const SINGLETON_SYSTEM_KEYS = new Set([
|
||||||
|
'__metadata_field_registry__',
|
||||||
|
'brainy:entityIdMapper',
|
||||||
|
'statistics',
|
||||||
|
'counts',
|
||||||
|
'hnsw-system',
|
||||||
|
'type-statistics',
|
||||||
|
])
|
||||||
|
|
||||||
|
const SINGLETON_SYSTEM_PREFIXES = [
|
||||||
|
'statistics_',
|
||||||
|
'distributed_',
|
||||||
|
]
|
||||||
|
|
||||||
|
function isSingletonSystemKey(key: string): boolean {
|
||||||
|
if (SINGLETON_SYSTEM_KEYS.has(key)) return true
|
||||||
|
return SINGLETON_SYSTEM_PREFIXES.some(p => key.startsWith(p))
|
||||||
|
}
|
||||||
|
|
||||||
// DEPRECATED: Temporary stubs for adapters not yet migrated
|
// DEPRECATED: Temporary stubs for adapters not yet migrated
|
||||||
// TODO: Remove after migrating remaining adapters
|
// TODO: Remove after migrating remaining adapters
|
||||||
export const NOUNS_DIR = 'entities/nouns/hnsw'
|
export const NOUNS_DIR = 'entities/nouns/hnsw'
|
||||||
|
|
@ -208,6 +241,7 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
||||||
id.startsWith('__sparse_index__') // Metadata sparse indices (zone maps + bloom filters)
|
id.startsWith('__sparse_index__') // Metadata sparse indices (zone maps + bloom filters)
|
||||||
|
|
||||||
if (isSystemKey) {
|
if (isSystemKey) {
|
||||||
|
if (isSingletonSystemKey(id)) {
|
||||||
return {
|
return {
|
||||||
original: id,
|
original: id,
|
||||||
isEntity: false,
|
isEntity: false,
|
||||||
|
|
@ -216,11 +250,21 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
||||||
fullPath: `${SYSTEM_DIR}/${id}.json`
|
fullPath: `${SYSTEM_DIR}/${id}.json`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const bucket = systemKeyBucket(id)
|
||||||
|
return {
|
||||||
|
original: id,
|
||||||
|
isEntity: false,
|
||||||
|
shardId: bucket,
|
||||||
|
directory: `${SYSTEM_DIR}/idx/${bucket}`,
|
||||||
|
fullPath: `${SYSTEM_DIR}/idx/${bucket}/${id}.json`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// UUID validation for entity keys
|
// UUID validation for entity keys
|
||||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
||||||
if (!uuidRegex.test(id)) {
|
if (!uuidRegex.test(id)) {
|
||||||
prodLog.warn(`[Storage] Unknown key format: ${id} - treating as system resource`)
|
prodLog.warn(`[Storage] Unknown key format: ${id} - treating as system resource`)
|
||||||
|
if (isSingletonSystemKey(id)) {
|
||||||
return {
|
return {
|
||||||
original: id,
|
original: id,
|
||||||
isEntity: false,
|
isEntity: false,
|
||||||
|
|
@ -229,6 +273,15 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
||||||
fullPath: `${SYSTEM_DIR}/${id}.json`
|
fullPath: `${SYSTEM_DIR}/${id}.json`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const bucket = systemKeyBucket(id)
|
||||||
|
return {
|
||||||
|
original: id,
|
||||||
|
isEntity: false,
|
||||||
|
shardId: bucket,
|
||||||
|
directory: `${SYSTEM_DIR}/idx/${bucket}`,
|
||||||
|
fullPath: `${SYSTEM_DIR}/idx/${bucket}/${id}.json`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Valid entity UUID - apply sharding
|
// Valid entity UUID - apply sharding
|
||||||
const shardId = getShardIdFromUuid(id)
|
const shardId = getShardIdFromUuid(id)
|
||||||
|
|
@ -1965,7 +2018,18 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
||||||
public async getMetadata(id: string): Promise<NounMetadata | null> {
|
public async getMetadata(id: string): Promise<NounMetadata | null> {
|
||||||
await this.ensureInitialized()
|
await this.ensureInitialized()
|
||||||
const keyInfo = this.analyzeKey(id, 'system')
|
const keyInfo = this.analyzeKey(id, 'system')
|
||||||
return this.readWithInheritance(keyInfo.fullPath)
|
|
||||||
|
// Try new distributed path first
|
||||||
|
const data = await this.readWithInheritance(keyInfo.fullPath)
|
||||||
|
if (data !== null) return data
|
||||||
|
|
||||||
|
// Backward compat: if key was distributed, fall back to legacy flat path
|
||||||
|
if (keyInfo.shardId !== null) {
|
||||||
|
const legacyPath = `${SYSTEM_DIR}/${id}.json`
|
||||||
|
return this.readWithInheritance(legacyPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue