Fix potential race cond in BufferedLinearRegionFile
Shiroha CI / build (push) Canceled after 0s
Shiroha CI / Event File (push) Canceled after 0s

This commit is contained in:
2026-08-13 13:45:28 +08:00
parent 9fff62f963
commit de4ccc40f0
@@ -302,6 +302,12 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
this.regionObjectLock.readLock().unlock(); this.regionObjectLock.readLock().unlock();
} }
private void guardAgainstClosed() throws IOException {
if (this.isClosedRaw()) {
throw new IOException("Closed");
}
}
public boolean isClosedRaw() { public boolean isClosedRaw() {
return (boolean) CLOSED_HANDLE.getVolatile(this); return (boolean) CLOSED_HANDLE.getVolatile(this);
} }
@@ -317,13 +323,15 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
public void syncIfNeeded() throws IOException { public void syncIfNeeded() throws IOException {
try { try {
this.syncToMasterFile(false, false); this.syncToMasterFile(false, false, false, false);
} finally { } finally {
BEING_SYNCED_HANDLE.setVolatile(this, false); // mark as not being synced BEING_SYNCED_HANDLE.setVolatile(this, false); // mark as not being synced
} }
} }
private void syncToMasterFile(boolean forceSync, boolean forceCompact) throws IOException { // noSwapLock/noMasterLock: caller (closeInternal) must already hold
// regionObjectLock.write and masterFileLock.write respectively.
private void syncToMasterFile(boolean forceSync, boolean forceCompact, boolean noSwapLock, boolean noMasterLock) throws IOException {
// serialized against close: the swap channel cannot go away under a running sync // serialized against close: the swap channel cannot go away under a running sync
synchronized (this.syncLock) { synchronized (this.syncLock) {
// skip if closed already // skip if closed already
@@ -338,7 +346,7 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
} }
try { try {
this.masterFileParser.sync(this.masterFilePath, forceCompact); this.masterFileParser.sync(this.masterFilePath, forceCompact, noSwapLock, noMasterLock);
} catch (Throwable e) { } catch (Throwable e) {
// set back // set back
SYNCED_HANDLE.setVolatile(this, false); SYNCED_HANDLE.setVolatile(this, false);
@@ -421,54 +429,74 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
// afterwards this is a single volatile read per chunk write. // afterwards this is a single volatile read per chunk write.
// prevent syncing after compact because it could be time costing sometimes // prevent syncing after compact because it could be time costing sometimes
if (!compactRequested && !this.masterFileParser.masterFileExists()) { if (!compactRequested && !this.masterFileParser.masterFileExists()) {
this.syncToMasterFile(false, false); this.syncToMasterFile(false, false, false, false);
} }
} }
private void closeInternal() throws IOException { private void closeInternal() throws IOException {
// note: any new sync attempt is blocked inside this block
synchronized (this.syncLock) { synchronized (this.syncLock) {
if (this.isClosedRaw()) { this.masterFileParser.masterFileLock.writeLock().lock();
// already closed (possibly by a compact disaster path): just make sure try {
// both channels are really gone — close is idempotent // note: any read/write ops is blocked inside this block
this.regionObjectLock.writeLock().lock(); this.regionObjectLock.writeLock().lock();
try { try {
this.swapFileChannel.close(); if (this.isClosedRaw()) {
} finally { boolean duplicateClosed = false;
if (this.swapFileChannel.isOpen()) {
this.swapFileChannel.close();
duplicateClosed = true;
}
duplicateClosed &= this.masterFileParser.tryCloseNoLock();
if (!duplicateClosed) {
throw new IOException("Already closed");
}
return;
}
IOException failure = null;
// final sync so no buffered data is lost; holding syncLock also guarantees no
// concurrent flusher sync is still running when we tear down below.
// if this throws we deliberately stay open: the flusher can retry the sync
// later, and the not-yet-synced swap data is not dropped on the floor
// since we hold the write lock and any read/write/sync ops is currently blocked all along the close logic, acquiring the locks inside sync is a disaster
try {
this.syncToMasterFile(true, true, true, true);
}catch (IOException ex) {
failure = ex;
}
try {
this.markClosed();
this.swapFileChannel.close();
} catch (IOException ex) {
if (failure == null) failure = ex; else failure.addSuppressed(ex);
}
try {
this.masterFileParser.closeNoLock();
} catch (IOException e) {
if (failure == null) failure = e; else failure.addSuppressed(e);
}
// finalize
this.markClosed();
if (failure != null) {
throw failure;
}
}finally {
this.regionObjectLock.writeLock().unlock(); this.regionObjectLock.writeLock().unlock();
} }
}finally {
this.masterFileParser.close(); this.masterFileParser.masterFileLock.writeLock().unlock();
return;
}
// final sync so no buffered data is lost; holding syncLock also guarantees no
// concurrent flusher sync is still running when we tear down below.
// if this throws we deliberately stay open: the flusher can retry the sync
// later, and the not-yet-synced swap data is not dropped on the floor
this.syncToMasterFile(true, true);
IOException failure = null;
this.regionObjectLock.writeLock().lock();
try {
this.markClosed();
this.swapFileChannel.close();
} catch (IOException e) {
failure = e;
} finally {
this.regionObjectLock.writeLock().unlock();
}
try {
// acquired after the region lock is fully released, never inside it (lock hierarchy)
this.masterFileParser.close();
} catch (IOException e) {
if (failure == null) failure = e; else failure.addSuppressed(e);
}
if (failure != null) {
throw failure;
} }
} }
} }
@@ -548,10 +576,6 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
try { try {
atomicReplace(targetTemp, this.swapFilePath); atomicReplace(targetTemp, this.swapFilePath);
} catch (Throwable e) { } catch (Throwable e) {
// recalculate counters
this.recalculateCounters();
// reopen closed channel
this.reopenSwapFileChannel();
// fast-fail // fast-fail
this.markClosed(); // prevent new writing & sync operations this.markClosed(); // prevent new writing & sync operations
throw new IOException("Failed to replace original swap file!", e); throw new IOException("Failed to replace original swap file!", e);
@@ -592,6 +616,8 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
private void storeSector(int index, @NotNull ByteBuffer encoded, boolean skipSync) throws IOException { private void storeSector(int index, @NotNull ByteBuffer encoded, boolean skipSync) throws IOException {
this.regionObjectLock.writeLock().lock(); this.regionObjectLock.writeLock().lock();
try { try {
this.guardAgainstClosed();
this.sectors[index].store(encoded, this.swapFileChannel); this.sectors[index].store(encoded, this.swapFileChannel);
if (!skipSync) { if (!skipSync) {
@@ -631,10 +657,14 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
} }
private void clearChunkData(int index) throws IOException { private void clearChunkData(int index) throws IOException {
this.guardAgainstClosed();
this.ensureBucketLoaded(index); this.ensureBucketLoaded(index);
this.regionObjectLock.writeLock().lock(); this.regionObjectLock.writeLock().lock();
try { try {
this.guardAgainstClosed();
this.sectors[index].clear(); this.sectors[index].clear();
this.markBucketDirty(index); this.markBucketDirty(index);
} finally { } finally {
@@ -654,10 +684,13 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
} }
private boolean hasData(int index) throws IOException { private boolean hasData(int index) throws IOException {
this.guardAgainstClosed();
this.ensureBucketLoaded(index); this.ensureBucketLoaded(index);
this.regionObjectLock.readLock().lock(); this.regionObjectLock.readLock().lock();
try { try {
this.guardAgainstClosed();
return this.sectors[index].hasData(); return this.sectors[index].hasData();
} finally { } finally {
this.regionObjectLock.readLock().unlock(); this.regionObjectLock.readLock().unlock();
@@ -665,6 +698,8 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
} }
private void writeChunk(int x, int z, @NotNull ByteBuffer data) throws IOException { private void writeChunk(int x, int z, @NotNull ByteBuffer data) throws IOException {
this.guardAgainstClosed();
final int chunkIndex = getChunkIndex(x, z); final int chunkIndex = getChunkIndex(x, z);
this.ensureBucketLoaded(chunkIndex); this.ensureBucketLoaded(chunkIndex);
@@ -692,6 +727,8 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
} }
private @Nullable ByteBuffer readChunk(int x, int z) throws IOException { private @Nullable ByteBuffer readChunk(int x, int z) throws IOException {
this.guardAgainstClosed();
final int chunkIndex = getChunkIndex(x, z); final int chunkIndex = getChunkIndex(x, z);
this.ensureBucketLoaded(chunkIndex); this.ensureBucketLoaded(chunkIndex);
@@ -700,6 +737,8 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
this.regionObjectLock.readLock().lock(); this.regionObjectLock.readLock().lock();
try { try {
this.guardAgainstClosed();
final Sector sector = this.sectors[chunkIndex]; final Sector sector = this.sectors[chunkIndex];
if (!sector.hasData()) { if (!sector.hasData()) {
@@ -1033,21 +1072,21 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
} }
// must be called under syncLock (see syncToMasterFile) // must be called under syncLock (see syncToMasterFile)
public void sync(@NotNull Path mainFile, boolean forceCompact) throws IOException { public void sync(@NotNull Path mainFile, boolean forceCompact, boolean noSwapLock, boolean noMasterLock) throws IOException {
this.masterFileLock.writeLock().lock(); if (!noMasterLock) this.masterFileLock.writeLock().lock();
try { try {
// full rewrite whenever no valid append state exists (fresh region / // full rewrite whenever no valid append state exists (fresh region /
// corrupted table / legacy migration), and afterwards whenever the // corrupted table / legacy migration), and afterwards whenever the
// appended garbage passed the auto-compact threshold: writes a tmp file, // appended garbage passed the auto-compact threshold: writes a tmp file,
// then atomically replaces the master file with it // then atomically replaces the master file with it
if (this.appendChannel == null || this.shouldCompactMasterFile() || forceCompact) { if (this.appendChannel == null || this.shouldCompactMasterFile() || forceCompact) {
this.rewriteFully(mainFile); this.rewriteFully(mainFile, noSwapLock);
} else { } else {
// WAL-style otherwise: only append the dirty buckets // WAL-style otherwise: only append the dirty buckets
this.appendDirtyBuckets(); this.appendDirtyBuckets(noSwapLock);
} }
} finally { } finally {
this.masterFileLock.writeLock().unlock(); if (!noMasterLock) this.masterFileLock.writeLock().unlock();
} }
} }
@@ -1063,7 +1102,7 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
return spareSize > MASTER_FILE_AUTO_COMPACT_SIZE && (double) spareSize > ((double) liveSize) * MASTER_FILE_AUTO_COMPACT_PERCENT; return spareSize > MASTER_FILE_AUTO_COMPACT_SIZE && (double) spareSize > ((double) liveSize) * MASTER_FILE_AUTO_COMPACT_PERCENT;
} }
private void rewriteFully(@NotNull Path mainFile) throws IOException { private void rewriteFully(@NotNull Path mainFile, boolean noSwapLock) throws IOException {
final boolean wal = this.appendChannel != null; final boolean wal = this.appendChannel != null;
final Path tmpFilePath = Path.of(mainFile + ".tmp"); final Path tmpFilePath = Path.of(mainFile + ".tmp");
final long[] syncedBucketEpochs = new long[BUCKET_COUNT]; final long[] syncedBucketEpochs = new long[BUCKET_COUNT];
@@ -1098,7 +1137,7 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
for (int bucketIndex = 0; bucketIndex < BUCKET_COUNT; bucketIndex++) { for (int bucketIndex = 0; bucketIndex < BUCKET_COUNT; bucketIndex++) {
if (BufferedLinearRegionFile.this.isBucketDirty(bucketIndex)) { if (BufferedLinearRegionFile.this.isBucketDirty(bucketIndex)) {
final BucketRecord record = this.buildBucketRecord(bucketIndex); final BucketRecord record = this.buildBucketRecord(bucketIndex, noSwapLock);
if (record.payload() != null) { if (record.payload() != null) {
final int recordSize = record.payload().remaining(); final int recordSize = record.payload().remaining();
@@ -1175,7 +1214,7 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
this.markBucketsSynced(syncedBucketEpochs); this.markBucketsSynced(syncedBucketEpochs);
} }
private void appendDirtyBuckets() throws IOException { private void appendDirtyBuckets(boolean noSwapLock) throws IOException {
final FileChannel channel = this.appendChannel; final FileChannel channel = this.appendChannel;
final long[] syncedBucketEpochs = new long[BUCKET_COUNT]; final long[] syncedBucketEpochs = new long[BUCKET_COUNT];
final long[] newPositionTable = this.positionTable.clone(); final long[] newPositionTable = this.positionTable.clone();
@@ -1190,7 +1229,7 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
continue; continue;
} }
final BucketRecord record = this.buildBucketRecord(bucketIndex); final BucketRecord record = this.buildBucketRecord(bucketIndex, noSwapLock);
final ByteBuffer payload = record.payload(); final ByteBuffer payload = record.payload();
if (payload != null) { if (payload != null) {
@@ -1243,7 +1282,7 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
// sectors that sit back to back in the swap file coalesced into single preads); // sectors that sit back to back in the swap file coalesced into single preads);
// LZ4 decompression and zstd compression both run outside any lock so writers // LZ4 decompression and zstd compression both run outside any lock so writers
// are only blocked while the raw bytes are copied // are only blocked while the raw bytes are copied
private @NotNull BucketRecord buildBucketRecord(int bucketIndex) throws IOException { private @NotNull BucketRecord buildBucketRecord(int bucketIndex, final boolean noLock) throws IOException {
final int baseChunkIndex = bucketIndex << BUCKET_SHIFT; final int baseChunkIndex = bucketIndex << BUCKET_SHIFT;
final ByteBuffer[] rawSectors = new ByteBuffer[BUCKET_SIZE]; // slices into run buffers, null = no data final ByteBuffer[] rawSectors = new ByteBuffer[BUCKET_SIZE]; // slices into run buffers, null = no data
@@ -1254,7 +1293,7 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
final long epoch; final long epoch;
BufferedLinearRegionFile.this.regionObjectLock.readLock().lock(); if (!noLock) BufferedLinearRegionFile.this.regionObjectLock.readLock().lock();
try { try {
// the epoch is taken before the data: writes completing afterwards bump // the epoch is taken before the data: writes completing afterwards bump
// it further, so they simply get picked up by the next sync round // it further, so they simply get picked up by the next sync round
@@ -1299,7 +1338,7 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
i = j + 1; i = j + 1;
} }
} finally { } finally {
BufferedLinearRegionFile.this.regionObjectLock.readLock().unlock(); if (!noLock) BufferedLinearRegionFile.this.regionObjectLock.readLock().unlock();
} }
// exact size budget up front: 4 bytes size prefix per chunk slot plus // exact size budget up front: 4 bytes size prefix per chunk slot plus
@@ -1448,15 +1487,20 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
return lens.flip(); return lens.flip();
} }
public void close() throws IOException { public boolean tryCloseNoLock() throws IOException {
this.masterFileLock.writeLock().lock(); if (this.appendChannel != null && this.appendChannel.isOpen()) {
try { this.appendChannel.close();
if (this.appendChannel != null) { this.appendChannel = null;
this.appendChannel.close(); return true;
this.appendChannel = null; }
}
} finally { return false;
this.masterFileLock.writeLock().unlock(); }
public void closeNoLock() throws IOException {
if (this.appendChannel != null) {
this.appendChannel.close();
this.appendChannel = null;
} }
} }
@@ -1465,6 +1509,8 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
this.masterFileLock.readLock().lock(); this.masterFileLock.readLock().lock();
try { try {
BufferedLinearRegionFile.this.guardAgainstClosed();
final ByteBuffer decompressed; final ByteBuffer decompressed;
if (this.appendChannel != null) { if (this.appendChannel != null) {
@@ -1781,7 +1827,7 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
// old parsed, remove the original file, and we will recreate it as we sync // old parsed, remove the original file, and we will recreate it as we sync
if (oldParsed) { if (oldParsed) {
// immediately do sync operation // immediately do sync operation
BufferedLinearRegionFile.this.syncToMasterFile(true, true); BufferedLinearRegionFile.this.syncToMasterFile(true, true, false, false);
return; return;
} }