sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: Try to provide a better coverage of error handling during `CoverageCanvas` rendering: - Replace the various `ErrorHander.Report.addFoo(…)` methods by a single `add(…)` method giving more control on the `LogRecord` content. - Make `ErrorHandler.Report` thread-safe and document the synchronization lock to use when modifying the `LogRecord` content. - Allow `PrefetchedImage` to perform rendering in a mode where exceptions are caught and tiles are replaced by place-holders. - Use above-cit [...]
Date Mon, 08 Feb 2021 15:24:51 GMT
This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 4fd2a67  Try to provide a better coverage of error handling during `CoverageCanvas` rendering: - Replace the various `ErrorHander.Report.addFoo(…)` methods by a single `add(…)` method giving more control on the `LogRecord` content. - Make `ErrorHandler.Report` thread-safe and document the synchronization lock to use when modifying the `LogRecord` content. - Allow `PrefetchedImage` to perform rendering in a mode where exceptions are caught and tiles are replaced by place-holders.  [...]
4fd2a67 is described below

commit 4fd2a672a76958d103175a7925bc12986571731b
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Mon Feb 8 11:20:16 2021 +0100

    Try to provide a better coverage of error handling during `CoverageCanvas` rendering:
    - Replace the various `ErrorHander.Report.addFoo(…)` methods by a single `add(…)` method giving more control on the `LogRecord` content.
    - Make `ErrorHandler.Report` thread-safe and document the synchronization lock to use when modifying the `LogRecord` content.
    - Allow `PrefetchedImage` to perform rendering in a mode where exceptions are caught and tiles are replaced by place-holders.
    - Use above-cited rendering mode in `CoverageCanvas`.
---
 .../apache/sis/gui/coverage/CoverageCanvas.java    |  37 +++++-
 .../java/org/apache/sis/gui/map/MapCanvas.java     |  12 +-
 .../java/org/apache/sis/image/AnnotatedImage.java  |  65 ++++-------
 .../java/org/apache/sis/image/ErrorAction.java     |  36 +++---
 .../java/org/apache/sis/image/ErrorHandler.java    | 125 +++++++++-----------
 .../java/org/apache/sis/image/PrefetchedImage.java |  84 +++++++++++--
 .../internal/coverage/j2d/TileErrorHandler.java    | 123 +++++++++++++++++++
 .../sis/internal/coverage/j2d/TileOpExecutor.java  | 130 ++++++++++-----------
 8 files changed, 400 insertions(+), 212 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
index 949c2ae..ff1a09e 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
@@ -22,6 +22,7 @@ import java.util.List;
 import java.util.Locale;
 import java.util.concurrent.Future;
 import java.util.function.Function;
+import java.util.logging.LogRecord;
 import java.io.IOException;
 import java.awt.Graphics2D;
 import java.awt.Rectangle;
@@ -55,21 +56,23 @@ import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.ImageRenderer;
-import org.apache.sis.internal.gui.ExceptionReporter;
 import org.apache.sis.referencing.operation.matrix.AffineTransforms2D;
 import org.apache.sis.referencing.operation.transform.LinearTransform;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.geometry.Envelope2D;
 import org.apache.sis.geometry.Shapes2D;
 import org.apache.sis.image.PlanarImage;
+import org.apache.sis.image.ErrorHandler;
 import org.apache.sis.image.Interpolation;
 import org.apache.sis.coverage.Category;
 import org.apache.sis.gui.map.MapCanvas;
 import org.apache.sis.gui.map.MapCanvasAWT;
 import org.apache.sis.gui.map.StatusBar;
 import org.apache.sis.portrayal.RenderException;
+import org.apache.sis.internal.coverage.j2d.TileErrorHandler;
 import org.apache.sis.internal.processing.image.Isolines;
 import org.apache.sis.internal.gui.BackgroundThreads;
+import org.apache.sis.internal.gui.ExceptionReporter;
 import org.apache.sis.internal.gui.GUIUtilities;
 import org.apache.sis.internal.gui.LogHandler;
 import org.apache.sis.internal.system.Modules;
@@ -525,7 +528,7 @@ public class CoverageCanvas extends MapCanvasAWT {
      *   <li>Paint the image.</li>
      * </ol>
      */
-    private static final class Worker extends Renderer {
+    private static final class Worker extends Renderer implements ErrorHandler {
         /**
          * Value of {@link CoverageCanvas#data} at the time this worker has been initialized.
          */
@@ -608,6 +611,11 @@ public class CoverageCanvas extends MapCanvasAWT {
         private IsolineRenderer.Snapshot[] isolines;
 
         /**
+         * If errors occurred during tile computations, details about the error. Otherwise {@code null}.
+         */
+        private LogRecord errorReport;
+
+        /**
          * Creates a new renderer. Shall be invoked in JavaFX thread.
          */
         Worker(final CoverageCanvas canvas) {
@@ -721,7 +729,13 @@ public class CoverageCanvas extends MapCanvasAWT {
         @Override
         protected void paint(final Graphics2D gr) {
             gr.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
-            gr.drawRenderedImage(prefetchedImage, resampledToDisplay);
+            if (prefetchedImage instanceof TileErrorHandler.Executor) {
+                ((TileErrorHandler.Executor) prefetchedImage).execute(
+                        () -> gr.drawRenderedImage(prefetchedImage, resampledToDisplay),
+                        new TileErrorHandler(this, CoverageCanvas.class, "paint"));
+            } else {
+                gr.drawRenderedImage(prefetchedImage, resampledToDisplay);
+            }
             if (isolines != null) {
                 final AffineTransform at = gr.getTransform();
                 final Stroke st = gr.getStroke();
@@ -737,6 +751,16 @@ public class CoverageCanvas extends MapCanvasAWT {
         }
 
         /**
+         * Invoked if an error occurred during a call to {@link RenderedImage#getTile(int, int)}.
+         * This method stores information about the error and let the rendering process continue
+         * with a tile placeholder (by default a cross (X) in a box).
+         */
+        @Override
+        public void handle(final Report details) {
+            errorReport = details.getDescription();
+        }
+
+        /**
          * Invoked in JavaFX thread after successful {@link #paint(Graphics2D)} completion.
          * This method stores the computation results.
          */
@@ -796,6 +820,13 @@ public class CoverageCanvas extends MapCanvasAWT {
             }
             statusBar.setLowestAccuracy(accuracy);
         }
+        /*
+         * If error(s) occurred during calls to `RenderedImage.getTile(tx, ty)`, reports those errors.
+         */
+        final LogRecord errorReport = worker.errorReport;
+        if (errorReport != null) {
+            errorOccurred(errorReport.getThrown());
+        }
     }
 
     /**
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java
index a5f403b..8c4cf60 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java
@@ -1048,8 +1048,11 @@ public abstract class MapCanvas extends PlanarCanvas {
         yPanStart = p.getY();
         changeInProgress.setToIdentity();
         transform.setToTransform(transformOnNewImage);
-        error.set(task.getException());
         isRendering.set(false);
+        final Throwable ex = task.getException();
+        if (ex != null) {
+            errorOccurred(ex);
+        }
     }
 
     /**
@@ -1167,7 +1170,12 @@ public abstract class MapCanvas extends PlanarCanvas {
      * @param  ex  the exception that occurred (can not be null).
      */
     protected void errorOccurred(final Throwable ex) {
-        error.set(Objects.requireNonNull(ex));
+        final Throwable current = error.get();
+        if (current != null) {
+            current.addSuppressed(ex);
+        } else {
+            error.set(Objects.requireNonNull(ex));
+        }
     }
 
     /**
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/AnnotatedImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/AnnotatedImage.java
index 8bbae85..7d669fb 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/AnnotatedImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/AnnotatedImage.java
@@ -16,8 +16,10 @@
  */
 package org.apache.sis.image;
 
+import java.util.Locale;
 import java.util.Objects;
 import java.util.WeakHashMap;
+import java.util.logging.Level;
 import java.util.logging.LogRecord;
 import java.util.stream.Collector;
 import java.awt.Image;
@@ -29,7 +31,6 @@ import java.awt.image.ImagingOpException;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.collection.Cache;
-import org.apache.sis.internal.system.Modules;
 import org.apache.sis.internal.coverage.j2d.TileOpExecutor;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
 import org.apache.sis.internal.util.Strings;
@@ -336,23 +337,21 @@ abstract class AnnotatedImage extends ImageAdapter {
                         if (value == null) value = NULL;
                         success = (errors == null);
                     } catch (Exception e) {
-                        /*
-                         * Stores the given exception in a log record. We use a log record in order to initialize
-                         * the timestamp and thread ID to the values they had at the time the first error occurred.
-                         */
                         if (failOnException) {
                             throw (ImagingOpException) new ImagingOpException(
                                     Errors.format(Errors.Keys.CanNotCompute_1, property)).initCause(e);
                         }
-                        synchronized (this) {
-                            ErrorHandler.Report report = errors;
-                            final boolean create = (report == null);
-                            if (create) {
-                                report = new ErrorHandler.Report();
-                            }
-                            report.addPropertyError(e, property);
-                            if (create) setError(report);
+                        /*
+                         * Stores the exception in a log record. We use a log record in order to initialize
+                         * the timestamp and thread ID to the values they had at the time the error occurred.
+                         * We do not synchronize because all worker threads should have finished now.
+                         */
+                        ErrorHandler.Report report = errors;
+                        if (report == null) {
+                            errors = report = new ErrorHandler.Report();
                         }
+                        report.add(null, e, () -> Errors.getResources((Locale) null)
+                                .getLogRecord(Level.WARNING, Errors.Keys.CanNotCompute_1, property));
                     }
                 } finally {
                     handler.putAndUnlock(success ? value : null);       // Cache only if no error occurred.
@@ -369,26 +368,6 @@ abstract class AnnotatedImage extends ImageAdapter {
     }
 
     /**
-     * Invoked by {@link TileOpExecutor} if an error occurred during calculation on a tiles.
-     * Can also be invoked by {@link #getProperty(String)} directly if the error occurred
-     * outside {@link TileOpExecutor}. This method should be invoked at most once, unless
-     * the calculation is attempted again.
-     *
-     * @param  report  a description of the error that occurred.
-     */
-    private void setError(final ErrorHandler.Report report) {
-        /*
-         * Complete record with source identification as if the error occurred from
-         * above `getProperty(String)` method (this is always the case, indirectly).
-         */
-        final LogRecord record = report.getDescription();
-        record.setSourceClassName(AnnotatedImage.class.getCanonicalName());
-        record.setSourceMethodName("getProperty");
-        record.setLoggerName(Modules.RASTER);
-        errors = report;
-    }
-
-    /**
      * If an error occurred, logs the message with the specified class and method as the source.
      * The {@code classe} and {@code method} arguments overwrite the {@link LogRecord#getSourceClassName()}
      * and {@link LogRecord#getSourceMethodName()} values. The log record is cleared by this method call
@@ -404,18 +383,16 @@ abstract class AnnotatedImage extends ImageAdapter {
      * @param  handler  where to send the log message.
      */
     final void logAndClearError(final Class<?> classe, final String method, final ErrorHandler handler) {
-        final ErrorHandler.Report report;
-        synchronized (this) {
-            report = errors;
-            if (report == null || report.isEmpty()) {
-                return;
+        final ErrorHandler.Report report = errors;
+        if (report != null) {
+            synchronized (report) {
+                final LogRecord record = report.getDescription();
+                record.setSourceClassName(classe.getCanonicalName());
+                record.setSourceMethodName(method);
+                errors = null;
             }
-            final LogRecord record = report.getDescription();
-            record.setSourceClassName(classe.getCanonicalName());
-            record.setSourceMethodName(method);
-            errors = null;          // Make sure that no other thread will use that `Report` instance.
+            handler.handle(report);
         }
-        handler.handle(report);
     }
 
     /**
@@ -442,7 +419,7 @@ abstract class AnnotatedImage extends ImageAdapter {
                 final Collector<? super Raster,?,?> collector = collector();
                 if (collector != null) {
                     if (!failOnException) {
-                        executor.setErrorHandler(this::setError);
+                        executor.setErrorHandler((e) -> errors = e, AnnotatedImage.class, "getProperty");
                     }
                     return executor.executeOnReadable(source, collector());
                 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ErrorAction.java b/core/sis-feature/src/main/java/org/apache/sis/image/ErrorAction.java
index d441f00..2691f8a 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ErrorAction.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ErrorAction.java
@@ -56,24 +56,26 @@ enum ErrorAction implements ErrorHandler {
      */
     @Override
     public void handle(final Report details) {
-        final LogRecord record = details.getDescription();
-        if (record != null) {
-            if (this == LOG) {
-                String logger = record.getLoggerName();
-                if (logger == null) {
-                    logger = Modules.RASTER;
-                    record.setLoggerName(logger);
-                }
-                Logging.getLogger(logger).log(record);
-            } else {
-                final Throwable ex = record.getThrown();
-                if (ex instanceof Error) {
-                    throw (Error) ex;
-                } else if (ex instanceof ImagingOpException) {
-                    throw (ImagingOpException) ex;
+        synchronized (details) {
+            final LogRecord record = details.getDescription();
+            if (record != null) {
+                if (this == LOG) {
+                    String logger = record.getLoggerName();
+                    if (logger == null) {
+                        logger = Modules.RASTER;
+                        record.setLoggerName(logger);
+                    }
+                    Logging.getLogger(logger).log(record);
                 } else {
-                    final String message = new SimpleFormatter().formatMessage(record);
-                    throw (ImagingOpException) new ImagingOpException(message).initCause(ex);
+                    final Throwable ex = record.getThrown();
+                    if (ex instanceof Error) {
+                        throw (Error) ex;
+                    } else if (ex instanceof ImagingOpException) {
+                        throw (ImagingOpException) ex;
+                    } else {
+                        final String message = new SimpleFormatter().formatMessage(record);
+                        throw (ImagingOpException) new ImagingOpException(message).initCause(ex);
+                    }
                 }
             }
         }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ErrorHandler.java b/core/sis-feature/src/main/java/org/apache/sis/image/ErrorHandler.java
index 0c940b6..3190120 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ErrorHandler.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ErrorHandler.java
@@ -18,13 +18,12 @@ package org.apache.sis.image;
 
 import java.awt.Point;
 import java.util.Arrays;
-import java.util.Locale;
 import java.util.Objects;
+import java.util.function.Supplier;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
 import java.awt.image.ImagingOpException;
 import org.apache.sis.util.ArgumentChecks;
-import org.apache.sis.util.resources.Errors;
 import org.apache.sis.internal.feature.Resources;
 
 
@@ -45,7 +44,7 @@ public interface ErrorHandler {
     ErrorHandler THROW = ErrorAction.THROW;
 
     /**
-     * Exceptions are wrapped in a {@link LogRecord} and logged at {@link java.util.logging.Level#WARNING}.
+     * Exceptions are wrapped in a {@link LogRecord} and logged, usually at {@link Level#WARNING}.
      * Only one log record is created for all tiles that failed for the same operation on the same image.
      * A partial result may be available.
      *
@@ -58,13 +57,25 @@ public interface ErrorHandler {
      * Invoked after errors occurred in one or many tiles. This method may be invoked an arbitrary
      * time after the error occurred, and may aggregate errors that occurred in more than one tile.
      *
-     * @param  details  information about errors.
+     * <h4>Multi-threading</h4>
+     * If the image processing was splitted between many worker threads, this method may be invoked
+     * from any of those threads. However the invocation should happen after all threads terminated,
+     * either successfully or with an error reported in {@code details}.
+     *
+     * @param  details  information about the first error. If more than one error occurred, the other
+     *         errors are reported as {@linkplain Throwable#getSuppressed() suppressed exceptions}.
      */
     void handle(Report details);
 
     /**
      * Information about errors that occurred while reading or writing tiles in an image.
      * A single {@code Report} may be generated for failures in more than one tiles.
+     *
+     * <h2>Multi-threading</h2>
+     * This class is safe for use in multi-threading. The synchronization lock is {@code this}.
+     * However the {@link LogRecord} instance returned by {@link #getDescription()} is not thread-safe.
+     * Operations applied on the {@code LogRecord} should be inside a block synchronized on the
+     * {@code Report.this} lock.
      */
     class Report {
         /**
@@ -84,18 +95,18 @@ public interface ErrorHandler {
 
         /**
          * Creates an initially empty report.
-         * Error reports can be added by calls to {@code add(Throwable, …)} methods.
+         * Error reports can be added by calls to {@link #add add(…)}.
          */
         public Report() {
         }
 
         /**
          * Returns {@code true} if no error has been reported.
-         * This is true only if no {@code add(Throwable, …)} method had been invoked.
+         * This is true only if the {@link #add add(…)} method has never been invoked.
          *
          * @return whether this report is empty.
          */
-        public boolean isEmpty() {
+        public synchronized boolean isEmpty() {
             return length == 0 && description == null;
         }
 
@@ -104,7 +115,8 @@ public interface ErrorHandler {
          * and the same stack trace. The cause and the suppressed exceptions are ignored.
          */
         private static boolean equals(final Throwable t1, final Throwable t2) {
-            return t1.getClass() == t2.getClass()
+            if (t1 == t2) return true;
+            return t1 != null && t2 != null && t1.getClass() == t2.getClass()
                     && Objects.equals(t1.getMessage(),    t2.getMessage())
                     &&  Arrays.equals(t1.getStackTrace(), t2.getStackTrace());
         }
@@ -113,7 +125,9 @@ public interface ErrorHandler {
          * Adds the {@code more} exception to the list if suppressed exceptions if not already present.
          */
         private static void addSuppressed(final Throwable error, final Throwable more) {
-            if (equals(error, more)) return;
+            if (equals(error, more) || equals(error.getCause(), more)) {
+                return;
+            }
             for (final Throwable s : error.getSuppressed()) {
                 if (equals(s, more)) return;
             }
@@ -121,77 +135,50 @@ public interface ErrorHandler {
         }
 
         /**
-         * Reports an error that occurred while computing an image property.
-         * This method can be invoked many times on the same {@code Report} instance.
-         *
-         * <h4>Logging information</h4>
-         * {@code Report} creates a {@link LogRecord} the first time that an {@code add(…)} method is invoked.
-         * The record will have its
-         * {@linkplain LogRecord#getLevel() level},
-         * {@linkplain LogRecord#getMessage() message} and
-         * {@linkplain LogRecord#getThrown() exception} properties initialized. But the
-         * {@linkplain LogRecord#getSourceClassName() source class name},
-         * {@linkplain LogRecord#getSourceMethodName() source method name} and
-         * {@linkplain LogRecord#getLoggerName() logger name} may be undefined;
-         * they may need to be completed by the caller.
-         *
-         * @param  error     the error that occurred.
-         * @param  property  name of the property which was computed, or {@code null} if none.
-         * @return {@code true} if this is the first time that an error is reported
-         *         (in which case a {@link LogRecord} instance has been created),
-         *         or {@code false} if a {@link LogRecord} already exists.
-         */
-        public boolean addPropertyError(final Throwable error, final String property) {
-            ArgumentChecks.ensureNonNull("error", error);
-            if (description == null) {
-                if (property != null) {
-                    description = Errors.getResources((Locale) null)
-                            .getLogRecord(Level.WARNING, Errors.Keys.CanNotCompute_1, property);
-                } else {
-                    description = new LogRecord(Level.WARNING, error.toString());
-                }
-                description.setThrown(error);
-                return true;
-            } else {
-                addSuppressed(description.getThrown(), error);
-                return false;
-            }
-        }
-
-        /**
          * Reports an error that occurred while computing an image tile.
          * This method can be invoked many times on the same {@code Report} instance.
          *
          * <h4>Logging information</h4>
-         * {@code Report} creates a {@link LogRecord} the first time that an {@code add(…)} method is invoked.
-         * The record will have its
+         * {@code Report} creates a {@link LogRecord} the first time that this {@code add(…)} method is invoked.
+         * The log record is created using the given supplier if non-null. That supplier should set the log
          * {@linkplain LogRecord#getLevel() level},
-         * {@linkplain LogRecord#getMessage() message} and
-         * {@linkplain LogRecord#getThrown() exception} properties initialized. But the
+         * {@linkplain LogRecord#getMessage() message},
          * {@linkplain LogRecord#getSourceClassName() source class name},
          * {@linkplain LogRecord#getSourceMethodName() source method name} and
-         * {@linkplain LogRecord#getLoggerName() logger name} may be undefined;
-         * they may need to be completed by the caller.
+         * {@linkplain LogRecord#getLoggerName() logger name}.
+         * The {@linkplain LogRecord#getThrown() exception} property will be set by this method.
          *
-         * @param  error  the error that occurred.
-         * @param  tx     column index of the tile where the error occurred.
-         * @param  ty     row index of the tile where the error occurred.
+         * @param  tile    column (x) and row (y) indices of the tile where the error occurred, or {@code null} if unknown.
+         * @param  error   the error that occurred.
+         * @param  record  the record supplier, invoked only when this method is invoked for the first time.
+         *                 If {@code null}, a default {@link LogRecord} will be created.
          * @return {@code true} if this is the first time that an error is reported
          *         (in which case a {@link LogRecord} instance has been created),
          *         or {@code false} if a {@link LogRecord} already exists.
          */
-        public boolean addTileError(final Throwable error, final int tx, final int ty) {
+        public synchronized boolean add(final Point tile, final Throwable error, final Supplier<LogRecord> record) {
             ArgumentChecks.ensureNonNull("error", error);
-            if (indices == null) {
-                indices = new int[8];
-            } else if (length >= indices.length) {
-                indices = Arrays.copyOf(indices, indices.length * 2);
+            if (tile != null) {
+                if (indices == null) {
+                    indices = new int[8];
+                } else if (length >= indices.length) {
+                    indices = Arrays.copyOf(indices, indices.length * 2);
+                }
+                indices[length++] = tile.x;
+                indices[length++] = tile.y;
             }
-            indices[length++] = tx;
-            indices[length++] = ty;
             if (description == null) {
-                description = Resources.forLocale(null)
-                        .getLogRecord(Level.WARNING, Resources.Keys.CanNotProcessTile_2, tx, ty);
+                if (record != null) {
+                    description = record.get();
+                }
+                if (description == null) {
+                    if (tile != null) {
+                        description = Resources.forLocale(null)
+                                .getLogRecord(Level.WARNING, Resources.Keys.CanNotProcessTile_2, tile.x, tile.y);
+                    } else {
+                        description = new LogRecord(Level.WARNING, error.toString());
+                    }
+                }
                 description.setThrown(error);
                 return true;
             } else {
@@ -205,7 +192,7 @@ public interface ErrorHandler {
          *
          * @return indices of all tiles in error, or an empty array if none.
          */
-        public Point[] getTileIndices() {
+        public synchronized Point[] getTileIndices() {
             final Point[] p = new Point[length >>> 1];
             for (int i=0; i<length;) {
                 p[i >>> 1] = new Point(indices[i++], indices[i++]);
@@ -214,13 +201,15 @@ public interface ErrorHandler {
         }
 
         /**
-         * Returns a description of errors as a log record.
+         * Returns a description of the first error as a log record.
          * The exception can be obtained by {@link LogRecord#getThrown()}.
+         * If more than one error occurred, the other errors are reported
+         * as {@linkplain Throwable#getSuppressed() suppressed exceptions}.
          * The return value is never null unless this report {@linkplain #isEmpty() is empty}.
          *
          * @return errors description, or {@code null} if this report is empty.
          */
-        public LogRecord getDescription() {
+        public synchronized LogRecord getDescription() {
             return description;
         }
     }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/PrefetchedImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/PrefetchedImage.java
index 3dea68e..453cd8d 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/PrefetchedImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/PrefetchedImage.java
@@ -16,21 +16,24 @@
  */
 package org.apache.sis.image;
 
-import java.awt.Point;
 import java.util.Arrays;
 import java.util.Vector;
+import java.awt.Point;
 import java.awt.Rectangle;
 import java.awt.color.ColorSpace;
 import java.awt.image.ColorModel;
+import java.awt.image.DataBuffer;
 import java.awt.image.SampleModel;
 import java.awt.image.RenderedImage;
 import java.awt.image.Raster;
 import java.awt.image.WritableRaster;
 import java.awt.image.RasterFormatException;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
+import org.apache.sis.internal.coverage.j2d.TileErrorHandler;
 import org.apache.sis.internal.coverage.j2d.TileOpExecutor;
 import org.apache.sis.internal.util.Numerics;
 import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.ArgumentChecks;
 
 
 /**
@@ -45,7 +48,7 @@ import org.apache.sis.util.resources.Errors;
  * @since 1.1
  * @module
  */
-final class PrefetchedImage extends PlanarImage {
+final class PrefetchedImage extends PlanarImage implements TileErrorHandler.Executor {
     /**
      * The source image from which to prefetch tiles.
      */
@@ -72,6 +75,22 @@ final class PrefetchedImage extends PlanarImage {
     private final Raster[] tiles;
 
     /**
+     * If error(s) occurred while computing one or more tiles, data shared by {@link Raster} placeholders.
+     * This is data for a tile showing a cross (X) in a box.
+     *
+     * @see #createPlaceholder(int, int)
+     */
+    private DataBuffer placeholderPixels;
+
+    /**
+     * Non-null if errors should be handled during {@link #getTile(int, int)} execution for tiles outside
+     * the area of interest specified at construction time.
+     *
+     * @see #execute(Runnable, TileErrorHandler)
+     */
+    private ErrorHandler.Report errorReport;
+
+    /**
      * Creates a new prefetched image.
      *
      * @param source          the image to compute immediately (may be {@code null}).
@@ -102,7 +121,7 @@ final class PrefetchedImage extends PlanarImage {
         numXTiles = ti.width;
         numYTiles = ti.height;
         tiles     = new Raster[Math.multiplyExact(numYTiles, numXTiles)];
-        worker.setErrorHandler(errorHandler);
+        worker.setErrorHandler(errorHandler, ImageProcessor.class, "prefetch");
         if (parallel) {
             worker.parallelReadFrom(source);
         } else {
@@ -113,12 +132,11 @@ final class PrefetchedImage extends PlanarImage {
          * to that tile still null. Replace them by a placeholder. Note that it may happen
          * only if the error handler is not `ErrorHandler.THROW`.
          */
-        Raster previous = null;
         for (int i=0; i<tiles.length; i++) {
             if (tiles[i] == null) {
                 final int tileX = (i % numXTiles) + minTileX;
                 final int tileY = (i / numXTiles) + minTileY;
-                tiles[i] = previous = createPlaceholder(tileX, tileY, previous);
+                tiles[i] = createPlaceholder(tileX, tileY);
             }
         }
     }
@@ -214,24 +232,65 @@ final class PrefetchedImage extends PlanarImage {
                 return tiles[tx + ty * numXTiles];
             }
         }
-        return source.getTile(tileX, tileY);
+        /*
+         * If the requested tile is not one of the tiles that we computed in advance,
+         * fetch directly from the source (may imply computation in current thread).
+         * If an error occurs and this method is invoked inside `execute(…)` block,
+         * apply a similar error handling than the one applied in constructor.
+         */
+        try {
+            return source.getTile(tileX, tileY);
+        } catch (RuntimeException e) {
+            final ErrorHandler.Report report = errorReport;
+            if (report == null) {
+                throw e;
+            }
+            report.add(new Point(tileX, tileY), e, null);
+            assert Thread.holdsLock(this);
+            return createPlaceholder(tileX, tileY);
+        }
+    }
+
+    /**
+     * Executes the given action in a mode where errors occurring in {@link RenderedImage#getTile(int, int)}
+     * are reported to the given handler instead of stopping the operation. The given action is typically
+     * some operation invoking, directly or indirectly, {@link #getTile(int, int)} with tile indices that
+     * may be outside the area of interest specified at construction time. Exceptions that occurred inside
+     * the area of interest were caught by the constructor and this method makes no difference for them.
+     * But exceptions occurring outside that area are interest are redirected to the {@link #source} image,
+     * which may fail. This method provides a way to catch also those errors.
+     *
+     * @param  action        the action to execute (for example drawing the image).
+     * @param  errorHandler  the handler to notify if errors occur.
+     */
+    @Override
+    public synchronized void execute(final Runnable action, final TileErrorHandler errorHandler) {
+        ArgumentChecks.ensureNonNull("action", action);
+        ArgumentChecks.ensureNonNull("errorHandler", errorHandler);
+        errorReport = new ErrorHandler.Report();
+        try {
+            action.run();
+        } finally {
+            final ErrorHandler.Report report = errorReport;
+            errorReport = null;
+            errorHandler.publish(report);
+        }
     }
 
     /**
      * Creates a tile to use as a placeholder when a tile can not be computed.
      *
-     * @param  tileX     column index of the tile for which to create a placeholder.
-     * @param  tileY     row index of the tile for which to create a placeholder.
-     * @param  previous  tile previously created by this method, or {@code null} if none.
+     * @param  tileX  column index of the tile for which to create a placeholder.
+     * @param  tileY  row index of the tile for which to create a placeholder.
      * @return placeholder for the tile at given indices.
      */
-    private Raster createPlaceholder(final int tileX, final int tileY, final Raster previous) {
+    private Raster createPlaceholder(final int tileX, final int tileY) {
         final SampleModel model = getSampleModel();
         final Point location = new Point(ImageUtilities.tileToPixelX(source, tileX),
                                          ImageUtilities.tileToPixelY(source, tileY));
-        if (previous != null) {
+        if (placeholderPixels != null) {
             // Reuse same `DataBuffer` with only a different location.
-            return Raster.createRaster(model, previous.getDataBuffer(), location);
+            return Raster.createRaster(model, placeholderPixels, location);
         }
         final double[] samples = new double[model.getNumBands()];
         if (ImageUtilities.isIntegerType(model)) {
@@ -296,6 +355,7 @@ final class PrefetchedImage extends PlanarImage {
                 tile.setPixel(xmax - x, y, samples);
             }
         }
+        placeholderPixels = tile.getDataBuffer();
         return tile;
     }
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TileErrorHandler.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TileErrorHandler.java
new file mode 100644
index 0000000..dd13960
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TileErrorHandler.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.coverage.j2d;
+
+import java.util.logging.LogRecord;
+import java.awt.image.RenderedImage;
+import java.awt.image.ImagingOpException;
+import org.apache.sis.image.ErrorHandler;
+import org.apache.sis.internal.system.Modules;
+
+
+/**
+ * A convenience class for reporting an error during computation of a tile.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public final class TileErrorHandler {
+    /**
+     * Exceptions are wrapped in an {@link ImagingOpException} and thrown.
+     * In such case, no result is available. This is the default handler.
+     */
+    public static final TileErrorHandler THROW = new TileErrorHandler(ErrorHandler.THROW, null, null);
+
+    /**
+     * Where to report exceptions, or {@link ErrorHandler#THROW} for throwing them.
+     */
+    final ErrorHandler handler;
+
+    /**
+     * The class to declare in {@link LogRecord} in an error occurred during calculation.
+     * If non-null, then {@link #sourceMethod} should also be non-null.
+     */
+    private final Class<?> sourceClass;
+
+    /**
+     * Name of the method to declare in {@link LogRecord} in an error occurred during calculation.
+     * If non-null, then {@link #sourceClass} should also be non-null.
+     */
+    private final String sourceMethod;
+
+    /**
+     * Creates a new tile error handler.
+     *
+     * @param  handler        where to report exceptions, or {@link ErrorHandler#THROW} for throwing them.
+     * @param  sourceClass    the class to declare in {@link LogRecord} in an error occurred during calculation.
+     * @param  sourceMethod   name of the method to declare in {@link LogRecord} in an error occurred during calculation.
+     */
+    public TileErrorHandler(final ErrorHandler handler, final Class<?> sourceClass, final String sourceMethod) {
+        this.handler      = handler;
+        this.sourceClass  = sourceClass;
+        this.sourceMethod = sourceMethod;
+    }
+
+    /**
+     * Returns {@code true} if the error handler is {@link ErrorHandler#THROW}.
+     * In such case there is not need to configure the {@link LogRecord} since
+     * nothing will be logged.
+     */
+    final boolean isThrow() {
+        return handler == ErrorHandler.THROW;
+    }
+
+    /**
+     * If the given report is non-empty, sends it to the error handler.
+     * This method sets the logger, source class and source method name
+     * on the {@link LogRecord} instance before to publish it.
+     *
+     * @param  report  the error report to send if non-empty.
+     */
+    public void publish(final ErrorHandler.Report report) {
+        synchronized (report) {
+            if (report.isEmpty()) {
+                return;
+            }
+            if (!isThrow()) {
+                final LogRecord record = report.getDescription();
+                if (sourceClass != null) {
+                    record.setSourceClassName(sourceClass.getCanonicalName());
+                }
+                if (sourceMethod != null) {
+                    record.setSourceMethodName(sourceMethod);
+                }
+                record.setLoggerName(Modules.RASTER);
+            }
+        }
+        handler.handle(report);
+    }
+
+    /**
+     * An object executing actions in a way where errors occurring during tile computation
+     * are reported to an error handler instead than causing the whole operation to fail.
+     *
+     * <p>This interface is currently used as a workaround for accessing
+     * {@link org.apache.sis.image.PrefetchedImage} without making that class public.</p>
+     */
+    public interface Executor {
+        /**
+         * Executes the given action in a mode where errors occurring in {@link RenderedImage#getTile(int, int)}
+         * are reported to the given handler instead of stopping the operation.
+         *
+         * @param  action        the action to execute (for example drawing the image).
+         * @param  errorHandler  the handler to notify if errors occur.
+         */
+        void execute(Runnable action, TileErrorHandler errorHandler);
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TileOpExecutor.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TileOpExecutor.java
index c81ca58..46e563d 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TileOpExecutor.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TileOpExecutor.java
@@ -23,6 +23,7 @@ import java.util.function.BinaryOperator;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
+import java.awt.Point;
 import java.awt.Rectangle;
 import java.awt.image.Raster;
 import java.awt.image.RenderedImage;
@@ -80,13 +81,13 @@ public class TileOpExecutor {
     private final int minTileX, minTileY, maxTileX, maxTileY;
 
     /**
-     * Where to report exceptions, or {@link ErrorHandler#THROW} for throwing them.
+     * Where to report exceptions, or {@link TileErrorHandler#THROW} for throwing them.
      * If at least one error occurred, then this handler will receive the {@link Cursor#errors} report
      * after all computation {@linkplain Cursor#finish finished}.
      *
-     * @see #setErrorHandler(ErrorHandler)
+     * @see #setErrorHandler(ErrorHandler, Class, String)
      */
-    private ErrorHandler errorHandler;
+    private TileErrorHandler errorHandler;
 
     /**
      * Creates a new operation for tiles in the specified region of the specified image.
@@ -98,7 +99,7 @@ public class TileOpExecutor {
      * @throws ArithmeticException if some tile indices are too large.
      */
     public TileOpExecutor(final RenderedImage image, final Rectangle aoi) {
-        errorHandler = ErrorHandler.THROW;
+        errorHandler = TileErrorHandler.THROW;
         if (aoi != null) {
             final int  tileWidth       = image.getTileWidth();
             final int  tileHeight      = image.getTileHeight();
@@ -120,23 +121,22 @@ public class TileOpExecutor {
      * Sets the handler where to report exceptions.
      * The exception can be obtained by {@link LogRecord#getThrown()}
      * on the value returned by {@link ErrorHandler.Report#getDescription()}.
-     * In addition the {@code LogRecord} will have the
-     * {@linkplain LogRecord#getLevel() level} and
-     * {@linkplain LogRecord#getMessage() message} properties set. But the
-     * {@linkplain LogRecord#getSourceClassName() source class name},
-     * {@linkplain LogRecord#getSourceMethodName() source method name} and
-     * {@linkplain LogRecord#getLoggerName() logger name} will be undefined;
-     * they should be set by the given {@link ErrorHandler}.
      *
      * <h4>Limitation</h4>
      * In current implementation this is used only during parallel computation.
      * A future version may need to use it for sequential computations as well for consistency.
      *
-     * @param  errorHandler  where to report exceptions, or {@link ErrorHandler#THROW} for throwing them.
+     * @param  handler       where to report exceptions, or {@link ErrorHandler#THROW} for throwing them.
+     * @param  sourceClass   class to declare in {@link LogRecord}, or {@code null} if none.
+     * @param  sourceMethod  method to declare in {@link LogRecord}, or {@code null} if none.
      */
-    public final void setErrorHandler(final ErrorHandler errorHandler) {
-        ArgumentChecks.ensureNonNull("errorHandler", errorHandler);
-        this.errorHandler = errorHandler;
+    public final void setErrorHandler(final ErrorHandler handler, final Class<?> sourceClass, final String sourceMethod) {
+        ArgumentChecks.ensureNonNull("handler", handler);
+        if (handler == ErrorHandler.THROW) {
+            errorHandler = TileErrorHandler.THROW;
+        } else {
+            errorHandler = new TileErrorHandler(handler, sourceClass, sourceMethod);
+        }
     }
 
     /**
@@ -388,36 +388,34 @@ public class TileOpExecutor {
      * </ul>
      *
      * <h4>Errors management</h4>
-     * If an error occurred during the processing of a tile, then there is a choice:
+     * If an error occurred during the processing of a tile, then there is a choice depending on the value given
+     * to {@link #setErrorHandler setErrorHandler(…)}:
      *
      * <ul class="verbose">
      *   <li>
-     *     If the {@code errorHandler} is {@code THROW}, then all threads will finish the tiles they were
-     *     processing at the time the error occurred, but will not take any other tile (i.e. remaining tiles
-     *     will be left unprocessed). The exception that occurred is wrapped in an {@link ImagingOpException}
-     *     and thrown.
+     *     If {@link ErrorHandler#THROW}, then all threads will finish the tiles they were processing at the time
+     *     the error occurred, but will not take any other tile (i.e. remaining tiles will be left unprocessed).
+     *     The exception that occurred is wrapped in an {@link ImagingOpException} and thrown.
      *   </li><li>
-     *     If the {@code errorHandler} is {@code LOG}, then the exception is wrapped in a {@link LogRecord} and
-     *     the processing continues with other tiles. If more exceptions happen, those subsequent exceptions
-     *     will be added to the first one by {@link Exception#addSuppressed(Throwable)}.
+     *     If {@link ErrorHandler#LOG}, then the exception is wrapped in a {@link LogRecord} and the processing
+     *     continues with other tiles. If more exceptions happen, those subsequent exceptions will be added to
+     *     the first one with {@link Exception#addSuppressed(Throwable)}.
      *     After all tiles have been processed, the error handler will be invoked with that {@link LogRecord}.
-     *     That {@code LogRecord} has the level, message and exception properties set, but not the source class name,
-     *     source method name or logger name. The error handler should set those properties itself.
      *   </li>
      * </ul>
      *
      * <h4>Concurrency requirements</h4>
      * The {@link RenderedImage#getTile(int, int)} implementation of the given image must support concurrency.
      *
-     * @param  <A>           the type of the thread-local object to be given to each thread.
-     * @param  <R>           the type of the final result. This is often the same as <var>A</var>.
-     * @param  source        the image to read. This is usually the image specified at construction time,
-     *                       but other images are okay if they share the same pixel and tile coordinate systems.
-     * @param  collector     the action to execute on each {@link Raster}, together with supplier and combiner
-     *                       of thread-local objects of type <var>A</var>. See above javadoc for more information.
+     * @param  <A>        the type of the thread-local object to be given to each thread.
+     * @param  <R>        the type of the final result. This is often the same as <var>A</var>.
+     * @param  source     the image to read. This is usually the image specified at construction time,
+     *                    but other images are okay if they share the same pixel and tile coordinate systems.
+     * @param  collector  the action to execute on each {@link Raster}, together with supplier and combiner
+     *                    of thread-local objects of type <var>A</var>. See above javadoc for more information.
      * @return the final result computed by finisher (may be {@code null}).
      * @throws ImagingOpException if an exception occurred during {@link RenderedImage#getTile(int, int)}
-     *         or {@link #readFrom(Raster)} execution, and {@code errorHandler} is {@code null}.
+     *         or {@link #readFrom(Raster)} execution, and the error handler is {@link ErrorHandler#THROW}.
      * @throws RuntimeException if an exception occurred elsewhere (for example in the combiner or finisher).
      */
     public final <A,R> R executeOnReadable(final RenderedImage source,
@@ -460,13 +458,11 @@ public class TileOpExecutor {
      * If an error occurred during the processing of a tile, the exception is remembered and the processing
      * continues with other tiles. If more exceptions happen, those subsequent exceptions will be added to
      * the first one by {@link Exception#addSuppressed(Throwable)}. After all tiles have been processed,
-     * there is a choice:
+     * there is a choice depending on the value given to {@link #setErrorHandler setErrorHandler(…)}:
      *
      * <ul>
-     *   <li>If the {@code errorHandler} is {@code THROW}, the exception is wrapped in an {@link ImagingOpException} and thrown.</li>
-     *   <li>If the {@code errorHandler} is {@code LOG}, the exception is wrapped in a {@link LogRecord} and given to the handler.
-     *       That {@code LogRecord} has the level, message and exception properties set, but not the source class name,
-     *       source method name or logger name. The error handler should set those properties itself.</li>
+     *   <li>If {@link ErrorHandler#THROW}, the exception is wrapped in an {@link ImagingOpException} and thrown.</li>
+     *   <li>If {@link ErrorHandler#LOG}, the exception is wrapped in a {@link LogRecord} and given to the handler.</li>
      * </ul>
      *
      * <h4>Concurrency requirements</h4>
@@ -474,17 +470,17 @@ public class TileOpExecutor {
      * {@link WritableRenderedImage#releaseWritableTile(int, int)} implementations
      * of the given image must support concurrency.
      *
-     * @param  <A>           the type of the thread-local object to be given to each thread.
-     * @param  <R>           the type of the final result. This is often the same as <var>A</var>.
-     * @param  target        the image where to write. This is usually the image specified at construction time,
-     *                       but other images are okay if they share the same pixel and tile coordinate systems.
-     * @param  collector     the action to execute on each {@link WritableRaster}, together with supplier and combiner
-     *                       of thread-local objects of type <var>A</var>. See above javadoc for more information.
+     * @param  <A>        the type of the thread-local object to be given to each thread.
+     * @param  <R>        the type of the final result. This is often the same as <var>A</var>.
+     * @param  target     the image where to write. This is usually the image specified at construction time,
+     *                    but other images are okay if they share the same pixel and tile coordinate systems.
+     * @param  collector  the action to execute on each {@link WritableRaster}, together with supplier and combiner
+     *                    of thread-local objects of type <var>A</var>. See above javadoc for more information.
      * @return the final result computed by finisher. This is often {@code null} because the purpose of calling
      *         {@code executeOnWritable(…)} is more often to update existing tiles instead than to compute a value.
      * @throws ImagingOpException if an exception occurred during {@link WritableRenderedImage#getWritableTile(int, int)},
      *         {@link #writeTo(WritableRaster)} or {@link WritableRenderedImage#releaseWritableTile(int, int)} execution,
-     *         and {@code errorHandler} is {@code null}.
+     *         and the error handler is {@link ErrorHandler#THROW}.
      * @throws RuntimeException if an exception occurred elsewhere (for example in the combiner or finisher).
      */
     public final <A,R> R executeOnWritable(final WritableRenderedImage target,
@@ -564,7 +560,7 @@ public class TileOpExecutor {
          * after all computation {@linkplain #finish finished}.</p>
          *
          * @see #stopOnError
-         * @see #recordError(Worker, Throwable)
+         * @see #recordError(Point, Throwable)
          */
         private final ErrorHandler.Report errors;
 
@@ -573,7 +569,7 @@ public class TileOpExecutor {
          * If {@code false}, processing of all tiles will be completed before the error is reported.
          *
          * @see #errors
-         * @see #recordError(Worker, Throwable)
+         * @see #recordError(Point, Throwable)
          */
         private final boolean stopOnError;
 
@@ -644,13 +640,13 @@ public class TileOpExecutor {
          * @param  workers       handlers of all worker threads other than the current threads.
          *                       Content of this array may be modified by this method.
          * @param  collector     provides the finisher to use for computing final result of type <var>R</var>.
-         * @param  errorHandler  where to report exceptions, or {@link ErrorHandler#THROW} for throwing them.
+         * @param  errorHandler  where to report exceptions, or {@link TileErrorHandler#THROW} for throwing them.
          * @return the final result computed by finisher (may be {@code null}).
          * @throws ImagingOpException if an exception occurred during {@link Worker#executeOnCurrentTile()}
-         *         and the {@code errorHandler} is {@code null}.
+         *         and the {@code errorHandler} is {@code THROW}.
          * @throws RuntimeException if an exception occurred elsewhere (for example in the combiner or finisher).
          */
-        final <R> R finish(final Future<?>[] workers, final Collector<?,A,R> collector, final ErrorHandler errorHandler) {
+        final <R> R finish(final Future<?>[] workers, final Collector<?,A,R> collector, final TileErrorHandler errorHandler) {
             /*
              * Before to wait for other threads to complete their work, we need to remove from executor queue all
              * workers that did not yet started their run. Those threads may be waiting for an executor thread to
@@ -676,9 +672,7 @@ public class TileOpExecutor {
                  * If someone does not want to let us wait, do not wait for other worker threads neither.
                  * We will report that interruption as an error.
                  */
-                synchronized (this) {
-                    errors.addPropertyError(ex, null);
-                }
+                recordError(null, ex);
                 break;
             }
             /*
@@ -689,28 +683,32 @@ public class TileOpExecutor {
             final R result;
             synchronized (this) {
                 result = collector.finisher().apply(accumulator);
-                if (!errors.isEmpty()) {
-                    errorHandler.handle(errors);
-                }
             }
+            /*
+             * If error(s) occurred, report them now. In the default configuration (`TileErrorHandler.THROW`),
+             * the exception is thrown.
+             */
+            errorHandler.publish(errors);
             return result;
         }
 
         /**
          * Stores the given exception in a log record. We use a log record in order to initialize
          * the timestamp and thread ID to the values they had at the time the first error occurred.
+         * The error is not notified immediately to the {@link ErrorHandler}; we wait for other errors
+         * in order to aggregate them in a single record. So the given error is <em>recorded</em>
+         * but not yet <em>reported</em>.
+         *
+         * @param  tile  indices of the tile where an error occurred, or {@code null} if unknown.
+         * @param  ex    the exception that occurred.
          *
-         * @param  indices  the worker thread where the exception occurred. Its {@link Worker#tx}
-         *                  and {@link Worker#ty} indices should identify the problematic tile.
-         * @param  ex       the exception that occurred.
+         * @see TileOpExecutor#setErrorHandler(ErrorHandler, Class, String)
          */
-        final void recordError(final Worker<RI,?,A> indices, final Throwable ex) {
+        final void recordError(final Point tile, final Throwable ex) {
             if (stopOnError) {
                 set(Integer.MIN_VALUE);         // Will cause other threads to stop fetching tiles.
             }
-            synchronized (this) {
-                errors.addTileError(ex, indices.tx, indices.ty);
-            }
+            errors.add(tile, ex, null);
         }
 
         /**
@@ -798,7 +796,7 @@ public class TileOpExecutor {
             while (cursor.next(this)) try {
                 executeOnCurrentTile();
             } catch (Exception ex) {
-                cursor.recordError(this, trimImagingWrapper(ex));
+                cursor.recordError(new Point(tx, ty), trimImagingWrapper(ex));
             }
             cursor.accumulate(accumulator);
         }
@@ -893,9 +891,9 @@ public class TileOpExecutor {
          * See the Javadoc of that method for details.
          */
         static <A,R> R execute(final TileOpExecutor executor, final RenderedImage source,
-                final Collector<? super Raster, A, R> collector, final ErrorHandler errorHandler)
+                final Collector<? super Raster, A, R> collector, final TileErrorHandler errorHandler)
         {
-            final Cursor<RenderedImage,A> cursor = executor.new Cursor<>(source, collector, errorHandler == ErrorHandler.THROW);
+            final Cursor<RenderedImage,A> cursor = executor.new Cursor<>(source, collector, errorHandler.isThrow());
             final Future<?>[] workers = new Future<?>[cursor.getNumWorkers()];
             for (int i=0; i<workers.length; i++) {
                 workers[i] = CommonExecutor.instance().submit(new ReadWork<>(cursor, collector));
@@ -950,7 +948,7 @@ public class TileOpExecutor {
          * See the Javadoc of that method for details.
          */
         static <A,R> R execute(final TileOpExecutor executor, final WritableRenderedImage target,
-                final Collector<? super WritableRaster,A,R> collector, final ErrorHandler errorHandler)
+                final Collector<? super WritableRaster,A,R> collector, final TileErrorHandler errorHandler)
         {
             final Cursor<WritableRenderedImage,A> cursor = executor.new Cursor<>(target, collector, false);
             final Future<?>[] workers = new Future<?>[cursor.getNumWorkers()];


Mime
View raw message