CRITICAL CHECKPOINT - DO NOT PUSH TO GITHUB Recovery Status: - Successfully recovered brainy.ts from compiled JavaScript - All core v3.0 API methods functional (add, get, update, delete, relate, find, etc.) - Neural subsystem intact (562KB embedded patterns, NLP working) - Augmentation pipeline operational (20+ augmentations) - HNSW clustering system complete - Triple Intelligence compiled (needs constructor fix) - Test suite validates functionality Changes preserved: - 898 files with changes from last 3 days - 144,475 insertions - All augmentation improvements - All test coverage enhancements - Complete v3.0 feature set This is a LOCAL checkpoint only - contains recovered work after corruption incident. Created backup in .backups/brainy-full-20250910-151314.tar.gz Branch: recovery-checkpoint-20250910-151433 Date: Wed Sep 10 03:18:04 PM PDT 2025
350 lines
No EOL
11 KiB
JavaScript
350 lines
No EOL
11 KiB
JavaScript
/**
|
|
* Read/Write Separation for Distributed Scaling
|
|
* Implements primary-replica architecture for scalable reads
|
|
*/
|
|
import { EventEmitter } from 'events';
|
|
/**
|
|
* Read/Write Separation Manager
|
|
*/
|
|
export class ReadWriteSeparation extends EventEmitter {
|
|
constructor(config, coordinator, _shardManager, cacheSync) {
|
|
super();
|
|
this.replicas = new Map();
|
|
this.isRunning = false;
|
|
this.config = config;
|
|
this.nodeId = config.nodeId;
|
|
this.role = config.role === 'auto' ? this.determineRole() : (config.role || 'replica');
|
|
this.coordinator = coordinator;
|
|
this.cacheSync = cacheSync;
|
|
this.replicationLog = {
|
|
operations: [],
|
|
lastSequence: 0,
|
|
primaryVersion: 0
|
|
};
|
|
// Setup connections based on role
|
|
this.setupConnections();
|
|
}
|
|
/**
|
|
* Start read/write separation
|
|
*/
|
|
async start() {
|
|
if (this.isRunning)
|
|
return;
|
|
this.isRunning = true;
|
|
// Start components
|
|
await this.coordinator.start();
|
|
// Setup role-specific behavior
|
|
if (this.role === 'primary') {
|
|
this.startAsPrimary();
|
|
}
|
|
else {
|
|
this.startAsReplica();
|
|
}
|
|
this.emit('started', { nodeId: this.nodeId, role: this.role });
|
|
}
|
|
/**
|
|
* Stop read/write separation
|
|
*/
|
|
async stop() {
|
|
if (!this.isRunning)
|
|
return;
|
|
this.isRunning = false;
|
|
if (this.syncTimer) {
|
|
clearInterval(this.syncTimer);
|
|
this.syncTimer = undefined;
|
|
}
|
|
await this.coordinator.stop();
|
|
this.emit('stopped', { nodeId: this.nodeId });
|
|
}
|
|
/**
|
|
* Execute a write operation (primary only)
|
|
*/
|
|
async write(operation) {
|
|
if (this.role !== 'primary') {
|
|
// Forward to primary
|
|
if (this.primaryConnection) {
|
|
return this.primaryConnection.forwardWrite(operation);
|
|
}
|
|
throw new Error('Cannot write: not connected to primary');
|
|
}
|
|
// Generate operation metadata
|
|
const writeOp = {
|
|
id: this.generateOperationId(),
|
|
...operation,
|
|
timestamp: Date.now(),
|
|
version: ++this.replicationLog.primaryVersion
|
|
};
|
|
// Add to replication log
|
|
this.replicationLog.operations.push(writeOp);
|
|
this.replicationLog.lastSequence++;
|
|
// Propagate to replicas
|
|
this.propagateToReplicas(writeOp);
|
|
// Update cache
|
|
if (operation.type !== 'delete') {
|
|
this.cacheSync.set(writeOp.id, operation.data);
|
|
}
|
|
else {
|
|
this.cacheSync.delete(writeOp.id);
|
|
}
|
|
this.emit('write', writeOp);
|
|
return writeOp.id;
|
|
}
|
|
/**
|
|
* Execute a read operation
|
|
*/
|
|
async read(key, options) {
|
|
const consistency = options?.consistency || this.config.consistencyLevel || 'eventual';
|
|
if (consistency === 'strong' && this.role === 'replica') {
|
|
// For strong consistency, read from primary
|
|
if (this.primaryConnection) {
|
|
return this.primaryConnection.read(key);
|
|
}
|
|
throw new Error('Cannot guarantee strong consistency: not connected to primary');
|
|
}
|
|
// Check cache first
|
|
const cached = this.cacheSync.get(key);
|
|
if (cached !== undefined) {
|
|
return cached;
|
|
}
|
|
// Read from appropriate source based on preference
|
|
if (this.config.readPreference === 'primary' && this.primaryConnection) {
|
|
return this.primaryConnection.read(key);
|
|
}
|
|
// Read locally (replica or primary)
|
|
return this.readLocal(key);
|
|
}
|
|
/**
|
|
* Get replication lag (replica only)
|
|
*/
|
|
getReplicationLag() {
|
|
if (this.role === 'primary')
|
|
return 0;
|
|
if (this.primaryConnection) {
|
|
return Date.now() - this.primaryConnection.lastSync;
|
|
}
|
|
return -1; // Unknown
|
|
}
|
|
/**
|
|
* Setup connections based on role
|
|
*/
|
|
setupConnections() {
|
|
if (this.role === 'primary') {
|
|
// Setup replica connections
|
|
if (this.config.replicaUrls) {
|
|
for (const url of this.config.replicaUrls) {
|
|
const replica = new ReplicaConnection(url);
|
|
this.replicas.set(url, replica);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// Setup primary connection
|
|
if (this.config.primaryUrl) {
|
|
this.primaryConnection = new PrimaryConnection(this.config.primaryUrl);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Start as primary node
|
|
*/
|
|
startAsPrimary() {
|
|
// Start accepting writes
|
|
this.emit('roleEstablished', { role: 'primary' });
|
|
// Start replication timer
|
|
this.syncTimer = setInterval(() => {
|
|
this.syncReplicas();
|
|
}, this.config.syncInterval || 1000);
|
|
}
|
|
/**
|
|
* Start as replica node
|
|
*/
|
|
startAsReplica() {
|
|
// Start syncing from primary
|
|
this.emit('roleEstablished', { role: 'replica' });
|
|
// Start sync timer
|
|
this.syncTimer = setInterval(() => {
|
|
this.syncFromPrimary();
|
|
}, this.config.syncInterval || 1000);
|
|
}
|
|
/**
|
|
* Sync replicas (primary only)
|
|
*/
|
|
async syncReplicas() {
|
|
const batch = this.replicationLog.operations.slice(-100); // Last 100 ops
|
|
for (const [url, replica] of this.replicas) {
|
|
try {
|
|
await replica.sync(batch, this.replicationLog.primaryVersion);
|
|
}
|
|
catch (error) {
|
|
this.emit('replicaSyncError', { url, error });
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Sync from primary (replica only)
|
|
*/
|
|
async syncFromPrimary() {
|
|
if (!this.primaryConnection)
|
|
return;
|
|
try {
|
|
const updates = await this.primaryConnection.getUpdates(this.replicationLog.lastSequence);
|
|
// Apply updates
|
|
for (const op of updates) {
|
|
await this.applyOperation(op);
|
|
}
|
|
this.emit('synced', { operations: updates.length });
|
|
}
|
|
catch (error) {
|
|
this.emit('primarySyncError', { error });
|
|
}
|
|
}
|
|
/**
|
|
* Apply a replicated operation
|
|
*/
|
|
async applyOperation(op) {
|
|
// Update local state
|
|
this.replicationLog.operations.push(op);
|
|
this.replicationLog.lastSequence = Math.max(this.replicationLog.lastSequence, op.version);
|
|
// Update cache
|
|
switch (op.type) {
|
|
case 'add':
|
|
case 'update':
|
|
this.cacheSync.set(op.id, op.data);
|
|
break;
|
|
case 'delete':
|
|
this.cacheSync.delete(op.id);
|
|
break;
|
|
}
|
|
this.emit('operationApplied', op);
|
|
}
|
|
/**
|
|
* Propagate operation to replicas
|
|
*/
|
|
propagateToReplicas(op) {
|
|
for (const replica of this.replicas.values()) {
|
|
replica.sendOperation(op).catch(error => {
|
|
this.emit('replicationError', { replica, error });
|
|
});
|
|
}
|
|
}
|
|
/**
|
|
* Determine role automatically
|
|
*/
|
|
determineRole() {
|
|
// Use coordinator's leader election
|
|
return this.coordinator.isLeader() ? 'primary' : 'replica';
|
|
}
|
|
/**
|
|
* Read from local storage
|
|
*/
|
|
async readLocal(key) {
|
|
// This would connect to actual storage
|
|
// For now, return from cache or undefined
|
|
return this.cacheSync.get(key);
|
|
}
|
|
/**
|
|
* Generate unique operation ID
|
|
*/
|
|
generateOperationId() {
|
|
return `${this.nodeId}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
}
|
|
/**
|
|
* Get replication statistics
|
|
*/
|
|
getStats() {
|
|
return {
|
|
role: this.role,
|
|
replicas: this.replicas.size,
|
|
replicationLag: this.getReplicationLag(),
|
|
operationsInLog: this.replicationLog.operations.length,
|
|
primaryVersion: this.replicationLog.primaryVersion
|
|
};
|
|
}
|
|
/**
|
|
* Check if node can accept writes
|
|
*/
|
|
canWrite() {
|
|
return this.role === 'primary';
|
|
}
|
|
/**
|
|
* Check if node can serve reads
|
|
*/
|
|
canRead() {
|
|
if (this.config.consistencyLevel === 'strong') {
|
|
return this.role === 'primary' || this.primaryConnection !== undefined;
|
|
}
|
|
return true;
|
|
}
|
|
/**
|
|
* Set whether this node is primary (for leader election integration)
|
|
*/
|
|
setPrimary(isPrimary) {
|
|
const newRole = isPrimary ? 'primary' : 'replica';
|
|
if (this.role !== newRole) {
|
|
this.role = newRole;
|
|
this.emit('roleChange', { oldRole: this.role, newRole });
|
|
if (isPrimary) {
|
|
// Became primary - stop syncing from old primary
|
|
this.primaryConnection = undefined;
|
|
}
|
|
else {
|
|
// Became replica - connect to new primary if URL is known
|
|
if (this.config.primaryUrl) {
|
|
this.primaryConnection = new PrimaryConnection(this.config.primaryUrl);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Connection to a replica (used by primary)
|
|
*/
|
|
class ReplicaConnection {
|
|
constructor(url) {
|
|
this.url = url;
|
|
// Store URL for connection
|
|
void this.url;
|
|
}
|
|
async sync(_operations, _version) {
|
|
// In real implementation, this would use HTTP/gRPC to this.url
|
|
// For now, simulate network call
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
}
|
|
async sendOperation(_op) {
|
|
// Send single operation to replica at this.url
|
|
await new Promise(resolve => setTimeout(resolve, 5));
|
|
}
|
|
}
|
|
/**
|
|
* Connection to primary (used by replicas)
|
|
*/
|
|
class PrimaryConnection {
|
|
constructor(url) {
|
|
this.url = url;
|
|
this.lastSync = Date.now();
|
|
// Store URL for connection
|
|
void this.url;
|
|
}
|
|
async getUpdates(_fromSequence) {
|
|
// In real implementation, fetch from primary at this.url
|
|
this.lastSync = Date.now();
|
|
return [];
|
|
}
|
|
async forwardWrite(_operation) {
|
|
// Forward write to primary at this.url
|
|
await new Promise(resolve => setTimeout(resolve, 20));
|
|
return `forwarded-${Date.now()}`;
|
|
}
|
|
async read(_key) {
|
|
// Read from primary at this.url for strong consistency
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
return undefined;
|
|
}
|
|
}
|
|
/**
|
|
* Create read/write separation manager
|
|
*/
|
|
export function createReadWriteSeparation(config, coordinator, shardManager, cacheSync) {
|
|
return new ReadWriteSeparation(config, coordinator, shardManager, cacheSync);
|
|
}
|
|
//# sourceMappingURL=readWriteSeparation.js.map
|