Browse Source
- Increase Badger cache defaults: block 512→1024MB, index 256→512MB - Increase serial cache defaults: pubkeys 100k→250k, event IDs 500k→1M - Change ZSTD default from level 1 (fast) to level 3 (balanced) - Add memory-only rate limiter for BBolt backend - Add BBolt to database backend docs with scaling recommendations - Document migration between Badger and BBolt backends Files modified: - app/config/config.go: Tuned defaults for large-scale deployments - main.go: Add BBolt rate limiter support - pkg/ratelimit/factory.go: Add NewMemoryOnlyLimiter factory - pkg/ratelimit/memory_monitor.go: New memory-only load monitor - CLAUDE.md: Add BBolt docs and scaling guide 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>main v0.48.12
6 changed files with 286 additions and 11 deletions
@ -0,0 +1,214 @@ |
|||||||
|
//go:build !(js && wasm)
|
||||||
|
|
||||||
|
package ratelimit |
||||||
|
|
||||||
|
import ( |
||||||
|
"sync" |
||||||
|
"sync/atomic" |
||||||
|
"time" |
||||||
|
|
||||||
|
"next.orly.dev/pkg/interfaces/loadmonitor" |
||||||
|
) |
||||||
|
|
||||||
|
// MemoryMonitor is a simple load monitor that only tracks process memory.
|
||||||
|
// Used for database backends that don't have their own load metrics (e.g., BBolt).
|
||||||
|
type MemoryMonitor struct { |
||||||
|
// Configuration
|
||||||
|
pollInterval time.Duration |
||||||
|
targetBytes atomic.Uint64 |
||||||
|
|
||||||
|
// State
|
||||||
|
running atomic.Bool |
||||||
|
stopChan chan struct{} |
||||||
|
doneChan chan struct{} |
||||||
|
|
||||||
|
// Metrics (protected by mutex)
|
||||||
|
mu sync.RWMutex |
||||||
|
currentMetrics loadmonitor.Metrics |
||||||
|
|
||||||
|
// Latency tracking
|
||||||
|
queryLatencies []time.Duration |
||||||
|
writeLatencies []time.Duration |
||||||
|
latencyMu sync.Mutex |
||||||
|
|
||||||
|
// Emergency mode
|
||||||
|
emergencyThreshold float64 // e.g., 1.167 (target + 1/6)
|
||||||
|
recoveryThreshold float64 // e.g., 0.833 (target - 1/6)
|
||||||
|
inEmergency atomic.Bool |
||||||
|
} |
||||||
|
|
||||||
|
// NewMemoryMonitor creates a memory-only load monitor.
|
||||||
|
// pollInterval controls how often memory is sampled (recommended: 100ms).
|
||||||
|
func NewMemoryMonitor(pollInterval time.Duration) *MemoryMonitor { |
||||||
|
m := &MemoryMonitor{ |
||||||
|
pollInterval: pollInterval, |
||||||
|
stopChan: make(chan struct{}), |
||||||
|
doneChan: make(chan struct{}), |
||||||
|
queryLatencies: make([]time.Duration, 0, 100), |
||||||
|
writeLatencies: make([]time.Duration, 0, 100), |
||||||
|
emergencyThreshold: 1.167, // Default: target + 1/6
|
||||||
|
recoveryThreshold: 0.833, // Default: target - 1/6
|
||||||
|
} |
||||||
|
return m |
||||||
|
} |
||||||
|
|
||||||
|
// GetMetrics returns the current load metrics.
|
||||||
|
func (m *MemoryMonitor) GetMetrics() loadmonitor.Metrics { |
||||||
|
m.mu.RLock() |
||||||
|
defer m.mu.RUnlock() |
||||||
|
return m.currentMetrics |
||||||
|
} |
||||||
|
|
||||||
|
// RecordQueryLatency records a query latency sample.
|
||||||
|
func (m *MemoryMonitor) RecordQueryLatency(latency time.Duration) { |
||||||
|
m.latencyMu.Lock() |
||||||
|
defer m.latencyMu.Unlock() |
||||||
|
|
||||||
|
m.queryLatencies = append(m.queryLatencies, latency) |
||||||
|
if len(m.queryLatencies) > 100 { |
||||||
|
m.queryLatencies = m.queryLatencies[1:] |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// RecordWriteLatency records a write latency sample.
|
||||||
|
func (m *MemoryMonitor) RecordWriteLatency(latency time.Duration) { |
||||||
|
m.latencyMu.Lock() |
||||||
|
defer m.latencyMu.Unlock() |
||||||
|
|
||||||
|
m.writeLatencies = append(m.writeLatencies, latency) |
||||||
|
if len(m.writeLatencies) > 100 { |
||||||
|
m.writeLatencies = m.writeLatencies[1:] |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// SetMemoryTarget sets the target memory limit in bytes.
|
||||||
|
func (m *MemoryMonitor) SetMemoryTarget(bytes uint64) { |
||||||
|
m.targetBytes.Store(bytes) |
||||||
|
} |
||||||
|
|
||||||
|
// SetEmergencyThreshold sets the memory threshold for emergency mode.
|
||||||
|
func (m *MemoryMonitor) SetEmergencyThreshold(threshold float64) { |
||||||
|
m.mu.Lock() |
||||||
|
defer m.mu.Unlock() |
||||||
|
m.emergencyThreshold = threshold |
||||||
|
} |
||||||
|
|
||||||
|
// GetEmergencyThreshold returns the current emergency threshold.
|
||||||
|
func (m *MemoryMonitor) GetEmergencyThreshold() float64 { |
||||||
|
m.mu.RLock() |
||||||
|
defer m.mu.RUnlock() |
||||||
|
return m.emergencyThreshold |
||||||
|
} |
||||||
|
|
||||||
|
// ForceEmergencyMode manually triggers emergency mode for a duration.
|
||||||
|
func (m *MemoryMonitor) ForceEmergencyMode(duration time.Duration) { |
||||||
|
m.inEmergency.Store(true) |
||||||
|
go func() { |
||||||
|
time.Sleep(duration) |
||||||
|
m.inEmergency.Store(false) |
||||||
|
}() |
||||||
|
} |
||||||
|
|
||||||
|
// Start begins background metric collection.
|
||||||
|
func (m *MemoryMonitor) Start() <-chan struct{} { |
||||||
|
if m.running.Swap(true) { |
||||||
|
// Already running
|
||||||
|
return m.doneChan |
||||||
|
} |
||||||
|
|
||||||
|
go m.pollLoop() |
||||||
|
return m.doneChan |
||||||
|
} |
||||||
|
|
||||||
|
// Stop halts background metric collection.
|
||||||
|
func (m *MemoryMonitor) Stop() { |
||||||
|
if !m.running.Swap(false) { |
||||||
|
return |
||||||
|
} |
||||||
|
close(m.stopChan) |
||||||
|
<-m.doneChan |
||||||
|
} |
||||||
|
|
||||||
|
// pollLoop continuously samples memory and updates metrics.
|
||||||
|
func (m *MemoryMonitor) pollLoop() { |
||||||
|
defer close(m.doneChan) |
||||||
|
|
||||||
|
ticker := time.NewTicker(m.pollInterval) |
||||||
|
defer ticker.Stop() |
||||||
|
|
||||||
|
for { |
||||||
|
select { |
||||||
|
case <-m.stopChan: |
||||||
|
return |
||||||
|
case <-ticker.C: |
||||||
|
m.updateMetrics() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// updateMetrics samples current memory and updates the metrics.
|
||||||
|
func (m *MemoryMonitor) updateMetrics() { |
||||||
|
target := m.targetBytes.Load() |
||||||
|
if target == 0 { |
||||||
|
target = 1 // Avoid division by zero
|
||||||
|
} |
||||||
|
|
||||||
|
// Get physical memory using the same method as other monitors
|
||||||
|
procMem := ReadProcessMemoryStats() |
||||||
|
physicalMemBytes := procMem.PhysicalMemoryBytes() |
||||||
|
physicalMemMB := physicalMemBytes / (1024 * 1024) |
||||||
|
|
||||||
|
// Calculate memory pressure
|
||||||
|
memPressure := float64(physicalMemBytes) / float64(target) |
||||||
|
|
||||||
|
// Check emergency mode thresholds
|
||||||
|
m.mu.RLock() |
||||||
|
emergencyThreshold := m.emergencyThreshold |
||||||
|
recoveryThreshold := m.recoveryThreshold |
||||||
|
m.mu.RUnlock() |
||||||
|
|
||||||
|
wasEmergency := m.inEmergency.Load() |
||||||
|
if memPressure > emergencyThreshold { |
||||||
|
m.inEmergency.Store(true) |
||||||
|
} else if memPressure < recoveryThreshold && wasEmergency { |
||||||
|
m.inEmergency.Store(false) |
||||||
|
} |
||||||
|
|
||||||
|
// Calculate average latencies
|
||||||
|
m.latencyMu.Lock() |
||||||
|
var avgQuery, avgWrite time.Duration |
||||||
|
if len(m.queryLatencies) > 0 { |
||||||
|
var total time.Duration |
||||||
|
for _, l := range m.queryLatencies { |
||||||
|
total += l |
||||||
|
} |
||||||
|
avgQuery = total / time.Duration(len(m.queryLatencies)) |
||||||
|
} |
||||||
|
if len(m.writeLatencies) > 0 { |
||||||
|
var total time.Duration |
||||||
|
for _, l := range m.writeLatencies { |
||||||
|
total += l |
||||||
|
} |
||||||
|
avgWrite = total / time.Duration(len(m.writeLatencies)) |
||||||
|
} |
||||||
|
m.latencyMu.Unlock() |
||||||
|
|
||||||
|
// Update metrics
|
||||||
|
m.mu.Lock() |
||||||
|
m.currentMetrics = loadmonitor.Metrics{ |
||||||
|
MemoryPressure: memPressure, |
||||||
|
WriteLoad: 0, // No database-specific load metric
|
||||||
|
ReadLoad: 0, // No database-specific load metric
|
||||||
|
QueryLatency: avgQuery, |
||||||
|
WriteLatency: avgWrite, |
||||||
|
Timestamp: time.Now(), |
||||||
|
InEmergencyMode: m.inEmergency.Load(), |
||||||
|
CompactionPending: false, // BBolt doesn't have compaction
|
||||||
|
PhysicalMemoryMB: physicalMemMB, |
||||||
|
} |
||||||
|
m.mu.Unlock() |
||||||
|
} |
||||||
|
|
||||||
|
// Ensure MemoryMonitor implements the required interfaces
|
||||||
|
var _ loadmonitor.Monitor = (*MemoryMonitor)(nil) |
||||||
|
var _ loadmonitor.EmergencyModeMonitor = (*MemoryMonitor)(nil) |
||||||
Loading…
Reference in new issue