5.6 KiB
EOFException Analysis: "Still have: 78 bytes left"
Problem Summary
Spark Parquet writes succeed, but subsequent reads fail with:
java.io.EOFException: Reached the end of stream. Still have: 78 bytes left
What the Logs Tell Us
Write Phase ✅ (Everything looks correct)
year=2020 file:
🔧 Created stream: position=0 bufferSize=1048576
🔒 close START: position=0 buffer.position()=696 totalBytesWritten=696
→ Submitted 696 bytes, new position=696
✅ close END: finalPosition=696 totalBytesWritten=696
Calculated file size: 696 (chunks: 696, attr: 696, #chunks: 1)
year=2021 file:
🔧 Created stream: position=0 bufferSize=1048576
🔒 close START: position=0 buffer.position()=684 totalBytesWritten=684
→ Submitted 684 bytes, new position=684
✅ close END: finalPosition=684 totalBytesWritten=684
Calculated file size: 684 (chunks: 684, attr: 684, #chunks: 1)
Key observations:
- ✅
totalBytesWritten == position == buffer == chunks == attr - ✅ All bytes received through
write()are flushed and stored - ✅ File metadata is consistent
- ✅ No bytes lost in SeaweedFS layer
Read Phase ❌ (Parquet expects more bytes)
Consistent pattern:
- year=2020: wrote 696 bytes, expects 774 bytes → missing 78
- year=2021: wrote 684 bytes, expects 762 bytes → missing 78
The 78-byte discrepancy is constant across both files, suggesting it's not random data loss.
Hypotheses
H1: Parquet Footer Not Fully Written
Parquet file structure:
[Magic "PAR1" 4B] [Data pages] [Footer] [Footer length 4B] [Magic "PAR1" 4B]
Possible scenario:
- Parquet writes 684 bytes of data pages
- Parquet intends to write 78 bytes of footer metadata
- Our
SeaweedOutputStream.close()is called - Only data pages (684 bytes) make it to the file
- Footer (78 bytes) is lost or never written
Evidence for:
- 78 bytes is a reasonable size for a Parquet footer with minimal metadata
- Files say "snappy.parquet" → compressed, so footer would be small
- Consistent 78-byte loss across files
Evidence against:
- Our
close()logs show all bytes received viawrite()were processed - If Parquet wrote footer to stream, we'd see
totalBytesWritten=762
H2: FSDataOutputStream Position Tracking Mismatch
Hadoop wraps our stream:
new FSDataOutputStream(seaweedOutputStream, statistics)
Possible scenario:
- Parquet writes 684 bytes →
FSDataOutputStreamincrements position to 684 - Parquet writes 78-byte footer →
FSDataOutputStreamincrements position to 762 - BUT only 684 bytes reach our
SeaweedOutputStream.write() - Parquet queries
FSDataOutputStream.getPos()→ returns 762 - Parquet writes "file size: 762" in its footer
- Actual file only has 684 bytes
Evidence for:
- Would explain why our logs show 684 but Parquet expects 762
- FSDataOutputStream might have its own buffering
Evidence against:
- FSDataOutputStream is well-tested Hadoop core component
- Unlikely to lose bytes
H3: Race Condition During File Rename
Files are written to _temporary/ then renamed to final location.
Possible scenario:
- Write completes successfully (684 bytes)
close()flushes and updates metadata- File is renamed while metadata is propagating
- Read happens before metadata sync completes
- Reader gets stale file size or incomplete footer
Evidence for:
- Distributed systems often have eventual consistency issues
- Rename might not sync metadata immediately
Evidence against:
- We added
fs.seaweed.write.flush.sync=trueto force sync - Error is consistent, not intermittent
H4: Compression-Related Size Confusion
Files use Snappy compression (*.snappy.parquet).
Possible scenario:
- Parquet tracks uncompressed size internally
- Writes compressed data to stream
- Size mismatch between compressed file and uncompressed metadata
Evidence against:
- Parquet handles compression internally and consistently
- Would affect all Parquet users, not just SeaweedFS
Next Debugging Steps
Added: getPos() Logging
public synchronized long getPos() {
long currentPos = position + buffer.position();
LOG.info("[DEBUG-2024] 📍 getPos() called: flushedPosition={} bufferPosition={} returning={}",
position, buffer.position(), currentPos);
return currentPos;
}
Will reveal:
- If/when Parquet queries position
- What value is returned vs what was actually written
- If FSDataOutputStream bypasses our position tracking
Next Steps if getPos() is NOT called:
→ Parquet is not using position tracking → Focus on footer write completion
Next Steps if getPos() returns 762 but we only wrote 684:
→ FSDataOutputStream has buffering issue or byte loss → Need to investigate Hadoop wrapper behavior
Next Steps if getPos() returns 684 (correct):
→ Issue is in footer metadata or read path → Need to examine Parquet footer contents
Parquet File Format Context
Typical small Parquet file (~700 bytes):
Offset Content
0-3 Magic "PAR1"
4-650 Row group data (compressed)
651-728 Footer metadata (schema, row group pointers)
729-732 Footer length (4 bytes, value: 78)
733-736 Magic "PAR1"
Total: 737 bytes
If footer length field says "78" but only data exists:
- File ends at byte 650
- Footer starts at byte 651 (but doesn't exist)
- Reader tries to read 78 bytes, gets EOFException
This matches our error pattern perfectly.
Recommended Fix Directions
- Ensure footer is fully written before close returns
- Add explicit fsync/hsync before metadata write
- Verify FSDataOutputStream doesn't buffer separately
- Check if Parquet needs special OutputStreamAdapter