Skip to content

Make concurrent PackData reads thread-safe - #2356

Open
bojanz wants to merge 3 commits into
jelmer:mainfrom
bojanz:pack-pread
Open

Make concurrent PackData reads thread-safe#2356
bojanz wants to merge 3 commits into
jelmer:mainfrom
bojanz:pack-pread

Conversation

@bojanz

@bojanz bojanz commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Use os.pread where available so concurrent pack readers no longer share a file position. Provide synchronized fallback reads for supplied file objects and systems without pread, coordinate reads with close operations, and protect the pack offset cache.

Add targeted coverage for concurrent reads, cursor behavior, checksum reads, and close races.

Issue: #2350

@bojanz

bojanz commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

@jelmer The pack _offset_cache is still unsynchronized. I couldn't get that fix to pass tests yet, so I've excluded it from the PR. I can add it back if you think that the PR shouldn't land without that.

bojanz added 2 commits August 14, 2026 22:31
Give each PackData reader its own buffered cursor. Use pread for files
PackData opens, and serialize seek-based reads for supplied file objects
and systems without pread.

Coordinate reads and close so active reads finish safely, concurrent
closes do not race, and cursors fail once the pack is closed. Record the
opened file's size for checksum reads and cover concurrency and cursor
edge cases.

Fixes jelmer#2350
Calculate checksums through cursors so reads no longer monopolize the
shared file position. Path-backed packs use pread; the seek-based
fallback locks each seek/read operation individually, allowing other
readers between chunks.

Derive checksum offsets from the opened descriptor and reject packs
with missing or truncated checksums.

@jelmer jelmer left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole point of pread is to avoid cursors, so adding a cursor wrapper for pread seems counterintuitive. This also adds a bunch of overhead in a performance-critical part of the code as well as a lot of complexity. In some quick and dirty benchmarking, I see a 10-20% performance degradation from this PR.

Maybe we can reevaluate using mmap for packs instead?

@bojanz

bojanz commented Aug 15, 2026

Copy link
Copy Markdown
Contributor Author

I am not thrilled by the amount of complexity either. FWIW the current work is about 20 commits squashed together, the first one was nice and simple, but getting the semantics right (e.g. close() races) took both code and effort.

That said, having a cursor abstraction on top of pread feels unavoidable, unpack_object() expects a stateful read(size) callback. Though I might be running into my limits of understanding the current architecture.

A 10-20% performance degradation is completely unacceptable, but I'd like to see if there's anything I can do to optimize it before jumping into an alternative approach with mmap, so that at least we have a fair comparison.

Can you share your quick&dirty benchmark?

Pack scans ask for 64KB chunks. With a fixed 512-byte readahead,
almost every object triggered another pread and another lock acquisition.
Scanning an 18MB test pack made about 34,000 pread calls and was roughly
30% slower than BufferedReader.

Double the readahead size after each buffer miss, up to 256KB. Short
random reads still start at 512 bytes, while sequential scans quickly
start reading larger chunks. In the same benchmark, the slowdown drops
to about 4%. Random reads and checksum performance are unchanged.
@bojanz

bojanz commented Aug 15, 2026

Copy link
Copy Markdown
Contributor Author

I've pushed an optimization. Will focus on mmap in the next two days, then we can compare.

@jelmer

jelmer commented Aug 17, 2026

Copy link
Copy Markdown
Owner

I am not thrilled by the amount of complexity either. FWIW the current work is about 20 commits squashed together, the first one was nice and simple, but getting the semantics right (e.g. close() races) took both code and effort.

That said, having a cursor abstraction on top of pread feels unavoidable, unpack_object() expects a stateful read(size) callback. Though I might be running into my limits of understanding the current architecture.

If we're going to use a cursor, then we might as well just use file descriptors - perhaps just one per thread? That said, I think mmap /or/ direct use of os.pread (rather than a cursor!) are going to keep the code much simpler.

A 10-20% performance degradation is completely unacceptable, but I'd like to see if there's anything I can do to optimize it before jumping into an alternative approach with mmap, so that at least we have a fair comparison.

Can you share your quick&dirty benchmark?

It was a quick one-off that I didn't save unfortunately. We do also have https://github.com/dulwich/benchmarks but I haven't run that in a while.

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.

2 participants