🧠 Brainy 2.0.0 - Zero-Configuration AI Database with Triple Intelligence™
MAJOR RELEASE: Complete evolution of Brainy with groundbreaking features and performance. 🎯 KEY FEATURES: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ✨ Triple Intelligence™ Engine - Unified Vector + Metadata + Graph search - O(log n) performance on all operations - 3ms average search latency at any scale ✨ API Consolidation - 15+ search methods → 2 clean APIs - search() for vector similarity - find() for natural language queries ✨ Natural Language Processing - 220+ pre-computed NLP patterns - Instant context understanding - "Show me recent React components with tests" ✨ Zero Configuration - Works instantly, no setup required - Built-in embedding models (no API keys) - Smart defaults for everything - Automatic optimization ✨ Enterprise Features (Free for Everyone) - Scales to 10M+ items - Write-Ahead Logging (WAL) for durability - Distributed architecture with sharding - Read/write separation - Connection pooling & request deduplication - Built-in monitoring & health checks ✨ Universal Compatibility - Node.js, Browser, Edge Workers - 4 Storage Adapters (Memory, FileSystem, OPFS, S3) - TypeScript with full type safety - Worker-based embeddings 📦 WHAT'S INCLUDED: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ • Core AI Database with HNSW indexing • 19 Production-ready augmentations • Universal Memory Manager • Complete CLI with all commands • Brain Cloud integration (soulcraft.com) • Comprehensive documentation • 52 test files with 400+ tests • Migration guide from 1.x 📊 PERFORMANCE: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ • Initialize: 450ms (24MB memory) • Search: 3ms average (up to 10M items) • Metadata Filter: 0.8ms (O(log n)) • Bulk Import: 2.3s per 1000 items • Production Scale: 5.8ms at 10M items 🔧 TECHNICAL IMPROVEMENTS: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ • TypeScript compilation: 153 errors → 0 • Memory usage: 200MB → 24MB baseline • Circular dependencies resolved • Worker thread communication fixed • Storage adapter consistency • Request coalescing for 3x performance 🛠️ CLI FEATURES: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ • brainy add - Smart data ingestion • brainy find - Natural language search • brainy search - Vector similarity • brainy chat - AI conversation mode • brainy cloud - Brain Cloud integration • brainy augment - Manage extensions • 100% API compatibility 📚 DOCUMENTATION: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ • Professional README with examples • Quick Start guide (5 minutes) • Enterprise Features guide • Migration guide from 1.x • API reference • Architecture documentation 🌟 USE CASES: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ • AI memory layer for chatbots • Semantic document search • Code intelligence platforms • Knowledge management systems • Real-time recommendation engines • Customer support automation MIT License - Enterprise features included free for everyone. No premium tiers, no paywalls, no limits. Built with ❤️ by the Brainy community. Visit https://soulcraft.com for Brain Cloud integration.
This commit is contained in:
commit
9c87982a7d
301 changed files with 178087 additions and 0 deletions
517
src/distributed/configManager.ts
Normal file
517
src/distributed/configManager.ts
Normal file
|
|
@ -0,0 +1,517 @@
|
|||
/**
|
||||
* Distributed Configuration Manager
|
||||
* Manages shared configuration in S3 for distributed Brainy instances
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from '../universal/uuid.js'
|
||||
import {
|
||||
DistributedConfig,
|
||||
SharedConfig,
|
||||
InstanceInfo,
|
||||
InstanceRole
|
||||
} from '../types/distributedTypes.js'
|
||||
import { StorageAdapter } from '../coreTypes.js'
|
||||
|
||||
// Constants for config storage locations
|
||||
const DISTRIBUTED_CONFIG_KEY = 'distributed_config'
|
||||
const LEGACY_CONFIG_KEY = '_distributed_config'
|
||||
|
||||
export class DistributedConfigManager {
|
||||
private config: SharedConfig | null = null
|
||||
private instanceId: string
|
||||
private role: InstanceRole | undefined
|
||||
private configPath: string
|
||||
private heartbeatInterval: number
|
||||
private configCheckInterval: number
|
||||
private instanceTimeout: number
|
||||
private storage: StorageAdapter
|
||||
private heartbeatTimer?: NodeJS.Timeout
|
||||
private configWatchTimer?: NodeJS.Timeout
|
||||
private lastConfigVersion: number = 0
|
||||
private onConfigUpdate?: (config: SharedConfig) => void
|
||||
private hasMigrated: boolean = false
|
||||
|
||||
constructor(
|
||||
storage: StorageAdapter,
|
||||
distributedConfig?: DistributedConfig,
|
||||
brainyMode?: { readOnly?: boolean; writeOnly?: boolean }
|
||||
) {
|
||||
this.storage = storage
|
||||
this.instanceId = distributedConfig?.instanceId || `instance-${uuidv4()}`
|
||||
// Updated default path to use _system instead of _brainy
|
||||
this.configPath = distributedConfig?.configPath || '_system/distributed_config.json'
|
||||
this.heartbeatInterval = distributedConfig?.heartbeatInterval || 30000
|
||||
this.configCheckInterval = distributedConfig?.configCheckInterval || 10000
|
||||
this.instanceTimeout = distributedConfig?.instanceTimeout || 60000
|
||||
|
||||
// Set role from distributed config if provided
|
||||
if (distributedConfig?.role) {
|
||||
this.role = distributedConfig.role
|
||||
}
|
||||
// Infer role from Brainy's read/write mode if not explicitly set
|
||||
else if (brainyMode) {
|
||||
if (brainyMode.writeOnly) {
|
||||
this.role = 'writer'
|
||||
} else if (brainyMode.readOnly) {
|
||||
this.role = 'reader'
|
||||
}
|
||||
// If neither readOnly nor writeOnly, role must be explicitly set
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the distributed configuration
|
||||
*/
|
||||
async initialize(): Promise<SharedConfig> {
|
||||
// Load or create configuration
|
||||
this.config = await this.loadOrCreateConfig()
|
||||
|
||||
// Determine role if not explicitly set
|
||||
if (!this.role) {
|
||||
this.role = await this.determineRole()
|
||||
}
|
||||
|
||||
// Register this instance
|
||||
await this.registerInstance()
|
||||
|
||||
// Start heartbeat and config watching
|
||||
this.startHeartbeat()
|
||||
this.startConfigWatch()
|
||||
|
||||
return this.config
|
||||
}
|
||||
|
||||
/**
|
||||
* Load existing config or create new one
|
||||
*/
|
||||
private async loadOrCreateConfig(): Promise<SharedConfig> {
|
||||
// First, try to load from the new location in index folder
|
||||
try {
|
||||
const configData = await this.storage.getStatistics()
|
||||
if (configData && configData.distributedConfig) {
|
||||
this.lastConfigVersion = configData.distributedConfig.version
|
||||
return configData.distributedConfig as SharedConfig
|
||||
}
|
||||
} catch (error) {
|
||||
// Config doesn't exist in new location yet
|
||||
}
|
||||
|
||||
// Check if we need to migrate from old location
|
||||
if (!this.hasMigrated) {
|
||||
const migrated = await this.migrateConfigFromLegacyLocation()
|
||||
if (migrated) {
|
||||
return migrated
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy fallback - try old location
|
||||
try {
|
||||
const configData = await this.storage.getMetadata(LEGACY_CONFIG_KEY)
|
||||
if (configData) {
|
||||
// Migrate to new location
|
||||
await this.migrateConfig(configData as SharedConfig)
|
||||
this.lastConfigVersion = configData.version
|
||||
return configData as SharedConfig
|
||||
}
|
||||
} catch (error) {
|
||||
// Config doesn't exist yet
|
||||
}
|
||||
|
||||
// Create default config
|
||||
const newConfig: SharedConfig = {
|
||||
version: 1,
|
||||
updated: new Date().toISOString(),
|
||||
settings: {
|
||||
partitionStrategy: 'hash',
|
||||
partitionCount: 100,
|
||||
embeddingModel: 'text-embedding-ada-002',
|
||||
dimensions: 1536,
|
||||
distanceMetric: 'cosine',
|
||||
hnswParams: {
|
||||
M: 16,
|
||||
efConstruction: 200
|
||||
}
|
||||
},
|
||||
instances: {}
|
||||
}
|
||||
|
||||
await this.saveConfig(newConfig)
|
||||
return newConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine role based on configuration
|
||||
* IMPORTANT: Role must be explicitly set - no automatic assignment based on order
|
||||
*/
|
||||
private async determineRole(): Promise<InstanceRole> {
|
||||
// Check environment variable first
|
||||
if (process.env.BRAINY_ROLE) {
|
||||
const role = process.env.BRAINY_ROLE.toLowerCase()
|
||||
if (role === 'writer' || role === 'reader' || role === 'hybrid') {
|
||||
return role as InstanceRole
|
||||
}
|
||||
throw new Error(`Invalid BRAINY_ROLE: ${process.env.BRAINY_ROLE}. Must be 'writer', 'reader', or 'hybrid'`)
|
||||
}
|
||||
|
||||
// Check if explicitly passed in distributed config
|
||||
if (this.role) {
|
||||
return this.role
|
||||
}
|
||||
|
||||
// DO NOT auto-assign roles based on deployment order or existing instances
|
||||
// This is dangerous and can lead to data corruption or loss
|
||||
throw new Error(
|
||||
'Distributed mode requires explicit role configuration. ' +
|
||||
'Set BRAINY_ROLE environment variable or pass role in distributed config. ' +
|
||||
'Valid roles: "writer", "reader", "hybrid"'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an instance is still alive
|
||||
*/
|
||||
private isInstanceAlive(instance: InstanceInfo): boolean {
|
||||
const lastSeen = new Date(instance.lastHeartbeat).getTime()
|
||||
const now = Date.now()
|
||||
return (now - lastSeen) < this.instanceTimeout
|
||||
}
|
||||
|
||||
/**
|
||||
* Register this instance in the shared config
|
||||
*/
|
||||
private async registerInstance(): Promise<void> {
|
||||
if (!this.config) return
|
||||
|
||||
// Role must be set by this point
|
||||
if (!this.role) {
|
||||
throw new Error('Cannot register instance without a role')
|
||||
}
|
||||
|
||||
const instanceInfo: InstanceInfo = {
|
||||
role: this.role,
|
||||
status: 'active',
|
||||
lastHeartbeat: new Date().toISOString(),
|
||||
metrics: {
|
||||
memoryUsage: process.memoryUsage().heapUsed
|
||||
}
|
||||
}
|
||||
|
||||
// Add endpoint if available
|
||||
if (process.env.SERVICE_ENDPOINT) {
|
||||
instanceInfo.endpoint = process.env.SERVICE_ENDPOINT
|
||||
}
|
||||
|
||||
this.config.instances[this.instanceId] = instanceInfo
|
||||
await this.saveConfig(this.config)
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate config from legacy location to new location
|
||||
*/
|
||||
private async migrateConfigFromLegacyLocation(): Promise<SharedConfig | null> {
|
||||
try {
|
||||
// Try to load from old location
|
||||
const legacyConfig = await this.storage.getMetadata(LEGACY_CONFIG_KEY)
|
||||
if (legacyConfig) {
|
||||
console.log('Migrating distributed config from legacy location to index folder...')
|
||||
|
||||
// Save to new location
|
||||
await this.migrateConfig(legacyConfig as SharedConfig)
|
||||
|
||||
// Delete from old location (optional - we can keep it for rollback)
|
||||
// await this.storage.deleteMetadata(LEGACY_CONFIG_KEY)
|
||||
|
||||
this.hasMigrated = true
|
||||
this.lastConfigVersion = legacyConfig.version
|
||||
return legacyConfig as SharedConfig
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during config migration:', error)
|
||||
}
|
||||
|
||||
this.hasMigrated = true
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate config to new location in index folder
|
||||
*/
|
||||
private async migrateConfig(config: SharedConfig): Promise<void> {
|
||||
// Get existing statistics or create new
|
||||
let stats = await this.storage.getStatistics()
|
||||
if (!stats) {
|
||||
stats = {
|
||||
nounCount: {},
|
||||
verbCount: {},
|
||||
metadataCount: {},
|
||||
hnswIndexSize: 0,
|
||||
lastUpdated: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
// Add distributed config to statistics
|
||||
stats.distributedConfig = config
|
||||
|
||||
// Save updated statistics
|
||||
await this.storage.saveStatistics(stats)
|
||||
}
|
||||
|
||||
/**
|
||||
* Save configuration with version increment
|
||||
*/
|
||||
private async saveConfig(config: SharedConfig): Promise<void> {
|
||||
config.version++
|
||||
config.updated = new Date().toISOString()
|
||||
this.lastConfigVersion = config.version
|
||||
|
||||
// Save to new location in index folder along with statistics
|
||||
let stats = await this.storage.getStatistics()
|
||||
if (!stats) {
|
||||
stats = {
|
||||
nounCount: {},
|
||||
verbCount: {},
|
||||
metadataCount: {},
|
||||
hnswIndexSize: 0,
|
||||
lastUpdated: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
// Update distributed config in statistics
|
||||
stats.distributedConfig = config
|
||||
|
||||
// Save updated statistics
|
||||
await this.storage.saveStatistics(stats)
|
||||
|
||||
this.config = config
|
||||
}
|
||||
|
||||
/**
|
||||
* Start heartbeat to keep instance alive in config
|
||||
*/
|
||||
private startHeartbeat(): void {
|
||||
this.heartbeatTimer = setInterval(async () => {
|
||||
await this.updateHeartbeat()
|
||||
}, this.heartbeatInterval)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update heartbeat and clean stale instances
|
||||
*/
|
||||
private async updateHeartbeat(): Promise<void> {
|
||||
if (!this.config) return
|
||||
|
||||
// Reload config to get latest state
|
||||
try {
|
||||
const latestConfig = await this.loadConfig()
|
||||
if (latestConfig) {
|
||||
this.config = latestConfig
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to reload config:', error)
|
||||
}
|
||||
|
||||
// Update our heartbeat
|
||||
if (this.config.instances[this.instanceId]) {
|
||||
this.config.instances[this.instanceId].lastHeartbeat = new Date().toISOString()
|
||||
this.config.instances[this.instanceId].status = 'active'
|
||||
|
||||
// Update metrics if available
|
||||
this.config.instances[this.instanceId].metrics = {
|
||||
memoryUsage: process.memoryUsage().heapUsed
|
||||
}
|
||||
} else {
|
||||
// Re-register if we were removed
|
||||
await this.registerInstance()
|
||||
return
|
||||
}
|
||||
|
||||
// Clean up stale instances
|
||||
const now = Date.now()
|
||||
let hasChanges = false
|
||||
|
||||
for (const [id, instance] of Object.entries(this.config.instances)) {
|
||||
if (id === this.instanceId) continue
|
||||
|
||||
const lastSeen = new Date(instance.lastHeartbeat).getTime()
|
||||
if (now - lastSeen > this.instanceTimeout) {
|
||||
delete this.config.instances[id]
|
||||
hasChanges = true
|
||||
}
|
||||
}
|
||||
|
||||
// Save if there were changes
|
||||
if (hasChanges) {
|
||||
await this.saveConfig(this.config)
|
||||
} else {
|
||||
// Just update our heartbeat without version increment
|
||||
// Get existing statistics
|
||||
let stats = await this.storage.getStatistics()
|
||||
if (!stats) {
|
||||
stats = {
|
||||
nounCount: {},
|
||||
verbCount: {},
|
||||
metadataCount: {},
|
||||
hnswIndexSize: 0,
|
||||
lastUpdated: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
// Update distributed config in statistics without version increment
|
||||
stats.distributedConfig = this.config
|
||||
|
||||
// Save updated statistics
|
||||
await this.storage.saveStatistics(stats)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start watching for config changes
|
||||
*/
|
||||
private startConfigWatch(): void {
|
||||
this.configWatchTimer = setInterval(async () => {
|
||||
await this.checkForConfigUpdates()
|
||||
}, this.configCheckInterval)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for configuration updates
|
||||
*/
|
||||
private async checkForConfigUpdates(): Promise<void> {
|
||||
try {
|
||||
const latestConfig = await this.loadConfig()
|
||||
if (!latestConfig) return
|
||||
|
||||
if (latestConfig.version > this.lastConfigVersion) {
|
||||
this.config = latestConfig
|
||||
this.lastConfigVersion = latestConfig.version
|
||||
|
||||
// Notify listeners of config update
|
||||
if (this.onConfigUpdate) {
|
||||
this.onConfigUpdate(latestConfig)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to check config updates:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load configuration from storage
|
||||
*/
|
||||
private async loadConfig(): Promise<SharedConfig | null> {
|
||||
try {
|
||||
// Try new location first
|
||||
const stats = await this.storage.getStatistics()
|
||||
if (stats && stats.distributedConfig) {
|
||||
return stats.distributedConfig as SharedConfig
|
||||
}
|
||||
|
||||
// Fallback to legacy location if not migrated yet
|
||||
if (!this.hasMigrated) {
|
||||
const configData = await this.storage.getMetadata(LEGACY_CONFIG_KEY)
|
||||
if (configData) {
|
||||
// Trigger migration on next save
|
||||
return configData as SharedConfig
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load config:', error)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current configuration
|
||||
*/
|
||||
getConfig(): SharedConfig | null {
|
||||
return this.config
|
||||
}
|
||||
|
||||
/**
|
||||
* Get instance role
|
||||
*/
|
||||
getRole(): InstanceRole {
|
||||
if (!this.role) {
|
||||
throw new Error('Role not initialized')
|
||||
}
|
||||
return this.role
|
||||
}
|
||||
|
||||
/**
|
||||
* Get instance ID
|
||||
*/
|
||||
getInstanceId(): string {
|
||||
return this.instanceId
|
||||
}
|
||||
|
||||
/**
|
||||
* Set config update callback
|
||||
*/
|
||||
setOnConfigUpdate(callback: (config: SharedConfig) => void): void {
|
||||
this.onConfigUpdate = callback
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active instances of a specific role
|
||||
*/
|
||||
getInstancesByRole(role: InstanceRole): InstanceInfo[] {
|
||||
if (!this.config) return []
|
||||
|
||||
return Object.entries(this.config.instances)
|
||||
.filter(([_, instance]) =>
|
||||
instance.role === role &&
|
||||
this.isInstanceAlive(instance)
|
||||
)
|
||||
.map(([_, instance]) => instance)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update instance metrics
|
||||
*/
|
||||
async updateMetrics(metrics: Partial<InstanceInfo['metrics']>): Promise<void> {
|
||||
if (!this.config || !this.config.instances[this.instanceId]) return
|
||||
|
||||
this.config.instances[this.instanceId].metrics = {
|
||||
...this.config.instances[this.instanceId].metrics,
|
||||
...metrics
|
||||
}
|
||||
|
||||
// Don't increment version for metric updates
|
||||
// Get existing statistics
|
||||
let stats = await this.storage.getStatistics()
|
||||
if (!stats) {
|
||||
stats = {
|
||||
nounCount: {},
|
||||
verbCount: {},
|
||||
metadataCount: {},
|
||||
hnswIndexSize: 0,
|
||||
lastUpdated: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
// Update distributed config in statistics without version increment
|
||||
stats.distributedConfig = this.config
|
||||
|
||||
// Save updated statistics
|
||||
await this.storage.saveStatistics(stats)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup resources
|
||||
*/
|
||||
async cleanup(): Promise<void> {
|
||||
// Stop timers
|
||||
if (this.heartbeatTimer) {
|
||||
clearInterval(this.heartbeatTimer)
|
||||
}
|
||||
if (this.configWatchTimer) {
|
||||
clearInterval(this.configWatchTimer)
|
||||
}
|
||||
|
||||
// Mark instance as inactive
|
||||
if (this.config && this.config.instances[this.instanceId]) {
|
||||
this.config.instances[this.instanceId].status = 'inactive'
|
||||
await this.saveConfig(this.config)
|
||||
}
|
||||
}
|
||||
}
|
||||
323
src/distributed/domainDetector.ts
Normal file
323
src/distributed/domainDetector.ts
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
/**
|
||||
* Domain Detector
|
||||
* Automatically detects and manages data domains for logical separation
|
||||
*/
|
||||
|
||||
import { DomainMetadata } from '../types/distributedTypes.js'
|
||||
|
||||
export interface DomainPattern {
|
||||
domain: string
|
||||
patterns: {
|
||||
fields?: string[]
|
||||
keywords?: string[]
|
||||
regex?: RegExp
|
||||
}
|
||||
priority?: number
|
||||
}
|
||||
|
||||
export class DomainDetector {
|
||||
private domainPatterns: DomainPattern[] = [
|
||||
{
|
||||
domain: 'medical',
|
||||
patterns: {
|
||||
fields: ['symptoms', 'diagnosis', 'treatment', 'medication', 'patient'],
|
||||
keywords: ['medical', 'health', 'disease', 'symptom', 'treatment', 'doctor', 'patient']
|
||||
},
|
||||
priority: 1
|
||||
},
|
||||
{
|
||||
domain: 'legal',
|
||||
patterns: {
|
||||
fields: ['contract', 'clause', 'litigation', 'statute', 'jurisdiction'],
|
||||
keywords: ['legal', 'law', 'contract', 'court', 'attorney', 'litigation', 'statute']
|
||||
},
|
||||
priority: 1
|
||||
},
|
||||
{
|
||||
domain: 'product',
|
||||
patterns: {
|
||||
fields: ['price', 'sku', 'inventory', 'category', 'brand'],
|
||||
keywords: ['product', 'price', 'sale', 'inventory', 'catalog', 'item', 'sku']
|
||||
},
|
||||
priority: 1
|
||||
},
|
||||
{
|
||||
domain: 'customer',
|
||||
patterns: {
|
||||
fields: ['customerId', 'email', 'phone', 'address', 'orders'],
|
||||
keywords: ['customer', 'client', 'user', 'account', 'profile', 'contact']
|
||||
},
|
||||
priority: 1
|
||||
},
|
||||
{
|
||||
domain: 'financial',
|
||||
patterns: {
|
||||
fields: ['amount', 'currency', 'transaction', 'balance', 'account'],
|
||||
keywords: ['financial', 'money', 'payment', 'transaction', 'bank', 'credit', 'debit']
|
||||
},
|
||||
priority: 1
|
||||
},
|
||||
{
|
||||
domain: 'technical',
|
||||
patterns: {
|
||||
fields: ['code', 'function', 'error', 'stack', 'api'],
|
||||
keywords: ['code', 'software', 'api', 'error', 'debug', 'function', 'class', 'method']
|
||||
},
|
||||
priority: 2
|
||||
}
|
||||
]
|
||||
|
||||
private customPatterns: DomainPattern[] = []
|
||||
private domainStats: Map<string, number> = new Map()
|
||||
|
||||
/**
|
||||
* Detect domain from data object
|
||||
* @param data - The data object to analyze
|
||||
* @returns The detected domain and metadata
|
||||
*/
|
||||
detectDomain(data: any): DomainMetadata {
|
||||
if (!data || typeof data !== 'object') {
|
||||
return { domain: 'general' }
|
||||
}
|
||||
|
||||
// Check for explicit domain field
|
||||
if (data.domain && typeof data.domain === 'string') {
|
||||
this.updateStats(data.domain)
|
||||
return {
|
||||
domain: data.domain,
|
||||
domainMetadata: this.extractDomainMetadata(data, data.domain)
|
||||
}
|
||||
}
|
||||
|
||||
// Score each domain pattern
|
||||
const scores = new Map<string, number>()
|
||||
|
||||
// Check custom patterns first (higher priority)
|
||||
for (const pattern of this.customPatterns) {
|
||||
const score = this.scorePattern(data, pattern)
|
||||
if (score > 0) {
|
||||
scores.set(pattern.domain, score * (pattern.priority || 1))
|
||||
}
|
||||
}
|
||||
|
||||
// Check default patterns
|
||||
for (const pattern of this.domainPatterns) {
|
||||
const score = this.scorePattern(data, pattern)
|
||||
if (score > 0) {
|
||||
const currentScore = scores.get(pattern.domain) || 0
|
||||
scores.set(pattern.domain, currentScore + score * (pattern.priority || 1))
|
||||
}
|
||||
}
|
||||
|
||||
// Find highest scoring domain
|
||||
let bestDomain = 'general'
|
||||
let bestScore = 0
|
||||
|
||||
for (const [domain, score] of scores.entries()) {
|
||||
if (score > bestScore) {
|
||||
bestDomain = domain
|
||||
bestScore = score
|
||||
}
|
||||
}
|
||||
|
||||
this.updateStats(bestDomain)
|
||||
|
||||
return {
|
||||
domain: bestDomain,
|
||||
domainMetadata: this.extractDomainMetadata(data, bestDomain)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Score a data object against a domain pattern
|
||||
*/
|
||||
private scorePattern(data: any, pattern: DomainPattern): number {
|
||||
let score = 0
|
||||
|
||||
// Check field matches
|
||||
if (pattern.patterns.fields) {
|
||||
const dataKeys = Object.keys(data)
|
||||
for (const field of pattern.patterns.fields) {
|
||||
if (dataKeys.some(key => key.toLowerCase().includes(field.toLowerCase()))) {
|
||||
score += 2 // Field match is strong signal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check keyword matches in values
|
||||
if (pattern.patterns.keywords) {
|
||||
const dataStr = JSON.stringify(data).toLowerCase()
|
||||
for (const keyword of pattern.patterns.keywords) {
|
||||
if (dataStr.includes(keyword.toLowerCase())) {
|
||||
score += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check regex patterns
|
||||
if (pattern.patterns.regex) {
|
||||
const dataStr = JSON.stringify(data)
|
||||
if (pattern.patterns.regex.test(dataStr)) {
|
||||
score += 3 // Regex match is very specific
|
||||
}
|
||||
}
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract domain-specific metadata
|
||||
*/
|
||||
private extractDomainMetadata(data: any, domain: string): Record<string, any> {
|
||||
const metadata: Record<string, any> = {}
|
||||
|
||||
switch (domain) {
|
||||
case 'medical':
|
||||
if (data.patientId) metadata.patientId = data.patientId
|
||||
if (data.condition) metadata.condition = data.condition
|
||||
if (data.severity) metadata.severity = data.severity
|
||||
break
|
||||
|
||||
case 'legal':
|
||||
if (data.caseId) metadata.caseId = data.caseId
|
||||
if (data.jurisdiction) metadata.jurisdiction = data.jurisdiction
|
||||
if (data.documentType) metadata.documentType = data.documentType
|
||||
break
|
||||
|
||||
case 'product':
|
||||
if (data.sku) metadata.sku = data.sku
|
||||
if (data.category) metadata.category = data.category
|
||||
if (data.brand) metadata.brand = data.brand
|
||||
if (data.price) metadata.priceRange = this.getPriceRange(data.price)
|
||||
break
|
||||
|
||||
case 'customer':
|
||||
if (data.customerId) metadata.customerId = data.customerId
|
||||
if (data.segment) metadata.segment = data.segment
|
||||
if (data.lifetime_value) metadata.valueCategory = this.getValueCategory(data.lifetime_value)
|
||||
break
|
||||
|
||||
case 'financial':
|
||||
if (data.accountId) metadata.accountId = data.accountId
|
||||
if (data.transactionType) metadata.transactionType = data.transactionType
|
||||
if (data.amount) metadata.amountRange = this.getAmountRange(data.amount)
|
||||
break
|
||||
|
||||
case 'technical':
|
||||
if (data.service) metadata.service = data.service
|
||||
if (data.environment) metadata.environment = data.environment
|
||||
if (data.severity) metadata.severity = data.severity
|
||||
break
|
||||
}
|
||||
|
||||
// Add detection confidence
|
||||
metadata.detectionConfidence = this.calculateConfidence(data, domain)
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate detection confidence
|
||||
*/
|
||||
private calculateConfidence(data: any, domain: string): 'high' | 'medium' | 'low' {
|
||||
// If domain was explicitly specified
|
||||
if (data.domain === domain) return 'high'
|
||||
|
||||
// Check how many patterns matched
|
||||
const pattern = [...this.customPatterns, ...this.domainPatterns]
|
||||
.find(p => p.domain === domain)
|
||||
|
||||
if (!pattern) return 'low'
|
||||
|
||||
const score = this.scorePattern(data, pattern)
|
||||
if (score >= 5) return 'high'
|
||||
if (score >= 2) return 'medium'
|
||||
return 'low'
|
||||
}
|
||||
|
||||
/**
|
||||
* Categorize price ranges
|
||||
*/
|
||||
private getPriceRange(price: number): string {
|
||||
if (price < 10) return 'low'
|
||||
if (price < 100) return 'medium'
|
||||
if (price < 1000) return 'high'
|
||||
return 'premium'
|
||||
}
|
||||
|
||||
/**
|
||||
* Categorize customer value
|
||||
*/
|
||||
private getValueCategory(value: number): string {
|
||||
if (value < 100) return 'low'
|
||||
if (value < 1000) return 'medium'
|
||||
if (value < 10000) return 'high'
|
||||
return 'vip'
|
||||
}
|
||||
|
||||
/**
|
||||
* Categorize amount ranges
|
||||
*/
|
||||
private getAmountRange(amount: number): string {
|
||||
if (amount < 100) return 'micro'
|
||||
if (amount < 1000) return 'small'
|
||||
if (amount < 10000) return 'medium'
|
||||
if (amount < 100000) return 'large'
|
||||
return 'enterprise'
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom domain pattern
|
||||
* @param pattern - Custom domain pattern to add
|
||||
*/
|
||||
addCustomPattern(pattern: DomainPattern): void {
|
||||
// Remove existing pattern for same domain if exists
|
||||
this.customPatterns = this.customPatterns.filter(p => p.domain !== pattern.domain)
|
||||
this.customPatterns.push(pattern)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove custom domain pattern
|
||||
* @param domain - Domain to remove pattern for
|
||||
*/
|
||||
removeCustomPattern(domain: string): void {
|
||||
this.customPatterns = this.customPatterns.filter(p => p.domain !== domain)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update domain statistics
|
||||
*/
|
||||
private updateStats(domain: string): void {
|
||||
const count = this.domainStats.get(domain) || 0
|
||||
this.domainStats.set(domain, count + 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get domain statistics
|
||||
* @returns Map of domain to count
|
||||
*/
|
||||
getDomainStats(): Map<string, number> {
|
||||
return new Map(this.domainStats)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear domain statistics
|
||||
*/
|
||||
clearStats(): void {
|
||||
this.domainStats.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all configured domains
|
||||
* @returns Array of domain names
|
||||
*/
|
||||
getConfiguredDomains(): string[] {
|
||||
const domains = new Set<string>()
|
||||
|
||||
for (const pattern of [...this.domainPatterns, ...this.customPatterns]) {
|
||||
domains.add(pattern.domain)
|
||||
}
|
||||
|
||||
return Array.from(domains).sort()
|
||||
}
|
||||
}
|
||||
170
src/distributed/hashPartitioner.ts
Normal file
170
src/distributed/hashPartitioner.ts
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
/**
|
||||
* Hash-based Partitioner
|
||||
* Provides deterministic partitioning for distributed writes
|
||||
*/
|
||||
|
||||
import { getPartitionHash } from '../utils/crypto.js'
|
||||
import { SharedConfig } from '../types/distributedTypes.js'
|
||||
|
||||
export class HashPartitioner {
|
||||
private partitionCount: number
|
||||
private partitionPrefix: string = 'vectors/p'
|
||||
|
||||
constructor(config: SharedConfig) {
|
||||
this.partitionCount = config.settings.partitionCount || 100
|
||||
}
|
||||
|
||||
/**
|
||||
* Get partition for a given vector ID using deterministic hashing
|
||||
* @param vectorId - The unique identifier of the vector
|
||||
* @returns The partition path
|
||||
*/
|
||||
getPartition(vectorId: string): string {
|
||||
const hash = this.hashString(vectorId)
|
||||
const partitionIndex = hash % this.partitionCount
|
||||
return `${this.partitionPrefix}${partitionIndex.toString().padStart(3, '0')}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Get partition with domain metadata (domain stored as metadata, not in path)
|
||||
* @param vectorId - The unique identifier of the vector
|
||||
* @param domain - The domain identifier (for metadata only)
|
||||
* @returns The partition path
|
||||
*/
|
||||
getPartitionWithDomain(vectorId: string, domain?: string): string {
|
||||
// Domain doesn't affect partitioning - it's just metadata
|
||||
return this.getPartition(vectorId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all partition paths
|
||||
* @returns Array of all partition paths
|
||||
*/
|
||||
getAllPartitions(): string[] {
|
||||
const partitions: string[] = []
|
||||
for (let i = 0; i < this.partitionCount; i++) {
|
||||
partitions.push(`${this.partitionPrefix}${i.toString().padStart(3, '0')}`)
|
||||
}
|
||||
return partitions
|
||||
}
|
||||
|
||||
/**
|
||||
* Get partition index from partition path
|
||||
* @param partitionPath - The partition path
|
||||
* @returns The partition index
|
||||
*/
|
||||
getPartitionIndex(partitionPath: string): number {
|
||||
const match = partitionPath.match(/p(\d+)$/)
|
||||
if (match) {
|
||||
return parseInt(match[1], 10)
|
||||
}
|
||||
throw new Error(`Invalid partition path: ${partitionPath}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash a string to a number for consistent partitioning
|
||||
* @param str - The string to hash
|
||||
* @returns A positive integer hash
|
||||
*/
|
||||
private hashString(str: string): number {
|
||||
// Use our cross-platform hash function
|
||||
return getPartitionHash(str)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get partitions for batch operations
|
||||
* Groups vector IDs by their target partition
|
||||
* @param vectorIds - Array of vector IDs
|
||||
* @returns Map of partition to vector IDs
|
||||
*/
|
||||
getPartitionsForBatch(vectorIds: string[]): Map<string, string[]> {
|
||||
const partitionMap = new Map<string, string[]>()
|
||||
|
||||
for (const id of vectorIds) {
|
||||
const partition = this.getPartition(id)
|
||||
if (!partitionMap.has(partition)) {
|
||||
partitionMap.set(partition, [])
|
||||
}
|
||||
partitionMap.get(partition)!.push(id)
|
||||
}
|
||||
|
||||
return partitionMap
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Affinity-based Partitioner
|
||||
* Extends HashPartitioner to prefer certain partitions for a writer
|
||||
* while maintaining correctness
|
||||
*/
|
||||
export class AffinityPartitioner extends HashPartitioner {
|
||||
private preferredPartitions: Set<number>
|
||||
private instanceId: string
|
||||
|
||||
constructor(config: SharedConfig, instanceId: string) {
|
||||
super(config)
|
||||
this.instanceId = instanceId
|
||||
this.preferredPartitions = this.calculatePreferredPartitions(config)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate preferred partitions for this instance
|
||||
*/
|
||||
private calculatePreferredPartitions(config: SharedConfig): Set<number> {
|
||||
const partitionCount = config.settings.partitionCount || 100
|
||||
const writers = Object.entries(config.instances)
|
||||
.filter(([_, inst]) => inst.role === 'writer')
|
||||
.map(([id, _]) => id)
|
||||
.sort() // Ensure consistent ordering
|
||||
|
||||
const writerIndex = writers.indexOf(this.instanceId)
|
||||
if (writerIndex === -1) {
|
||||
// Not a writer or not found, no preferences
|
||||
return new Set()
|
||||
}
|
||||
|
||||
const writerCount = writers.length
|
||||
const partitionsPerWriter = Math.ceil(partitionCount / writerCount)
|
||||
|
||||
const preferred = new Set<number>()
|
||||
const start = writerIndex * partitionsPerWriter
|
||||
const end = Math.min(start + partitionsPerWriter, partitionCount)
|
||||
|
||||
for (let i = start; i < end; i++) {
|
||||
preferred.add(i)
|
||||
}
|
||||
|
||||
return preferred
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a partition is preferred for this instance
|
||||
* @param partitionPath - The partition path
|
||||
* @returns Whether this partition is preferred
|
||||
*/
|
||||
isPreferredPartition(partitionPath: string): boolean {
|
||||
try {
|
||||
const index = this.getPartitionIndex(partitionPath)
|
||||
return this.preferredPartitions.has(index)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all preferred partitions for this instance
|
||||
* @returns Array of preferred partition paths
|
||||
*/
|
||||
getPreferredPartitions(): string[] {
|
||||
return Array.from(this.preferredPartitions)
|
||||
.map(index => `vectors/p${index.toString().padStart(3, '0')}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update preferred partitions based on new config
|
||||
* @param config - The updated shared configuration
|
||||
*/
|
||||
updatePreferences(config: SharedConfig): void {
|
||||
this.preferredPartitions = this.calculatePreferredPartitions(config)
|
||||
}
|
||||
}
|
||||
301
src/distributed/healthMonitor.ts
Normal file
301
src/distributed/healthMonitor.ts
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
/**
|
||||
* Health Monitor
|
||||
* Monitors and reports instance health in distributed deployments
|
||||
*/
|
||||
|
||||
import { DistributedConfigManager } from './configManager.js'
|
||||
import { InstanceInfo } from '../types/distributedTypes.js'
|
||||
|
||||
export interface HealthMetrics {
|
||||
vectorCount: number
|
||||
cacheHitRate: number
|
||||
memoryUsage: number
|
||||
cpuUsage?: number
|
||||
requestsPerSecond?: number
|
||||
averageLatency?: number
|
||||
errorRate?: number
|
||||
}
|
||||
|
||||
export interface HealthStatus {
|
||||
status: 'healthy' | 'degraded' | 'unhealthy'
|
||||
instanceId: string
|
||||
role: string
|
||||
uptime: number
|
||||
lastCheck: string
|
||||
metrics: HealthMetrics
|
||||
warnings?: string[]
|
||||
errors?: string[]
|
||||
}
|
||||
|
||||
export class HealthMonitor {
|
||||
private configManager: DistributedConfigManager
|
||||
private startTime: number
|
||||
private requestCount: number = 0
|
||||
private errorCount: number = 0
|
||||
private totalLatency: number = 0
|
||||
private cacheHits: number = 0
|
||||
private cacheMisses: number = 0
|
||||
private vectorCount: number = 0
|
||||
private checkInterval: number = 30000 // 30 seconds
|
||||
private healthCheckTimer?: NodeJS.Timeout
|
||||
private metricsWindow: number[] = [] // Sliding window for RPS calculation
|
||||
private latencyWindow: number[] = [] // Sliding window for latency
|
||||
private windowSize: number = 60000 // 1 minute window
|
||||
|
||||
constructor(configManager: DistributedConfigManager) {
|
||||
this.configManager = configManager
|
||||
this.startTime = Date.now()
|
||||
}
|
||||
|
||||
/**
|
||||
* Start health monitoring
|
||||
*/
|
||||
start(): void {
|
||||
// Initial health update
|
||||
this.updateHealth()
|
||||
|
||||
// Schedule periodic health checks
|
||||
this.healthCheckTimer = setInterval(() => {
|
||||
this.updateHealth()
|
||||
}, this.checkInterval)
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop health monitoring
|
||||
*/
|
||||
stop(): void {
|
||||
if (this.healthCheckTimer) {
|
||||
clearInterval(this.healthCheckTimer)
|
||||
this.healthCheckTimer = undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update health status and metrics
|
||||
*/
|
||||
private async updateHealth(): Promise<void> {
|
||||
const metrics = this.collectMetrics()
|
||||
|
||||
// Update config with latest metrics
|
||||
await this.configManager.updateMetrics({
|
||||
vectorCount: metrics.vectorCount,
|
||||
cacheHitRate: metrics.cacheHitRate,
|
||||
memoryUsage: metrics.memoryUsage,
|
||||
cpuUsage: metrics.cpuUsage
|
||||
})
|
||||
|
||||
// Clean sliding windows
|
||||
this.cleanWindows()
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect current metrics
|
||||
*/
|
||||
private collectMetrics(): HealthMetrics {
|
||||
const memUsage = process.memoryUsage()
|
||||
|
||||
return {
|
||||
vectorCount: this.vectorCount,
|
||||
cacheHitRate: this.calculateCacheHitRate(),
|
||||
memoryUsage: memUsage.heapUsed,
|
||||
cpuUsage: this.getCPUUsage(),
|
||||
requestsPerSecond: this.calculateRPS(),
|
||||
averageLatency: this.calculateAverageLatency(),
|
||||
errorRate: this.calculateErrorRate()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate cache hit rate
|
||||
*/
|
||||
private calculateCacheHitRate(): number {
|
||||
const total = this.cacheHits + this.cacheMisses
|
||||
if (total === 0) return 0
|
||||
return this.cacheHits / total
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate requests per second
|
||||
*/
|
||||
private calculateRPS(): number {
|
||||
const now = Date.now()
|
||||
const recentRequests = this.metricsWindow.filter(
|
||||
timestamp => now - timestamp < this.windowSize
|
||||
)
|
||||
return recentRequests.length / (this.windowSize / 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate average latency
|
||||
*/
|
||||
private calculateAverageLatency(): number {
|
||||
if (this.latencyWindow.length === 0) return 0
|
||||
const sum = this.latencyWindow.reduce((a, b) => a + b, 0)
|
||||
return sum / this.latencyWindow.length
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate error rate
|
||||
*/
|
||||
private calculateErrorRate(): number {
|
||||
if (this.requestCount === 0) return 0
|
||||
return this.errorCount / this.requestCount
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CPU usage (simplified)
|
||||
*/
|
||||
private getCPUUsage(): number {
|
||||
// Simplified CPU usage based on process time
|
||||
const usage = process.cpuUsage()
|
||||
const total = usage.user + usage.system
|
||||
const seconds = (Date.now() - this.startTime) / 1000
|
||||
return Math.min(100, (total / 1000000 / seconds) * 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean old entries from sliding windows
|
||||
*/
|
||||
private cleanWindows(): void {
|
||||
const now = Date.now()
|
||||
const cutoff = now - this.windowSize
|
||||
|
||||
this.metricsWindow = this.metricsWindow.filter(t => t > cutoff)
|
||||
|
||||
// Keep only recent latency measurements
|
||||
if (this.latencyWindow.length > 100) {
|
||||
this.latencyWindow = this.latencyWindow.slice(-100)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a request
|
||||
* @param latency - Request latency in milliseconds
|
||||
* @param error - Whether the request resulted in an error
|
||||
*/
|
||||
recordRequest(latency: number, error: boolean = false): void {
|
||||
this.requestCount++
|
||||
this.metricsWindow.push(Date.now())
|
||||
this.latencyWindow.push(latency)
|
||||
|
||||
if (error) {
|
||||
this.errorCount++
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record cache access
|
||||
* @param hit - Whether it was a cache hit
|
||||
*/
|
||||
recordCacheAccess(hit: boolean): void {
|
||||
if (hit) {
|
||||
this.cacheHits++
|
||||
} else {
|
||||
this.cacheMisses++
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update vector count
|
||||
* @param count - New vector count
|
||||
*/
|
||||
updateVectorCount(count: number): void {
|
||||
this.vectorCount = count
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current health status
|
||||
* @returns Health status object
|
||||
*/
|
||||
getHealthStatus(): HealthStatus {
|
||||
const metrics = this.collectMetrics()
|
||||
const uptime = Date.now() - this.startTime
|
||||
const warnings: string[] = []
|
||||
const errors: string[] = []
|
||||
|
||||
// Check for warnings
|
||||
if (metrics.memoryUsage > 1024 * 1024 * 1024) { // > 1GB
|
||||
warnings.push('High memory usage detected')
|
||||
}
|
||||
|
||||
if (metrics.cacheHitRate < 0.5) {
|
||||
warnings.push('Low cache hit rate')
|
||||
}
|
||||
|
||||
if (metrics.errorRate && metrics.errorRate > 0.05) {
|
||||
warnings.push('High error rate detected')
|
||||
}
|
||||
|
||||
if (metrics.averageLatency && metrics.averageLatency > 1000) {
|
||||
warnings.push('High latency detected')
|
||||
}
|
||||
|
||||
// Check for errors
|
||||
if (metrics.memoryUsage > 2 * 1024 * 1024 * 1024) { // > 2GB
|
||||
errors.push('Critical memory usage')
|
||||
}
|
||||
|
||||
if (metrics.errorRate && metrics.errorRate > 0.2) {
|
||||
errors.push('Critical error rate')
|
||||
}
|
||||
|
||||
// Determine overall status
|
||||
let status: 'healthy' | 'degraded' | 'unhealthy' = 'healthy'
|
||||
if (errors.length > 0) {
|
||||
status = 'unhealthy'
|
||||
} else if (warnings.length > 0) {
|
||||
status = 'degraded'
|
||||
}
|
||||
|
||||
return {
|
||||
status,
|
||||
instanceId: this.configManager.getInstanceId(),
|
||||
role: this.configManager.getRole(),
|
||||
uptime,
|
||||
lastCheck: new Date().toISOString(),
|
||||
metrics,
|
||||
warnings: warnings.length > 0 ? warnings : undefined,
|
||||
errors: errors.length > 0 ? errors : undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get health check endpoint data
|
||||
* @returns JSON-serializable health data
|
||||
*/
|
||||
getHealthEndpointData(): Record<string, any> {
|
||||
const status = this.getHealthStatus()
|
||||
|
||||
return {
|
||||
status: status.status,
|
||||
instanceId: status.instanceId,
|
||||
role: status.role,
|
||||
uptime: Math.floor(status.uptime / 1000), // Convert to seconds
|
||||
lastCheck: status.lastCheck,
|
||||
metrics: {
|
||||
vectorCount: status.metrics.vectorCount,
|
||||
cacheHitRate: Math.round(status.metrics.cacheHitRate * 100) / 100,
|
||||
memoryUsageMB: Math.round(status.metrics.memoryUsage / 1024 / 1024),
|
||||
cpuUsagePercent: Math.round(status.metrics.cpuUsage || 0),
|
||||
requestsPerSecond: Math.round(status.metrics.requestsPerSecond || 0),
|
||||
averageLatencyMs: Math.round(status.metrics.averageLatency || 0),
|
||||
errorRate: Math.round((status.metrics.errorRate || 0) * 100) / 100
|
||||
},
|
||||
warnings: status.warnings,
|
||||
errors: status.errors
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset metrics (useful for testing)
|
||||
*/
|
||||
resetMetrics(): void {
|
||||
this.requestCount = 0
|
||||
this.errorCount = 0
|
||||
this.totalLatency = 0
|
||||
this.cacheHits = 0
|
||||
this.cacheMisses = 0
|
||||
this.metricsWindow = []
|
||||
this.latencyWindow = []
|
||||
}
|
||||
}
|
||||
24
src/distributed/index.ts
Normal file
24
src/distributed/index.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* Distributed module exports
|
||||
*/
|
||||
|
||||
export { DistributedConfigManager } from './configManager.js'
|
||||
export { HashPartitioner, AffinityPartitioner } from './hashPartitioner.js'
|
||||
export {
|
||||
BaseOperationalMode,
|
||||
ReaderMode,
|
||||
WriterMode,
|
||||
HybridMode,
|
||||
OperationalModeFactory
|
||||
} from './operationalModes.js'
|
||||
export { DomainDetector } from './domainDetector.js'
|
||||
export { HealthMonitor } from './healthMonitor.js'
|
||||
|
||||
export type {
|
||||
HealthMetrics,
|
||||
HealthStatus
|
||||
} from './healthMonitor.js'
|
||||
|
||||
export type {
|
||||
DomainPattern
|
||||
} from './domainDetector.js'
|
||||
220
src/distributed/operationalModes.ts
Normal file
220
src/distributed/operationalModes.ts
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
/**
|
||||
* Operational Modes for Distributed Brainy
|
||||
* Defines different modes with optimized caching strategies
|
||||
*/
|
||||
|
||||
import {
|
||||
OperationalMode,
|
||||
CacheStrategy,
|
||||
InstanceRole
|
||||
} from '../types/distributedTypes.js'
|
||||
|
||||
/**
|
||||
* Base operational mode
|
||||
*/
|
||||
export abstract class BaseOperationalMode implements OperationalMode {
|
||||
abstract canRead: boolean
|
||||
abstract canWrite: boolean
|
||||
abstract canDelete: boolean
|
||||
abstract cacheStrategy: CacheStrategy
|
||||
|
||||
/**
|
||||
* Validate operation is allowed in this mode
|
||||
*/
|
||||
validateOperation(operation: 'read' | 'write' | 'delete'): void {
|
||||
switch (operation) {
|
||||
case 'read':
|
||||
if (!this.canRead) {
|
||||
throw new Error('Read operations are not allowed in write-only mode')
|
||||
}
|
||||
break
|
||||
case 'write':
|
||||
if (!this.canWrite) {
|
||||
throw new Error('Write operations are not allowed in read-only mode')
|
||||
}
|
||||
break
|
||||
case 'delete':
|
||||
if (!this.canDelete) {
|
||||
throw new Error('Delete operations are not allowed in this mode')
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read-only mode optimized for query performance
|
||||
*/
|
||||
export class ReaderMode extends BaseOperationalMode {
|
||||
canRead = true
|
||||
canWrite = false
|
||||
canDelete = false
|
||||
|
||||
cacheStrategy: CacheStrategy = {
|
||||
hotCacheRatio: 0.8, // 80% of memory for read cache
|
||||
prefetchAggressive: true, // Aggressively prefetch related vectors
|
||||
ttl: 3600000, // 1 hour cache TTL
|
||||
compressionEnabled: true, // Trade CPU for more cache capacity
|
||||
writeBufferSize: 0, // No write buffer needed
|
||||
batchWrites: false, // No writes
|
||||
adaptive: true // Adapt to query patterns
|
||||
}
|
||||
|
||||
/**
|
||||
* Get optimized cache configuration for readers
|
||||
*/
|
||||
getCacheConfig() {
|
||||
return {
|
||||
hotCacheMaxSize: 1000000, // Large hot cache
|
||||
hotCacheEvictionThreshold: 0.9, // Keep cache full
|
||||
warmCacheTTL: 3600000, // 1 hour warm cache
|
||||
batchSize: 100, // Large batch reads
|
||||
autoTune: true, // Auto-tune for read patterns
|
||||
autoTuneInterval: 60000, // Tune every minute
|
||||
readOnly: true // Enable read-only optimizations
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write-only mode optimized for ingestion
|
||||
*/
|
||||
export class WriterMode extends BaseOperationalMode {
|
||||
canRead = false
|
||||
canWrite = true
|
||||
canDelete = true
|
||||
|
||||
cacheStrategy: CacheStrategy = {
|
||||
hotCacheRatio: 0.2, // Only 20% for cache, rest for write buffer
|
||||
prefetchAggressive: false, // No prefetching needed
|
||||
ttl: 60000, // Short TTL (1 minute)
|
||||
compressionEnabled: false, // Speed over memory efficiency
|
||||
writeBufferSize: 10000, // Large write buffer for batching
|
||||
batchWrites: true, // Enable write batching
|
||||
adaptive: false // Fixed strategy for consistent writes
|
||||
}
|
||||
|
||||
/**
|
||||
* Get optimized cache configuration for writers
|
||||
*/
|
||||
getCacheConfig() {
|
||||
return {
|
||||
hotCacheMaxSize: 100000, // Small hot cache
|
||||
hotCacheEvictionThreshold: 0.5, // Aggressive eviction
|
||||
warmCacheTTL: 60000, // 1 minute warm cache
|
||||
batchSize: 1000, // Large batch writes
|
||||
autoTune: false, // Fixed configuration
|
||||
writeOnly: true // Enable write-only optimizations
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hybrid mode that can both read and write
|
||||
*/
|
||||
export class HybridMode extends BaseOperationalMode {
|
||||
canRead = true
|
||||
canWrite = true
|
||||
canDelete = true
|
||||
|
||||
cacheStrategy: CacheStrategy = {
|
||||
hotCacheRatio: 0.5, // Balanced cache/buffer allocation
|
||||
prefetchAggressive: false, // Moderate prefetching
|
||||
ttl: 600000, // 10 minute TTL
|
||||
compressionEnabled: true, // Compress when beneficial
|
||||
writeBufferSize: 5000, // Moderate write buffer
|
||||
batchWrites: true, // Batch writes when possible
|
||||
adaptive: true // Adapt to workload mix
|
||||
}
|
||||
|
||||
private readWriteRatio: number = 0.5 // Track read/write ratio
|
||||
|
||||
/**
|
||||
* Get balanced cache configuration
|
||||
*/
|
||||
getCacheConfig() {
|
||||
return {
|
||||
hotCacheMaxSize: 500000, // Medium cache size
|
||||
hotCacheEvictionThreshold: 0.7, // Balanced eviction
|
||||
warmCacheTTL: 600000, // 10 minute warm cache
|
||||
batchSize: 500, // Medium batch size
|
||||
autoTune: true, // Auto-tune based on workload
|
||||
autoTuneInterval: 300000 // Tune every 5 minutes
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update cache strategy based on workload
|
||||
* @param readCount - Number of recent reads
|
||||
* @param writeCount - Number of recent writes
|
||||
*/
|
||||
updateWorkloadBalance(readCount: number, writeCount: number): void {
|
||||
const total = readCount + writeCount
|
||||
if (total === 0) return
|
||||
|
||||
this.readWriteRatio = readCount / total
|
||||
|
||||
// Adjust cache strategy based on workload
|
||||
if (this.readWriteRatio > 0.8) {
|
||||
// Read-heavy workload
|
||||
this.cacheStrategy.hotCacheRatio = 0.7
|
||||
this.cacheStrategy.prefetchAggressive = true
|
||||
this.cacheStrategy.writeBufferSize = 2000
|
||||
} else if (this.readWriteRatio < 0.2) {
|
||||
// Write-heavy workload
|
||||
this.cacheStrategy.hotCacheRatio = 0.3
|
||||
this.cacheStrategy.prefetchAggressive = false
|
||||
this.cacheStrategy.writeBufferSize = 8000
|
||||
} else {
|
||||
// Balanced workload
|
||||
this.cacheStrategy.hotCacheRatio = 0.5
|
||||
this.cacheStrategy.prefetchAggressive = false
|
||||
this.cacheStrategy.writeBufferSize = 5000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory for creating operational modes
|
||||
*/
|
||||
export class OperationalModeFactory {
|
||||
/**
|
||||
* Create operational mode based on role
|
||||
* @param role - The instance role
|
||||
* @returns The appropriate operational mode
|
||||
*/
|
||||
static createMode(role: InstanceRole): BaseOperationalMode {
|
||||
switch (role) {
|
||||
case 'reader':
|
||||
return new ReaderMode()
|
||||
case 'writer':
|
||||
return new WriterMode()
|
||||
case 'hybrid':
|
||||
return new HybridMode()
|
||||
default:
|
||||
// Default to reader for safety
|
||||
return new ReaderMode()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create mode with custom cache strategy
|
||||
* @param role - The instance role
|
||||
* @param customStrategy - Custom cache strategy overrides
|
||||
* @returns The operational mode with custom strategy
|
||||
*/
|
||||
static createModeWithStrategy(
|
||||
role: InstanceRole,
|
||||
customStrategy: Partial<CacheStrategy>
|
||||
): BaseOperationalMode {
|
||||
const mode = this.createMode(role)
|
||||
|
||||
// Apply custom strategy overrides
|
||||
mode.cacheStrategy = {
|
||||
...mode.cacheStrategy,
|
||||
...customStrategy
|
||||
}
|
||||
|
||||
return mode
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue