Concurrency Model & Design Decisions
This document outlines the concurrency model of TangoDB and summarizes the core design decisions and tradeoffs made during V1 development.
Concurrency Model
TangoDB is designed for highly concurrent multi-threaded workloads where multiple application threads concurrently invoke put(), get(), and delete() operations while background threads handle disk flushing and compaction.
Client Thread 1 ──┐
Client Thread 2 ──┼──► [ Active MemTable ] ──► Atomic Freeze ──► [ Async Flush Queue ]
Client Thread N ──┘ │ │
│ ▼
Client Reader Threads ─────────┴──────────────────────────────► [ SstManager / Disk ]
Key Concurrency Mechanics
-
Lock-Free Active Ingest: Active
MemTableinsertions use Java's lock-freeConcurrentSkipListMapfor index updates paired with atomic native memory allocation offset increments. -
Atomic MemTable Freeze & Swap: When the active
MemTablereaches its memory limit (memTableSize), an atomic reference swap replaces the active table with a freshMemTable. Write threads experience zero blocking locks. -
Immutable Reader Isolation: Frozen
ImmutableMemTables and written SSTables are strictly read-only. Reader threads can safely search active tables, frozen tables, and SSTables concurrently without acquiring read-locks. -
Background Flush & Compaction Isolation: Disk I/O is offloaded to dedicated background thread pools (
FlushManagerandCompactionManager), ensuring client write latencies are completely decoupled from disk speed.
Summary of Key Design Decisions
1. Off-Heap FFM API vs. On-Heap Java Objects
- Decision: Store record byte payloads off-heap using JDK 22+ FFM API (
MemorySegment&Arena) while maintaining a lightweight on-heap index (ConcurrentSkipListMap). - Why: Eliminates JVM Garbage Collection pauses during heavy write traffic.
2. Fixed-Size 64 MiB Arenas vs. Dynamic Sizing
- Decision: Allocate native memory blocks in fixed 64 MiB
Arenaunits perMemTable. - Why: Simplifies native memory fragmentation management and enables deterministic block deallocation upon flushing.
3. Memory-Mapped File I/O vs. Traditional Byte Streams
- Decision: Use
FileChannel.mapwrapped in FFMMemorySegmentfor SSTable reading (SstReader). - Why: Offloads OS page caching to the kernel and enables zero-copy native memory slicing during record lookups.
4. Lock-Free Atomic Table Swapping vs. Coarse Synchronization
- Decision: Use lock-free state transitions when freezing full
MemTables. - Why: Prevents write thread stalls during high-concurrency write surges.