Changeset 19153 in josm for trunk/src


Ignore:
Timestamp:
2024-07-25T22:54:48+02:00 (10 months ago)
Author:
taylor.smock
Message:

Fix #23821: Ensure that a new layer is loaded prior to loading additional data to that layer

This occurred due to a race condition, whereby the /load_and_zoom call would
return immediately prior to the download finishing for the new layer and the
next /load_and_zoom call merging onto a pre-existing layer.

This could be fixed in one of two different ways:

  1. Block the RemoteControl thread
  2. Have some method for ensuring that a new layer is loaded first

While we are effectively doing (1), it was easier to do (2) as well for testing
purposes. This means the RemoteControl thread could spin off a thread for each
request to /load_and_zoom and this particular issue should not reappear.

This does not control for cases where a user calls /load_and_zoom like so:

  1. new_layer=true + layer_name=first
  2. new_layer=true + layer_name=second
  3. new_layer=false + layer_name=first
  4. new_layer=false + layer_name=second

Both (1) and (2) will complete before (3) and (4) are run. However, both (3) and
(4) will be loaded into the last layer loaded.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • TabularUnified trunk/src/org/openstreetmap/josm/io/remotecontrol/handler/LoadAndZoomHandler.java

    r19152 r19153  
    88import java.util.Collection;
    99import java.util.Collections;
     10import java.util.HashMap;
    1011import java.util.LinkedHashSet;
    1112import java.util.List;
     
    1617import java.util.concurrent.TimeUnit;
    1718import java.util.concurrent.TimeoutException;
     19import java.util.concurrent.locks.ReadWriteLock;
     20import java.util.concurrent.locks.ReentrantReadWriteLock;
    1821import java.util.stream.Collectors;
    1922import java.util.stream.Stream;
     
    7376    private static final String SEARCH = "search";
    7477
     78    private static final Map<String, ReadWriteLock> layerLockMap = new HashMap<>();
     79
    7580    // Mandatory arguments
    7681    private double minlat;
     
    161166    private void download() throws RequestHandlerErrorException {
    162167        DownloadOsmTask osmTask = new DownloadOsmTask();
     168        ReadWriteLock lock = null;
     169        DownloadParams settings = null;
    163170        try {
    164             DownloadParams settings = getDownloadParams();
     171            settings = getDownloadParams();
    165172
    166173            if (command.equals(myCommand)) {
     
    168175                    Logging.info("RemoteControl: download forbidden by preferences");
    169176                } else {
     177                    // The lock ensures that a download that creates a new layer will finish before
     178                    // downloads that do not create a new layer. This should be thread-safe.
     179                    lock = obtainLock(settings);
     180                    // We need to ensure that we only try to download new areas
    170181                    Area toDownload = null;
    171182                    if (!settings.isNewLayer()) {
     
    179190                }
    180191            }
     192        } catch (InterruptedException ex) {
     193            Thread.currentThread().interrupt();
     194            throw new RequestHandlerErrorException(ex);
    181195        } catch (RuntimeException ex) { // NOPMD
    182196            Logging.warn("RemoteControl: Error parsing load_and_zoom remote control request:");
    183197            Logging.error(ex);
    184198            throw new RequestHandlerErrorException(ex);
     199        } finally {
     200            releaseLock(settings, lock);
     201        }
     202    }
     203
     204    /**
     205     * Obtain a lock to ensure that a new layer is created before downloading non-new layers
     206     * @param settings The settings with the appropriate layer name; if no layer name is given, we assume that
     207     *                 the caller doesn't care where the data goes.
     208     * @return The lock to pass to {@link #releaseLock(DownloadParams, ReadWriteLock)} or {@code null} if no lock is needed.
     209     * @throws InterruptedException If the lock could not be obtained.
     210     */
     211    private static ReadWriteLock obtainLock(DownloadParams settings) throws InterruptedException {
     212        final ReadWriteLock lock;
     213        if (settings.isNewLayer() && !Utils.isEmpty(settings.getLayerName())) {
     214            synchronized (layerLockMap) {
     215                lock = layerLockMap.computeIfAbsent(settings.getLayerName(), k -> new ReentrantReadWriteLock());
     216                lock.writeLock().lock();
     217            }
     218        } else {
     219            synchronized (layerLockMap) {
     220                lock = layerLockMap.get(settings.getLayerName());
     221            }
     222            if (lock != null) {
     223                lock.readLock().lockInterruptibly();
     224            }
     225        }
     226        return lock;
     227    }
     228
     229    /**
     230     * Release the lock preventing data from being downloaded into an old layer
     231     * @param settings The settings with information on the new layer status
     232     * @param lock The lock to unlock
     233     */
     234    private static void releaseLock(DownloadParams settings, ReadWriteLock lock) {
     235        if (lock != null) {
     236            if (settings != null && settings.isNewLayer()) {
     237                lock.writeLock().unlock();
     238                synchronized (layerLockMap) {
     239                    layerLockMap.remove(settings.getLayerName());
     240                }
     241            } else {
     242                lock.readLock().unlock();
     243            }
    185244        }
    186245    }
     
    213272    }
    214273
    215     private void performDownload(DownloadOsmTask osmTask, DownloadParams settings) {
     274    /**
     275     * Perform the actual download; this is synchronized to ensure that we only have one download going on at a time
     276     * @param osmTask The task that will show a dialog
     277     * @param settings The download settings
     278     * @throws RequestHandlerErrorException If there is an issue getting data
     279     */
     280    private void performDownload(DownloadOsmTask osmTask, DownloadParams settings) throws RequestHandlerErrorException {
    216281        Future<?> future = MainApplication.worker.submit(
    217282                new PostDownloadHandler(osmTask, osmTask.download(settings, new Bounds(minlat, minlon, maxlat, maxlon),
     
    245310            }
    246311        });
     312        // Don't block forever, but do wait some period of time.
     313        try {
     314            future.get(OSM_DOWNLOAD_TIMEOUT.get(), TimeUnit.SECONDS);
     315        } catch (InterruptedException e) {
     316            Thread.currentThread().interrupt();
     317            throw new RequestHandlerErrorException(e);
     318        } catch (TimeoutException | ExecutionException e) {
     319            throw new RequestHandlerErrorException(e);
     320        }
    247321    }
    248322
Note: See TracChangeset for help on using the changeset viewer.