Skip to content

sdk/java: run positioned reads on one stream concurrently - #7506

Open
mwkang wants to merge 3 commits into
juicedata:mainfrom
mwkang:feature/hadoop-concurrent-pread
Open

mwkang wants to merge 3 commits into
juicedata:mainfrom
mwkang:feature/hadoop-concurrent-pread

Conversation

@mwkang

@mwkang mwkang commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

close #7504

Positioned reads on one Hadoop stream were serialized by synchronized in FileInputStream and by the fs.File mutex held across the object storage wait in File.Pread, so a file with cache misses had at most one in-flight read.

The second commit keeps cache hits cheap once those reads overlap. Every random read used to start a sliceReader goroutine and park until it finished, even for a cached block; for a caller pinned to an OS thread by cgo (the Hadoop SDK) each park is a P hand-off and a futex wake-up, which adds up when several threads read one file.

fileReader.Read now copies a fully cached range on the calling goroutine and leaves everything else (cache misses, offset 0, sequential sessions with readahead) to the regular path.

A pread on an open Hadoop stream was serialized twice: `synchronized` on
FileInputStream.read(long, ...) in the Java SDK, and fs.File's mutex held
across the object storage wait in Pread. Random reads of uncached blocks on
one file were therefore limited to one in-flight request per file, which
caps HBase gets on a cold cache at the number of HFiles per region server.

Java: drop `synchronized` from the two positioned read methods. A
ReentrantReadWriteLock keeps close() from freeing the fd under an in-flight
pread (read lock around jfs_pread, write lock around jfs_close), so preads
overlap each other but never a close. Sequential reads, seek and close keep
the stream monitor as before.

Go: File.Pread holds the lock only while it checks the size, flushes pending
writes and obtains the reader; the vfs.FileReader.Read call runs outside it,
as the FUSE path already does. File.Close marks the file closed and waits for
in-flight preads before releasing the reader, so a slow read cannot see the
closed reader and return EOF, and a read after close fails with EBADF instead
of opening a new reader that is never closed.

Tests: TestFileConcurrentPread shows four preads on one file reach the object
storage at the same time (one before), TestFileCloseWaitsForPread covers the
close ordering, and JuiceFileSystemTest gains a check that a pread completes
while another thread holds the stream monitor plus concurrent-read and
close-during-read regression tests.
@codecov

codecov Bot commented Sep 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.68468% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 57.12%. Comparing base (888ec4d) to head (f360790).
⚠️ Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
pkg/chunk/cached_store.go 76.47% 6 Missing and 2 partials ⚠️
pkg/vfs/reader.go 87.23% 4 Missing and 2 partials ⚠️
pkg/fs/fs.go 90.00% 3 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##             main    #7506       +/-   ##
===========================================
+ Coverage   22.03%   57.12%   +35.08%     
===========================================
  Files          31      180      +149     
  Lines       24803    59801    +34998     
===========================================
+ Hits         5466    34161    +28695     
- Misses      18742    22028     +3286     
- Partials      595     3612     +3017     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Every random read went through a sliceReader: fileReader.Read created a
slice, started `go s.run()` and parked on the slice's cond until the block
arrived, even when the block was already in the local cache. That hand-off
is cheap for FUSE but not for the Hadoop SDK, where the caller is an OS
thread pinned by a cgo call: each park hands its P to another M and the
wake-up goes through a futex, so cache hits get slower the more threads
read one file at once. With positioned reads no longer serialized per
stream (5488d6a), an HBase workload on a warm cache lost throughput at
high concurrency and the process grew extra OS threads under load; this
commit brings both back to where they were.

fileReader.Read now first tries readCached: when the range lies in one
chunk, is not part of a sequential session (readahead stays with the slice
readers), and every block is in the local cache, it copies the data on the
calling goroutine without a slice, a goroutine or the dataReader lock. A
miss anywhere falls back to the regular path unchanged, so cold reads keep
their concurrency. The read at offset 0 and reads inside a session's
readahead window also take the regular path so sequential detection and
readahead behave as before; the fast path still records the session as
checkReadahead would.

chunk: add CachedReader with ReadCachedAt, implemented by rSlice as the
cache-only half of ReadAt (same cache-hit metrics, same removal of a
partial block). Readers that do not implement it simply never take the
fast path.

Tests: TestReadCachedAt covers block boundaries, eviction of one block,
reads beyond the slice and a disabled cache. TestReadCached shows a cached
read reaching neither the object storage nor a slice reader, and a partly
cached read falling back to one.
@mwkang
mwkang force-pushed the feature/hadoop-concurrent-pread branch from f360790 to 229d111 Compare September 8, 2026 01:26
Drop the short-read check in ReadCachedAt: io.ReaderAt returns a non-nil
error whenever n < len(p), and both cache readers (os.File.ReadAt, the page
reader) follow it, so the branch could not run.

Tests for the paths a cached read leaves the fast path on: a range past
the end of the file, offset 0, a closed reader, an inode the metadata
engine does not know, a chunk store whose reader has no ReadCachedAt, and
a cached block shorter than its key claims (which is removed). A read
inside a hole is served as zeros without a slice reader or the object
storage; a read spanning the hole and uncached data takes the regular
path.
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.

Hadoop SDK: positioned reads on one stream are serialized

1 participant