Move region-file watch service into World interface

This commit is contained in:
Lukas Rieger (Blue) 2024-05-22 15:45:06 +02:00
parent 20aa0a72f5
commit 3db6833fc6
No known key found for this signature in database
GPG Key ID: AA33883B1BBA03E6
10 changed files with 257 additions and 121 deletions

View File

@ -29,23 +29,20 @@
import de.bluecolored.bluemap.common.rendermanager.WorldRegionRenderTask;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.util.FileHelper;
import de.bluecolored.bluemap.core.world.World;
import de.bluecolored.bluemap.core.world.mca.MCAWorld;
import de.bluecolored.bluemap.core.world.mca.region.RegionType;
import de.bluecolored.bluemap.core.util.WatchService;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.ClosedWatchServiceException;
import java.util.HashMap;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
public class RegionFileWatchService extends Thread {
public class MapUpdateService extends Thread {
private final BmMap map;
private final RenderManager renderManager;
private final WatchService watchService;
private final WatchService<Vector2i> watchService;
private volatile boolean closed;
@ -53,25 +50,12 @@ public class RegionFileWatchService extends Thread {
private final Map<Vector2i, TimerTask> scheduledUpdates;
public RegionFileWatchService(RenderManager renderManager, BmMap map) throws IOException {
public MapUpdateService(RenderManager renderManager, BmMap map) throws IOException {
this.renderManager = renderManager;
this.map = map;
this.closed = false;
this.scheduledUpdates = new HashMap<>();
World world = map.getWorld();
if (!(world instanceof MCAWorld)) throw new UnsupportedOperationException("world-type is not supported");
Path folder = ((MCAWorld) world).getRegionFolder();
FileHelper.createDirectories(folder);
this.watchService = folder.getFileSystem().newWatchService();
folder.register(this.watchService,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE
);
Logger.global.logDebug("Created region-file watch-service for map '" + map.getId() + "' at '" + folder + "'.");
this.watchService = map.getWorld().createRegionWatchService();
}
@Override
@ -81,24 +65,8 @@ public void run() {
Logger.global.logDebug("Started watching map '" + map.getId() + "' for updates...");
try {
while (!closed) {
WatchKey key = this.watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
if (kind == StandardWatchEventKinds.OVERFLOW) continue;
Object fileObject = event.context();
if (!(fileObject instanceof Path)) continue;
Path file = (Path) fileObject;
String regionFileName = file.toFile().getName();
updateRegion(regionFileName);
}
if (!key.reset()) return;
}
while (!closed)
this.watchService.take().forEach(this::updateRegion);
} catch (ClosedWatchServiceException ignore) {
} catch (InterruptedException iex) {
Thread.currentThread().interrupt();
@ -111,36 +79,25 @@ public void run() {
}
}
private synchronized void updateRegion(String regionFileName) {
if (RegionType.forFileName(regionFileName) == null) return;
private synchronized void updateRegion(Vector2i regionPos) {
// we only want to start the render when there were no changes on a file for 5 seconds
TimerTask task = scheduledUpdates.remove(regionPos);
if (task != null) task.cancel();
try {
String[] filenameParts = regionFileName.split("\\.");
if (filenameParts.length < 3) return;
task = new TimerTask() {
@Override
public void run() {
synchronized (MapUpdateService.this) {
WorldRegionRenderTask task = new WorldRegionRenderTask(map, regionPos);
scheduledUpdates.remove(regionPos);
renderManager.scheduleRenderTask(task);
int rX = Integer.parseInt(filenameParts[1]);
int rZ = Integer.parseInt(filenameParts[2]);
Vector2i regionPos = new Vector2i(rX, rZ);
// we only want to start the render when there were no changes on a file for 5 seconds
TimerTask task = scheduledUpdates.remove(regionPos);
if (task != null) task.cancel();
task = new TimerTask() {
@Override
public void run() {
synchronized (RegionFileWatchService.this) {
WorldRegionRenderTask task = new WorldRegionRenderTask(map, regionPos);
scheduledUpdates.remove(regionPos);
renderManager.scheduleRenderTask(task);
Logger.global.logDebug("Scheduled update for region-file: " + regionPos + " (Map: " + map.getId() + ")");
}
Logger.global.logDebug("Scheduled update for region-file: " + regionPos + " (Map: " + map.getId() + ")");
}
};
scheduledUpdates.put(regionPos, task);
delayTimer.schedule(task, 5000);
} catch (NumberFormatException ignore) {}
}
};
scheduledUpdates.put(regionPos, task);
delayTimer.schedule(task, 5000);
}
public void close() {
@ -151,7 +108,7 @@ public void close() {
try {
this.watchService.close();
} catch (IOException ex) {
} catch (Exception ex) {
Logger.global.logError("Exception while trying to close WatchService!", ex);
}
}

View File

@ -94,7 +94,7 @@ public class Plugin implements ServerEventListener {
private Timer daemonTimer;
private Map<String, RegionFileWatchService> regionFileWatchServices;
private Map<String, MapUpdateService> mapUpdateServices;
private PlayerSkinUpdater skinUpdater;
@ -316,8 +316,8 @@ public void run() {
TimerTask fileWatcherRestartTask = new TimerTask() {
@Override
public void run() {
regionFileWatchServices.values().forEach(RegionFileWatchService::close);
regionFileWatchServices.clear();
mapUpdateServices.values().forEach(MapUpdateService::close);
mapUpdateServices.clear();
initFileWatcherTasks();
}
};
@ -351,7 +351,7 @@ public void run() {
daemonTimer.scheduleAtFixedRate(metricsTask, TimeUnit.MINUTES.toMillis(1), TimeUnit.MINUTES.toMillis(30));
//watch map-changes
this.regionFileWatchServices = new HashMap<>();
this.mapUpdateServices = new HashMap<>();
initFileWatcherTasks();
//register listener
@ -408,11 +408,11 @@ public void unload(boolean keepWebserver) {
daemonTimer = null;
//stop file-watchers
if (regionFileWatchServices != null) {
regionFileWatchServices.values().forEach(RegionFileWatchService::close);
regionFileWatchServices.clear();
if (mapUpdateServices != null) {
mapUpdateServices.values().forEach(MapUpdateService::close);
mapUpdateServices.clear();
}
regionFileWatchServices = null;
mapUpdateServices = null;
// stop render-manager
if (renderManager != null){
@ -567,16 +567,20 @@ public synchronized void startWatchingMap(BmMap map) {
stopWatchingMap(map);
try {
RegionFileWatchService watcher = new RegionFileWatchService(renderManager, map);
MapUpdateService watcher = new MapUpdateService(renderManager, map);
watcher.start();
regionFileWatchServices.put(map.getId(), watcher);
mapUpdateServices.put(map.getId(), watcher);
} catch (IOException ex) {
Logger.global.logError("Failed to create file-watcher for map: " + map.getId() + " (This means the map might not automatically update)", ex);
Logger.global.logError("Failed to create update-watcher for map: " + map.getId() +
" (This means the map might not automatically update)", ex);
} catch (UnsupportedOperationException ex) {
Logger.global.logWarning("Update-watcher for map '" + map.getId() + "' is not supported for the world-type." +
" (This means the map might not automatically update)");
}
}
public synchronized void stopWatchingMap(BmMap map) {
RegionFileWatchService watcher = regionFileWatchServices.remove(map.getId());
MapUpdateService watcher = mapUpdateServices.remove(map.getId());
if (watcher != null) {
watcher.close();
}

View File

@ -0,0 +1,45 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.util;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* A watch service that watches for changes and events.
* @param <T> The type of the events or changes this WatchService provides
*/
public interface WatchService<T> extends AutoCloseable {
@Nullable
List<T> poll();
@Nullable List<T> poll(long timeout, TimeUnit unit) throws InterruptedException;
List<T> take() throws InterruptedException;
}

View File

@ -27,7 +27,9 @@
import com.flowpowered.math.vector.Vector2i;
import com.flowpowered.math.vector.Vector3i;
import de.bluecolored.bluemap.core.util.Grid;
import de.bluecolored.bluemap.core.util.WatchService;
import java.io.IOException;
import java.util.Collection;
import java.util.function.Predicate;
@ -72,6 +74,15 @@ public interface World {
*/
Collection<Vector2i> listRegions();
/**
* Creates and returns a new {@link WatchService} which watches for any changes in this worlds regions.
* @throws IOException if an IOException occurred while creating the watch-service
* @throws UnsupportedOperationException if watching this world is not supported
*/
default WatchService<Vector2i> createRegionWatchService() throws IOException {
throw new UnsupportedOperationException();
}
/**
* Loads all chunks from the specified region into the chunk cache (if there is a cache)
*/
@ -79,6 +90,9 @@ default void preloadRegionChunks(int x, int z) {
preloadRegionChunks(x, z, pos -> true);
}
/**
* Loads the filtered chunks from the specified region into the chunk cache (if there is a cache)
*/
void preloadRegionChunks(int x, int z, Predicate<Vector2i> chunkFilter);
/**
@ -91,9 +105,4 @@ default void preloadRegionChunks(int x, int z) {
*/
void invalidateChunkCache(int x, int z);
/**
* Cleans up invalid cache-entries to free up memory
*/
void cleanUpChunkCache();
}

View File

@ -36,6 +36,7 @@
import de.bluecolored.bluemap.core.util.Grid;
import de.bluecolored.bluemap.core.util.Key;
import de.bluecolored.bluemap.core.util.Vector2iCache;
import de.bluecolored.bluemap.core.util.WatchService;
import de.bluecolored.bluemap.core.world.*;
import de.bluecolored.bluemap.core.world.mca.chunk.ChunkLoader;
import de.bluecolored.bluemap.core.world.mca.data.DimensionTypeDeserializer;
@ -165,21 +166,11 @@ public Collection<Vector2i> listRegions() {
return stream
.map(file -> {
try {
String fileName = file.getFileName().toString();
if (RegionType.forFileName(fileName) == null) return null;
if (Files.size(file) <= 0) return null;
String[] filenameParts = fileName.split("\\.");
int rX = Integer.parseInt(filenameParts[1]);
int rZ = Integer.parseInt(filenameParts[2]);
return new Vector2i(rX, rZ);
return RegionType.regionForFileName(file.getFileName().toString());
} catch (IOException ex) {
Logger.global.logError("Failed to read region-file: " + file, ex);
return null;
} catch (NumberFormatException ignore) {
return null;
}
})
.filter(Objects::nonNull)
@ -190,6 +181,11 @@ public Collection<Vector2i> listRegions() {
}
}
@Override
public WatchService<Vector2i> createRegionWatchService() throws IOException {
return new MCAWorldRegionWatchService(this.regionFolder);
}
@Override
public void preloadRegionChunks(int x, int z, Predicate<Vector2i> chunkFilter) {
try {
@ -223,11 +219,6 @@ public void invalidateChunkCache(int x, int z) {
chunkCache.invalidate(VECTOR_2_I_CACHE.get(x, z));
}
@Override
public void cleanUpChunkCache() {
chunkCache.cleanUp();
}
private Region loadRegion(Vector2i regionPos) {
return loadRegion(regionPos.getX(), regionPos.getY());
}

View File

@ -0,0 +1,95 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.world.mca;
import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.core.util.WatchService;
import de.bluecolored.bluemap.core.world.mca.region.RegionType;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchKey;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
public class MCAWorldRegionWatchService implements WatchService<Vector2i> {
private final java.nio.file.WatchService watchService;
public MCAWorldRegionWatchService(Path regionFolder) throws IOException {
this.watchService = regionFolder.getFileSystem().newWatchService();
regionFolder.register(this.watchService,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE
);
}
@Override
public @Nullable List<Vector2i> poll() {
WatchKey key = watchService.poll();
if (key == null) return null;
return processWatchKey(key);
}
@Override
public @Nullable List<Vector2i> poll(long timeout, TimeUnit unit) throws InterruptedException {
WatchKey key = watchService.poll(timeout, unit);
if (key == null) return null;
return processWatchKey(key);
}
@Override
public List<Vector2i> take() throws InterruptedException {
WatchKey key = watchService.take();
return processWatchKey(key);
}
@Override
public void close() throws IOException {
watchService.close();
}
private List<Vector2i> processWatchKey(WatchKey key) {
try {
return key.pollEvents().stream()
.map(event -> {
if (event.context() instanceof Path path) {
return RegionType.regionForFileName(path.getFileName().toString());
} else {
return null;
}
})
.filter(Objects::nonNull)
.toList();
} finally {
key.reset();
}
}
}

View File

@ -36,6 +36,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.regex.Pattern;
/*
* LinearFormat:
@ -63,6 +64,7 @@
public class LinearRegion implements Region {
public static final String FILE_SUFFIX = ".linear";
public static final Pattern FILE_PATTERN = Pattern.compile("^r\\.(-?\\d+)\\.(-?\\d+)\\.linear$");
private static final long MAGIC = 0xc3ff13183cca9d9aL;

View File

@ -40,11 +40,14 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.regex.Pattern;
@Getter
public class MCARegion implements Region {
public static final String FILE_SUFFIX = ".mca";
public static final Pattern FILE_PATTERN = Pattern.compile("^r\\.(-?\\d+)\\.(-?\\d+)\\.mca$");
public static final Compression[] CHUNK_COMPRESSION_MAP = new Compression[255];
static {
CHUNK_COMPRESSION_MAP[0] = Compression.NONE;

View File

@ -24,6 +24,7 @@
*/
package de.bluecolored.bluemap.core.world.mca.region;
import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.core.util.Key;
import de.bluecolored.bluemap.core.util.Keyed;
import de.bluecolored.bluemap.core.util.Registry;
@ -31,16 +32,17 @@
import de.bluecolored.bluemap.core.world.mca.MCAWorld;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public interface RegionType extends Keyed {
RegionType MCA = new Impl(Key.bluemap("mca"), MCARegion.FILE_SUFFIX, MCARegion::new, MCARegion::getRegionFileName);
RegionType LINEAR = new Impl(Key.bluemap("linear"), LinearRegion.FILE_SUFFIX, LinearRegion::new, LinearRegion::getRegionFileName);
RegionType MCA = new Impl(Key.bluemap("mca"), MCARegion::new, MCARegion::getRegionFileName, MCARegion.FILE_PATTERN);
RegionType LINEAR = new Impl(Key.bluemap("linear"), LinearRegion::new, LinearRegion::getRegionFileName, LinearRegion.FILE_PATTERN);
RegionType DEFAULT = MCA;
Registry<RegionType> REGISTRY = new Registry<>(
@ -48,36 +50,55 @@ public interface RegionType extends Keyed {
LINEAR
);
String getFileSuffix();
/**
* Creates a new {@link Region} from the given world and region-file
*/
Region createRegion(MCAWorld world, Path regionFile);
Path getRegionFile(Path regionFolder, int regionX, int regionZ);
/**
* Converts region coordinates into the region-file name.
*/
String getRegionFileName(int regionX, int regionZ);
/**
* Converts the region-file name into region coordinates.
* Returns null if the name does not match the expected format.
*/
@Nullable Vector2i getRegionFromFileName(String fileName);
static @Nullable RegionType forFileName(String fileName) {
for (RegionType regionType : REGISTRY.values()) {
if (fileName.endsWith(regionType.getFileSuffix()))
if (regionType.getRegionFromFileName(fileName) != null)
return regionType;
}
return null;
}
static @NotNull Region loadRegion(MCAWorld world, Path regionFolder, int regionX, int regionZ) {
static @Nullable Vector2i regionForFileName(String fileName) {
for (RegionType regionType : REGISTRY.values()) {
Path regionFile = regionType.getRegionFile(regionFolder, regionX, regionZ);
Vector2i pos = regionType.getRegionFromFileName(fileName);
if (pos != null) return pos;
}
return null;
}
static Region loadRegion(MCAWorld world, Path regionFolder, int regionX, int regionZ) {
for (RegionType regionType : REGISTRY.values()) {
Path regionFile = regionFolder.resolve(regionType.getRegionFileName(regionX, regionZ));
if (Files.exists(regionFile)) return regionType.createRegion(world, regionFile);
}
return DEFAULT.createRegion(world, DEFAULT.getRegionFile(regionFolder, regionX, regionZ));
return DEFAULT.createRegion(world, regionFolder.resolve(DEFAULT.getRegionFileName(regionX, regionZ)));
}
@RequiredArgsConstructor
class Impl implements RegionType {
@Getter private final Key key;
@Getter private final String fileSuffix;
private final RegionFactory regionFactory;
private final RegionFileNameFunction regionFileNameFunction;
private final Pattern regionFileNamePattern;
public Region createRegion(MCAWorld world, Path regionFile) {
return this.regionFactory.create(world, regionFile);
@ -87,8 +108,14 @@ public String getRegionFileName(int regionX, int regionZ) {
return regionFileNameFunction.getRegionFileName(regionX, regionZ);
}
public Path getRegionFile(Path regionFolder, int regionX, int regionZ) {
return regionFolder.resolve(getRegionFileName(regionX, regionZ));
@Override
public @Nullable Vector2i getRegionFromFileName(String fileName) {
Matcher matcher = regionFileNamePattern.matcher(fileName);
if (!matcher.matches()) return null;
return new Vector2i(
Integer.parseInt(matcher.group(1)),
Integer.parseInt(matcher.group(2))
);
}
}

View File

@ -31,7 +31,7 @@
import de.bluecolored.bluemap.common.config.ConfigurationException;
import de.bluecolored.bluemap.common.config.CoreConfig;
import de.bluecolored.bluemap.common.config.WebserverConfig;
import de.bluecolored.bluemap.common.plugin.RegionFileWatchService;
import de.bluecolored.bluemap.common.plugin.MapUpdateService;
import de.bluecolored.bluemap.common.rendermanager.MapUpdateTask;
import de.bluecolored.bluemap.common.rendermanager.RenderManager;
import de.bluecolored.bluemap.common.rendermanager.RenderTask;
@ -88,16 +88,19 @@ public void renderMaps(BlueMapService blueMap, boolean watch, boolean forceRende
Map<String, BmMap> maps = blueMap.getOrLoadMaps(mapFilter);
//watcher
List<RegionFileWatchService> regionFileWatchServices = new ArrayList<>();
List<MapUpdateService> mapUpdateServices = new ArrayList<>();
if (watch) {
for (BmMap map : maps.values()) {
try {
RegionFileWatchService watcher = new RegionFileWatchService(renderManager, map);
MapUpdateService watcher = new MapUpdateService(renderManager, map);
watcher.start();
regionFileWatchServices.add(watcher);
mapUpdateServices.add(watcher);
} catch (IOException ex) {
Logger.global.logError("Failed to create file-watcher for map: " + map.getId() +
" (This map might not automatically update)", ex);
" (This map might not automatically update)", ex);
} catch (UnsupportedOperationException ex) {
Logger.global.logWarning("Update-watcher for map '" + map.getId() + "' is not supported for the world-type." +
" (This means the map might not automatically update)");
}
}
}
@ -150,8 +153,8 @@ public void run() {
updateInfoTask.cancel();
saveTask.cancel();
regionFileWatchServices.forEach(RegionFileWatchService::close);
regionFileWatchServices.clear();
mapUpdateServices.forEach(MapUpdateService::close);
mapUpdateServices.clear();
renderManager.removeAllRenderTasks();
try {