Fix potential race cond in BufferedLinearRegionFile
This commit is contained in:
+114
-68
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user