brainy/src/distributed/shardMigration.ts
David Snelling 92c96246fb feat(v4.0.0): Complete metadata/vector separation architecture with Azure support
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>
2025-10-17 12:29:27 -07:00

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)
}
}