Skip to content

OAK-12450: Fix concurrency and memory-retention issues in ThreadSpecificSegmentBufferWriterPool - #3205

Open
lweitzendorf wants to merge 1 commit into
apache:trunkfrom
lweitzendorf:issue/OAK-12450
Open

lweitzendorf wants to merge 1 commit into
apache:trunkfrom
lweitzendorf:issue/OAK-12450

Conversation

@lweitzendorf

Copy link
Copy Markdown
Contributor

https://issues.apache.org/jira/browse/OAK-12450

Summary

Fixes three issues in ThreadSpecificSegmentBufferWriterPool:

  • Read lock leak: execute() obtained the per-thread writer before entering try/finally. If creating the writer threw, the read lock was never released and a later flush() blocked forever. The writer is now obtained inside try/finally.
  • Data race on writerId: writerId was a plain short incremented inside computeIfAbsent's mapping function, which can run concurrently for different keys. It is now an AtomicInteger updated with a bounded getAndUpdate.
  • Dead threads kept alive: the pool was keyed by Thread, retaining terminated threads and their ThreadLocals until the next flush(). It is now keyed by thread ID. IDs are unique among live threads, so concurrent execute() calls never share a writer; a recycled ID simply reuses a dead thread's writer.

Tests

New cases in SegmentBufferWriterPoolTest.

Hardens `ThreadSpecificSegmentBufferWriterPool` in `oak-segment-tar`.
Review surfaced two concurrency defects plus a memory-retention issue in
the read/write-lock based pool used on the segment write path.

### Changes
- **Read-lock leak (correctness).** `execute()` obtained the per-thread
writer *before* the `try`, so an exception while creating the writer
(e.g. inside `computeIfAbsent` / `newWriter`) leaked the read lock and
would block a later `flush()` (write lock) forever. The writer is now
fetched inside the `try/finally`.
- **`writerId` data race (correctness).** `writerId` was a plain `short`
mutated with `++`. In the thread-specific pool, `newWriter` runs inside
`computeIfAbsent`'s mapping function, which `ConcurrentHashMap` can
invoke concurrently for distinct keys — an unsynchronized
read-modify-write that can lose updates or produce duplicate writer ids.
Now an `AtomicInteger` with a bounded `getAndUpdate`.
- **Dead-thread pinning (memory).** The pool was keyed by the `Thread`
object, holding it (and its `ThreadLocal`s) alive until the next
`flush()`. Now keyed by thread id, so dead threads are collectable
immediately. Thread ids are unique among live threads, so concurrent
`execute()` calls never share a writer; a recycled id simply reuses a
dead thread's writer, which is safe.

### Not included
Avoiding the per-call key/lambda allocation in `getWriter` is deferred
pending profiling; the allocation is small relative to the lock acquire
and the write operation itself.

### Testing
`mvn test -pl oak-segment-tar
-Dtest='SegmentBufferWriterPoolTest,SegmentBufferWriterPoolMonitorTest,SingleSegmentBufferWriterPoolTest'`
— 19 tests pass. Module compiles clean under `--release 17`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant