Rewrite buffered linear master sync logic & sync locks
Generated by ai(uhm idk which model(x) but the code is ok)
This commit is contained in:
+429
-268
@@ -29,15 +29,30 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
/**
|
||||
* Lock hierarchy (always acquire top to bottom, never the reverse):
|
||||
* <ol>
|
||||
* <li>{@code syncLock} — serializes master file syncs against close</li>
|
||||
* <li>{@code Bucket.lock} — per-bucket lazy-load guard</li>
|
||||
* <li>{@code masterFileLock} — master file read / append / replace</li>
|
||||
* <li>{@code regionObjectLock} — in-memory sector table + swap file channel</li>
|
||||
* </ol>
|
||||
* The atomic flags (closed / synced / beingSynced / lastWritten) and the bucket
|
||||
* epochs are lock-free and may be touched while holding any (or no) lock.
|
||||
*/
|
||||
public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.RegionFile {
|
||||
private static final double SWAP_FILE_AUTO_COMPACT_PERCENT = 3.0 / 5.0; // 60 %
|
||||
private static final long SWAP_FILE_AUTO_COMPACT_SIZE = 1024 * 1024; // 1 MiB
|
||||
|
||||
// master file WAL appends leave the replaced bucket records behind as garbage; once
|
||||
// it piles up past this threshold the next sync compacts via a full tmp-file rewrite
|
||||
private static final double MASTER_FILE_AUTO_COMPACT_PERCENT = SWAP_FILE_AUTO_COMPACT_PERCENT;
|
||||
private static final long MASTER_FILE_AUTO_COMPACT_SIZE = SWAP_FILE_AUTO_COMPACT_SIZE;
|
||||
|
||||
private static final long SWAP_FILE_SUPER_BLOCK = 0x1145141919810L;
|
||||
private static final int SWAP_FILE_HASH_SEED = 0x0721; // ~(∠・ω< )⌒★
|
||||
private static final byte SWAP_FILE_VERSION = 0x02; // ver 2.0
|
||||
@@ -74,6 +89,10 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
|
||||
private final Path masterFilePath;
|
||||
private final Path swapFilePath;
|
||||
|
||||
// outermost lock: serializes syncToMasterFile() against closeInternal(), so the
|
||||
// swap channel can never be torn down while a sync is still reading from it
|
||||
private final Object syncLock = new Object();
|
||||
|
||||
private final ReadWriteLock regionObjectLock = new ReentrantReadWriteLock();
|
||||
private final XXHash32 xxHash32 = XXHashFactory.fastestInstance().hash32();
|
||||
private Sector[] sectors = new Sector[1024];
|
||||
@@ -134,6 +153,34 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
|
||||
}
|
||||
}
|
||||
|
||||
private static void transferFully(FileChannel source, long sourceOffset, long count, FileChannel target, long targetOffset) throws IOException {
|
||||
target.position(targetOffset);
|
||||
|
||||
long transferred = 0;
|
||||
while (transferred < count) {
|
||||
transferred += source.transferTo(sourceOffset + transferred, count - transferred, target);
|
||||
}
|
||||
}
|
||||
|
||||
// replaces target with source, deleting source if both attempts fail
|
||||
private static void atomicReplace(Path source, Path target) throws IOException {
|
||||
try {
|
||||
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (Throwable e) {
|
||||
// atomic move might be unsupported on some file systems, so give it an attempt to retry without atomic move
|
||||
try {
|
||||
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (Throwable ex) {
|
||||
e.addSuppressed(ex);
|
||||
|
||||
// delete file that failed to replace
|
||||
Files.deleteIfExists(source);
|
||||
|
||||
throw new IOException("Failed to replace " + target + "!", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanUpSwapFile() throws IOException {
|
||||
Files.deleteIfExists(this.swapFilePath);
|
||||
}
|
||||
@@ -142,6 +189,10 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
|
||||
final int bucketIndex = chunkIndex >> BUCKET_SHIFT;
|
||||
final Bucket bucket = this.buckets[bucketIndex];
|
||||
|
||||
if (bucket.loaded) { // volatile fast path
|
||||
return;
|
||||
}
|
||||
|
||||
// bucket lock -> master read lock -> swap write lock
|
||||
synchronized (bucket.lock) {
|
||||
if (bucket.loaded) {
|
||||
@@ -153,32 +204,26 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
|
||||
}
|
||||
}
|
||||
|
||||
private long markBucketDirty(int chunkIndex) {
|
||||
return this.markBucketDirtyByIndex(chunkIndex >> BUCKET_SHIFT);
|
||||
// used by the legacy parsers: their data goes through the write path directly,
|
||||
// so the bucket must be flagged loaded first to avoid a recursive lazy-load
|
||||
private void markBucketLoaded(int chunkIndex) {
|
||||
final Bucket bucket = this.buckets[chunkIndex >> BUCKET_SHIFT];
|
||||
|
||||
synchronized (bucket.lock) {
|
||||
bucket.loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
private long markBucketDirtyByIndex(int bucketIndex) {
|
||||
final Bucket bucket = this.buckets[bucketIndex];
|
||||
|
||||
return bucket.writeEpoch.incrementAndGet();
|
||||
private void markBucketDirty(int chunkIndex) {
|
||||
this.buckets[chunkIndex >> BUCKET_SHIFT].writeEpoch.incrementAndGet();
|
||||
}
|
||||
|
||||
private long getBucketWriteEpoch(int bucketIndex) {
|
||||
final Bucket bucket = this.buckets[bucketIndex];
|
||||
|
||||
return bucket.writeEpoch.get();
|
||||
}
|
||||
|
||||
private long getBucketSyncedEpoch(int bucketIndex) {
|
||||
final Bucket bucket = this.buckets[bucketIndex];
|
||||
|
||||
return bucket.syncedEpoch.get();
|
||||
return this.buckets[bucketIndex].writeEpoch.get();
|
||||
}
|
||||
|
||||
private void markBucketSynced(int bucketIndex, long syncedEpoch) {
|
||||
final Bucket bucket = this.buckets[bucketIndex];
|
||||
|
||||
bucket.syncedEpoch.accumulateAndGet(syncedEpoch, Math::max);
|
||||
this.buckets[bucketIndex].syncedEpoch.accumulateAndGet(syncedEpoch, Math::max);
|
||||
}
|
||||
|
||||
private boolean isBucketDirty(int bucketIndex) {
|
||||
@@ -223,14 +268,7 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
|
||||
}
|
||||
|
||||
public void syncIfNeeded() throws IOException {
|
||||
// the sync operation is just coping the data from swap file to the master file
|
||||
// so we could acquire read lock simply so that we won't block any other read operations
|
||||
try {
|
||||
// skip if closed already
|
||||
if (this.isClosed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.syncToMasterFile();
|
||||
} finally {
|
||||
BEING_SYNCED_HANDLE.setVolatile(this, false); // mark as not being synced
|
||||
@@ -238,14 +276,21 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
|
||||
}
|
||||
|
||||
private void syncToMasterFile() throws IOException {
|
||||
// prevent multiple syncs in the same time
|
||||
// serialized against close: the swap channel cannot go away under a running sync
|
||||
synchronized (this.syncLock) {
|
||||
// skip if closed already
|
||||
if (this.isClosedRaw()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// fast skip when there is nothing to sync; writers flip the flag back
|
||||
// via markAsToSync() which triggers the next round
|
||||
if (!SYNCED_HANDLE.compareAndSet(this, false, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// this.masterFileParser.writeMainFile(this.masterFilePath);
|
||||
this.masterFileParser.writeMainFileBucketed(this.masterFilePath);
|
||||
this.masterFileParser.sync(this.masterFilePath);
|
||||
} catch (Throwable e) {
|
||||
// set back
|
||||
SYNCED_HANDLE.setVolatile(this, false);
|
||||
@@ -253,6 +298,7 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
|
||||
throw new IOException("Failed to sync to master file!", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void tryLoadOldBlinearMasterFileData() throws IOException {
|
||||
this.masterFileParser.tryParseMainFileOld(this.masterFilePath);
|
||||
@@ -329,29 +375,20 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
|
||||
return;
|
||||
}
|
||||
|
||||
long spareSize = this.currentAcquiredIndex;
|
||||
|
||||
spareSize -= this.headerSize();
|
||||
long liveSize = 0;
|
||||
for (Sector sector : this.sectors) {
|
||||
// skip no data sectors
|
||||
if (!sector.hasData()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
spareSize -= sector.length;
|
||||
liveSize += sector.length;
|
||||
}
|
||||
|
||||
long sectorSize = 0;
|
||||
for (Sector sector : this.sectors) {
|
||||
// skip no data sectors
|
||||
if (!sector.hasData()) {
|
||||
continue;
|
||||
}
|
||||
// everything acquired but not covered by a live sector is garbage
|
||||
final long spareSize = this.currentAcquiredIndex - this.headerSize() - liveSize;
|
||||
|
||||
sectorSize += sector.length;
|
||||
}
|
||||
|
||||
final boolean compactRequested = spareSize > SWAP_FILE_AUTO_COMPACT_SIZE && (double) spareSize > ((double) sectorSize) * SWAP_FILE_AUTO_COMPACT_PERCENT;
|
||||
final boolean compactRequested = spareSize > SWAP_FILE_AUTO_COMPACT_SIZE && (double) spareSize > ((double) liveSize) * SWAP_FILE_AUTO_COMPACT_PERCENT;
|
||||
|
||||
// try auto compact to clean the garbage area
|
||||
if (compactRequested) {
|
||||
@@ -371,25 +408,59 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
|
||||
}
|
||||
|
||||
private void closeInternal() throws IOException {
|
||||
this.syncIfNeeded();
|
||||
synchronized (this.syncLock) {
|
||||
if (this.isClosedRaw()) {
|
||||
// already closed (possibly by a compact disaster path): just make sure
|
||||
// both channels are really gone — close is idempotent
|
||||
this.regionObjectLock.writeLock().lock();
|
||||
try {
|
||||
this.swapFileChannel.close();
|
||||
} finally {
|
||||
this.regionObjectLock.writeLock().unlock();
|
||||
}
|
||||
|
||||
this.masterFileParser.close();
|
||||
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();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private void markClosed() throws IOException {
|
||||
if (!CLOSED_HANDLE.compareAndSet(this, false, true)) {
|
||||
throw new IOException("Already closed!");
|
||||
if (failure != null) {
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void markClosed() {
|
||||
// lenient CAS: the disaster path of compactSwapFile() may have closed us already
|
||||
if (CLOSED_HANDLE.compareAndSet(this, false, true)) {
|
||||
this.flusher.removeFile(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void compactSwapFile() throws IOException {
|
||||
this.writeSwapFileHeaders(true, true); // save headers for compact
|
||||
@@ -413,7 +484,7 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
|
||||
|
||||
long newAcquiredIndex;
|
||||
|
||||
final Path targetTemp = new File(this.swapFilePath.toString() + ".tmp").toPath();
|
||||
final Path targetTemp = Path.of(this.swapFilePath + ".tmp");
|
||||
|
||||
try (FileChannel tempChannel = FileChannel.open(
|
||||
targetTemp,
|
||||
@@ -423,7 +494,6 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
|
||||
StandardOpenOption.TRUNCATE_EXISTING
|
||||
)) {
|
||||
long offsetPointer = this.headerSize();
|
||||
tempChannel.position(offsetPointer);
|
||||
|
||||
for (Sector sector : newSectorsToBeReplaced) {
|
||||
// skip cleared or no data-contained sectors
|
||||
@@ -432,7 +502,7 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
|
||||
}
|
||||
|
||||
// transfer to target
|
||||
sector.transferTo(this.swapFileChannel, tempChannel);
|
||||
transferFully(this.swapFileChannel, sector.offset, sector.length, tempChannel, offsetPointer);
|
||||
|
||||
// recalculate the offset and length
|
||||
final Sector newRecalculated = new Sector(sector.index, offsetPointer, sector.length);
|
||||
@@ -459,26 +529,8 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
|
||||
|
||||
// replace swap file
|
||||
try {
|
||||
Files.move(
|
||||
targetTemp,
|
||||
this.swapFilePath,
|
||||
StandardCopyOption.REPLACE_EXISTING,
|
||||
StandardCopyOption.ATOMIC_MOVE
|
||||
);
|
||||
atomicReplace(targetTemp, this.swapFilePath);
|
||||
} catch (Throwable e) {
|
||||
// atomic move might be unsupported on some file systems, so give it an attempt to retry without atomic move
|
||||
try {
|
||||
Files.move(
|
||||
targetTemp,
|
||||
this.swapFilePath,
|
||||
StandardCopyOption.REPLACE_EXISTING
|
||||
);
|
||||
} catch (Throwable ex) {
|
||||
// now we are totally failed
|
||||
e.addSuppressed(ex);
|
||||
|
||||
// delete file that failed to replace
|
||||
Files.deleteIfExists(targetTemp);
|
||||
// recalculate acquired index
|
||||
this.recalculateAcquiredIndex();
|
||||
// reopen closed channel
|
||||
@@ -487,8 +539,6 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
|
||||
this.markClosed(); // prevent new writing & sync operations
|
||||
throw new IOException("Failed to replace original swap file!", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// reopen file channel
|
||||
@@ -545,10 +595,6 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
|
||||
}
|
||||
|
||||
private @Nullable ByteBuffer readChunkDataRaw(int index) throws IOException {
|
||||
return this.readChunkDataRaw(index, true);
|
||||
}
|
||||
|
||||
private @Nullable ByteBuffer readChunkDataRaw(int index, boolean acquireLock) throws IOException {
|
||||
final ByteBuffer raw;
|
||||
|
||||
this.regionObjectLock.readLock().lock();
|
||||
@@ -606,6 +652,8 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
|
||||
private void writeChunk(int x, int z, @NotNull ByteBuffer data) throws IOException {
|
||||
final int chunkIndex = getChunkIndex(x, z);
|
||||
|
||||
this.ensureBucketLoaded(chunkIndex);
|
||||
|
||||
if (data.remaining() > MAX_SIZE_PER_CHUNK) {
|
||||
throw new RegionFileStorage.RegionFileSizeException("Writing too large chunk, limit : " + MAX_SIZE_PER_CHUNK + " but got : " + data.remaining());
|
||||
}
|
||||
@@ -703,11 +751,6 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
|
||||
|
||||
@Override
|
||||
public void write(@NotNull ChunkPos pos, ByteBuffer buf) throws IOException {
|
||||
|
||||
final int chunkIndex = getChunkIndex(pos.x(), pos.z());
|
||||
|
||||
this.ensureBucketLoaded(chunkIndex);
|
||||
|
||||
this.writeChunk(pos.x(), pos.z(), buf);
|
||||
}
|
||||
|
||||
@@ -819,16 +862,6 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public void transferTo(@NotNull FileChannel source, @NotNull FileChannel target) throws IOException {
|
||||
long transferred = 0;
|
||||
while (transferred < this.length) {
|
||||
transferred += source.transferTo(
|
||||
this.offset + transferred,
|
||||
this.length - transferred,
|
||||
target);
|
||||
}
|
||||
}
|
||||
|
||||
public @NotNull ByteBuffer read(@NotNull FileChannel channel) throws IOException {
|
||||
final ByteBuffer result = ByteBuffer.allocate((int) this.length);
|
||||
|
||||
@@ -906,9 +939,6 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
|
||||
public void close() throws IOException {
|
||||
ByteBuffer bytebuffer = ByteBuffer.wrap(this.buf, 0, this.count);
|
||||
|
||||
final int chunkIndex = getChunkIndex(this.pos.x(), this.pos.z());
|
||||
|
||||
BufferedLinearRegionFile.this.ensureBucketLoaded(chunkIndex);
|
||||
BufferedLinearRegionFile.this.writeChunk(this.pos.x(), this.pos.z(), bytebuffer);
|
||||
|
||||
BufferedLinearRegionFile.this.flushInternal();
|
||||
@@ -916,197 +946,341 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
|
||||
}
|
||||
|
||||
private class LinearMasterFileParser {
|
||||
// V3 new format layout:
|
||||
// V3 bucketed format layout:
|
||||
// [0, 14): header — superblock(8) + version(1) + compressionLevel(1) + xxHash32Seed(4)
|
||||
// [14, 142): position table — BUCKET_COUNT(16) × long(8) each; 0 = no data for that bucket
|
||||
// [526, EOF): bucket data — originalLen(int) + compressedLen(int) + compressedData
|
||||
private static final long V3_POS_TABLE_OFFSET = 14L;
|
||||
// [142, EOF): bucket records — originalLen(int) + compressedLen(int) + compressedData
|
||||
private static final int V3_HEADER_SIZE = 14;
|
||||
private static final long V3_POS_TABLE_OFFSET = V3_HEADER_SIZE;
|
||||
private static final int V3_POS_TABLE_SIZE = BUCKET_COUNT * Long.BYTES; // 128
|
||||
private static final long V3_DATA_AREA_OFFSET = V3_POS_TABLE_OFFSET + V3_POS_TABLE_SIZE; // 142
|
||||
private static final int V3_RECORD_HEADER_SIZE = Integer.BYTES * 2; // originalLen + compressedLen
|
||||
|
||||
private final ReadWriteLock masterFileLock = new ReentrantReadWriteLock();
|
||||
|
||||
public void writeMainFileBucketed(@NotNull Path mainFile) throws IOException {
|
||||
// WAL(append) state, guarded by masterFileLock: null until the first sync after
|
||||
// open has fully rewritten the master file; afterwards syncs only append changed
|
||||
// buckets to the tail and update the position table in place.
|
||||
// recordSizes mirrors positionTable (size of each live record) so the garbage
|
||||
// ratio can be computed without touching the disk
|
||||
private @Nullable FileChannel appendChannel;
|
||||
private long[] positionTable;
|
||||
private long[] recordSizes;
|
||||
private long appendOffset;
|
||||
|
||||
// a consistent snapshot of one bucket taken from the swap file;
|
||||
// payload == null means the bucket holds no chunks at all
|
||||
private record BucketRecord(long epoch, byte @Nullable [] payload) {
|
||||
}
|
||||
|
||||
// must be called under syncLock (see syncToMasterFile)
|
||||
public void sync(@NotNull Path mainFile) throws IOException {
|
||||
this.masterFileLock.writeLock().lock();
|
||||
try {
|
||||
// full rewrite on the first sync after open, and afterwards whenever the
|
||||
// appended garbage passed the auto-compact threshold: writes a tmp file,
|
||||
// then atomically replaces the master file with it
|
||||
if (this.appendChannel == null || this.shouldCompactMasterFile()) {
|
||||
this.rewriteFully(mainFile);
|
||||
} else {
|
||||
// WAL-style otherwise: only append the dirty buckets
|
||||
this.appendDirtyBuckets();
|
||||
}
|
||||
} finally {
|
||||
this.masterFileLock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
// only valid in WAL mode (appendChannel != null); mirrors the swap file heuristic
|
||||
private boolean shouldCompactMasterFile() {
|
||||
long liveSize = 0;
|
||||
for (final long size : this.recordSizes) {
|
||||
liveSize += size;
|
||||
}
|
||||
|
||||
final long spareSize = this.appendOffset - V3_DATA_AREA_OFFSET - liveSize;
|
||||
|
||||
return spareSize > MASTER_FILE_AUTO_COMPACT_SIZE && (double) spareSize > ((double) liveSize) * MASTER_FILE_AUTO_COMPACT_PERCENT;
|
||||
}
|
||||
|
||||
private void rewriteFully(@NotNull Path mainFile) throws IOException {
|
||||
// a compacting rewrite replaces the whole file, so drop the old append channel
|
||||
// first; if anything below fails, the next sync just retries via this path
|
||||
if (this.appendChannel != null) {
|
||||
this.appendChannel.close();
|
||||
this.appendChannel = null;
|
||||
}
|
||||
|
||||
final Path tmpFilePath = Path.of(mainFile + ".tmp");
|
||||
final long[] syncedBucketEpochs = new long[BUCKET_COUNT];
|
||||
final long[] newPositionTable = new long[BUCKET_COUNT];
|
||||
final long[] newRecordSizes = new long[BUCKET_COUNT];
|
||||
final long newAppendOffset;
|
||||
|
||||
// note: there is no necessary hold the write lock for this stuff
|
||||
// as the truly write operations only happens on replacing the master file (see the file move call below this hunk)
|
||||
// and we had CAS flags to prevent multiple synchronization happening at the same time
|
||||
// open the old file to copy the non-dirty buckets over
|
||||
try (FileChannel oldChannel = this.openV3MasterFile(mainFile)) {
|
||||
final long[] oldPositionTable = oldChannel == null ? null : this.parseOffsetTable(oldChannel);
|
||||
|
||||
// Open old file to copy non-dirty buckets
|
||||
long[] oldPositionTable = null;
|
||||
FileChannel oldChannel = null;
|
||||
|
||||
this.masterFileLock.writeLock().lock();
|
||||
try {
|
||||
if (Files.exists(mainFile)) {
|
||||
try {
|
||||
oldChannel = FileChannel.open(mainFile, StandardOpenOption.READ);
|
||||
if (oldChannel.size() >= V3_DATA_AREA_OFFSET) {
|
||||
final ByteBuffer hdr = ByteBuffer.allocate(14);
|
||||
readFullyAt(oldChannel, hdr, 0);
|
||||
hdr.flip();
|
||||
if (hdr.getLong() == MASTER_FILE_SUPER_BLOCK && hdr.get() == MASTER_FILE_VERSION_BUCKET) {
|
||||
oldPositionTable = this.parseOffsetTable(oldChannel);
|
||||
} else {
|
||||
oldChannel.close();
|
||||
oldChannel = null;
|
||||
}
|
||||
} else {
|
||||
oldChannel.close();
|
||||
oldChannel = null;
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
if (oldChannel != null) {
|
||||
try {
|
||||
oldChannel.close();
|
||||
} catch (IOException e2) {
|
||||
e.addSuppressed(e2);
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
BufferedLinearRegionFile.this.regionObjectLock.readLock().lock();
|
||||
try (FileChannel outChannel = FileChannel.open(tmpFilePath,
|
||||
StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
|
||||
this.writeV3Header(outChannel);
|
||||
|
||||
// Write header (14 bytes)
|
||||
final ByteBuffer header = ByteBuffer.allocate(14);
|
||||
header.putLong(MASTER_FILE_SUPER_BLOCK);
|
||||
header.put(MASTER_FILE_VERSION_BUCKET);
|
||||
header.put(BufferedLinearRegionFile.this.compressionLevel);
|
||||
header.putInt(BufferedLinearRegionFile.this.xxHash32Seed);
|
||||
header.flip();
|
||||
writeFullyAt(outChannel, header, 0);
|
||||
|
||||
// Write position table placeholder (all zeros, filled in at the end)
|
||||
// position table placeholder (all zeros, filled in at the end)
|
||||
writeFullyAt(outChannel, ByteBuffer.allocate(V3_POS_TABLE_SIZE), V3_POS_TABLE_OFFSET);
|
||||
|
||||
long dataOffset = V3_DATA_AREA_OFFSET;
|
||||
|
||||
for (int bucketIndex = 0; bucketIndex < BUCKET_COUNT; bucketIndex++) {
|
||||
final long bucketWriteEpoch = BufferedLinearRegionFile.this.getBucketWriteEpoch(bucketIndex);
|
||||
final boolean isBucketDirty = bucketWriteEpoch != BufferedLinearRegionFile.this.getBucketSyncedEpoch(bucketIndex);
|
||||
if (BufferedLinearRegionFile.this.isBucketDirty(bucketIndex)) {
|
||||
final BucketRecord record = this.buildBucketRecord(bucketIndex);
|
||||
|
||||
if (record.payload() != null) {
|
||||
writeFullyAt(outChannel, ByteBuffer.wrap(record.payload()), dataOffset);
|
||||
newPositionTable[bucketIndex] = dataOffset;
|
||||
newRecordSizes[bucketIndex] = record.payload().length;
|
||||
dataOffset += record.payload().length;
|
||||
}
|
||||
// else: the bucket is empty now, its table entry stays 0
|
||||
|
||||
syncedBucketEpochs[bucketIndex] = record.epoch();
|
||||
} else if (oldPositionTable != null && oldPositionTable[bucketIndex] != 0) {
|
||||
// not dirty: copy the record bytes straight from the old file
|
||||
final long oldOffset = oldPositionTable[bucketIndex];
|
||||
final ByteBuffer lens = this.readRecordLengths(oldChannel, oldOffset);
|
||||
lens.getInt(); // skip originalLen
|
||||
final long recordSize = V3_RECORD_HEADER_SIZE + (long) lens.getInt();
|
||||
|
||||
transferFully(oldChannel, oldOffset, recordSize, outChannel, dataOffset);
|
||||
newPositionTable[bucketIndex] = dataOffset;
|
||||
newRecordSizes[bucketIndex] = recordSize;
|
||||
dataOffset += recordSize;
|
||||
}
|
||||
}
|
||||
|
||||
// write the finalized position table
|
||||
writeFullyAt(outChannel, this.encodePositionTable(newPositionTable), V3_POS_TABLE_OFFSET);
|
||||
|
||||
outChannel.force(true);
|
||||
|
||||
newAppendOffset = dataOffset;
|
||||
}
|
||||
}
|
||||
|
||||
atomicReplace(tmpFilePath, mainFile);
|
||||
|
||||
// enter WAL mode: keep the freshly written master file open for appending syncs
|
||||
this.appendChannel = FileChannel.open(mainFile, StandardOpenOption.READ, StandardOpenOption.WRITE);
|
||||
this.positionTable = newPositionTable;
|
||||
this.recordSizes = newRecordSizes;
|
||||
this.appendOffset = newAppendOffset;
|
||||
|
||||
this.markBucketsSynced(syncedBucketEpochs);
|
||||
}
|
||||
|
||||
private void appendDirtyBuckets() throws IOException {
|
||||
final FileChannel channel = this.appendChannel;
|
||||
final long[] syncedBucketEpochs = new long[BUCKET_COUNT];
|
||||
final long[] newPositionTable = this.positionTable.clone();
|
||||
final long[] newRecordSizes = this.recordSizes.clone();
|
||||
long dataOffset = this.appendOffset;
|
||||
boolean anyDirty = false;
|
||||
|
||||
for (int bucketIndex = 0; bucketIndex < BUCKET_COUNT; bucketIndex++) {
|
||||
if (!BufferedLinearRegionFile.this.isBucketDirty(bucketIndex)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final BucketRecord record = this.buildBucketRecord(bucketIndex);
|
||||
|
||||
if (record.payload() != null) {
|
||||
writeFullyAt(channel, ByteBuffer.wrap(record.payload()), dataOffset);
|
||||
newPositionTable[bucketIndex] = dataOffset;
|
||||
newRecordSizes[bucketIndex] = record.payload().length;
|
||||
dataOffset += record.payload().length;
|
||||
} else {
|
||||
// the bucket is empty now
|
||||
newPositionTable[bucketIndex] = 0;
|
||||
newRecordSizes[bucketIndex] = 0;
|
||||
}
|
||||
|
||||
syncedBucketEpochs[bucketIndex] = record.epoch();
|
||||
anyDirty = true;
|
||||
}
|
||||
|
||||
if (!anyDirty) {
|
||||
return;
|
||||
}
|
||||
|
||||
// make the appended records durable before the position table may point at them
|
||||
channel.force(false);
|
||||
|
||||
// commit the new tail first: even a torn position table write can then never
|
||||
// cause a later append to overwrite records the on-disk table already references
|
||||
this.appendOffset = dataOffset;
|
||||
|
||||
writeFullyAt(channel, this.encodePositionTable(newPositionTable), V3_POS_TABLE_OFFSET);
|
||||
channel.force(true);
|
||||
|
||||
this.positionTable = newPositionTable;
|
||||
this.recordSizes = newRecordSizes;
|
||||
|
||||
this.markBucketsSynced(syncedBucketEpochs);
|
||||
}
|
||||
|
||||
// snapshots one bucket under a short read lock (raw sector bytes only);
|
||||
// LZ4 decompression and zstd compression both run outside any lock so
|
||||
// writers are only blocked while the raw bytes are copied
|
||||
private @NotNull BucketRecord buildBucketRecord(int bucketIndex) throws IOException {
|
||||
final int baseChunkIndex = bucketIndex << BUCKET_SHIFT;
|
||||
final ByteBuffer[] rawSectors = new ByteBuffer[BUCKET_SIZE];
|
||||
final long epoch;
|
||||
|
||||
BufferedLinearRegionFile.this.regionObjectLock.readLock().lock();
|
||||
try {
|
||||
// the epoch is taken before the data: writes completing afterwards bump
|
||||
// it further, so they simply get picked up by the next sync round
|
||||
epoch = BufferedLinearRegionFile.this.getBucketWriteEpoch(bucketIndex);
|
||||
|
||||
for (int i = 0; i < BUCKET_SIZE; i++) {
|
||||
final Sector sector = BufferedLinearRegionFile.this.sectors[baseChunkIndex + i];
|
||||
|
||||
rawSectors[i] = sector.hasData() ? sector.read(BufferedLinearRegionFile.this.swapFileChannel) : null;
|
||||
}
|
||||
} finally {
|
||||
BufferedLinearRegionFile.this.regionObjectLock.readLock().unlock();
|
||||
}
|
||||
|
||||
if (isBucketDirty) {
|
||||
final int baseChunk = bucketIndex << BUCKET_SHIFT;
|
||||
final ByteArrayOutputStream rawBuf = new ByteArrayOutputStream();
|
||||
final DataOutputStream rawOut = new DataOutputStream(rawBuf);
|
||||
boolean hasAny = false;
|
||||
|
||||
for (int i = 0; i < BUCKET_SIZE; i++) {
|
||||
// swap read lock
|
||||
final ByteBuffer data = BufferedLinearRegionFile.this.readChunkDataRaw(baseChunk + i, false);
|
||||
final ByteBuffer rawSector = rawSectors[i];
|
||||
|
||||
// note: null -> no data contained
|
||||
if (data == null) {
|
||||
if (rawSector == null) {
|
||||
rawOut.writeInt(0);
|
||||
} else {
|
||||
final byte[] arr = new byte[data.remaining()];
|
||||
data.get(arr);
|
||||
continue;
|
||||
}
|
||||
|
||||
final ByteBuffer chunkData = BufferedLinearRegionFile.this.compressingOps.decompress(rawSector);
|
||||
final byte[] arr = new byte[chunkData.remaining()];
|
||||
chunkData.get(arr);
|
||||
|
||||
rawOut.writeInt(arr.length);
|
||||
rawOut.write(arr);
|
||||
hasAny = true;
|
||||
}
|
||||
}
|
||||
rawOut.flush();
|
||||
|
||||
if (hasAny) {
|
||||
if (!hasAny) {
|
||||
return new BucketRecord(epoch, null);
|
||||
}
|
||||
|
||||
final byte[] raw = rawBuf.toByteArray();
|
||||
final byte[] compressed = Zstd.compress(raw, BufferedLinearRegionFile.this.compressionLevel);
|
||||
|
||||
newPositionTable[bucketIndex] = dataOffset;
|
||||
final ByteBuffer payload = ByteBuffer.allocate(V3_RECORD_HEADER_SIZE + compressed.length);
|
||||
payload.putInt(raw.length); // original (uncompressed) length
|
||||
payload.putInt(compressed.length); // compressed length
|
||||
payload.put(compressed);
|
||||
|
||||
final ByteBuffer bucketBuf = ByteBuffer.allocate(8 + compressed.length);
|
||||
bucketBuf.putInt(raw.length); // original (uncompressed) length
|
||||
bucketBuf.putInt(compressed.length); // compressed length
|
||||
bucketBuf.put(compressed);
|
||||
bucketBuf.flip();
|
||||
writeFullyAt(outChannel, bucketBuf, dataOffset);
|
||||
dataOffset += bucketBuf.limit();
|
||||
return new BucketRecord(epoch, payload.array());
|
||||
}
|
||||
// else: newPositionTable[bucketIndex] stays 0
|
||||
|
||||
syncedBucketEpochs[bucketIndex] = bucketWriteEpoch;
|
||||
} else {
|
||||
// Not dirty: copy bytes from old file if available
|
||||
if (oldPositionTable != null && oldPositionTable[bucketIndex] != 0) {
|
||||
final long oldOffset = oldPositionTable[bucketIndex];
|
||||
final ByteBuffer lensBuf = ByteBuffer.allocate(8);
|
||||
readFullyAt(oldChannel, lensBuf, oldOffset);
|
||||
lensBuf.flip();
|
||||
lensBuf.getInt(); // skip originalLen
|
||||
final int compressedLen = lensBuf.getInt();
|
||||
|
||||
final long bucketTotalSize = 8L + compressedLen;
|
||||
newPositionTable[bucketIndex] = dataOffset;
|
||||
outChannel.position(dataOffset);
|
||||
long remaining = bucketTotalSize;
|
||||
long srcPos = oldOffset;
|
||||
while (remaining > 0) {
|
||||
final long transferred = oldChannel.transferTo(srcPos, remaining, outChannel);
|
||||
srcPos += transferred;
|
||||
remaining -= transferred;
|
||||
}
|
||||
dataOffset += bucketTotalSize;
|
||||
private void markBucketsSynced(long[] syncedBucketEpochs) {
|
||||
for (int i = 0; i < syncedBucketEpochs.length; i++) {
|
||||
// note: a dirty bucket always has a write epoch >= 1, so 0 = untouched
|
||||
if (syncedBucketEpochs[i] != 0L) {
|
||||
BufferedLinearRegionFile.this.markBucketSynced(i, syncedBucketEpochs[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write the finalized position table
|
||||
final ByteBuffer posTableBuf = ByteBuffer.allocate(V3_POS_TABLE_SIZE);
|
||||
for (final long pos : newPositionTable) {
|
||||
posTableBuf.putLong(pos);
|
||||
}
|
||||
posTableBuf.flip();
|
||||
writeFullyAt(outChannel, posTableBuf, V3_POS_TABLE_OFFSET);
|
||||
|
||||
outChannel.force(true);
|
||||
} finally {
|
||||
BufferedLinearRegionFile.this.regionObjectLock.readLock().unlock();
|
||||
|
||||
if (oldChannel != null) {
|
||||
oldChannel.close();
|
||||
}
|
||||
// opens the master file for reading if it exists and is a valid V3 bucketed file, else null
|
||||
private @Nullable FileChannel openV3MasterFile(@NotNull Path mainFile) throws IOException {
|
||||
if (!Files.exists(mainFile)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final FileChannel channel = FileChannel.open(mainFile, StandardOpenOption.READ);
|
||||
try {
|
||||
Files.move(tmpFilePath, mainFile, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
if (channel.size() >= V3_DATA_AREA_OFFSET) {
|
||||
final ByteBuffer header = ByteBuffer.allocate(V3_HEADER_SIZE);
|
||||
readFullyAt(channel, header, 0);
|
||||
header.flip();
|
||||
|
||||
if (header.getLong() == MASTER_FILE_SUPER_BLOCK && header.get() == MASTER_FILE_VERSION_BUCKET) {
|
||||
return channel;
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
|
||||
try {
|
||||
Files.move(tmpFilePath, mainFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (Throwable ex) {
|
||||
Files.deleteIfExists(tmpFilePath);
|
||||
|
||||
e.addSuppressed(ex);
|
||||
|
||||
throw new IOException("Failed to replace master file!", e);
|
||||
channel.close();
|
||||
} catch (IOException e2) {
|
||||
e.addSuppressed(e2);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
channel.close();
|
||||
return null;
|
||||
}
|
||||
|
||||
private void writeV3Header(@NotNull FileChannel channel) throws IOException {
|
||||
final ByteBuffer header = ByteBuffer.allocate(V3_HEADER_SIZE);
|
||||
|
||||
header.putLong(MASTER_FILE_SUPER_BLOCK);
|
||||
header.put(MASTER_FILE_VERSION_BUCKET);
|
||||
header.put(BufferedLinearRegionFile.this.compressionLevel);
|
||||
header.putInt(BufferedLinearRegionFile.this.xxHash32Seed);
|
||||
header.flip();
|
||||
|
||||
writeFullyAt(channel, header, 0);
|
||||
}
|
||||
|
||||
private @NotNull ByteBuffer encodePositionTable(long[] table) {
|
||||
final ByteBuffer buf = ByteBuffer.allocate(V3_POS_TABLE_SIZE);
|
||||
|
||||
for (final long pos : table) {
|
||||
buf.putLong(pos);
|
||||
}
|
||||
|
||||
return buf.flip();
|
||||
}
|
||||
|
||||
private @NotNull ByteBuffer readRecordLengths(@NotNull FileChannel channel, long recordOffset) throws IOException {
|
||||
final ByteBuffer lens = ByteBuffer.allocate(V3_RECORD_HEADER_SIZE);
|
||||
|
||||
readFullyAt(channel, lens, recordOffset);
|
||||
|
||||
return lens.flip();
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
this.masterFileLock.writeLock().lock();
|
||||
try {
|
||||
if (this.appendChannel != null) {
|
||||
this.appendChannel.close();
|
||||
this.appendChannel = null;
|
||||
}
|
||||
} finally {
|
||||
this.masterFileLock.writeLock().unlock();
|
||||
}
|
||||
|
||||
for (int i = 0; i < syncedBucketEpochs.length; i++) {
|
||||
final long syncedEpoch = syncedBucketEpochs[i];
|
||||
if (syncedEpoch != 0L) {
|
||||
BufferedLinearRegionFile.this.markBucketSynced(i, syncedEpoch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadBucketsFor(Path file, int bucketIndex) throws IOException {
|
||||
private void loadBucketsFor(@NotNull Path file, int bucketIndex) throws IOException {
|
||||
final int beginChunkIndex = bucketIndex << BUCKET_SHIFT;
|
||||
|
||||
this.masterFileLock.readLock().lock();
|
||||
|
||||
try {
|
||||
final ByteBuffer decompressed;
|
||||
|
||||
if (this.appendChannel != null) {
|
||||
// WAL mode: reuse the always-open channel and the cached position table
|
||||
decompressed = this.readBucketData(this.appendChannel, this.positionTable[bucketIndex]);
|
||||
} else {
|
||||
if (!Files.exists(file)) {
|
||||
return;
|
||||
}
|
||||
@@ -1116,7 +1290,22 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
|
||||
return;
|
||||
}
|
||||
|
||||
final ByteBuffer headerBuf = ByteBuffer.allocate(14);
|
||||
this.checkV3Header(channel);
|
||||
|
||||
decompressed = this.readBucketData(channel, this.parseOffsetTable(channel)[bucketIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
if (decompressed != null) {
|
||||
this.loadChunksFromBucketData(decompressed, beginChunkIndex);
|
||||
}
|
||||
} finally {
|
||||
this.masterFileLock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void checkV3Header(@NotNull FileChannel channel) throws IOException {
|
||||
final ByteBuffer headerBuf = ByteBuffer.allocate(V3_HEADER_SIZE);
|
||||
readFullyAt(channel, headerBuf, 0);
|
||||
headerBuf.flip();
|
||||
|
||||
@@ -1128,31 +1317,23 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
|
||||
if (version != MASTER_FILE_VERSION_BUCKET)
|
||||
throw new IOException("Unknown version: " + version);
|
||||
|
||||
// compressionLevel and hashSeed consumed but not used here
|
||||
headerBuf.get();
|
||||
headerBuf.getInt();
|
||||
// compressionLevel and hashSeed are not used here
|
||||
}
|
||||
|
||||
final long[] posTable = this.parseOffsetTable(channel);
|
||||
// reads and decompresses one bucket record; null when the table entry is empty
|
||||
private @Nullable ByteBuffer readBucketData(@NotNull FileChannel channel, long recordOffset) throws IOException {
|
||||
if (recordOffset == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// New format: jump directly to bucket data
|
||||
final long bucketDataOffset = posTable[bucketIndex];
|
||||
if (bucketDataOffset == 0) return;
|
||||
|
||||
final ByteBuffer lensBuf = ByteBuffer.allocate(8);
|
||||
readFullyAt(channel, lensBuf, bucketDataOffset);
|
||||
lensBuf.flip();
|
||||
final int originalLen = lensBuf.getInt();
|
||||
final int compressedLen = lensBuf.getInt();
|
||||
final ByteBuffer lens = this.readRecordLengths(channel, recordOffset);
|
||||
final int originalLen = lens.getInt();
|
||||
final int compressedLen = lens.getInt();
|
||||
|
||||
final byte[] compressedData = new byte[compressedLen];
|
||||
readFullyAt(channel, ByteBuffer.wrap(compressedData), bucketDataOffset + 8);
|
||||
readFullyAt(channel, ByteBuffer.wrap(compressedData), recordOffset + V3_RECORD_HEADER_SIZE);
|
||||
|
||||
final ByteBuffer decompressed = ByteBuffer.wrap(Zstd.decompress(compressedData, originalLen));
|
||||
this.loadChunksFromBucketData(decompressed, beginChunkIndex);
|
||||
}
|
||||
} finally {
|
||||
this.masterFileLock.readLock().unlock();
|
||||
}
|
||||
return ByteBuffer.wrap(Zstd.decompress(compressedData, originalLen));
|
||||
}
|
||||
|
||||
private long @NonNull [] parseOffsetTable(FileChannel channel) throws IOException {
|
||||
@@ -1162,10 +1343,8 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
|
||||
|
||||
final long[] table = new long[BUCKET_COUNT];
|
||||
|
||||
Arrays.fill(table, 0L);
|
||||
for (int i = 0; i < BUCKET_COUNT; i++) {
|
||||
final long pos = buf.getLong();
|
||||
table[i] = pos;
|
||||
table[i] = buf.getLong();
|
||||
}
|
||||
|
||||
return table;
|
||||
@@ -1253,12 +1432,7 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
|
||||
bucketBuffer.get(chunkData);
|
||||
|
||||
// Mark bucket as loaded. writeChunk() bumps the bucket epoch so it gets synced to the new master format.
|
||||
final int blinearBucketIndex = chunkIndex >> BUCKET_SHIFT;
|
||||
final Bucket bucket = BufferedLinearRegionFile.this.buckets[blinearBucketIndex];
|
||||
|
||||
synchronized (bucket.lock) {
|
||||
bucket.loaded = true;
|
||||
}
|
||||
BufferedLinearRegionFile.this.markBucketLoaded(chunkIndex);
|
||||
|
||||
// Use writeChunk to go through the full path (adds length + timestamp + xxhash header)
|
||||
BufferedLinearRegionFile.this.writeChunk(chunkX, chunkZ, ByteBuffer.wrap(chunkData));
|
||||
@@ -1305,13 +1479,7 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
|
||||
|
||||
final ByteBuffer sectorDataNioBuffer = ByteBuffer.wrap(sectorData);
|
||||
|
||||
final int bucketIndex = index >> BUCKET_SHIFT;
|
||||
final Bucket bucket = BufferedLinearRegionFile.this.buckets[bucketIndex];
|
||||
|
||||
synchronized (bucket.lock) {
|
||||
bucket.loaded = true;
|
||||
}
|
||||
|
||||
BufferedLinearRegionFile.this.markBucketLoaded(index);
|
||||
BufferedLinearRegionFile.this.writeChunkDataRaw(index, sectorDataNioBuffer, false);
|
||||
}
|
||||
}
|
||||
@@ -1359,14 +1527,7 @@ public class BufferedLinearRegionFile implements io.nanachiyo0721.shiroha.data.R
|
||||
final int x = posByAxis[0];
|
||||
final int z = posByAxis[1];
|
||||
|
||||
|
||||
final int bucketIndex = i >> BUCKET_SHIFT;
|
||||
final Bucket bucket = BufferedLinearRegionFile.this.buckets[bucketIndex];
|
||||
|
||||
synchronized (bucket.lock) {
|
||||
bucket.loaded = true;
|
||||
}
|
||||
|
||||
BufferedLinearRegionFile.this.markBucketLoaded(i);
|
||||
BufferedLinearRegionFile.this.writeChunk(x, z, chunkDataNioBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user