Fix data race in OptimisticAllocator during parallel COPY - #949
Conversation
LocalStorage caches one OptimisticAllocator per StorageManager and shares it across all COPY worker threads, but allocatePageRange() mutated the optimisticallyAllocatedPages vector without synchronization. Concurrent push_backs corrupted the heap, crashing later mallocs (e.g. partitioner merge during parallel rel COPY). Guard the tracked page ranges with a mutex; the underlying PageManager is already internally synchronized.
Design note: why shared + locked, not thread-localConstraint — one worker writes to many files. Why a mutex is proportionate here. The hot per-row path (appends into chunked groups) is already thread-local per worker — TSan confirmed the allocator vector was the only shared state. The lock is taken once per flushed column chunk (once per 2048 rows per column; ~18k acquisitions over a multi-minute load), each critical section a vector push (tens of ns, one cache line). No measurable contention or scalability impact at this granularity. A per-(worker x file) refactor would restore zero-sharing at the cost of touching all three flush sites (node plain, node partitioned, rel) plus lifecycle management for N x M allocator objects. Left as a possible follow-up; deliberately out of scope for this crash fix. |
Symptom
Parallel COPY (16 workers) intermittently aborts with a glibc
sysmallocassertion. The crashing thread is typically inside an unrelated latermalloc— e.g.InMemChunkedNodeGroupCollection::mergegrowingchunkedGroupsduring the rel partitioner merge — while another worker waits on the merge mutex. Classic use-after-corruption: the heap was already trashed by an earlier bad write.Root cause
Blame: ba66140
LocalStorage::addOptimisticAllocator()caches oneOptimisticAllocatorper StorageManager and hands the same instance to every COPY worker thread (node and rel paths). ButOptimisticAllocator::allocatePageRange()did an unsynchronizedpush_backontooptimisticallyAllocatedPages, despite the class being documented as thread-local-only. Concurrent vector mutation corrupts the heap; the crash detonates at the next malloc that touches it.ThreadSanitizer report (TSan + BM_MALLOC build, LSQB SF=1 load):
Fix
Synchronize the internally-tracked page ranges with a mutex in
allocatePageRange(),rollback()andcommit(). The underlyingPageManager::allocatePageRangeis already internally synchronized, so this is the only unprotected layer. Allocation happens per flushed chunk group, so lock contention is negligible. Also updates the stale thread-local docstring.Verification