diff --git a/src/main/java/org/dynmap/flat/FlatMap.java b/src/main/java/org/dynmap/flat/FlatMap.java index 769e3509..1d641307 100644 --- a/src/main/java/org/dynmap/flat/FlatMap.java +++ b/src/main/java/org/dynmap/flat/FlatMap.java @@ -27,6 +27,7 @@ import org.dynmap.debug.Debug; import org.dynmap.kzedmap.KzedMap; import org.dynmap.kzedmap.KzedMap.KzedBufferedImage; import org.dynmap.MapChunkCache; +import org.dynmap.utils.FileLockManager; import org.json.simple.JSONObject; public class FlatMap extends MapType { @@ -236,6 +237,7 @@ public class FlatMap extends MapType { } } /* Test to see if we're unchanged from older tile */ + FileLockManager.getWriteLock(outputFile); TileHashManager hashman = MapManager.mapman.hashman; long crc = hashman.calculateTileHash(argb_buf); boolean tile_update = false; @@ -257,12 +259,13 @@ public class FlatMap extends MapType { Debug.debug("skipping image " + outputFile.getPath() + " - hash match"); } KzedMap.freeBufferedImage(im); + FileLockManager.releaseWriteLock(outputFile); MapManager.mapman.updateStatistics(tile, null, true, tile_update, !rendered); /* If day too, handle it */ if(night_and_day) { File dayfile = new File(outputFile.getParent(), tile.getDayFilename()); - + FileLockManager.getWriteLock(dayfile); crc = hashman.calculateTileHash(argb_buf_day); if((!dayfile.exists()) || (crc != hashman.getImageHashCode(tile.getKey(), "day", t.x, t.y))) { Debug.debug("saving image " + dayfile.getPath()); @@ -282,6 +285,7 @@ public class FlatMap extends MapType { tile_update = false; } KzedMap.freeBufferedImage(im_day); + FileLockManager.releaseWriteLock(dayfile); MapManager.mapman.updateStatistics(tile, "day", true, tile_update, !rendered); } diff --git a/src/main/java/org/dynmap/kzedmap/DefaultTileRenderer.java b/src/main/java/org/dynmap/kzedmap/DefaultTileRenderer.java index eb62f8ba..a7f6dbfd 100644 --- a/src/main/java/org/dynmap/kzedmap/DefaultTileRenderer.java +++ b/src/main/java/org/dynmap/kzedmap/DefaultTileRenderer.java @@ -20,6 +20,7 @@ import org.dynmap.TileHashManager; import org.dynmap.debug.Debug; import org.dynmap.MapChunkCache; import org.dynmap.kzedmap.KzedMap.KzedBufferedImage; +import org.dynmap.utils.FileLockManager; import org.json.simple.JSONObject; public class DefaultTileRenderer implements MapTileRenderer { @@ -228,6 +229,7 @@ public class DefaultTileRenderer implements MapTileRenderer { int oy = (mtile.py == zmtile.getTileY())?0:KzedMap.tileHeight/2; /* Test to see if we're unchanged from older tile */ + FileLockManager.getWriteLock(fname); TileHashManager hashman = MapManager.mapman.hashman; long crc = hashman.calculateTileHash(img.argb_buf); boolean updated_fname = false; @@ -245,6 +247,7 @@ public class DefaultTileRenderer implements MapTileRenderer { updated_fname = true; } KzedMap.freeBufferedImage(img); + FileLockManager.releaseWriteLock(fname); MapManager.mapman.updateStatistics(mtile, null, true, updated_fname, !rendered); mtile.file = fname; @@ -252,6 +255,7 @@ public class DefaultTileRenderer implements MapTileRenderer { boolean updated_dfname = false; File dfname = new File(fname.getParent(), mtile.getDayFilename()); if(img_day != null) { + FileLockManager.getWriteLock(dfname); crc = hashman.calculateTileHash(img.argb_buf); if((!dfname.exists()) || (crc != hashman.getImageHashCode(mtile.getKey(), "day", mtile.px, mtile.py))) { Debug.debug("saving image " + dfname.getPath()); @@ -267,12 +271,14 @@ public class DefaultTileRenderer implements MapTileRenderer { updated_dfname = true; } KzedMap.freeBufferedImage(img_day); + FileLockManager.releaseWriteLock(dfname); MapManager.mapman.updateStatistics(mtile, "day", true, updated_dfname, !rendered); } // Since we've already got the new tile, and we're on an async thread, just // make the zoomed tile here boolean ztile_updated = false; + FileLockManager.getWriteLock(zoomFile); if(updated_fname || (!zoomFile.exists())) { saveZoomedTile(zmtile, zoomFile, zimg, ox, oy); MapManager.mapman.pushUpdate(zmtile.getWorld(), @@ -280,11 +286,13 @@ public class DefaultTileRenderer implements MapTileRenderer { ztile_updated = true; } KzedMap.freeBufferedImage(zimg); + FileLockManager.releaseWriteLock(zoomFile); MapManager.mapman.updateStatistics(zmtile, null, true, ztile_updated, !rendered); if(zimg_day != null) { File zoomFile_day = new File(zoomFile.getParent(), zmtile.getDayFilename()); ztile_updated = false; + FileLockManager.getWriteLock(zoomFile_day); if(updated_dfname || (!zoomFile_day.exists())) { saveZoomedTile(zmtile, zoomFile_day, zimg_day, ox, oy); MapManager.mapman.pushUpdate(zmtile.getWorld(), @@ -292,6 +300,7 @@ public class DefaultTileRenderer implements MapTileRenderer { ztile_updated = true; } KzedMap.freeBufferedImage(zimg_day); + FileLockManager.releaseWriteLock(zoomFile_day); MapManager.mapman.updateStatistics(zmtile, "day", true, ztile_updated, !rendered); } } diff --git a/src/main/java/org/dynmap/utils/FileLockManager.java b/src/main/java/org/dynmap/utils/FileLockManager.java new file mode 100644 index 00000000..e6f5b7a4 --- /dev/null +++ b/src/main/java/org/dynmap/utils/FileLockManager.java @@ -0,0 +1,105 @@ +package org.dynmap.utils; +import java.io.File; +import java.util.HashMap; + +import org.dynmap.Log; +/** + * Implements soft-locks for prevent concurrency issues with file updates + */ +public class FileLockManager { + private static Object lock = new Object(); + private static HashMap filelocks = new HashMap(); + private static final Integer WRITELOCK = new Integer(-1); + /** + * Get write lock on file - exclusive lock, no other writers or readers + * @throws InterruptedException + */ + public static void getWriteLock(File f) { + String fn = f.getPath(); + synchronized(lock) { + boolean got_lock = false; + while(!got_lock) { + Integer lockcnt = filelocks.get(fn); /* Get lock count */ + if(lockcnt != null) { /* If any locks, can't get write lock */ + try { + lock.wait(); + } catch (InterruptedException ix) { + Log.severe("getWriteLock(" + fn + ") interrupted"); + } + } + else { + filelocks.put(fn, WRITELOCK); + got_lock = true; + } + } + } + //Log.info("getWriteLock(" + f + ")"); + } + /** + * Release write lock + */ + public static void releaseWriteLock(File f) { + String fn = f.getPath(); + synchronized(lock) { + Integer lockcnt = filelocks.get(fn); /* Get lock count */ + if(lockcnt == null) + Log.severe("releaseWriteLock(" + fn + ") on unlocked file"); + else if(lockcnt.equals(WRITELOCK)) { + filelocks.remove(fn); /* Remove lock */ + lock.notifyAll(); /* Wake up folks waiting for locks */ + } + else + Log.severe("releaseWriteLock(" + fn + ") on read-locked file"); + } + //Log.info("releaseWriteLock(" + f + ")"); + } + /** + * Get read lock on file - multiple readers allowed, blocks writers + */ + public static void getReadLock(File f) { + String fn = f.getPath(); + synchronized(lock) { + boolean got_lock = false; + while(!got_lock) { + Integer lockcnt = filelocks.get(fn); /* Get lock count */ + if(lockcnt == null) { + filelocks.put(fn, Integer.valueOf(1)); /* First lock */ + got_lock = true; + } + else if(!lockcnt.equals(WRITELOCK)) { /* Other read locks */ + filelocks.put(fn, Integer.valueOf(lockcnt+1)); + got_lock = true; + } + else { /* Write lock in place */ + try { + lock.wait(); + } catch (InterruptedException ix) { + Log.severe("getReadLock(" + fn + ") interrupted"); + } + } + } + } + //Log.info("getReadLock(" + f + ")"); + } + /** + * Release read lock + */ + public static void releaseReadLock(File f) { + String fn = f.getPath(); + synchronized(lock) { + Integer lockcnt = filelocks.get(fn); /* Get lock count */ + if(lockcnt == null) + Log.severe("releaseReadLock(" + fn + ") on unlocked file"); + else if(lockcnt.equals(WRITELOCK)) + Log.severe("releaseReadLock(" + fn + ") on write-locked file"); + else if(lockcnt > 1) { + filelocks.put(fn, Integer.valueOf(lockcnt-1)); + } + else { + filelocks.remove(fn); /* Remove lock */ + lock.notifyAll(); /* Wake up folks waiting for locks */ + } + } + //Log.info("releaseReadLock(" + f + ")"); + } +} diff --git a/src/main/java/org/dynmap/web/handlers/FileHandler.java b/src/main/java/org/dynmap/web/handlers/FileHandler.java index 9c6a2daa..439f78e2 100644 --- a/src/main/java/org/dynmap/web/handlers/FileHandler.java +++ b/src/main/java/org/dynmap/web/handlers/FileHandler.java @@ -13,6 +13,7 @@ import org.dynmap.web.HttpHandler; import org.dynmap.web.HttpRequest; import org.dynmap.web.HttpResponse; import org.dynmap.web.HttpStatus; +import org.dynmap.utils.FileLockManager; public abstract class FileHandler implements HttpHandler { protected static final Logger log = Logger.getLogger("Minecraft"); @@ -42,6 +43,10 @@ public abstract class FileHandler implements HttpHandler { } protected abstract InputStream getFileInput(String path, HttpRequest request, HttpResponse response); + + protected void closeFileInput(String path, InputStream in) throws IOException { + in.close(); + } protected String getExtension(String path) { int dotindex = path.lastIndexOf('.'); @@ -113,7 +118,7 @@ public abstract class FileHandler implements HttpHandler { } finally { freeReadBuffer(readBuffer); } - fileInput.close(); + closeFileInput(path, fileInput); } catch (Exception e) { if (fileInput != null) { try { fileInput.close(); } catch (IOException ex) { } diff --git a/src/main/java/org/dynmap/web/handlers/FilesystemHandler.java b/src/main/java/org/dynmap/web/handlers/FilesystemHandler.java index fa630c37..de4e0091 100644 --- a/src/main/java/org/dynmap/web/handlers/FilesystemHandler.java +++ b/src/main/java/org/dynmap/web/handlers/FilesystemHandler.java @@ -3,8 +3,10 @@ package org.dynmap.web.handlers; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; +import java.io.IOException; import java.io.InputStream; +import org.dynmap.utils.FileLockManager; import org.dynmap.web.HttpField; import org.dynmap.web.HttpRequest; import org.dynmap.web.HttpResponse; @@ -20,6 +22,7 @@ public class FilesystemHandler extends FileHandler { @Override protected InputStream getFileInput(String path, HttpRequest request, HttpResponse response) { File file = new File(root, path); + FileLockManager.getReadLock(file); if (file.getAbsolutePath().startsWith(root.getAbsolutePath()) && file.isFile()) { FileInputStream result; try { @@ -30,6 +33,13 @@ public class FilesystemHandler extends FileHandler { response.fields.put(HttpField.ContentLength, Long.toString(file.length())); return result; } + FileLockManager.releaseReadLock(file); return null; } + protected void closeFileInput(String path, InputStream in) throws IOException { + super.closeFileInput(path, in); + File file = new File(root, path); + FileLockManager.releaseReadLock(file); + } + }