This commit completes the core v4.0.0 architecture changes for billion-scale performance with metadata/vector separation. NO RELEASE YET - remaining optimizations and testing required before production release. ## Core v4.0.0 Architecture Changes ### Type System Updates - Fixed all TypeScript compilation errors (zero errors achieved) - Updated HNSWNoun/HNSWVerb to separate core fields from metadata - Implemented HNSWNounWithMetadata/HNSWVerbWithMetadata for API boundaries - Added required 'noun' field to NounMetadata for semantic structure - Renamed verb.type to verb.verb for consistency ### Storage Adapter Updates **All adapters updated for v4.0.0 two-file storage pattern:** - memoryStorage: Proper metadata/vector separation - fileSystemStorage: Two-file pattern with sharding - opfsStorage: Browser persistent storage updated - s3CompatibleStorage: AWS/MinIO/DigitalOcean support - r2Storage: Cloudflare R2 optimization - gcsStorage: Google Cloud with ADC support - **azureBlobStorage: NEW - Full Azure Blob Storage support** ### Storage Features - BaseStorage: Internal vs public method separation (_getNoun vs getNoun) - Two-file storage: Vectors in one file, metadata in another - Change tracking: getChangesSince return type updated - Pagination: getNounsWithPagination returns WithMetadata types ### Azure Blob Storage Integration (NEW) - Native @azure/storage-blob SDK integration - Four authentication methods: * DefaultAzureCredential (Managed Identity) - recommended * Connection String - simplest setup * Account Name + Key - traditional auth * SAS Token - delegated access - High-volume mode with write buffering - Adaptive backpressure for throttling - UUID-based sharding for billion-scale - Full HNSW support with graph persistence ### Utility Updates - EmbeddingManager: Updated to accept Record<string, unknown> - LSMTree: Wrapped data in NounMetadata structure with 'noun' field - EntityIdMapper: Fixed nested metadata.data structure access - MetadataIndex: Fixed field type inference integration - PeriodicCleanup: Updated for new metadata structure ### Core API Updates - Brainy: Updated verb property access from v.type to v.verb - ConfigAPI: Fixed NounMetadata access patterns - DataAPI: Updated metadata handling ### Documentation Updates - CREATING-AUGMENTATIONS.md: v4.0.0 breaking changes guide - DEVELOPER-GUIDE.md: Migration checklist and examples - COMPLETE-REFERENCE.md: v4.0.0 architecture improvements - **finite-type-system.md: NEW - Revolutionary type system benefits** ### Build & Dependencies - Zero TypeScript compilation errors - Added @azure/storage-blob and @azure/identity - 591 tests passing (23 timeout in long-running neural tests) ## What's NOT in This Release This is a work-in-progress commit. Before v4.0.0 release we need: - Storage adapter optimizations (batch operations, compression) - Azure blob tier management (Hot/Cool/Archive) - Cost optimization implementations - Additional performance testing at billion-scale - Migration guides for v3.x users ## Testing - Clean build: ✅ - Type checking: ✅ (zero errors) - Test suite: ✅ (591/614 passing, timeouts in neural tests only) 🔐 Generated with Claude Code https://claude.com/claude-code Co-Authored-By: Claude <noreply@anthropic.com>
398 lines
No EOL
11 KiB
TypeScript
398 lines
No EOL
11 KiB
TypeScript
/**
|
|
* Shard Migration System for Brainy 3.0
|
|
*
|
|
* Handles zero-downtime migration of data between nodes
|
|
* Uses streaming for efficient transfer of large datasets
|
|
*/
|
|
|
|
import { EventEmitter } from 'node:events'
|
|
import type { StorageAdapter } from '../coreTypes.js'
|
|
import type { ShardManager } from './shardManager.js'
|
|
import type { HTTPTransport } from './httpTransport.js'
|
|
import type { DistributedCoordinator } from './coordinator.js'
|
|
|
|
export interface MigrationTask {
|
|
id: string
|
|
shardId: string
|
|
sourceNode: string
|
|
targetNode: string
|
|
status: 'pending' | 'transferring' | 'validating' | 'switching' | 'completed' | 'failed'
|
|
progress: number // 0-100
|
|
itemsTransferred: number
|
|
totalItems: number
|
|
startTime: number
|
|
endTime?: number
|
|
error?: string
|
|
}
|
|
|
|
export interface MigrationOptions {
|
|
batchSize?: number
|
|
validateData?: boolean
|
|
maxRetries?: number
|
|
timeout?: number
|
|
}
|
|
|
|
export class ShardMigrationManager extends EventEmitter {
|
|
private storage: StorageAdapter
|
|
private shardManager: ShardManager
|
|
private transport: HTTPTransport
|
|
private coordinator: DistributedCoordinator
|
|
private nodeId: string
|
|
|
|
private activeMigrations = new Map<string, MigrationTask>()
|
|
private migrationQueue: MigrationTask[] = []
|
|
private maxConcurrentMigrations = 2
|
|
|
|
constructor(
|
|
nodeId: string,
|
|
storage: StorageAdapter,
|
|
shardManager: ShardManager,
|
|
transport: HTTPTransport,
|
|
coordinator: DistributedCoordinator
|
|
) {
|
|
super()
|
|
this.nodeId = nodeId
|
|
this.storage = storage
|
|
this.shardManager = shardManager
|
|
this.transport = transport
|
|
this.coordinator = coordinator
|
|
}
|
|
|
|
/**
|
|
* Initiate migration of a shard to a new node
|
|
*/
|
|
async migrateShard(
|
|
shardId: string,
|
|
targetNode: string,
|
|
options: MigrationOptions = {}
|
|
): Promise<MigrationTask> {
|
|
const task: MigrationTask = {
|
|
id: `migration-${Date.now()}-${Math.random().toString(36).substring(7)}`,
|
|
shardId,
|
|
sourceNode: this.nodeId,
|
|
targetNode,
|
|
status: 'pending',
|
|
progress: 0,
|
|
itemsTransferred: 0,
|
|
totalItems: 0,
|
|
startTime: Date.now()
|
|
}
|
|
|
|
// Add to queue
|
|
this.migrationQueue.push(task)
|
|
this.processMigrationQueue()
|
|
|
|
return task
|
|
}
|
|
|
|
/**
|
|
* Process migration queue
|
|
*/
|
|
private async processMigrationQueue(): Promise<void> {
|
|
while (this.migrationQueue.length > 0 &&
|
|
this.activeMigrations.size < this.maxConcurrentMigrations) {
|
|
const task = this.migrationQueue.shift()!
|
|
this.activeMigrations.set(task.id, task)
|
|
|
|
// Execute migration in background
|
|
this.executeMigration(task).catch(error => {
|
|
console.error(`Migration ${task.id} failed:`, error)
|
|
task.status = 'failed'
|
|
task.error = error.message
|
|
this.emit('migrationFailed', task)
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute a single migration task
|
|
*/
|
|
private async executeMigration(task: MigrationTask): Promise<void> {
|
|
try {
|
|
this.emit('migrationStarted', task)
|
|
|
|
// Phase 1: Start transferring data
|
|
task.status = 'transferring'
|
|
await this.transferData(task)
|
|
|
|
// Phase 2: Validate transferred data
|
|
task.status = 'validating'
|
|
await this.validateData(task)
|
|
|
|
// Phase 3: Switch ownership atomically
|
|
task.status = 'switching'
|
|
await this.switchOwnership(task)
|
|
|
|
// Phase 4: Cleanup source
|
|
task.status = 'completed'
|
|
task.endTime = Date.now()
|
|
task.progress = 100
|
|
|
|
this.activeMigrations.delete(task.id)
|
|
this.emit('migrationCompleted', task)
|
|
|
|
// Process next in queue
|
|
this.processMigrationQueue()
|
|
|
|
} catch (error) {
|
|
task.status = 'failed'
|
|
task.error = (error as Error).message
|
|
this.activeMigrations.delete(task.id)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Transfer data from source to target node
|
|
*/
|
|
private async transferData(task: MigrationTask): Promise<void> {
|
|
const batchSize = 1000
|
|
let offset = 0
|
|
|
|
// Get total count
|
|
const totalItems = await this.getShardItemCount(task.shardId)
|
|
task.totalItems = totalItems
|
|
|
|
while (offset < totalItems) {
|
|
// Get batch of items
|
|
const items = await this.getShardItems(task.shardId, offset, batchSize)
|
|
|
|
if (items.length === 0) break
|
|
|
|
// Send batch to target node
|
|
await this.transport.call(task.targetNode, 'receiveMigrationBatch', {
|
|
migrationId: task.id,
|
|
shardId: task.shardId,
|
|
items,
|
|
offset,
|
|
total: totalItems
|
|
})
|
|
|
|
offset += items.length
|
|
task.itemsTransferred = offset
|
|
task.progress = Math.floor((offset / totalItems) * 80) // 80% for transfer
|
|
|
|
this.emit('migrationProgress', task)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get items from a shard
|
|
*/
|
|
private async getShardItems(
|
|
shardId: string,
|
|
offset: number,
|
|
limit: number
|
|
): Promise<any[]> {
|
|
// Get all noun IDs for this shard
|
|
const nounKey = `shard:${shardId}:nouns`
|
|
const verbKey = `shard:${shardId}:verbs`
|
|
|
|
const items: any[] = []
|
|
|
|
try {
|
|
// Get nouns
|
|
const nouns = await this.storage.getNounsByNounType('*')
|
|
const shardNouns = nouns.filter(n => {
|
|
const assignment = this.shardManager.getShardForKey(n.id)
|
|
return assignment?.shardId === shardId
|
|
}).slice(offset, offset + limit)
|
|
|
|
items.push(...shardNouns.map(n => ({ type: 'noun', data: n })))
|
|
|
|
// Get verbs if we have room
|
|
if (items.length < limit) {
|
|
const verbs = await this.storage.getVerbsByType('*')
|
|
const shardVerbs = verbs.filter(v => {
|
|
const assignment = this.shardManager.getShardForKey(v.id)
|
|
return assignment?.shardId === shardId
|
|
}).slice(0, limit - items.length)
|
|
|
|
items.push(...shardVerbs.map(v => ({ type: 'verb', data: v })))
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error(`Failed to get shard items for ${shardId}:`, error)
|
|
}
|
|
|
|
return items
|
|
}
|
|
|
|
/**
|
|
* Get count of items in a shard
|
|
*/
|
|
private async getShardItemCount(shardId: string): Promise<number> {
|
|
// For now, estimate based on total items / shard count
|
|
// In production, maintain accurate per-shard counts
|
|
const status = await this.storage.getStorageStatus()
|
|
const totalShards = this.shardManager.getTotalShards()
|
|
return Math.ceil(status.used / totalShards)
|
|
}
|
|
|
|
/**
|
|
* Validate transferred data
|
|
*/
|
|
private async validateData(task: MigrationTask): Promise<void> {
|
|
// Request validation from target node
|
|
const response = await this.transport.call(task.targetNode, 'validateMigration', {
|
|
migrationId: task.id,
|
|
shardId: task.shardId,
|
|
expectedCount: task.totalItems
|
|
})
|
|
|
|
if (!response.valid) {
|
|
throw new Error(`Validation failed: ${response.error}`)
|
|
}
|
|
|
|
task.progress = 90 // 90% after validation
|
|
this.emit('migrationProgress', task)
|
|
}
|
|
|
|
/**
|
|
* Switch shard ownership atomically
|
|
*/
|
|
private async switchOwnership(task: MigrationTask): Promise<void> {
|
|
// Coordinate with all nodes to update shard assignment
|
|
await this.coordinator.proposeMigration({
|
|
shardId: task.shardId,
|
|
fromNode: task.sourceNode,
|
|
toNode: task.targetNode,
|
|
migrationId: task.id
|
|
})
|
|
|
|
// Wait for consensus
|
|
await this.waitForConsensus(task.id)
|
|
|
|
// Update local shard manager
|
|
this.shardManager.updateShardAssignment(task.shardId, task.targetNode)
|
|
|
|
task.progress = 95 // 95% after ownership switch
|
|
this.emit('migrationProgress', task)
|
|
|
|
// Cleanup local data
|
|
await this.cleanupShardData(task.shardId)
|
|
}
|
|
|
|
/**
|
|
* Wait for consensus on migration
|
|
*/
|
|
private async waitForConsensus(migrationId: string): Promise<void> {
|
|
const maxWait = 30000 // 30 seconds
|
|
const startTime = Date.now()
|
|
|
|
while (Date.now() - startTime < maxWait) {
|
|
const status = await this.coordinator.getMigrationStatus(migrationId)
|
|
|
|
if (status === 'committed') {
|
|
return
|
|
} else if (status === 'rejected') {
|
|
throw new Error('Migration rejected by cluster')
|
|
}
|
|
|
|
// Wait a bit before checking again
|
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
}
|
|
|
|
throw new Error('Consensus timeout')
|
|
}
|
|
|
|
/**
|
|
* Cleanup local shard data after migration
|
|
*/
|
|
private async cleanupShardData(shardId: string): Promise<void> {
|
|
// Mark shard data for deletion
|
|
// Don't delete immediately in case of rollback
|
|
const cleanupKey = `cleanup:${shardId}:${Date.now()}`
|
|
await this.storage.saveMetadata(cleanupKey, {
|
|
noun: 'Document',
|
|
shardId,
|
|
scheduledFor: Date.now() + 3600000 // Delete after 1 hour
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Handle incoming migration batch (when we're the target)
|
|
*/
|
|
async receiveMigrationBatch(data: {
|
|
migrationId: string
|
|
shardId: string
|
|
items: any[]
|
|
offset: number
|
|
total: number
|
|
}): Promise<void> {
|
|
// Store items
|
|
for (const item of data.items) {
|
|
if (item.type === 'noun') {
|
|
await this.storage.saveNoun(item.data)
|
|
} else if (item.type === 'verb') {
|
|
await this.storage.saveVerb(item.data)
|
|
}
|
|
}
|
|
|
|
// Track progress
|
|
const progress = {
|
|
noun: 'Document',
|
|
migrationId: data.migrationId,
|
|
shardId: data.shardId,
|
|
received: data.offset + data.items.length,
|
|
total: data.total
|
|
}
|
|
|
|
await this.storage.saveMetadata(`migration:${data.migrationId}:progress`, progress)
|
|
}
|
|
|
|
/**
|
|
* Validate received migration data
|
|
*/
|
|
async validateMigration(data: {
|
|
migrationId: string
|
|
shardId: string
|
|
expectedCount: number
|
|
}): Promise<{ valid: boolean; error?: string }> {
|
|
// Check if we received all expected items
|
|
const progressKey = `migration:${data.migrationId}:progress`
|
|
const progress = await this.storage.getMetadata(progressKey)
|
|
|
|
if (!progress) {
|
|
return { valid: false, error: 'No migration progress found' }
|
|
}
|
|
|
|
if (progress.received !== data.expectedCount) {
|
|
return {
|
|
valid: false,
|
|
error: `Expected ${data.expectedCount} items, received ${progress.received}`
|
|
}
|
|
}
|
|
|
|
// Verify data integrity (could add checksums)
|
|
return { valid: true }
|
|
}
|
|
|
|
/**
|
|
* Get status of all active migrations
|
|
*/
|
|
getActiveMigrations(): MigrationTask[] {
|
|
return Array.from(this.activeMigrations.values())
|
|
}
|
|
|
|
/**
|
|
* Cancel a migration
|
|
*/
|
|
async cancelMigration(migrationId: string): Promise<void> {
|
|
const task = this.activeMigrations.get(migrationId)
|
|
if (!task) {
|
|
throw new Error(`Migration ${migrationId} not found`)
|
|
}
|
|
|
|
task.status = 'failed'
|
|
task.error = 'Cancelled by user'
|
|
this.activeMigrations.delete(migrationId)
|
|
|
|
// Notify target node
|
|
await this.transport.call(task.targetNode, 'cancelMigration', {
|
|
migrationId
|
|
})
|
|
|
|
this.emit('migrationCancelled', task)
|
|
}
|
|
} |