001/* 002 * PlotSquared, a land and world management plugin for Minecraft. 003 * Copyright (C) IntellectualSites <https://intellectualsites.com> 004 * Copyright (C) IntellectualSites team and contributors 005 * 006 * This program is free software: you can redistribute it and/or modify 007 * it under the terms of the GNU General Public License as published by 008 * the Free Software Foundation, either version 3 of the License, or 009 * (at your option) any later version. 010 * 011 * This program is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 014 * GNU General Public License for more details. 015 * 016 * You should have received a copy of the GNU General Public License 017 * along with this program. If not, see <https://www.gnu.org/licenses/>. 018 */ 019package com.plotsquared.bukkit.queue; 020 021import com.google.inject.Inject; 022import com.google.inject.assistedinject.Assisted; 023import com.plotsquared.bukkit.BukkitPlatform; 024import com.plotsquared.core.PlotSquared; 025import com.plotsquared.core.queue.ChunkCoordinator; 026import com.plotsquared.core.queue.subscriber.ProgressSubscriber; 027import com.plotsquared.core.util.task.PlotSquaredTask; 028import com.plotsquared.core.util.task.TaskManager; 029import com.plotsquared.core.util.task.TaskTime; 030import com.sk89q.worldedit.math.BlockVector2; 031import com.sk89q.worldedit.world.World; 032import io.papermc.lib.PaperLib; 033import org.apache.logging.log4j.LogManager; 034import org.apache.logging.log4j.Logger; 035import org.bukkit.Bukkit; 036import org.bukkit.Chunk; 037import org.bukkit.plugin.Plugin; 038import org.bukkit.plugin.java.JavaPlugin; 039import org.checkerframework.checker.nullness.qual.NonNull; 040 041import java.util.Collection; 042import java.util.LinkedList; 043import java.util.List; 044import java.util.Queue; 045import java.util.concurrent.LinkedBlockingQueue; 046import java.util.concurrent.TimeUnit; 047import java.util.concurrent.TimeoutException; 048import java.util.concurrent.atomic.AtomicInteger; 049import java.util.function.Consumer; 050 051/** 052 * Utility that allows for the loading and coordination of chunk actions 053 * <p> 054 * The coordinator takes in collection of chunk coordinates, loads them 055 * and allows the caller to specify a sink for the loaded chunks. The 056 * coordinator will prevent the chunks from being unloaded until the sink 057 * has fully consumed the chunk 058 * </p> 059 **/ 060public final class BukkitChunkCoordinator extends ChunkCoordinator { 061 062 private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + BukkitChunkCoordinator.class.getSimpleName()); 063 064 private final List<ProgressSubscriber> progressSubscribers = new LinkedList<>(); 065 066 private final Queue<BlockVector2> requestedChunks; 067 private final Queue<Chunk> availableChunks; 068 private final long maxIterationTime; 069 private final Plugin plugin; 070 private final Consumer<BlockVector2> chunkConsumer; 071 private final org.bukkit.World bukkitWorld; 072 private final Runnable whenDone; 073 private final Consumer<Throwable> throwableConsumer; 074 private final boolean unloadAfter; 075 private final int totalSize; 076 private final AtomicInteger expectedSize; 077 private final AtomicInteger loadingChunks = new AtomicInteger(); 078 private final boolean forceSync; 079 private final boolean shouldGen; 080 081 private int batchSize; 082 private PlotSquaredTask task; 083 private volatile boolean shouldCancel; 084 private boolean finished; 085 086 @Inject 087 private BukkitChunkCoordinator( 088 @Assisted final long maxIterationTime, 089 @Assisted final int initialBatchSize, 090 @Assisted final @NonNull Consumer<BlockVector2> chunkConsumer, 091 @Assisted final @NonNull World world, 092 @Assisted final @NonNull Collection<BlockVector2> requestedChunks, 093 @Assisted final @NonNull Runnable whenDone, 094 @Assisted final @NonNull Consumer<Throwable> throwableConsumer, 095 @Assisted("unloadAfter") final boolean unloadAfter, 096 @Assisted final @NonNull Collection<ProgressSubscriber> progressSubscribers, 097 @Assisted("forceSync") final boolean forceSync, 098 @Assisted("shouldGen") final boolean shouldGen 099 ) { 100 this.requestedChunks = new LinkedBlockingQueue<>(requestedChunks); 101 this.availableChunks = new LinkedBlockingQueue<>(); 102 this.totalSize = requestedChunks.size(); 103 this.expectedSize = new AtomicInteger(this.totalSize); 104 this.batchSize = initialBatchSize; 105 this.chunkConsumer = chunkConsumer; 106 this.maxIterationTime = maxIterationTime; 107 this.whenDone = whenDone; 108 this.throwableConsumer = throwableConsumer; 109 this.unloadAfter = unloadAfter; 110 this.plugin = JavaPlugin.getPlugin(BukkitPlatform.class); 111 this.bukkitWorld = Bukkit.getWorld(world.getName()); 112 this.progressSubscribers.addAll(progressSubscribers); 113 this.forceSync = forceSync; 114 this.shouldGen = shouldGen; 115 } 116 117 @Override 118 public void start() { 119 if (!forceSync) { 120 // Request initial batch 121 this.requestBatch(); 122 // Wait until next tick to give the chunks a chance to be loaded 123 TaskManager.runTaskLater(() -> task = TaskManager.runTaskRepeat(this, TaskTime.ticks(1)), TaskTime.ticks(1)); 124 } else { 125 try { 126 while (!shouldCancel && !requestedChunks.isEmpty()) { 127 chunkConsumer.accept(requestedChunks.poll()); 128 } 129 } catch (Throwable t) { 130 throwableConsumer.accept(t); 131 } finally { 132 finish(); 133 } 134 } 135 } 136 137 @Override 138 public void cancel() { 139 shouldCancel = true; 140 } 141 142 private void finish() { 143 try { 144 this.whenDone.run(); 145 } catch (final Throwable throwable) { 146 this.throwableConsumer.accept(throwable); 147 } finally { 148 for (final ProgressSubscriber subscriber : this.progressSubscribers) { 149 subscriber.notifyEnd(); 150 } 151 if (task != null) { 152 task.cancel(); 153 } 154 finished = true; 155 } 156 } 157 158 @Override 159 public void run() { 160 if (shouldCancel) { 161 if (unloadAfter) { 162 Chunk chunk; 163 while ((chunk = availableChunks.poll()) != null) { 164 freeChunk(chunk); 165 } 166 } 167 finish(); 168 return; 169 } 170 171 Chunk chunk = this.availableChunks.poll(); 172 if (chunk == null) { 173 if (this.availableChunks.isEmpty()) { 174 if (this.requestedChunks.isEmpty() && loadingChunks.get() == 0) { 175 finish(); 176 } else { 177 requestBatch(); 178 } 179 } 180 return; 181 } 182 long[] iterationTime = new long[2]; 183 int processedChunks = 0; 184 do { 185 final long start = System.currentTimeMillis(); 186 try { 187 this.chunkConsumer.accept(BlockVector2.at(chunk.getX(), chunk.getZ())); 188 } catch (final Throwable throwable) { 189 this.throwableConsumer.accept(throwable); 190 } 191 if (unloadAfter) { 192 this.freeChunk(chunk); 193 } 194 processedChunks++; 195 final long end = System.currentTimeMillis(); 196 // Update iteration time 197 iterationTime[0] = iterationTime[1]; 198 iterationTime[1] = end - start; 199 } while (iterationTime[0] + iterationTime[1] < this.maxIterationTime * 2 && (chunk = availableChunks.poll()) != null); 200 if (processedChunks < this.batchSize) { 201 // Adjust batch size based on the amount of processed chunks per tick 202 this.batchSize = processedChunks; 203 } 204 205 final int expected = this.expectedSize.addAndGet(-processedChunks); 206 207 if (expected <= 0) { 208 finish(); 209 } else { 210 if (this.availableChunks.size() < processedChunks) { 211 final double progress = ((double) totalSize - (double) expected) / (double) totalSize; 212 for (final ProgressSubscriber subscriber : this.progressSubscribers) { 213 subscriber.notifyProgress(this, progress); 214 } 215 this.requestBatch(); 216 } 217 } 218 } 219 220 /** 221 * Requests a batch of chunks to be loaded 222 */ 223 private void requestBatch() { 224 for (int i = 0; i < this.batchSize && this.requestedChunks.peek() != null; i++) { 225 // This required PaperLib to be bumped to version 1.0.4 to mark the request as urgent 226 final BlockVector2 chunk = this.requestedChunks.poll(); 227 loadingChunks.incrementAndGet(); 228 PaperLib 229 .getChunkAtAsync(this.bukkitWorld, chunk.getX(), chunk.getZ(), shouldGen, true) 230 .orTimeout(10L, TimeUnit.SECONDS) 231 .whenComplete((chunkObject, throwable) -> { 232 loadingChunks.decrementAndGet(); 233 if (throwable != null) { 234 if (throwable instanceof TimeoutException) { 235 LOGGER.warn("Timed out awaiting chunk load {}", chunk); 236 this.requestedChunks.offer(chunk); 237 } else { 238 LOGGER.error("Failed to load chunk {}", chunk, throwable); 239 // We want one less because this couldn't be processed 240 this.expectedSize.decrementAndGet(); 241 } 242 } else if (chunkObject == null) { 243 if (shouldGen) { 244 LOGGER.error("Null chunk returned for chunk at {}", chunk); 245 } 246 } else if (PlotSquared.get().isMainThread(Thread.currentThread())) { 247 this.processChunk(chunkObject); 248 } else { 249 TaskManager.runTask(() -> this.processChunk(chunkObject)); 250 } 251 }); 252 } 253 } 254 255 /** 256 * Once a chunk has been loaded, process it (add a plugin ticket and add to 257 * available chunks list). It is important that this gets executed on the 258 * server's main thread. 259 */ 260 private void processChunk(final @NonNull Chunk chunk) { 261 /* Chunk#isLoaded does not necessarily return true shortly after PaperLib#getChunkAtAsync completes, but the chunk is 262 still loaded. 263 if (!chunk.isLoaded()) { 264 throw new IllegalArgumentException(String.format("Chunk %d;%d is is not loaded", chunk.getX(), chunk.getZ()); 265 }*/ 266 if (finished) { 267 return; 268 } 269 chunk.addPluginChunkTicket(this.plugin); 270 this.availableChunks.add(chunk); 271 } 272 273 /** 274 * Once a chunk has been used, free it up for unload by removing the plugin ticket 275 */ 276 private void freeChunk(final @NonNull Chunk chunk) { 277 if (!chunk.isLoaded()) { 278 throw new IllegalArgumentException(String.format("Chunk %d;%d is is not loaded", chunk.getX(), chunk.getZ())); 279 } 280 chunk.removePluginChunkTicket(this.plugin); 281 } 282 283 @Override 284 public int getRemainingChunks() { 285 return this.expectedSize.get(); 286 } 287 288 @Override 289 public int getTotalChunks() { 290 return this.totalSize; 291 } 292 293 /** 294 * Subscribe to coordinator progress updates 295 * 296 * @param subscriber Subscriber 297 */ 298 public void subscribeToProgress(final @NonNull ProgressSubscriber subscriber) { 299 this.progressSubscribers.add(subscriber); 300 } 301 302}