Did the generation stuff change

This commit is contained in:
TomTheFurry
2022-08-18 18:29:04 +08:00
parent 3995e07d91
commit 1cc00b7174
14 changed files with 529 additions and 523 deletions
@@ -1,8 +0,0 @@
package com.seibel.lod.core.a7;
import com.seibel.lod.core.a7.datatype.PlaceHolderRenderSource;
@Deprecated
public interface PlaceHolderQueue {
void track(PlaceHolderRenderSource source); // Note: Implementation should only track a weak reference to the source.
}
@@ -1,26 +0,0 @@
package com.seibel.lod.core.a7.generation;
import com.seibel.lod.core.a7.datatype.LodDataSource;
import com.seibel.lod.core.a7.datatype.full.ChunkSizedData;
import com.seibel.lod.core.a7.pos.DhSectionPos;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
public class BlockingGenerationQueue {
final BiConsumer<DhSectionPos, ChunkSizedData> writeConsumer;
public BlockingGenerationQueue(BiConsumer<DhSectionPos, ChunkSizedData> writeConsumer) {
this.writeConsumer = writeConsumer;
}
public CompletableFuture<LodDataSource> generate(DhSectionPos sectPos, Supplier<LodDataSource> creator) {
}
}
@@ -1,13 +1,12 @@
package com.seibel.lod.core.a7.generation;
import com.seibel.lod.core.a7.datatype.LodDataSource;
import com.seibel.lod.core.a7.datatype.PlaceHolderRenderSource;
import com.seibel.lod.core.a7.datatype.full.ChunkSizedData;
import com.seibel.lod.core.a7.datatype.full.FullDataSource;
import com.seibel.lod.core.a7.pos.DhBlockPos2D;
import com.seibel.lod.core.a7.pos.DhLodPos;
import com.seibel.lod.core.a7.pos.DhSectionPos;
import com.seibel.lod.core.a7.save.io.file.IDataSourceProvider;
import com.seibel.lod.core.a7.util.ConcurrentQuadCombinableProviderTree;
import com.seibel.lod.core.a7.util.UncheckedInterruptedException;
import com.seibel.lod.core.logging.DhLoggerBuilder;
import com.seibel.lod.core.objects.DHChunkPos;
@@ -15,112 +14,55 @@ import com.seibel.lod.core.util.LodUtil;
import com.seibel.lod.core.util.gridList.ArrayGridList;
import org.apache.logging.log4j.Logger;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.BiConsumer;
public class GenerationQueue {
static class Request implements Comparable<Request> {
long cachedDist;
final DhSectionPos sectionPos;
Request(DhSectionPos sectionPos) {
this.sectionPos = sectionPos;
}
@Override
public int compareTo(Request o) {
return Long.compare(cachedDist, o.cachedDist);
}
@Override
public int hashCode() {
return sectionPos.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Request) {
return sectionPos.equals(((Request) obj).sectionPos);
}
return false;
}
}
public CompletableFuture<LodDataSource> generate(DhSectionPos sectionPos) {
return CompletableFuture.supplyAsync(() -> {
try {
return generate0(sectionPos);
} catch (Exception e) {
throw new CompletionException(e);
}
});
}
public class GenerationQueue implements AutoCloseable {
final ConcurrentQuadCombinableProviderTree<GenerationResult> cqcpTree = new ConcurrentQuadCombinableProviderTree<>();
IGenerator generator = null;
private final Logger logger = DhLoggerBuilder.getLogger();
DhBlockPos2D lastPlayerPos = new DhBlockPos2D(0, 0);
final ConcurrentHashMap<DhSectionPos, WeakReference<PlaceHolderRenderSource>> trackers = new ConcurrentHashMap<>();
final BiConsumer<DhSectionPos, ChunkSizedData> writeConsumer;
final HashSet<Request, CompletableFuture<?>> inProgressSections = new HashSet<>();
private final ConcurrentHashMap<DhLodPos, CompletableFuture<GenerationResult>> taskMap = new ConcurrentHashMap<>();
private final LinkedList<CompletableFuture<GenerationResult>> inProgress = new LinkedList<>();
public GenerationQueue(BiConsumer<DhSectionPos, ChunkSizedData> writeConsumer) {
this.writeConsumer = writeConsumer;
}
public void track(PlaceHolderRenderSource source) {
//logger.info("Tracking source {} at {}", source, source.getSectionPos());
trackers.put(source.getSectionPos(), new WeakReference<>(source));
}
private void update() {
LinkedList<DhSectionPos> toRemove = new LinkedList<>();
for (DhSectionPos pos : trackers.keySet()) {
WeakReference<PlaceHolderRenderSource> ref = trackers.get(pos);
if (ref.get() == null) {
toRemove.add(pos);
}
}
for (DhSectionPos pos : toRemove) {
trackers.remove(pos);
}
}
//FIXME: Do optimizations on polling closest to player. (Currently its a O(n) search!)
//FIXME: Do not return sections that is already being generated.
//FIXME: Optimize the checks for inProgressSections.
private DhSectionPos pollClosest(DhBlockPos2D playerPos) {
update();
DhSectionPos closest = null;
long closestDist = Long.MAX_VALUE;
for (DhSectionPos pos : trackers.keySet()) {
if (inProgressSections.contains(pos)) {
continue;
}
long distSqr = pos.getCenter().getCenter().distSquared(playerPos);
if (distSqr < closestDist) {
closest = pos;
closestDist = distSqr;
}
}
if (closest != null) inProgressSections.add(closest);
return closest;
}
public void doGeneration(IGenerator generator) {
if (generator == null) return;
public GenerationQueue() {}
public void pollAndStartClosest(DhBlockPos2D targetPos) {
if (generator == null) throw new IllegalStateException("generator is null");
if (generator.isBusy()) return;
DhLodPos closest = null;
long closestDist = Long.MAX_VALUE;
for (DhLodPos key : taskMap.keySet()) {
long dist = key.getCenter().distSquared(targetPos);
if (dist < closestDist) {
closest = key;
closestDist = dist;
}
}
if (closest != null) {
CompletableFuture<GenerationResult> future = taskMap.remove(closest);
startFuture(closest, future);
}
}
DhSectionPos pos = pollClosest(lastPlayerPos);
if (pos == null) return;
public void setGenerator(IGenerator generator) {
LodUtil.assertTrue(generator != null);
LodUtil.assertTrue(this.generator == null);
this.generator = generator;
}
public void removeGenerator() {
LodUtil.assertTrue(generator != null);
this.generator = null;
inProgress.forEach(f -> f.cancel(true));
inProgress.clear();
}
private CompletableFuture<GenerationResult> createFuture(DhLodPos pos) {
CompletableFuture<GenerationResult> future = new CompletableFuture<>();
CompletableFuture<GenerationResult> swapped = taskMap.put(pos, future);
LodUtil.assertTrue(swapped == null);
return future;
}
private void startFuture(DhLodPos pos, CompletableFuture<GenerationResult> resultFuture) {
byte dataDetail = generator.getDataDetail();
byte minGenGranularity = generator.getMinGenerationGranularity();
byte maxGenGranularity = generator.getMaxGenerationGranularity();
@@ -134,18 +76,18 @@ public class GenerationQueue {
byte granularity;
int count;
DHChunkPos chunkPosMin;
if (pos.sectionDetail < minUnitDetail) {
if (pos.detail < minUnitDetail) {
granularity = minGenGranularity;
count = 1;
chunkPosMin = new DHChunkPos(pos.getSectionBBoxPos().convertUpwardsTo(minUnitDetail).getCorner());
} else if (pos.sectionDetail > maxUnitDetail) {
chunkPosMin = new DHChunkPos(pos.convertUpwardsTo(minUnitDetail).getCorner());
} else if (pos.detail > maxUnitDetail) {
granularity = maxGenGranularity;
count = 1 << (pos.sectionDetail - maxUnitDetail);
chunkPosMin = new DHChunkPos(pos.getCorner().getCorner());
count = 1 << (pos.detail - maxUnitDetail);
chunkPosMin = new DHChunkPos(pos.getCorner());
} else {
granularity = (byte) (pos.sectionDetail - dataDetail);
granularity = (byte) (pos.detail - dataDetail);
count = 1;
chunkPosMin = new DHChunkPos(pos.getCorner().getCorner());
chunkPosMin = new DHChunkPos(pos.getCorner());
}
assert granularity >= minGenGranularity && granularity <= maxGenGranularity;
assert count > 0;
@@ -154,27 +96,27 @@ public class GenerationQueue {
int perCallChunksWidth = 1 << (granularity - 4);
final byte sectionDetail = (byte) (dataDetail + FullDataSource.SECTION_SIZE_OFFSET);
ArrayList<CompletableFuture<Void>> futures = new ArrayList<>(count*count);
ArrayList<CompletableFuture<Collection<ChunkSizedData>>> futures = new ArrayList<>(count*count);
for (int dx = 0; dx < count; dx++) {
for (int dz = 0; dz < count; dz++) { // TODO: Unroll this loop to yield when generator is busy.
DHChunkPos subCallChunkPosMin = new DHChunkPos(chunkPosMin.x + dx * perCallChunksWidth, chunkPosMin.z + dz * perCallChunksWidth);
CompletableFuture<ArrayGridList<ChunkSizedData>> dataFuture = generator.generate(subCallChunkPosMin, granularity);
futures.add(dataFuture.whenComplete((data, ex) -> {
futures.add(dataFuture.handle((data, ex) -> {
if (ex != null) {
if (ex instanceof CompletionException) {
ex = ex.getCause();
}
if (ex instanceof InterruptedException) return; // Ignore interrupted exceptions.
if (ex instanceof UncheckedInterruptedException) return; // Ignore unchecked interrupted exceptions.
if (ex instanceof InterruptedException) return null; // Ignore interrupted exceptions.
if (ex instanceof UncheckedInterruptedException) return null; // Ignore unchecked interrupted exceptions.
logger.error("Error generating data for section {}", pos, ex);
return;
return null;
}
assert data != null;
LodUtil.assertTrue(data != null);
if (data.gridSize < (1 << (granularity-4))) {
logger.error(
"Generator at {} returned {} by {} chunks but requested granularity was {}, which expect at least {} by {} chunks! ",
pos, data.gridSize, data.gridSize, granularity, perCallChunksWidth, perCallChunksWidth);
return;
return null;
}
DhLodPos minSectPos = new DhLodPos((byte)(dataDetail+4), data.getFirst().x, data.getFirst().z).convertUpwardsTo(sectionDetail);
@@ -186,49 +128,88 @@ public class GenerationQueue {
logger.info("Writing {} by {} chunks (at {}) with data detail {} to {} by {} sections (at {})",
data.gridSize, data.gridSize, subCallChunkPosMin, dataDetail,
sectionCount, sectionCount, minSectPos);
data.forEachPos((x,z) -> {
ChunkSizedData chunkData = data.get(x,z);
DhLodPos chunkDataPos = new DhLodPos((byte)(chunkData.dataDetail + 4), chunkData.x, chunkData.z);
if (pos.getSectionBBoxPos().overlaps(chunkDataPos))
writeConsumer.accept(pos, chunkData);
//DhSectionPos sectionPos = new DhSectionPos(chunkDataPos.detail, chunkDataPos.x, chunkDataPos.z);
//logger.info("Writing chunk {} with data detail {} to section {}",
// new DhLodPos((byte)(chunkData.dataDetail + 4), chunkData.x, chunkData.z),
// dataDetail, sectionPos);
});
//
// for (int dsx = 0; dsx < sectionCount; dsx++) {
// for (int dsz = 0; dsz < sectionCount; dsz++) {
// WeakReference<PlaceHolderRenderSource> ref = trackers.remove(new DhSectionPos(
// sectionDetail, minSectPos.x + dsx, minSectPos.z + dsz));
// if (ref == null) return; // No placeholder there, so no need to trigger a refresh on it.
// PlaceHolderRenderSource source = ref.get();
// if (source == null) return; // Same as above.
// source.markInvalid(); // Mark the placeholder as invalid, so it will be refreshed on next lodTree update.
// }
// }
}).exceptionally(ex -> {
if (ex instanceof CompletionException) {
ex = ex.getCause();
}
if (ex instanceof InterruptedException) return null; // Ignore interrupted exceptions.
if (ex instanceof UncheckedInterruptedException) return null; // Ignore unchecked interrupted exceptions.
logger.error("Error generating data for {} by {} chunks (at {}) with data detail {}",
perCallChunksWidth, perCallChunksWidth, subCallChunkPosMin, dataDetail, ex);
return null;
}).thenRun(()->{})); // Convert to a CompletableFuture<Void>.
return data;
}));
}
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).thenRun(() -> {
//try {
//Thread.sleep(10000); // FIXME: Only for current debug testing. REMOVE THIS!
//} catch (InterruptedException ignored) {}
WeakReference<PlaceHolderRenderSource> ref = trackers.remove(pos);
if (ref == null) return; // No placeholder there, so no need to trigger a refresh on it.
PlaceHolderRenderSource source = ref.get();
if (source == null) return; // Same as above.
source.markInvalid(); // Mark the placeholder as invalid, so it will be refreshed on next lodTree update.
});
inProgress.add(
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).thenApply((v) -> {
GenerationResult result = new GenerationResult();
for (CompletableFuture<Collection<ChunkSizedData>> future : futures) {
Collection<ChunkSizedData> data = future.join();
if (data == null) continue;
result.dataList.addAll(data);
}
return result;
}).handle((r, e) -> {
if (e!=null) resultFuture.completeExceptionally(e); else resultFuture.complete(r);
return null;
})
);
}
public CompletableFuture<LodDataSource> generate(DhSectionPos sectionPos) {
DhLodPos lodPos = sectionPos.getSectionBBoxPos();
return cqcpTree.createOrUseExisting(lodPos, this::createFuture).thenApply(
(result) -> {
if (result == null || result.dataList.isEmpty()) return FullDataSource.createEmpty(sectionPos);
FullDataSource newSource = FullDataSource.createEmpty(sectionPos);
for (ChunkSizedData data : result.dataList) {
newSource.update(data);
}
return newSource;
});
}
@Override
public void close() {
//TODO
}
//
// DhBlockPos2D lastPlayerPos = new DhBlockPos2D(0, 0);
// final ConcurrentHashMap<DhSectionPos, WeakReference<PlaceHolderRenderSource>> trackers = new ConcurrentHashMap<>();
// final BiConsumer<DhSectionPos, ChunkSizedData> writeConsumer;
// final HashSet<Request, CompletableFuture<?>> inProgressSections = new HashSet<>();
//
// public void track(PlaceHolderRenderSource source) {
// //logger.info("Tracking source {} at {}", source, source.getSectionPos());
// trackers.put(source.getSectionPos(), new WeakReference<>(source));
// }
//
// private void update() {
// LinkedList<DhSectionPos> toRemove = new LinkedList<>();
// for (DhSectionPos pos : trackers.keySet()) {
// WeakReference<PlaceHolderRenderSource> ref = trackers.get(pos);
// if (ref.get() == null) {
// toRemove.add(pos);
// }
// }
// for (DhSectionPos pos : toRemove) {
// trackers.remove(pos);
// }
// }
// //FIXME: Do optimizations on polling closest to player. (Currently its a O(n) search!)
// //FIXME: Do not return sections that is already being generated.
// //FIXME: Optimize the checks for inProgressSections.
// private DhSectionPos pollClosest(DhBlockPos2D playerPos) {
// update();
// DhSectionPos closest = null;
// long closestDist = Long.MAX_VALUE;
// for (DhSectionPos pos : trackers.keySet()) {
// if (inProgressSections.contains(pos)) {
// continue;
// }
// long distSqr = pos.getCenter().getCenter().distSquared(playerPos);
// if (distSqr < closestDist) {
// closest = pos;
// closestDist = distSqr;
// }
// }
// if (closest != null) inProgressSections.add(closest);
// return closest;
// }
}
@@ -0,0 +1,19 @@
package com.seibel.lod.core.a7.generation;
import com.seibel.lod.core.a7.datatype.full.ChunkSizedData;
import com.seibel.lod.core.a7.util.CombinableResult;
import java.util.ArrayList;
public class GenerationResult implements CombinableResult<GenerationResult> {
public final ArrayList<ChunkSizedData> dataList = new ArrayList<>();
@Override
public GenerationResult combineWith(GenerationResult b, GenerationResult c, GenerationResult d) {
dataList.ensureCapacity(dataList.size() + b.dataList.size() + c.dataList.size() + d.dataList.size());
dataList.addAll(b.dataList);
dataList.addAll(c.dataList);
dataList.addAll(d.dataList);
return this;
}
}
@@ -38,7 +38,7 @@ public class DhClientLevel implements IClientLevel {
this.save = save;
save.getDataFolder(level).mkdirs();
save.getRenderCacheFolder(level).mkdirs();
dataFileHandler = new RemoteDataFileHandler();
dataFileHandler = new RemoteDataFileHandler(this, save.getDataFolder(level));
renderFileHandler = new RenderFileHandler(dataFileHandler, this, save.getRenderCacheFolder(level));
tree = new LodQuadTree(this, Config.Client.Graphics.Quality.lodChunkRenderDistance.get()*16,
MC_CLIENT.getPlayerBlockPos().x, MC_CLIENT.getPlayerBlockPos().z, renderFileHandler);
@@ -3,6 +3,7 @@ package com.seibel.lod.core.a7.level;
import com.seibel.lod.core.a7.generation.GenerationQueue;
import com.seibel.lod.core.a7.generation.IGenerator;
import com.seibel.lod.core.a7.render.LodQuadTree;
import com.seibel.lod.core.a7.save.io.file.GeneratedDataFileHandler;
import com.seibel.lod.core.a7.util.FileScanner;
import com.seibel.lod.core.a7.save.io.file.DataFileHandler;
import com.seibel.lod.core.a7.save.io.render.RenderFileHandler;
@@ -38,14 +39,15 @@ public class DhClientServerLevel implements IClientLevel, IServerLevel {
public IClientLevelWrapper clientLevel;
public a7LodRenderer renderer = null;
public LodQuadTree tree = null;
public IGenerator worldGenerator = null;
public BatchGenerator worldGenerator = null;
public DhClientServerLevel(LocalSaveStructure save, IServerLevelWrapper level) {
this.serverLevel = level;
this.save = save;
save.getDataFolder(level).mkdirs();
save.getRenderCacheFolder(level).mkdirs();
dataFileHandler = new DataFileHandler(this, save.getDataFolder(level));
generationQueue = new GenerationQueue();
dataFileHandler = new GeneratedDataFileHandler(this, save.getDataFolder(level), generationQueue);
FileScanner.scanFile(save, serverLevel, dataFileHandler, null);
LOGGER.info("Started DHLevel for {} with saves at {}", level, save);
}
@@ -61,6 +63,7 @@ public class DhClientServerLevel implements IClientLevel, IServerLevel {
public void serverTick() {
//TODO Update network packet and stuff or state or etc..
}
public void startRenderer(IClientLevelWrapper clientLevel) {
LOGGER.info("Starting renderer for {}", this);
if (renderBufferHandler != null || this.clientLevel != null) {
@@ -68,13 +71,10 @@ public class DhClientServerLevel implements IClientLevel, IServerLevel {
return;
}
this.clientLevel = clientLevel;
// FIXME: This A need B and B need A messes needs to be reworked!
// TODO: Make a registry for generators for modding support.
worldGenerator = new BatchGenerator(this);
generationQueue.setGenerator(worldGenerator);
renderFileHandler = new RenderFileHandler(dataFileHandler, this, save.getRenderCacheFolder(serverLevel));
final RenderFileHandler f_renderFileHandler = renderFileHandler;
generationQueue = new GenerationQueue(f_renderFileHandler::write);
renderFileHandler.setPlaceHolderQueue(generationQueue);
tree = new LodQuadTree(this, Config.Client.Graphics.Quality.lodChunkRenderDistance.get()*16,
MC_CLIENT.getPlayerBlockPos().x, MC_CLIENT.getPlayerBlockPos().z, renderFileHandler);
renderBufferHandler = new RenderBufferHandler(tree);
@@ -103,10 +103,16 @@ public class DhClientServerLevel implements IClientLevel, IServerLevel {
tree = null;
renderBufferHandler.close();
renderBufferHandler = null;
generationQueue = null;
renderFileHandler.flushAndSave(); //Ignore the completion feature so that this action is async
renderFileHandler.close();
renderFileHandler = null;
generationQueue.removeGenerator();
try {
worldGenerator.close();
} catch (Exception e) {
LOGGER.error("Error closing world generator", e);
}
worldGenerator = null;
}
@Override
@@ -139,11 +145,9 @@ public class DhClientServerLevel implements IClientLevel, IServerLevel {
return renderFileHandler == null ? dataFileHandler.flushAndSave() : renderFileHandler.flushAndSave();
//Note: saving renderFileHandler will also save the dataFileHandler.
}
private BatchGenerator batchGenerator = null;
@Override
public void close() {
if (batchGenerator != null) batchGenerator.close();
if (worldGenerator != null) worldGenerator.close();
if (renderer != null) renderer.close();
if (tree != null) tree.close();
if (renderBufferHandler != null) renderBufferHandler.close();
@@ -155,14 +159,10 @@ public class DhClientServerLevel implements IClientLevel, IServerLevel {
@Override
public void doWorldGen() {
if (worldGenerator == null) {
// TODO: Make a registry for generators for modding support.
batchGenerator = new BatchGenerator(this);
worldGenerator = batchGenerator;
} else {
batchGenerator.update();
if (worldGenerator != null) {
worldGenerator.update();
if (generationQueue != null)
generationQueue.doGeneration(batchGenerator);
generationQueue.pollAndStartClosest(new DhBlockPos2D(MC_CLIENT.getPlayerBlockPos()));
}
}
@@ -20,7 +20,7 @@ public class DhServerLevel implements IServerLevel {
this.save = save;
this.level = level;
save.getDataFolder(level).mkdirs();
dataFileHandler = new DataFileHandler(this, save.getDataFolder(level));
dataFileHandler = new DataFileHandler(this, save.getDataFolder(level), null); //FIXME: GenerationQueue
FileScanner.scanFile(save, level, dataFileHandler, null);
LOGGER.info("Started DHLevel for {} with saves at {}", level, save);
}
@@ -4,6 +4,7 @@ import com.google.common.collect.HashMultimap;
import com.seibel.lod.core.a7.datatype.LodDataSource;
import com.seibel.lod.core.a7.datatype.full.ChunkSizedData;
import com.seibel.lod.core.a7.datatype.full.FullDataSource;
import com.seibel.lod.core.a7.level.ILevel;
import com.seibel.lod.core.a7.level.IServerLevel;
import com.seibel.lod.core.a7.pos.DhLodPos;
import com.seibel.lod.core.a7.pos.DhSectionPos;
@@ -28,14 +29,14 @@ public class DataFileHandler implements IDataSourceProvider {
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
final ExecutorService fileReaderThread = LodUtil.makeThreadPool(4, "FileReaderThread");
final ConcurrentHashMap<DhSectionPos, DataMetaFile> files = new ConcurrentHashMap<>();
final IServerLevel level;
final ILevel level;
final File saveDir;
AtomicInteger topDetailLevel = new AtomicInteger(-1);
final int minDetailLevel = FullDataSource.SECTION_SIZE_OFFSET;
final Function<DhSectionPos, CompletableFuture<LodDataSource>> dataSourceCreator;
public DataFileHandler(IServerLevel level, File saveRootDir,
public DataFileHandler(ILevel level, File saveRootDir,
Function<DhSectionPos, CompletableFuture<LodDataSource>> dataSourceCreator) {
this.saveDir = saveRootDir;
this.level = level;
@@ -139,15 +140,6 @@ public class DataFileHandler implements IDataSourceProvider {
return metaFile.loadOrGetCached(fileReaderThread);
}
// This prevents new higher detail levels from being created by not updating the topDetailLevel.
private CompletableFuture<LodDataSource> readWithoutUpdate(DhSectionPos pos) {
DataMetaFile metaFile = files.get(pos);
if (metaFile == null) {
return CompletableFuture.completedFuture(null);
}
return metaFile.loadOrGetCached(fileReaderThread);
}
/*
* This call is concurrent. I.e. it supports multiple threads calling this method at the same time.
*/
@@ -163,55 +155,6 @@ public class DataFileHandler implements IDataSourceProvider {
if (metaFile != null) { // Fast path: if there is a file for this section, just write to it.
metaFile.addToWriteQueue(chunkData);
}
/* else if (sectionPos.sectionDetail <= minDetailLevel) {
File file = computeDefaultFilePath(sectionPos);
//FIXME: Handle file already exists issue. Possibly by renaming the file.
LodUtil.assertTrue(!file.exists(), "File {} already exist for path {}", file, sectionPos);
CompletableFuture<LodDataSource> gen = new CompletableFuture<>();
DataMetaFile newMetaFile = new DataMetaFile(level, file, sectionPos, gen);
metaFile = files.putIfAbsent(sectionPos, newMetaFile); // This is a CAS with expected null value.
if (metaFile == null) {
newMetaFile.addToWriteQueue(chunkData);
CompletableFuture.runAsync(() -> gen.complete(FullDataSource.createEmpty(sectionPos)), fileReaderThread)
.exceptionally((e) -> {
gen.completeExceptionally(e);
return null;
});
} else {
metaFile.addToWriteQueue(chunkData);
gen.cancel(true);
}
} else {
File file = computeDefaultFilePath(sectionPos);
//FIXME: Handle file already exists issue. Possibly by renaming the file.
LodUtil.assertTrue(!file.exists(), "File {} already exist for path {}", file, sectionPos);
CompletableFuture<LodDataSource> gen = new CompletableFuture<>();
DataMetaFile newMetaFile = new DataMetaFile(level, file, sectionPos, gen);
metaFile = files.putIfAbsent(sectionPos, newMetaFile); // This is a CAS with expected null value.
if (metaFile == null) {
newMetaFile.addToWriteQueue(chunkData);
// Create future that generate downsized file
CompletableFuture<LodDataSource> subChunk0 = readWithoutUpdate(sectionPos.getChild(0));
CompletableFuture<LodDataSource> subChunk1 = readWithoutUpdate(sectionPos.getChild(1));
CompletableFuture<LodDataSource> subChunk2 = readWithoutUpdate(sectionPos.getChild(2));
CompletableFuture<LodDataSource> subChunk3 = readWithoutUpdate(sectionPos.getChild(3));
CompletableFuture.allOf(subChunk0, subChunk1, subChunk2, subChunk3)
.thenAccept(v ->
gen.complete(FullDataSource.createFromLower(sectionPos, new FullDataSource[]{
(FullDataSource) subChunk0.join(),
(FullDataSource) subChunk1.join(),
(FullDataSource) subChunk2.join(),
(FullDataSource) subChunk3.join()
}))
).exceptionally((e) -> {
gen.completeExceptionally(e);
return null;
});
} else {
metaFile.addToWriteQueue(chunkData);
gen.cancel(true);
}
}*/
if (sectionPos.sectionDetail <= topDetailLevel.get()) {
recursiveWrite(sectionPos.getParent(), chunkData);
}
@@ -1,17 +1,12 @@
package com.seibel.lod.core.a7.save.io.file;
import com.seibel.lod.core.a7.datatype.LodDataSource;
import com.seibel.lod.core.a7.generation.GenerationQueue;
import com.seibel.lod.core.a7.level.IServerLevel;
import com.seibel.lod.core.a7.pos.DhSectionPos;
import java.io.File;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
public class GeneratedDataFileHandler extends DataFileHandler {
public GeneratedDataFileHandler(IServerLevel level, File saveRootDir, GenerationQueue queue) {
super(level, saveRootDir, dataSourceCreator);
super(level, saveRootDir, queue::generate);
}
}
@@ -1,42 +1,15 @@
package com.seibel.lod.core.a7.save.io.file;
import com.seibel.lod.core.a7.datatype.LodDataSource;
import com.seibel.lod.core.a7.datatype.full.ChunkSizedData;
import com.seibel.lod.core.a7.datatype.full.FullFormat;
import com.seibel.lod.core.a7.pos.DhSectionPos;
import com.seibel.lod.core.a7.level.ILevel;
import com.seibel.lod.core.util.LodUtil;
import java.io.File;
import java.util.Collection;
import java.util.concurrent.CompletableFuture;
public class RemoteDataFileHandler implements IDataSourceProvider {
@Override
public void addScannedFile(Collection<File> detectedFiles) {
}
@Override
public CompletableFuture<LodDataSource> read(DhSectionPos pos) {
return null;
}
@Override
public void write(DhSectionPos sectionPos, ChunkSizedData chunkData) {
}
@Override
public CompletableFuture<Void> flushAndSave() {
return null;
}
@Override
public boolean isCacheValid(DhSectionPos sectionPos, long timestamp) {
return false;
}
@Override
public void close() throws Exception {
public class RemoteDataFileHandler extends DataFileHandler {
public RemoteDataFileHandler(ILevel level, File saveRootDir) {
super(level, saveRootDir, (pos) -> {
LodUtil.assertNotReach("TODO");
return null;
});
}
}
@@ -1,7 +1,6 @@
package com.seibel.lod.core.a7.save.io.render;
import com.google.common.collect.HashMultimap;
import com.seibel.lod.core.a7.PlaceHolderQueue;
import com.seibel.lod.core.a7.datatype.PlaceHolderRenderSource;
import com.seibel.lod.core.a7.datatype.LodRenderSource;
import com.seibel.lod.core.a7.datatype.column.ColumnRenderSource;
@@ -29,8 +28,6 @@ public class RenderFileHandler implements IRenderSourceProvider {
final IClientLevel level;
final File saveDir;
final IDataSourceProvider dataSourceProvider;
@Nullable
PlaceHolderQueue placeHolderQueue = null;
public RenderFileHandler(IDataSourceProvider sourceProvider, IClientLevel level, File saveRootDir) {
this.dataSourceProvider = sourceProvider;
@@ -38,10 +35,6 @@ public class RenderFileHandler implements IRenderSourceProvider {
this.saveDir = saveRootDir;
}
public void setPlaceHolderQueue(@Nullable PlaceHolderQueue queue) {
this.placeHolderQueue = queue;
}
/*
* Caller must ensure that this method is called only once,
* and that this object is not used before this method is called.
@@ -122,7 +115,6 @@ public class RenderFileHandler implements IRenderSourceProvider {
}
if (render != null) return render;
PlaceHolderRenderSource placeHolder = new PlaceHolderRenderSource(pos);
if (placeHolderQueue != null) placeHolderQueue.track(placeHolder);
return placeHolder;
}
);
@@ -0,0 +1,5 @@
package com.seibel.lod.core.a7.util;
public interface CombinableResult<T> {
T combineWith(T b, T c, T d);
}
@@ -1,202 +0,0 @@
package com.seibel.lod.core.a7.util;
import com.seibel.lod.core.a7.pos.DhLodPos;
import com.seibel.lod.core.util.Atomics;
import com.seibel.lod.core.util.LodUtil;
import com.sun.jna.platform.unix.X11;
import org.apache.commons.lang3.NotImplementedException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicReferenceArray;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
interface CombinableResult<T> {
T combineWith(T b, T c, T d);
}
public class ConcurrentGenerationPosTree<R extends CombinableResult<R>> {
public class Node {
private final DhLodPos pos;
public final AtomicReference<CompletableFuture<R>> future;
public final AtomicReferenceArray<Node> children = new AtomicReferenceArray<>(4);
private Node(DhLodPos pos, CompletableFuture<R> future) {
this.pos = pos;
this.future = new AtomicReference<>(future);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Node node = (Node) o;
return pos.equals(node.pos);
}
@Override
public int hashCode() {
return pos.hashCode();
}
}
class RootMap {
private final ConcurrentSkipListMap<DhLodPos, Node> roots = new ConcurrentSkipListMap<>();
private final int topLevel;
RootMap(int topLevel) {
this.topLevel = topLevel;
}
Node get(DhLodPos pos) {
return roots.get(pos);
}
Node swap(DhLodPos pos, Node newRoot) {
return roots.put(pos, newRoot);
}
Node compareNullAndExchange(DhLodPos pos, Node newRoot) {
return roots.putIfAbsent(pos, newRoot);
}
boolean compareNullAndSet(DhLodPos pos, Node newRoot) {
Node oldRoot = roots.putIfAbsent(pos, newRoot);
return oldRoot == null;
}
Node setIfNullAndGet(DhLodPos pos, Node newRoot) {
Node oldRoot = roots.putIfAbsent(pos, newRoot);
return oldRoot == null ? newRoot : oldRoot;
}
Node swapNull(DhLodPos pos) {
return roots.remove(pos);
}
}
private final AtomicReference<RootMap> rootMap = new AtomicReference<>(new RootMap(0));
public ConcurrentGenerationPosTree() {}
@Override
public int hashCode() {
throw new NotImplementedException();
}
@Override
public boolean equals(Object obj) {
throw new NotImplementedException();
}
@Override
protected Object clone() throws CloneNotSupportedException {
throw new NotImplementedException();
}
@Override
public String toString() {
throw new NotImplementedException();
}
// Atomically update and get the generation future
private CompletableFuture<R> checkAndMakeFuture(Node node, BiConsumer<DhLodPos, CompletableFuture<R>> allNullCompleter) {
CompletableFuture<R> future = new CompletableFuture<>();
CompletableFuture<R> casValue = Atomics.compareAndExchange(node.future, null, future);
if (casValue != null) { // cas failed. Existing future. Return it.
return future;
}
// Next, we need to make the future completable.
// We first check for each child connection if it exists. If it does, we store it for a later 'allOf'.
boolean allNull = true;
@SuppressWarnings("unchecked")
CompletableFuture<R>[] childFutures = new CompletableFuture[4];
for (int i = 0; i < 4; i++) {
Node nextChild = node.children.get(i);
if (nextChild != null) { // child node exists. Recursively make or get the child's future.
allNull = false;
childFutures[i] = checkAndMakeFuture(nextChild, allNullCompleter);
}
}
if (allNull) { // all children are null. We can then just run the allNullCompleter in this node.
allNullCompleter.accept(node.pos, future);
} else { // some children exist. We need to wait for some or all of them to complete.
// But before that, we need to create the children node where they are missing.
for (int i = 0; i < 4; i++) {
if (childFutures[i] == null) {
CompletableFuture<R> newChildFuture = new CompletableFuture<>();
node.children.set(i, new Node(node.pos.getChild(i), newChildFuture));
// Since the child is new, we can be sure that it doesn't have any children.
// So, we need to make the new child's future completable by running the allNullCompleter.
// (The above relies on the fact that we did a CAS on the beginning of this method,
// which means that we have unique access to the node and it's links to the children, and that
// no other thread can be concurrently modifying its links)
allNullCompleter.accept(node.pos.getChild(i), newChildFuture);
}
}
// Now, we can wait for all the child futures to complete, and then complete this node's future with
// the combined result of all child futures.
CompletableFuture.allOf(childFutures).thenRun(() ->
future.complete(childFutures[0].join().combineWith(
childFutures[1].join(), childFutures[2].join(), childFutures[3].join())));
}
return future;
}
public CompletableFuture<R> createOrUseExisting(DhLodPos pos, BiConsumer<DhLodPos, CompletableFuture<R>> completer) {
RootMap map = rootMap.get();
if (map.topLevel == pos.detail) {
CompletableFuture<R> future = new CompletableFuture<>();
Node newNode = new Node(pos, future);
Node cas = map.compareNullAndExchange(pos, newNode);
if (cas == null) { // cas succeeded. No existing overlapping node in same detail level.
// Since any lower level nodes should have upper level nodes as parent up to the top level,
// and that there are no same level nodes, we can assume that the new node does not overlap any existing nodes.
// Therefore, we can apply the completer function to the new node, and return the future.
completer.accept(pos, future);
return future;
} else { // cas failed. Existing overlapping node.
// Run the checkAndMakeFuture method on the existing node to update and get the generation future.
return checkAndMakeFuture(cas, completer);
}
} else if (map.topLevel > pos.detail) {
// We need to traverse down the tree with the following rules during the traversal:
// 1. If the next node is not null and has a future, halt and return that future.
// 2. If the next node is not null with no future, continue traversing down the tree.
// 3. if the next node is null, create a new node and CompareExchange it into the current node, and run rule 1/2.
// Note that DO NOT assume that all subsequent nodes will fall into case 3, as someone else can concurrently
// use and modify the newly created node!
// To start, just treat the rootMap as the... well, root, and it's content as the children node.
// We can then traverse down the tree until we reach the target node or hit the 1st case and return prematurely.
// First iteration:
Node currentNode;
Node childNode = map.setIfNullAndGet(pos.convertUpwardsTo((byte) map.topLevel), new Node(pos, null)); // rule 3: if null, create a new node.
CompletableFuture<R> future = childNode.future.get();
if (future != null) { // rule 1: if future is not null, halt and return the future.
return future;
} else { // rule 2: if future is null, continue traversing down the tree.
currentNode = childNode;
// Second and subsequent iterations:
while (currentNode.pos.detail > pos.detail) {
Node newNode = new Node(pos.convertUpwardsTo((byte)(currentNode.pos.detail-1)), null);
childNode = Atomics.compareAndSetThenGet(currentNode.children,
newNode.pos.getChildIndexOfParent(), null, newNode); // rule 3: if null, create a new node.
if (childNode == null) { // rule 1: if future is not null, halt and return the future.
return newNode.future.get();
} else { // rule 2: if future is null, continue traversing down the tree.
currentNode = childNode;
}
}
}
// At this point, we have reached the target node.
LodUtil.assertTrue(currentNode.pos.equals(pos));
// We can now run the checkAndMakeFuture method on the target node to update and get the generation future.
return checkAndMakeFuture(currentNode, completer); // Technically, this will rerun the 1st rule. But code is cleaner this way.
} else { // map.topLevel < pos.detail
// Now, this is the complex case. We need to rebase the tree to the higher detail level.
// For now, this implementation will do a lock based version. However, I will figure out a way to do this without a lock.
// Before we do anything, we ...
// TODO!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
return null;
}
}
}
@@ -0,0 +1,334 @@
package com.seibel.lod.core.a7.util;
import com.seibel.lod.core.a7.pos.DhLodPos;
import com.seibel.lod.core.logging.DhLoggerBuilder;
import com.seibel.lod.core.util.Atomics;
import com.seibel.lod.core.util.LodUtil;
import org.apache.commons.lang3.NotImplementedException;
import org.apache.logging.log4j.Logger;
import java.lang.ref.WeakReference;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicReferenceArray;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Function;
public class ConcurrentQuadCombinableProviderTree<R extends CombinableResult<R>> {
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
public static class Node<R> {
private final DhLodPos pos;
public final AtomicReference<CompletableFuture<R>> future;
// The child node is stored as a weak reference so that it can be garbage collected when that node's future is completed
// and which then releases the hold on that node, thus allowing automatic garbage collection.
public final AtomicReferenceArray<WeakReference<Node<R>>> children = new AtomicReferenceArray<>(4);
@SuppressWarnings("unused")
AtomicReference<Node<R>> parent = null; // This is only used to ensure that the parent is not garbage collected before the child.
private Node(DhLodPos pos, CompletableFuture<R> future) {
this.pos = pos;
this.future = new AtomicReference<>(future);
}
private Node(DhLodPos pos, CompletableFuture<R> future, Node<R> parent) {
this.pos = pos;
this.future = new AtomicReference<>(future);
this.parent = new AtomicReference<>(parent);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Node<R> node = (Node<R>) o;
return pos.equals(node.pos);
}
@Override
public int hashCode() {
return pos.hashCode();
}
public Node<R> setIfNullAndGet(int childIndex, Node<R> newChild) {
WeakReference<Node<R>> newRef = new WeakReference<>(newChild);
WeakReference<Node<R>> oldRef;
do {
oldRef = Atomics.compareAndExchange(children, childIndex, null, newRef);
if (oldRef == null) return newChild; // CompareAndExchange succeeded
Node<R> oldNode = oldRef.get();
if (oldNode != null) return oldNode; // CompareAndExchange failed with old node not null
// Otherwise, the old node weak reference is null.
} while (!children.compareAndSet(childIndex, oldRef, newRef)); // If this cas fails, then try again. (Some other thread beat us to it.)
return newChild; // If we get here, then we successfully replaced the old node weak reference with the new one.
}
}
static class RootMap<R> {
private final ConcurrentHashMap<DhLodPos, WeakReference<Node<R>>> roots = new ConcurrentHashMap<>();
private final int topLevel;
RootMap(int topLevel) {
this.topLevel = topLevel;
}
public int getTopLevel() {
return topLevel;
}
public Node<R> get(DhLodPos pos) {
WeakReference<Node<R>> ref = roots.get(pos);
return ref == null ? null : ref.get();
}
public Node<R> compareNullAndExchange(DhLodPos pos, Node<R> newRoot) {
WeakReference<Node<R>> newRef = new WeakReference<>(newRoot);
WeakReference<Node<R>> oldRef;
do {
oldRef = roots.putIfAbsent(pos, newRef);
if (oldRef == null) return null; // putIfAbsent succeeded
Node<R> oldRoot = oldRef.get();
if (oldRoot != null) return oldRoot; // putIfAbsent failed with old root not null
// Otherwise, the old root weak reference is null.
} while (!roots.replace(pos, oldRef, newRef)); // If this cas fails, then try again. (Some other thread beat us to it.)
return null; // If we get here, then we successfully replaced the old root weak reference with the new one, so return null.
}
public boolean compareNullAndSet(DhLodPos pos, Node<R> newRoot) {
WeakReference<Node<R>> newRef = new WeakReference<>(newRoot);
WeakReference<Node<R>> oldRef;
do {
oldRef = roots.putIfAbsent(pos, newRef);
if (oldRef == null) return true; // putIfAbsent succeeded
Node<R> oldRoot = oldRef.get();
if (oldRoot != null) return false; // putIfAbsent failed with old root not null
// Otherwise, the old root weak reference is null.
} while (!roots.replace(pos, oldRef, newRef)); // If this cas fails, then try again. (Some other thread beat us to it.)
return true; // If we get here, then we successfully replaced the old root weak reference with the new one.
}
public Node<R> setIfNullAndGet(DhLodPos pos, Node<R> newRoot) {
WeakReference<Node<R>> newRef = new WeakReference<>(newRoot);
WeakReference<Node<R>> oldRef;
do {
oldRef = roots.putIfAbsent(pos, newRef);
if (oldRef == null) return newRoot; // putIfAbsent succeeded
Node<R> oldRoot = oldRef.get();
if (oldRoot != null) return oldRoot; // putIfAbsent failed with old root not null
// Otherwise, the old root weak reference is null.
} while (!roots.replace(pos, oldRef, newRef)); // If this cas fails, then try again. (Some other thread beat us to it.)
return newRoot; // If we get here, then we successfully replaced the old root weak reference with the new one.
}
public void clean() {
roots.forEach((k,v) -> {
if (v.get() == null) // Remove the entry if the root is null
roots.remove(k, v); // But only if what we check is what we will be removing. (A CAS operation)
// Otherwise, continue.
// (It is not important that we must remove the entry if the root is null,
// as this is just a cleanup op to shrink the map.)
});
}
}
private final ReentrantReadWriteLock rootMapGlobalLock = new ReentrantReadWriteLock();
private final AtomicReference<RootMap<R>> rootMap = new AtomicReference<>(new RootMap<>(0));
public ConcurrentQuadCombinableProviderTree() {}
@Override
public String toString() {
return "CQCPT@" + rootMap.get().topLevel + "(~" + rootMap.get().roots.size() + ")";
}
// Atomically update and get the generation future
private CompletableFuture<R> checkAndMakeFuture(Node<R> node, Function<DhLodPos, CompletableFuture<R>> allNullCompleter) {
CompletableFuture<R> future = new CompletableFuture<>();
CompletableFuture<R> casValue = Atomics.compareAndExchange(node.future, null, future);
if (casValue != null) { // cas failed. Existing future. Return it.
return future;
}
// Next, we need to make the future completable.
// We first check for each child connection if it exists. If it does, we store it for a later 'allOf'.
boolean allNull = true;
@SuppressWarnings("unchecked")
CompletableFuture<R>[] childFutures = new CompletableFuture[4];
for (int i = 0; i < 4; i++) {
WeakReference<Node<R>> childRef = node.children.get(i);
Node<R> nextChild = childRef == null ? null : childRef.get();
if (nextChild != null) { // child node exists. Recursively make or get the child's future.
allNull = false;
childFutures[i] = checkAndMakeFuture(nextChild, allNullCompleter);
}
}
if (allNull) { // all children are null. We can then just run the allNullCompleter in this node.
allNullCompleter.apply(node.pos).thenAccept(future::complete);
} else { // some children exist. We need to wait for some or all of them to complete.
// But before that, we need to create the children node where they are missing.
for (int i = 0; i < 4; i++) {
if (childFutures[i] == null) {
CompletableFuture<R> newChildFuture = new CompletableFuture<>();
Node<R> newChild = new Node<>(node.pos.getChild(i), newChildFuture, node);
node.children.set(i, new WeakReference<>(newChild));
// Since the child is new, we can be sure that it doesn't have any children.
// So, we need to make the new child's future completable by running the allNullCompleter.
// (The above relies on the fact that we did a CAS on the beginning of this method,
// which means that we have unique access to the node and its links to the children, and that
// no other thread can be concurrently modifying its links)
allNullCompleter.apply(node.pos.getChild(i)).whenComplete((r, e) -> {
// NOTE(*1): This *HAVE* to get the future via the node reference instead of directly capturing the future,
// as otherwise the node will be garbage collected before the future is completed.
// With this, we can guarantee that the node is garbage collected only when the future is (being) completed.
// (The actual order is not important however as long as the node is still alive when the generation is in progress)
CompletableFuture<R> f = node.future.get();
LodUtil.assertTrue(f != null, "Future should not be null");
if (e != null) {
f.completeExceptionally(e);
} else {
f.complete(r);
}
});
}
}
// Now, we can wait for all the child futures to complete, and then complete this node's future with
// the combined result of all child futures.
CompletableFuture.allOf(childFutures).handle((v, e) -> {
// NOTE: Same as 'NOTE(*1)', we *HAVE* to get the future via the node reference instead of directly capturing the future.
CompletableFuture<R> f = node.future.get();
LodUtil.assertTrue(f != null, "Future should not be null");
if (e != null) {
f.completeExceptionally(e);
} else {
try {
f.complete(childFutures[0].join().combineWith(
childFutures[1].join(), childFutures[2].join(), childFutures[3].join()));
} catch (Throwable e2) {
f.completeExceptionally(e2);
}
}
return null;
});
}
return future;
}
public CompletableFuture<R> createOrUseExisting(DhLodPos pos, Function<DhLodPos, CompletableFuture<R>> completer) {
int cleanRng = ThreadLocalRandom.current().nextInt(0, 10);
if (cleanRng == 0) cleanIfNeeded();
// First, ensure that the root map is locked for reading. (The lock is for the structure of the map, not the values)
rootMapGlobalLock.readLock().lock();
RootMap<R> map = rootMap.get();
// Next, do different thing depending on the top level of the map compared to the target position.
if (map.topLevel == pos.detail) { // The target position is at the top level, meaning that we can directly use the root.
// Make the future and node first for the later CAS on null.
CompletableFuture<R> future = new CompletableFuture<>();
Node<R> newNode = new Node<R>(pos, future); // No parent node as it's the root.
Node<R> cas = map.compareNullAndExchange(pos, newNode); // CAS the node into the map.
rootMapGlobalLock.readLock().unlock(); // We're done with the map, as following code no longer accesses it.
if (cas == null) { // cas succeeded. Which means no existing overlapping node in same detail level.
// Reason: Since any lower level nodes should have upper level nodes as parent up to the top level,
// and that there are no same level nodes, we can assume that the new node does not overlap any existing nodes.
// Therefore, we can apply the completer function to the new node, and return the future.
completer.apply(pos).whenComplete((r, e) -> {
// See NOTE(*1) above.
CompletableFuture<R> f = newNode.future.get();
LodUtil.assertTrue(f != null, "Future should not be null");
if (e != null) {
f.completeExceptionally(e);
} else {
f.complete(r);
}
});
return future;
} else { // cas failed. Existing overlapping node.
// Run the checkAndMakeFuture method on the existing node to update and get the generation future.
return checkAndMakeFuture(cas, completer);
}
} else if (map.topLevel > pos.detail) {
// We need to traverse down the tree with the following rules during the traversal:
// 1. If the next node is not null and has a future, halt and return that future.
// 2. If the next node is not null with no future, continue traversing down the tree.
// 3. if the next node is null, create a new node and CompareExchange it into the current node, and run rule 1/2.
// Note that DO NOT assume that all subsequent nodes will fall into case 3, as someone else can concurrently
// use and modify the newly created node!
// To start, just treat the rootMap as the... well, root, and it's content as the children node.
// We can then traverse down the tree until we reach the target node or hit the 1st case and return prematurely.
// First iteration:
Node<R> currentNode;
Node<R> childNode = map.setIfNullAndGet( // rule 3: if null, create a new node.
pos.convertUpwardsTo((byte) map.topLevel),
new Node<R>(pos, null)); // No parent node as it's the root.
rootMapGlobalLock.readLock().unlock(); // We're done with the map, as following code no longer accesses it.
CompletableFuture<R> future = childNode.future.get();
if (future != null) { // rule 1: if future is not null, halt and return the future.
return future;
} else { // rule 2: if future is null, continue traversing down the tree.
currentNode = childNode;
// Second and subsequent iterations:
while (currentNode.pos.detail > pos.detail) {
Node<R> newNode = new Node<R>(pos.convertUpwardsTo((byte) (currentNode.pos.detail - 1)), null, currentNode);
// Note: It is important that child link is set and created before we check the child future,
// so to avoid race conditions with checkAndMakeFuture.
childNode = currentNode.setIfNullAndGet(newNode.pos.getChildIndexOfParent(), newNode); // rule 3: if null, create a new node.
CompletableFuture<R> childFuture = childNode.future.get();
if (childFuture != null) { // rule 1: if future is not null, halt and return the future.
return childFuture;
} else { // rule 2: if future is null, continue traversing down the tree.
currentNode = childNode;
}
}
}
// At this point, we have reached the target node.
LodUtil.assertTrue(currentNode.pos.equals(pos));
// We can now run the checkAndMakeFuture method on the target node to update and get the generation future.
return checkAndMakeFuture(currentNode, completer); // Technically, this will rerun the 1st rule. But code is cleaner this way.
} else { // map.topLevel < pos.detail
// Now, this is the complex case. We need to rebase the tree to the higher detail level.
// For now, this implementation will do a lock based version. However, I will figure out a way to do this without a lock.
rootMapGlobalLock.readLock().unlock();
while (map.topLevel < pos.detail) {
map = rebaseUpward(pos.detail);
}
LodUtil.assertTrue(map.topLevel >= pos.detail);
return createOrUseExisting(pos, completer); // After rebasing, we can just call the createOrUseExisting method again.
}
}
private RootMap<R> rebaseUpward(int targetLevel) {
rootMapGlobalLock.writeLock().lock();
RootMap<R> map = rootMap.get();
if (map.topLevel >= targetLevel) {
rootMapGlobalLock.writeLock().unlock();
return map;
}
// At this point, we have exclusive access to the rootMap.
map.clean(); // Clean the map. (Could actually be done with just readLock.)
RootMap<R> newMap = new RootMap<>(map.topLevel + 1);
if (map.roots.isEmpty()) return newMap; // If there are no roots, just return the new map.
map.roots.forEach((pos, nodeRef) -> {
Node<R> node = nodeRef.get();
if (node == null) return; // If null, ignore that node.
LodUtil.assertTrue(pos.detail+1 == newMap.topLevel);
LodUtil.assertTrue(node.parent.get() == null);
LodUtil.assertTrue(node.pos.equals(pos));
DhLodPos newPos = pos.convertUpwardsTo((byte) (pos.detail+1));
// Create the parent node, or if it already exists, use it to set the child node's parent.
// NOTE: While this section is protected by the rootMapGlobalLock, we still need to use the normal
// CAS methods to setAndGet the parent node, as the parent node may be GC'd concurrently by other threads
// who have just completed the node's future, and caused the GC parent chain up to the new map.
Node<R> newParentNode = newMap.setIfNullAndGet(newPos, new Node<R>(newPos, null));
node.parent.set(newParentNode);
});
boolean casWorked = rootMap.compareAndSet(map, newMap);
LodUtil.assertTrue(casWorked);
rootMapGlobalLock.writeLock().unlock();
return newMap;
}
public void cleanIfNeeded() {
if (rootMapGlobalLock.readLock().tryLock()) {
rootMap.get().clean();
rootMapGlobalLock.readLock().unlock();
}
}
}