Documented all findings, hypotheses, and debugging approach. Key insight: 78 bytes is likely the Parquet footer size. The file has data pages (684 bytes) but missing footer (78 bytes). Next run will show if getPos() reveals the cause.
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