brainy/src/utils/writeBuffer.ts

411 lines
11 KiB
TypeScript
Raw Normal View History

/**
* Write Buffer
* Accumulates writes and flushes them in bulk to reduce S3 operations
* Implements intelligent deduplication and compression
*/
import { HNSWNoun, HNSWVerb } from '../coreTypes.js'
import { createModuleLogger } from './logger.js'
import { getGlobalBackpressure } from './adaptiveBackpressure.js'
interface BufferedWrite<T> {
id: string
data: T
timestamp: number
type: 'noun' | 'verb' | 'metadata'
retryCount: number
}
interface FlushResult {
successful: number
failed: number
duration: number
}
/**
* High-performance write buffer for bulk operations
*/
export class WriteBuffer<T> {
private logger = createModuleLogger('WriteBuffer')
// Buffer storage
private buffer = new Map<string, BufferedWrite<T>>()
// Configuration - More aggressive for high volume
private maxBufferSize = 2000 // Allow larger buffers
private flushInterval = 500 // Flush more frequently (0.5 seconds)
private minFlushSize = 50 // Lower minimum to flush sooner
private maxRetries = 3 // Maximum retry attempts
// State
private flushTimer: NodeJS.Timeout | null = null
private isFlushing = false
private lastFlush = Date.now()
private pendingFlush: Promise<FlushResult> | null = null
// Statistics
private totalWrites = 0
private totalFlushes = 0
private failedWrites = 0
private duplicatesRemoved = 0
// Write function
private writeFunction: (items: Map<string, T>) => Promise<void>
private type: 'noun' | 'verb' | 'metadata'
// Backpressure integration
private backpressure = getGlobalBackpressure()
constructor(
type: 'noun' | 'verb' | 'metadata',
writeFunction: (items: Map<string, T>) => Promise<void>,
options?: {
maxBufferSize?: number
flushInterval?: number
minFlushSize?: number
}
) {
this.type = type
this.writeFunction = writeFunction
if (options) {
this.maxBufferSize = options.maxBufferSize || this.maxBufferSize
this.flushInterval = options.flushInterval || this.flushInterval
this.minFlushSize = options.minFlushSize || this.minFlushSize
}
// Start periodic flush
this.startPeriodicFlush()
}
/**
* Add item to buffer
*/
public async add(id: string, data: T): Promise<void> {
// Check if we're already at capacity
if (this.buffer.size >= this.maxBufferSize) {
// Wait for current flush to complete
if (this.pendingFlush) {
await this.pendingFlush
}
// Force flush if still at capacity
if (this.buffer.size >= this.maxBufferSize) {
await this.flush('capacity')
}
}
// Check for duplicate and update if newer
const existing = this.buffer.get(id)
if (existing) {
// Update with newer data
existing.data = data
existing.timestamp = Date.now()
this.duplicatesRemoved++
} else {
// Add new item
this.buffer.set(id, {
id,
data,
timestamp: Date.now(),
type: this.type,
retryCount: 0
})
}
this.totalWrites++
// Log buffer growth periodically
if (this.totalWrites % 100 === 0) {
this.logger.info(`📈 BUFFER GROWTH: ${this.buffer.size} ${this.type} items buffered (${this.totalWrites} total writes, ${this.duplicatesRemoved} deduplicated)`)
}
// Check if we should flush
this.checkFlush()
}
/**
* Check if we should flush
*/
private checkFlush(): void {
const bufferSize = this.buffer.size
const timeSinceFlush = Date.now() - this.lastFlush
// Immediate flush conditions
if (bufferSize >= this.maxBufferSize) {
this.flush('size')
return
}
// Time-based flush with minimum size
if (timeSinceFlush >= this.flushInterval && bufferSize >= this.minFlushSize) {
this.flush('time')
return
}
// Adaptive flush based on system load
const backpressureStatus = this.backpressure.getStatus()
if (backpressureStatus.queueLength > 1000 && bufferSize > 10) {
// System under pressure - flush smaller batches more frequently
this.flush('pressure')
}
}
/**
* Flush buffer to storage
*/
public async flush(reason: string = 'manual'): Promise<FlushResult> {
// Prevent concurrent flushes
if (this.isFlushing) {
if (this.pendingFlush) {
return this.pendingFlush
}
return { successful: 0, failed: 0, duration: 0 }
}
// Nothing to flush
if (this.buffer.size === 0) {
return { successful: 0, failed: 0, duration: 0 }
}
this.isFlushing = true
const startTime = Date.now()
// Create flush promise
this.pendingFlush = this.doFlush(reason, startTime)
try {
const result = await this.pendingFlush
return result
} finally {
this.isFlushing = false
this.pendingFlush = null
}
}
/**
* Perform the actual flush
*/
private async doFlush(reason: string, startTime: number): Promise<FlushResult> {
const itemsToFlush = new Map<string, T>()
const flushingItems = new Map<string, BufferedWrite<T>>()
// Take items from buffer
let count = 0
for (const [id, item] of this.buffer.entries()) {
itemsToFlush.set(id, item.data)
flushingItems.set(id, item)
count++
// Limit batch size for better performance
if (count >= 500) {
break
}
}
// Remove from buffer
for (const id of itemsToFlush.keys()) {
this.buffer.delete(id)
}
this.logger.warn(`🔄 BUFFERING: Flushing ${itemsToFlush.size} ${this.type} items (buffer had ${this.buffer.size + itemsToFlush.size}) - reason: ${reason}`)
try {
// Request permission from backpressure system
const opId = `flush-${Date.now()}`
await this.backpressure.requestPermission(opId, 2) // Higher priority
try {
// Perform bulk write
await this.writeFunction(itemsToFlush)
// Success
this.backpressure.releasePermission(opId, true)
this.totalFlushes++
this.lastFlush = Date.now()
const duration = Date.now() - startTime
this.logger.warn(`🚀 BATCH FLUSH: ${itemsToFlush.size} ${this.type} items → 1 bulk S3 operation (${duration}ms, reason: ${reason})`)
return {
successful: itemsToFlush.size,
failed: 0,
duration
}
} catch (error) {
// Release with error
this.backpressure.releasePermission(opId, false)
throw error
}
} catch (error) {
this.logger.error(`Flush failed: ${error}`)
// Put items back with retry count
for (const [id, item] of flushingItems.entries()) {
item.retryCount++
if (item.retryCount < this.maxRetries) {
// Put back for retry
this.buffer.set(id, item)
} else {
// Max retries exceeded
this.failedWrites++
this.logger.error(`Max retries exceeded for ${this.type} ${id}`)
}
}
const duration = Date.now() - startTime
return {
successful: 0,
failed: itemsToFlush.size,
duration
}
}
}
/**
* Start periodic flush timer
*/
private startPeriodicFlush(): void {
if (this.flushTimer) {
return
}
this.flushTimer = setInterval(() => {
if (this.buffer.size > 0) {
const timeSinceFlush = Date.now() - this.lastFlush
// Flush if we have items and enough time has passed
if (timeSinceFlush >= this.flushInterval) {
this.flush('periodic').catch(error => {
this.logger.error('Periodic flush failed:', error)
})
}
}
}, Math.min(100, this.flushInterval / 2))
}
/**
* Stop periodic flush timer
*/
public stop(): void {
if (this.flushTimer) {
clearInterval(this.flushTimer)
this.flushTimer = null
}
}
/**
* Force flush all pending writes
*/
public async forceFlush(): Promise<FlushResult> {
// Flush everything regardless of size
const oldMinSize = this.minFlushSize
this.minFlushSize = 0
try {
const result = await this.flush('force')
// Flush any remaining items
while (this.buffer.size > 0) {
const additionalResult = await this.flush('force-remaining')
result.successful += additionalResult.successful
result.failed += additionalResult.failed
result.duration += additionalResult.duration
}
return result
} finally {
this.minFlushSize = oldMinSize
}
}
/**
* Get buffer statistics
*/
public getStats(): {
bufferSize: number
totalWrites: number
totalFlushes: number
failedWrites: number
duplicatesRemoved: number
avgFlushSize: number
} {
return {
bufferSize: this.buffer.size,
totalWrites: this.totalWrites,
totalFlushes: this.totalFlushes,
failedWrites: this.failedWrites,
duplicatesRemoved: this.duplicatesRemoved,
avgFlushSize: this.totalFlushes > 0 ? this.totalWrites / this.totalFlushes : 0
}
}
/**
* Adjust parameters based on load
*/
public adjustForLoad(pendingRequests: number): void {
if (pendingRequests > 10000) {
// Extreme load - buffer more aggressively
this.maxBufferSize = 5000
this.flushInterval = 500
this.minFlushSize = 500
} else if (pendingRequests > 1000) {
// High load
this.maxBufferSize = 2000
this.flushInterval = 1000
this.minFlushSize = 200
} else if (pendingRequests > 100) {
// Moderate load
this.maxBufferSize = 1000
this.flushInterval = 2000
this.minFlushSize = 100
} else {
// Low load - optimize for latency
this.maxBufferSize = 500
this.flushInterval = 5000
this.minFlushSize = 50
}
}
}
// Global write buffers
const writeBuffers = new Map<string, WriteBuffer<any>>()
/**
* Get or create a write buffer
*/
export function getWriteBuffer<T>(
id: string,
type: 'noun' | 'verb' | 'metadata',
writeFunction: (items: Map<string, T>) => Promise<void>
): WriteBuffer<T> {
if (!writeBuffers.has(id)) {
writeBuffers.set(id, new WriteBuffer<T>(type, writeFunction))
}
return writeBuffers.get(id)!
}
/**
* Flush all write buffers
*/
export async function flushAllBuffers(): Promise<void> {
const promises: Promise<FlushResult>[] = []
for (const buffer of writeBuffers.values()) {
promises.push(buffer.forceFlush())
}
await Promise.all(promises)
}
/**
* Clear all write buffers
*/
export function clearWriteBuffers(): void {
for (const buffer of writeBuffers.values()) {
buffer.stop()
}
writeBuffers.clear()
}