From 955c18714a33a4c2b811933049a87c7957708a00 Mon Sep 17 00:00:00 2001
From: brinn <brinn>
Date: Sat, 5 Jul 2008 15:31:23 +0000
Subject: [PATCH] [LMS-466, LMS-472] refactor: Status and
 RemoteStoreCopyActivitySensor, replace DateStatus with StatusWithResult add:
 AbstractCopyActivitySensor, StatusWithResult

SVN: 7065
---
 .../compression/file/FailureRecord.java       |   2 +-
 .../file/InPlaceCompressionMethod.java        |   3 +-
 .../compression/tiff/TiffCompressor.java      |   2 +-
 .../cisd/common/exceptions/Status.java        |  88 +++++++++--
 .../cisd/common/exceptions/StatusFlag.java    |   6 +-
 .../common/exceptions/StatusWithResult.java   | 149 ++++++++++++++++++
 .../common/filesystem/rsync/RsyncCopier.java  |  23 +--
 .../rsync/RsyncExitValueTranslator.java       |   2 +-
 .../utilities/AbstractCopyActivitySensor.java | 138 ++++++++++++++++
 .../file/CompressionWorkerTest.java           |  16 +-
 .../filesystem/rsync/RsyncCopierTest.java     |   4 +-
 .../filesystem/RetryingPathRemover.java       |   3 +-
 .../datamover/filesystem/intf/DateStatus.java |  92 -----------
 .../datamover/filesystem/intf/IFileStore.java |   5 +-
 .../filesystem/remote/RemotePathMover.java    |   2 +-
 .../remote/RemoteStoreCopyActivitySensor.java | 105 ++----------
 .../filesystem/store/FileStoreLocal.java      |  14 +-
 .../filesystem/store/FileStoreRemote.java     |  19 ++-
 .../store/FileStoreRemoteMounted.java         |  18 +--
 .../utils/QuietPeriodFileFilter.java          |  10 +-
 .../RemoteStoreCopyActivitySensorTest.java    |  28 ++--
 .../utils/QuietPeriodFileFilterTest.java      |  21 +--
 22 files changed, 466 insertions(+), 284 deletions(-)
 create mode 100644 common/source/java/ch/systemsx/cisd/common/exceptions/StatusWithResult.java
 create mode 100644 common/source/java/ch/systemsx/cisd/common/utilities/AbstractCopyActivitySensor.java
 delete mode 100644 datamover/source/java/ch/systemsx/cisd/datamover/filesystem/intf/DateStatus.java

diff --git a/common/source/java/ch/systemsx/cisd/common/compression/file/FailureRecord.java b/common/source/java/ch/systemsx/cisd/common/compression/file/FailureRecord.java
index 243df7ec808..c6ffb27b9a0 100644
--- a/common/source/java/ch/systemsx/cisd/common/compression/file/FailureRecord.java
+++ b/common/source/java/ch/systemsx/cisd/common/compression/file/FailureRecord.java
@@ -45,7 +45,7 @@ public class FailureRecord
     {
         this.failedFile = failedFile;
         this.failureStatus =
-                new Status(StatusFlag.FATAL_ERROR, "Exceptional condition: "
+                Status.createError("Exceptional condition: "
                         + throwableOrNull.getClass().getSimpleName());
         this.throwableOrNull = throwableOrNull;
     }
diff --git a/common/source/java/ch/systemsx/cisd/common/compression/file/InPlaceCompressionMethod.java b/common/source/java/ch/systemsx/cisd/common/compression/file/InPlaceCompressionMethod.java
index 3d56e2260ca..14b706f1815 100644
--- a/common/source/java/ch/systemsx/cisd/common/compression/file/InPlaceCompressionMethod.java
+++ b/common/source/java/ch/systemsx/cisd/common/compression/file/InPlaceCompressionMethod.java
@@ -24,7 +24,6 @@ import org.apache.log4j.Logger;
 import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException;
 import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException;
 import ch.systemsx.cisd.common.exceptions.Status;
-import ch.systemsx.cisd.common.exceptions.StatusFlag;
 import ch.systemsx.cisd.common.logging.LogCategory;
 import ch.systemsx.cisd.common.logging.LogFactory;
 import ch.systemsx.cisd.common.process.ProcessExecutionHelper;
@@ -94,7 +93,7 @@ public abstract class InPlaceCompressionMethod implements ICompressionMethod, IS
     {
         final String msg = String.format(msgTemplate, params);
         operationLog.error(msg);
-        return new Status(StatusFlag.FATAL_ERROR, msg);
+        return Status.createError(msg);
     }
 
     /**
diff --git a/common/source/java/ch/systemsx/cisd/common/compression/tiff/TiffCompressor.java b/common/source/java/ch/systemsx/cisd/common/compression/tiff/TiffCompressor.java
index ad6e60609ed..5892fe58186 100644
--- a/common/source/java/ch/systemsx/cisd/common/compression/tiff/TiffCompressor.java
+++ b/common/source/java/ch/systemsx/cisd/common/compression/tiff/TiffCompressor.java
@@ -44,7 +44,7 @@ public class TiffCompressor extends Compressor
         for (FailureRecord r : failed)
         {
             System.err.printf("%s (%s)\n", r.getFailedFile().getName(), r.getFailureStatus()
-                    .getMessage());
+                    .tryGetErrorMessage());
         }
     }
 
diff --git a/common/source/java/ch/systemsx/cisd/common/exceptions/Status.java b/common/source/java/ch/systemsx/cisd/common/exceptions/Status.java
index 6e4e057ef40..42809e39829 100644
--- a/common/source/java/ch/systemsx/cisd/common/exceptions/Status.java
+++ b/common/source/java/ch/systemsx/cisd/common/exceptions/Status.java
@@ -15,29 +15,78 @@
  */
 package ch.systemsx.cisd.common.exceptions;
 
+import org.apache.commons.lang.ObjectUtils;
+import org.apache.commons.lang.StringUtils;
+
 /**
  * A class that holds the information about the status of an operation. To be used whenever a
  * failure of an operation is signalled back via a return value rather than an exception.
  * 
  * @author Bernd Rinn
  */
-public final class Status
+public class Status
 {
 
     private final StatusFlag flag;
 
-    private final String message;
+    private final String errorMessageOrNull;
 
     /** The status indicating that the operation went fine. */
     public static final Status OK = new Status(StatusFlag.OK, null);
 
-    public Status(StatusFlag flag, String message)
+    /**
+     * Create an error.
+     * 
+     * @param retriable If <code>true</code>, the error will be marked 'retriable'.
+     */
+    public static Status createError(boolean retriable)
+    {
+        return new Status(getErrorFlag(retriable), "");
+    }
+
+    public static Status createError(boolean retriable, String message)
+    {
+        assert message != null;
+        
+        return new Status(getErrorFlag(retriable), message);
+    }
+    
+    public static Status createError()
+    {
+        return new Status(StatusFlag.ERROR, "");
+    }
+    
+    public static Status createError(String message)
+    {
+        assert message != null;
+        
+        return new Status(StatusFlag.ERROR, message);
+    }
+    
+    public static Status createRetriableError()
+    {
+        return new Status(StatusFlag.RETRIABLE_ERROR, "");
+    }
+
+    public static Status createRetriableError(String message)
+    {
+        assert message != null;
+        
+        return new Status(StatusFlag.RETRIABLE_ERROR, message);
+    }
+
+    protected static StatusFlag getErrorFlag(boolean retriable)
+    {
+        return retriable ? StatusFlag.RETRIABLE_ERROR : StatusFlag.ERROR;
+    }
+    
+    protected Status(StatusFlag flag, String message)
     {
         assert flag != null;
         assert StatusFlag.OK.equals(flag) || message != null;
 
         this.flag = flag;
-        this.message = message;
+        this.errorMessageOrNull = message;
     }
 
     /**
@@ -49,14 +98,26 @@ public final class Status
     }
 
     /**
-     * @return The message of the operation if <code>getFlag() != OK</code>, or <code>null</code>
-     *         otherwise.
+     * @return <code>true</code> if this status represents an error.
      */
-    public String getMessage()
+    public final boolean isError()
     {
-        return message;
+        return flag != StatusFlag.OK;
     }
 
+    /**
+     * @return The error message of the operation if <code>getFlag() != OK</code> (can be empty), or
+     *         <code>null</code> otherwise.
+     */
+    public String tryGetErrorMessage()
+    {
+        return errorMessageOrNull;
+    }
+
+    //
+    // Object
+    //
+
     @Override
     public boolean equals(Object obj)
     {
@@ -68,22 +129,23 @@ public final class Status
         {
             return false;
         }
-        final Status status = (Status) obj;
-        return flag.equals(status.flag);
+        final Status that = (Status) obj;
+        return getFlag() == that.getFlag()
+                && ObjectUtils.equals(this.tryGetErrorMessage(), that.tryGetErrorMessage());
     }
 
     @Override
     public int hashCode()
     {
-        return flag.hashCode();
+        return (17 + flag.hashCode()) * 37 + ObjectUtils.hashCode(tryGetErrorMessage());
     }
 
     @Override
     public String toString()
     {
-        if (message != null)
+        if (StringUtils.isNotBlank(errorMessageOrNull))
         {
-            return flag.toString() + ": \"" + message + "\"";
+            return flag.toString() + ": \"" + errorMessageOrNull + "\"";
         } else
         {
             return flag.toString();
diff --git a/common/source/java/ch/systemsx/cisd/common/exceptions/StatusFlag.java b/common/source/java/ch/systemsx/cisd/common/exceptions/StatusFlag.java
index f88bcd87baa..bcf0888fe36 100644
--- a/common/source/java/ch/systemsx/cisd/common/exceptions/StatusFlag.java
+++ b/common/source/java/ch/systemsx/cisd/common/exceptions/StatusFlag.java
@@ -27,9 +27,9 @@ public enum StatusFlag
 
     /** The operation has been successful. */
     OK,
-    /** An error has occurred. Retrying the operation might remedy the problem. */
+    /** An error has occurred. Retrying the operation might remove the problem. */
     RETRIABLE_ERROR,
-    /** A fatal error has occurred. */
-    FATAL_ERROR
+    /** An error has occurred. */
+    ERROR
 
 }
\ No newline at end of file
diff --git a/common/source/java/ch/systemsx/cisd/common/exceptions/StatusWithResult.java b/common/source/java/ch/systemsx/cisd/common/exceptions/StatusWithResult.java
new file mode 100644
index 00000000000..b7413607edd
--- /dev/null
+++ b/common/source/java/ch/systemsx/cisd/common/exceptions/StatusWithResult.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2008 ETH Zuerich, CISD
+ *
+ * Licensed 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 ch.systemsx.cisd.common.exceptions;
+
+import org.apache.commons.lang.ObjectUtils;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.builder.HashCodeBuilder;
+
+/**
+ * A {@link Status} that can also hold a result.
+ * 
+ * @author Bernd Rinn
+ */
+public class StatusWithResult<T> extends Status
+{
+
+    private final T resultOrNull;
+
+    /**
+     * Creates a new result with status {@link StatusFlag#OK} and <var>resultOrNull</var>.
+     */
+    public static <T> StatusWithResult<T> create(T resultOrNull)
+    {
+        return new StatusWithResult<T>(StatusFlag.OK, null, resultOrNull);
+    }
+    
+    /**
+     * Create an error.
+     * 
+     * @param retriable If <code>true</code>, the error will be marked 'retriable'.
+     */
+    public static <T> StatusWithResult<T>  createError(boolean retriable)
+    {
+        return new StatusWithResult<T>(getErrorFlag(retriable), "", null);
+    }
+
+    public static <T> StatusWithResult<T> createError(boolean retriable, String message)
+    {
+        assert message != null;
+        
+        return new StatusWithResult<T>(getErrorFlag(retriable), message, null);
+    }
+    
+    public static <T> StatusWithResult<T>  createError()
+    {
+        return new StatusWithResult<T>(StatusFlag.ERROR, "", null);
+    }
+    
+    public static <T> StatusWithResult<T>  createError(String message)
+    {
+        assert message != null;
+        
+        return new StatusWithResult<T>(StatusFlag.ERROR, message, null);
+    }
+    
+    public static <T> StatusWithResult<T>  createRetriableError()
+    {
+        return new StatusWithResult<T>(StatusFlag.RETRIABLE_ERROR, "", null);
+    }
+
+    public static <T> StatusWithResult<T>  createRetriableError(String message)
+    {
+        assert message != null;
+        
+        return new StatusWithResult<T>(StatusFlag.RETRIABLE_ERROR, message, null);
+    }
+
+    protected StatusWithResult(StatusFlag flag, String messageOrNull, T resultOrNull)
+    {
+        super(flag, messageOrNull);
+        this.resultOrNull = resultOrNull;
+    }
+
+    /**
+     * Returns the result of the operation (may be <code>null</code>).
+     */
+    public final T tryGetResult()
+    {
+        return resultOrNull;
+    }
+
+    //
+    // Object
+    //
+
+    @SuppressWarnings("unchecked")
+    private StatusWithResult<T> toStatusWithResult(Object obj)
+    {
+        return (StatusWithResult) obj;
+    }
+
+    @Override
+    public boolean equals(Object obj)
+    {
+        if (obj == this)
+        {
+            return true;
+        }
+        if (obj == null || obj instanceof StatusWithResult == false)
+        {
+            return false;
+        }
+        final StatusWithResult<T> that = toStatusWithResult(obj);
+        return getFlag() == that.getFlag()
+                && ObjectUtils.equals(this.tryGetErrorMessage(), that.tryGetErrorMessage())
+                && ObjectUtils.equals(this.tryGetResult(), that.tryGetResult());
+    }
+
+    @Override
+    public int hashCode()
+    {
+        final HashCodeBuilder builder = new HashCodeBuilder();
+        builder.append(getFlag());
+        builder.append(tryGetErrorMessage());
+        builder.append(tryGetResult());
+        return builder.toHashCode();
+    }
+
+    @Override
+    public String toString()
+    {
+        final String messageOrNull = tryGetErrorMessage();
+        if (StringUtils.isNotBlank(messageOrNull))
+        {
+            return getFlag().toString() + ": \"" + messageOrNull + "\"";
+        } else if (resultOrNull != null)
+        {
+            return getFlag().toString() + ": result is \"" + resultOrNull + "\"";
+        } else
+        {
+            return getFlag().toString();
+        }
+    }
+
+}
diff --git a/common/source/java/ch/systemsx/cisd/common/filesystem/rsync/RsyncCopier.java b/common/source/java/ch/systemsx/cisd/common/filesystem/rsync/RsyncCopier.java
index a11886e473f..5be3ffde79d 100644
--- a/common/source/java/ch/systemsx/cisd/common/filesystem/rsync/RsyncCopier.java
+++ b/common/source/java/ch/systemsx/cisd/common/filesystem/rsync/RsyncCopier.java
@@ -24,6 +24,7 @@ import java.util.concurrent.atomic.AtomicReference;
 
 import org.apache.log4j.Logger;
 
+import ch.rinn.restrictions.Private;
 import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException;
 import ch.systemsx.cisd.common.exceptions.Status;
 import ch.systemsx.cisd.common.exceptions.StatusFlag;
@@ -45,23 +46,24 @@ import ch.systemsx.cisd.common.utilities.OSUtilities;
  */
 public final class RsyncCopier implements IPathCopier, IDirectoryImmutableCopier
 {
-    /**
-     * The {@link Status} returned if the process was terminated by {@link Process#destroy()}.
-     */
-    protected static final Status TERMINATED_STATUS =
-            new Status(StatusFlag.RETRIABLE_ERROR, "Process was terminated.");
-
     private static final Logger machineLog =
             LogFactory.getLogger(LogCategory.MACHINE, RsyncCopier.class);
 
     private static final Logger operationLog =
             LogFactory.getLogger(LogCategory.OPERATION, RsyncCopier.class);
 
+    /**
+     * The {@link Status} returned if the process was terminated by {@link Process#destroy()}.
+     */
+    @Private
+    static final Status TERMINATED_STATUS =
+            Status.createRetriableError("Process was terminated.");
+
     private static final Status INTERRUPTED_STATUS =
-            new Status(StatusFlag.RETRIABLE_ERROR, "Process was interrupted.");
+        Status.createRetriableError("Process was interrupted.");
 
     private static final Status TIMEOUT_STATUS =
-            new Status(StatusFlag.RETRIABLE_ERROR, "Process has stopped because of timeout.");
+        Status.createRetriableError("Process has stopped because of timeout.");
 
     private final String rsyncExecutable;
 
@@ -421,11 +423,12 @@ public final class RsyncCopier implements IPathCopier, IDirectoryImmutableCopier
         }
         int exitValue = processResult.getExitValue();
         final StatusFlag flag = RsyncExitValueTranslator.getStatus(exitValue);
-        if (StatusFlag.OK.equals(flag))
+        if (flag == StatusFlag.OK)
         {
             return Status.OK;
         }
-        return new Status(flag, RsyncExitValueTranslator.getMessage(exitValue));
+        final boolean retriableError = (flag == StatusFlag.RETRIABLE_ERROR);
+        return Status.createError(retriableError, RsyncExitValueTranslator.getMessage(exitValue));
     }
 
 }
diff --git a/common/source/java/ch/systemsx/cisd/common/filesystem/rsync/RsyncExitValueTranslator.java b/common/source/java/ch/systemsx/cisd/common/filesystem/rsync/RsyncExitValueTranslator.java
index af4cf1e5618..748860de314 100644
--- a/common/source/java/ch/systemsx/cisd/common/filesystem/rsync/RsyncExitValueTranslator.java
+++ b/common/source/java/ch/systemsx/cisd/common/filesystem/rsync/RsyncExitValueTranslator.java
@@ -125,7 +125,7 @@ final class RsyncExitValueTranslator
             case 125:
                 return StatusFlag.RETRIABLE_ERROR;
             default:
-                return StatusFlag.FATAL_ERROR;
+                return StatusFlag.ERROR;
         }
     }
 
diff --git a/common/source/java/ch/systemsx/cisd/common/utilities/AbstractCopyActivitySensor.java b/common/source/java/ch/systemsx/cisd/common/utilities/AbstractCopyActivitySensor.java
new file mode 100644
index 00000000000..7dcc6025895
--- /dev/null
+++ b/common/source/java/ch/systemsx/cisd/common/utilities/AbstractCopyActivitySensor.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2008 ETH Zuerich, CISD
+ *
+ * Licensed 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 ch.systemsx.cisd.common.utilities;
+
+import org.apache.commons.lang.time.DurationFormatUtils;
+import org.apache.log4j.Logger;
+
+import ch.systemsx.cisd.common.concurrent.InactivityMonitor.IActivitySensor;
+import ch.systemsx.cisd.common.exceptions.StatusFlag;
+import ch.systemsx.cisd.common.exceptions.StatusWithResult;
+import ch.systemsx.cisd.common.logging.LogCategory;
+import ch.systemsx.cisd.common.logging.LogFactory;
+
+/**
+ * A super class for {@link IActivitySensor}s that sense changes in some sort of copy operation to
+ * a "target".
+ * 
+ * @author Bernd Rinn
+ */
+public abstract class AbstractCopyActivitySensor implements IActivitySensor
+{
+    protected static final Logger machineLog =
+            LogFactory.getLogger(LogCategory.MACHINE, AbstractCopyActivitySensor.class);
+
+    protected static final int DEFAULT_MAX_ERRORS_TO_IGNORE = 3;
+
+    protected final int maxErrorsToIgnore;
+
+    protected final long timeOfCreation = System.currentTimeMillis();
+
+    protected long timeOfLastConfirmedActivity = timeOfCreation;
+
+    protected long timeOfLastReportedActivity = timeOfCreation;
+
+    protected long lastNonErrorResult = -1L;
+
+    protected StatusWithResult<Long> currentResult;
+
+    protected int errorCount = 0;
+
+    protected AbstractCopyActivitySensor()
+    {
+        this(DEFAULT_MAX_ERRORS_TO_IGNORE);
+    }
+
+    protected AbstractCopyActivitySensor(int maxErrorsToIgnore)
+    {
+        this.maxErrorsToIgnore = maxErrorsToIgnore;
+        this.currentResult = null;
+    }
+
+    /**
+     * Returns the result of obtaining the last activity of the target that is more recent than
+     * <var>thresholdMillis</var> (relative to the current point in time).
+     * <p>
+     * If the status of the result is {@link StatusFlag#OK}, the result must be the time of last
+     * activity in milli-seconds (and <i>must not</i> be <code>null</code>).
+     */
+    protected abstract StatusWithResult<Long> getTargetTimeOfLastActivityMoreRecentThan(
+            long thresholdMillis);
+
+    /**
+     * Returns a textual description of the target.
+     */
+    protected abstract String getTargetDescription();
+
+    //
+    // IActivitySensor
+    //
+
+    public long getTimeOfLastActivityMoreRecentThan(long thresholdMillis)
+    {
+        currentResult = getTargetTimeOfLastActivityMoreRecentThan(thresholdMillis);
+        final long now = System.currentTimeMillis();
+        if (currentResult.isError())
+        {
+            ++errorCount;
+            if (errorCount <= maxErrorsToIgnore)
+            {
+                timeOfLastReportedActivity = now;
+                machineLog.error(describeInactivity(now)
+                        + String.format(" (error count: %d <= %d, goes unreported)", errorCount,
+                                maxErrorsToIgnore));
+            } else
+            {
+                machineLog.error(describeInactivity(now)
+                        + " (error count: %s, reported to monitor)");
+            }
+        } else
+        {
+            if (currentResult.tryGetResult() != lastNonErrorResult)
+            {
+                timeOfLastConfirmedActivity = now;
+                lastNonErrorResult = currentResult.tryGetResult();
+                if (machineLog.isDebugEnabled())
+                {
+                    machineLog.debug("Observing write activity on " + getTargetDescription());
+                }
+            }
+            // Implementation note: This means we can report an older time of activity than what we
+            // reported the last time if the last time we had an error. This is on purpose as it
+            // helps avoiding a situation where error and non-error situations do "flip-flop" and we
+            // could report progress where there is no progress.
+            timeOfLastReportedActivity = timeOfLastConfirmedActivity;
+            errorCount = 0;
+        }
+
+        return timeOfLastReportedActivity;
+    }
+
+    public String describeInactivity(long now)
+    {
+        if (currentResult.isError())
+        {
+            return "Error: Unable to determine the time of write activity on "
+                    + getTargetDescription();
+        } else
+        {
+            final String inactivityPeriod =
+                    DurationFormatUtils.formatDurationHMS(now - timeOfLastConfirmedActivity);
+            return "No write activity on " + getTargetDescription() + " for " + inactivityPeriod;
+        }
+    }
+}
diff --git a/common/sourceTest/java/ch/systemsx/cisd/common/compression/file/CompressionWorkerTest.java b/common/sourceTest/java/ch/systemsx/cisd/common/compression/file/CompressionWorkerTest.java
index 39ad51041ae..db4d906f391 100644
--- a/common/sourceTest/java/ch/systemsx/cisd/common/compression/file/CompressionWorkerTest.java
+++ b/common/sourceTest/java/ch/systemsx/cisd/common/compression/file/CompressionWorkerTest.java
@@ -125,7 +125,7 @@ public class CompressionWorkerTest
     public void testCompressionWorkerWithRetriableFailure()
     {
         final String faultyFile = "b";
-        final Status faultyStatus = new Status(StatusFlag.RETRIABLE_ERROR, "some problem");
+        final Status faultyStatus = Status.createRetriableError("some problem");
         final File[] files = new File[]
             { new File("a"), new File(faultyFile), new File("c") };
         context.checking(new Expectations()
@@ -159,7 +159,7 @@ public class CompressionWorkerTest
     public void testCompressionWorkerWithRetriableFailureFinallyFailed()
     {
         final String faultyFile = "b";
-        final Status faultyStatus = new Status(StatusFlag.RETRIABLE_ERROR, "some problem");
+        final Status faultyStatus = Status.createRetriableError("some problem");
         final File[] files = new File[]
             { new File("a"), new File(faultyFile), new File("c") };
         context.checking(new Expectations()
@@ -191,7 +191,7 @@ public class CompressionWorkerTest
         final FailureRecord record = failed.iterator().next();
         assertEquals(faultyFile, record.getFailedFile().getName());
         assertEquals(StatusFlag.RETRIABLE_ERROR, record.getFailureStatus().getFlag());
-        assertEquals(faultyStatus.getMessage(), record.getFailureStatus().getMessage());
+        assertEquals(faultyStatus.tryGetErrorMessage(), record.getFailureStatus().tryGetErrorMessage());
         assertTrue(logRecorder.getLogContent().indexOf(CompressionWorker.EXITING_MSG) >= 0);
     }
 
@@ -199,7 +199,7 @@ public class CompressionWorkerTest
     public void testCompressionWorkerWithFatalFailure()
     {
         final String faultyFile = "b";
-        final Status fatalStatus = new Status(StatusFlag.FATAL_ERROR, "some problem");
+        final Status fatalStatus = Status.createError("some problem");
         final File[] files = new File[]
             { new File("a"), new File(faultyFile), new File("c") };
         context.checking(new Expectations()
@@ -226,8 +226,8 @@ public class CompressionWorkerTest
         assertEquals(1, failed.size());
         final FailureRecord record = failed.iterator().next();
         assertEquals(faultyFile, record.getFailedFile().getName());
-        assertEquals(StatusFlag.FATAL_ERROR, record.getFailureStatus().getFlag());
-        assertEquals(fatalStatus.getMessage(), record.getFailureStatus().getMessage());
+        assertEquals(StatusFlag.ERROR, record.getFailureStatus().getFlag());
+        assertEquals(fatalStatus.tryGetErrorMessage(), record.getFailureStatus().tryGetErrorMessage());
         assertTrue(logRecorder.getLogContent().indexOf(CompressionWorker.EXITING_MSG) >= 0);
     }
 
@@ -308,9 +308,9 @@ public class CompressionWorkerTest
         assertEquals(1, failed.size());
         final FailureRecord record = failed.iterator().next();
         assertEquals(faultyFile, record.getFailedFile().getName());
-        assertEquals(StatusFlag.FATAL_ERROR, record.getFailureStatus().getFlag());
+        assertEquals(StatusFlag.ERROR, record.getFailureStatus().getFlag());
         assertEquals("Exceptional condition: " + FakedException.class.getSimpleName(), record
-                .getFailureStatus().getMessage());
+                .getFailureStatus().tryGetErrorMessage());
         assertEquals(ex, record.tryGetThrowable());
         assertTrue(logRecorder.getLogContent().indexOf(
                 String.format(CompressionWorker.EXCEPTION_COMPRESSING_MSG_TEMPLATE, faultyFile)) >= 0);
diff --git a/common/sourceTest/java/ch/systemsx/cisd/common/filesystem/rsync/RsyncCopierTest.java b/common/sourceTest/java/ch/systemsx/cisd/common/filesystem/rsync/RsyncCopierTest.java
index 514bbc17422..d22a93d2246 100644
--- a/common/sourceTest/java/ch/systemsx/cisd/common/filesystem/rsync/RsyncCopierTest.java
+++ b/common/sourceTest/java/ch/systemsx/cisd/common/filesystem/rsync/RsyncCopierTest.java
@@ -179,7 +179,7 @@ public final class RsyncCopierTest
     public void testRsyncFatalFailure() throws IOException, InterruptedException
     {
         final int exitValue = 1;
-        StatusFlag expectedStatus = StatusFlag.FATAL_ERROR;
+        StatusFlag expectedStatus = StatusFlag.ERROR;
 
         testRsyncFailure(exitValue, expectedStatus);
     }
@@ -191,7 +191,7 @@ public final class RsyncCopierTest
         final RsyncCopier copier = new RsyncCopier(buggyRsyncBinary, null, false, false);
         final Status status = copier.copy(sourceFile, destinationDirectory);
         assertEquals(expectedStatus, status.getFlag());
-        assertEquals(RsyncExitValueTranslator.getMessage(exitValue), status.getMessage());
+        assertEquals(RsyncExitValueTranslator.getMessage(exitValue), status.tryGetErrorMessage());
     }
 
     @Test(groups =
diff --git a/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/RetryingPathRemover.java b/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/RetryingPathRemover.java
index e3ae9b46dea..9e216548ea3 100644
--- a/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/RetryingPathRemover.java
+++ b/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/RetryingPathRemover.java
@@ -21,7 +21,6 @@ import java.io.File;
 import org.apache.log4j.Logger;
 
 import ch.systemsx.cisd.common.exceptions.Status;
-import ch.systemsx.cisd.common.exceptions.StatusFlag;
 import ch.systemsx.cisd.common.logging.LogCategory;
 import ch.systemsx.cisd.common.logging.LogFactory;
 import ch.systemsx.cisd.common.utilities.FileUtilities;
@@ -53,7 +52,7 @@ final class RetryingPathRemover implements IPathRemover
     }
 
     private final static Status STATUS_FAILED_DELETION =
-            new Status(StatusFlag.FATAL_ERROR, "Failed to remove path.");
+            Status.createError("Failed to remove path.");
 
     public Status remove(File path)
     {
diff --git a/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/intf/DateStatus.java b/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/intf/DateStatus.java
deleted file mode 100644
index dd5fd15b63a..00000000000
--- a/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/intf/DateStatus.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright 2008 ETH Zuerich, CISD
- *
- * Licensed 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 ch.systemsx.cisd.datamover.filesystem.intf;
-
-import org.apache.commons.lang.builder.ToStringBuilder;
-
-import ch.systemsx.cisd.common.utilities.ModifiedShortPrefixToStringStyle;
-
-/**
- * A class that holds the information about the result of an operation which is a number. There is a
- * way to find out if an error occurred during operation execution, the result is unavailable then.
- * 
- * @author Tomasz Pylak
- */
-public final class DateStatus
-{
-    private final ResultStatus<Long> result;
-
-    private DateStatus(final Long result, final boolean errorOccurred, final String messageOrNull)
-    {
-        this.result = new ResultStatus<Long>(result, errorOccurred, messageOrNull);
-    }
-
-    public static final DateStatus create(final long result)
-    {
-        return new DateStatus(result, false, null);
-    }
-
-    public static final DateStatus createError(final String message)
-    {
-        return new DateStatus(null, true, message);
-    }
-
-    public static final DateStatus createError()
-    {
-        return new DateStatus(null, true, null);
-    }
-
-    /**
-     * can be called only if no error occurred, otherwise it fails.
-     */
-    public final long getResult()
-    {
-        return result.getResult();
-    }
-
-    /** has operation finished with an error? */
-    public final boolean isError()
-    {
-        return result.isError();
-    }
-
-    public final String tryGetMessage()
-    {
-        return result.tryGetMessage();
-    }
-
-    //
-    // Object
-    //
-
-    @Override
-    public final String toString()
-    {
-        final ToStringBuilder builder =
-                new ToStringBuilder(this,
-                        ModifiedShortPrefixToStringStyle.MODIFIED_SHORT_PREFIX_STYLE);
-        if (isError())
-        {
-            builder.append("[DateStatus, error: ", tryGetMessage() + "]");
-        } else
-        {
-            final Long thisResult = result.getResult();
-            builder.append("[DateStatus, result: ", String.format("%1$tF %1$tT", thisResult) + "]");
-        }
-        return builder.toString();
-    }
-}
diff --git a/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/intf/IFileStore.java b/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/intf/IFileStore.java
index 9dca9a01215..15d6525622b 100644
--- a/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/intf/IFileStore.java
+++ b/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/intf/IFileStore.java
@@ -17,6 +17,7 @@
 package ch.systemsx.cisd.datamover.filesystem.intf;
 
 import ch.systemsx.cisd.common.exceptions.Status;
+import ch.systemsx.cisd.common.exceptions.StatusWithResult;
 import ch.systemsx.cisd.common.highwatermark.HighwaterMarkWatcher;
 import ch.systemsx.cisd.common.logging.ISimpleLogger;
 import ch.systemsx.cisd.common.utilities.ISelfTestable;
@@ -71,7 +72,7 @@ public interface IFileStore extends ISelfTestable
      * @return The time (in milliseconds since the start of the epoch) when <var>resource</var> was
      *         last changed or error status if checking failed.
      */
-    public DateStatus lastChanged(StoreItem item, long stopWhenFindYounger);
+    public StatusWithResult<Long> lastChanged(StoreItem item, long stopWhenFindYounger);
 
     /**
      * Returns the last time when there was a write access to <var>item</var>.
@@ -83,7 +84,7 @@ public interface IFileStore extends ISelfTestable
      * @return The time (in milliseconds since the start of the epoch) when <var>resource</var> was
      *         last changed or error status if checking failed.
      */
-    public DateStatus lastChangedRelative(StoreItem item, long stopWhenFindYoungerRelative);
+    public StatusWithResult<Long> lastChangedRelative(StoreItem item, long stopWhenFindYoungerRelative);
 
     /**
      * List files in the scanned store. Sort in order of "oldest first".
diff --git a/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/remote/RemotePathMover.java b/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/remote/RemotePathMover.java
index 54c71efe40d..5447963bbff 100644
--- a/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/remote/RemotePathMover.java
+++ b/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/remote/RemotePathMover.java
@@ -368,7 +368,7 @@ public final class RemotePathMover implements IStoreHandler
             {
                 operationLog.warn(String.format(COPYING_PATH_TO_REMOTE_FAILED, getSrcPath(item),
                         destinationDirectory, copyStatus));
-                if (StatusFlag.FATAL_ERROR.equals(copyStatus.getFlag()))
+                if (StatusFlag.ERROR.equals(copyStatus.getFlag()))
                 {
                     break;
                 }
diff --git a/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/remote/RemoteStoreCopyActivitySensor.java b/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/remote/RemoteStoreCopyActivitySensor.java
index a65c768d18e..9945474900f 100644
--- a/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/remote/RemoteStoreCopyActivitySensor.java
+++ b/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/remote/RemoteStoreCopyActivitySensor.java
@@ -16,15 +16,10 @@
 
 package ch.systemsx.cisd.datamover.filesystem.remote;
 
-import org.apache.commons.lang.StringUtils;
-import org.apache.commons.lang.time.DurationFormatUtils;
-import org.apache.log4j.Logger;
-
 import ch.systemsx.cisd.common.concurrent.InactivityMonitor.IActivitySensor;
-import ch.systemsx.cisd.common.logging.LogCategory;
-import ch.systemsx.cisd.common.logging.LogFactory;
+import ch.systemsx.cisd.common.exceptions.StatusWithResult;
+import ch.systemsx.cisd.common.utilities.AbstractCopyActivitySensor;
 import ch.systemsx.cisd.common.utilities.StoreItem;
-import ch.systemsx.cisd.datamover.filesystem.intf.DateStatus;
 import ch.systemsx.cisd.datamover.filesystem.intf.IFileStore;
 
 /**
@@ -33,111 +28,37 @@ import ch.systemsx.cisd.datamover.filesystem.intf.IFileStore;
  * 
  * @author Bernd Rinn
  */
-public class RemoteStoreCopyActivitySensor implements IActivitySensor
+public class RemoteStoreCopyActivitySensor extends AbstractCopyActivitySensor
 {
-    private static final Logger machineLog =
-            LogFactory.getLogger(LogCategory.MACHINE, RemoteStoreCopyActivitySensor.class);
-
-    private static final int MAX_ERROR_COUNT = 1;
-
-    private final int maxErrorsToIgnore;
-
     private final IFileStore destinationStore;
 
     private final StoreItem copyItem;
 
-    private final long timeOfCreation = System.currentTimeMillis();
-
-    private long timeOfLastConfirmedActivity = timeOfCreation;
-
-    private long timeOfLastReportedActivity = timeOfCreation;
-
-    private long lastNonErrorResult = -1L;
-
-    private DateStatus currentResult;
-
-    private int errorCount = 0;
-
     public RemoteStoreCopyActivitySensor(IFileStore destinationStore, StoreItem copyItem)
     {
-        this(destinationStore, copyItem, MAX_ERROR_COUNT);
+        super();
+        this.destinationStore = destinationStore;
+        this.copyItem = copyItem;
     }
 
     public RemoteStoreCopyActivitySensor(IFileStore destinationStore, StoreItem copyItem,
             int maxErrorsToIgnore)
     {
+        super(maxErrorsToIgnore);
         this.destinationStore = destinationStore;
         this.copyItem = copyItem;
-        this.maxErrorsToIgnore = maxErrorsToIgnore;
-        this.currentResult =
-                DateStatus.createError(String.format(
-                        "Last activity on item '%s' of store '%s' never checked", copyItem,
-                        destinationStore));
     }
 
-    public long getTimeOfLastActivityMoreRecentThan(long thresholdMillis)
+    @Override
+    protected StatusWithResult<Long> getTargetTimeOfLastActivityMoreRecentThan(long thresholdMillis)
     {
-        currentResult = destinationStore.lastChangedRelative(copyItem, thresholdMillis);
-        final long now = System.currentTimeMillis();
-        if (currentResult.isError())
-        {
-            ++errorCount;
-            if (errorCount <= maxErrorsToIgnore)
-            {
-                timeOfLastReportedActivity = now;
-                machineLog.error(describeInactivity(now)
-                        + String.format(" (error count: %d <= %d, goes unreported)", errorCount,
-                                maxErrorsToIgnore));
-            } else
-            {
-                machineLog.error(describeInactivity(now)
-                        + " (error count: %s, reported to monitor)");
-            }
-        } else
-        {
-            if (currentResult.getResult() != lastNonErrorResult)
-            {
-                timeOfLastConfirmedActivity = now;
-                lastNonErrorResult = currentResult.getResult();
-                if (machineLog.isDebugEnabled())
-                {
-                    machineLog.debug(String.format(
-                            "Observing write activity on item '%s' in store '%s'", copyItem,
-                            destinationStore));
-                }
-            }
-            // Implementation note: This means we can report an older time of activity than what we
-            // reported the last time if the last time we had an error. This is on purpose as it
-            // helps avoiding a situation where error and non-error situations do "flip-flop" and we
-            // could report progress where there is no progress.
-            timeOfLastReportedActivity = timeOfLastConfirmedActivity;
-            errorCount = 0;
-        }
-
-        return timeOfLastReportedActivity;
+        return destinationStore.lastChangedRelative(copyItem, thresholdMillis);
     }
 
-    public String describeInactivity(long now)
+    @Override
+    protected String getTargetDescription()
     {
-        if (currentResult.isError())
-        {
-            final String msg = currentResult.tryGetMessage();
-            if (StringUtils.isBlank(msg))
-            {
-                return String.format("Error: Unable to determine the time of write activity "
-                        + "on item '%s' in store '%s'", copyItem, destinationStore);
-            } else
-            {
-                return String.format("Error [%s]: Unable to determine the time of write activity "
-                        + "on item '%s' in store '%s'", msg, copyItem, destinationStore);
-            }
-        } else
-        {
-            final String inactivityPeriod =
-                    DurationFormatUtils.formatDurationHMS(now - timeOfLastConfirmedActivity);
-            return String.format("No write activity on item '%s' in store '%s' for %s", copyItem,
-                    destinationStore, inactivityPeriod);
-        }
+        return String.format("item '%s' in store '%s'", copyItem, destinationStore);
     }
 
 }
diff --git a/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/store/FileStoreLocal.java b/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/store/FileStoreLocal.java
index 24f78d224b2..f17cebee3a2 100644
--- a/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/store/FileStoreLocal.java
+++ b/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/store/FileStoreLocal.java
@@ -24,6 +24,7 @@ import org.apache.commons.lang.time.DateUtils;
 import org.apache.log4j.Logger;
 
 import ch.systemsx.cisd.common.exceptions.Status;
+import ch.systemsx.cisd.common.exceptions.StatusWithResult;
 import ch.systemsx.cisd.common.exceptions.UnknownLastChangedException;
 import ch.systemsx.cisd.common.highwatermark.HighwaterMarkWatcher;
 import ch.systemsx.cisd.common.highwatermark.HostAwareFileWithHighwaterMark;
@@ -41,7 +42,6 @@ import ch.systemsx.cisd.datamover.filesystem.intf.IFileSysOperationsFactory;
 import ch.systemsx.cisd.datamover.filesystem.intf.IPathMover;
 import ch.systemsx.cisd.datamover.filesystem.intf.IPathRemover;
 import ch.systemsx.cisd.datamover.filesystem.intf.IStoreCopier;
-import ch.systemsx.cisd.datamover.filesystem.intf.DateStatus;
 
 /**
  * An {@link IFileStore} implementation for local stores.
@@ -88,20 +88,20 @@ public class FileStoreLocal extends AbstractFileStore implements IExtendedFileSt
         return BooleanStatus.createFromBoolean(exists);
     }
 
-    public final DateStatus lastChanged(final StoreItem item, final long stopWhenFindYounger)
+    public final StatusWithResult<Long> lastChanged(final StoreItem item, final long stopWhenFindYounger)
     {
         try
         {
             long lastChanged =
                     FileUtilities.lastChanged(getChildFile(item), true, stopWhenFindYounger);
-            return DateStatus.create(lastChanged);
+            return StatusWithResult.<Long>create(lastChanged);
         } catch (UnknownLastChangedException ex)
         {
             return createLastChangedError(item, ex);
         }
     }
 
-    public final DateStatus lastChangedRelative(final StoreItem item,
+    public final StatusWithResult<Long> lastChangedRelative(final StoreItem item,
             final long stopWhenFindYoungerRelative)
     {
         try
@@ -109,20 +109,20 @@ public class FileStoreLocal extends AbstractFileStore implements IExtendedFileSt
             long lastChanged =
                     FileUtilities.lastChangedRelative(getChildFile(item), true,
                             stopWhenFindYoungerRelative);
-            return DateStatus.create(lastChanged);
+            return StatusWithResult.<Long>create(lastChanged);
         } catch (UnknownLastChangedException ex)
         {
             return createLastChangedError(item, ex);
         }
     }
 
-    private static DateStatus createLastChangedError(final StoreItem item,
+    private static StatusWithResult<Long> createLastChangedError(final StoreItem item,
             UnknownLastChangedException ex)
     {
         String errorMsg =
                 String.format("Could not determine \"last changed time\" of %s: %s", item, ex
                         .getCause());
-        return DateStatus.createError(errorMsg);
+        return StatusWithResult.<Long>createError(errorMsg);
     }
 
     public final BooleanStatus tryCheckDirectoryFullyAccessible(final long timeOutMillis)
diff --git a/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/store/FileStoreRemote.java b/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/store/FileStoreRemote.java
index eccd25b7370..8991bad427c 100644
--- a/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/store/FileStoreRemote.java
+++ b/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/store/FileStoreRemote.java
@@ -26,7 +26,7 @@ import org.apache.log4j.Logger;
 import ch.rinn.restrictions.Private;
 import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException;
 import ch.systemsx.cisd.common.exceptions.Status;
-import ch.systemsx.cisd.common.exceptions.StatusFlag;
+import ch.systemsx.cisd.common.exceptions.StatusWithResult;
 import ch.systemsx.cisd.common.highwatermark.HighwaterMarkWatcher;
 import ch.systemsx.cisd.common.highwatermark.HostAwareFileWithHighwaterMark;
 import ch.systemsx.cisd.common.highwatermark.HighwaterMarkWatcher.IFreeSpaceProvider;
@@ -43,7 +43,6 @@ import ch.systemsx.cisd.datamover.filesystem.intf.IExtendedFileStore;
 import ch.systemsx.cisd.datamover.filesystem.intf.IFileStore;
 import ch.systemsx.cisd.datamover.filesystem.intf.IFileSysOperationsFactory;
 import ch.systemsx.cisd.datamover.filesystem.intf.IStoreCopier;
-import ch.systemsx.cisd.datamover.filesystem.intf.DateStatus;
 
 /**
  * @author Tomasz Pylak
@@ -200,7 +199,7 @@ public class FileStoreRemote extends AbstractFileStore
             return Status.OK;
         } else
         {
-            return new Status(StatusFlag.RETRIABLE_ERROR, errMsg);
+            return Status.createRetriableError(errMsg);
         }
     }
 
@@ -230,12 +229,12 @@ public class FileStoreRemote extends AbstractFileStore
         return constructStoreCopier(destinationDirectory, requiresDeletion);
     }
 
-    public final DateStatus lastChanged(final StoreItem item, final long stopWhenFindYounger)
+    public final StatusWithResult<Long> lastChanged(final StoreItem item, final long stopWhenFindYounger)
     {
         return lastChanged(item);
     }
 
-    private final DateStatus lastChanged(final StoreItem item)
+    private final StatusWithResult<Long> lastChanged(final StoreItem item)
     {
         final String itemPath = StoreItem.asFile(getPath(), item).getPath();
 
@@ -249,7 +248,7 @@ public class FileStoreRemote extends AbstractFileStore
             try
             {
                 long lastChanged = Long.parseLong(resultLine) * 1000;
-                return DateStatus.create(lastChanged);
+                return StatusWithResult.<Long> create(lastChanged);
             } catch (final NumberFormatException e)
             {
                 return createLastChangeError(item, "The result of " + cmd + " on remote host "
@@ -261,10 +260,10 @@ public class FileStoreRemote extends AbstractFileStore
         }
     }
 
-    private static DateStatus createLastChangeError(StoreItem item, String errorMsg)
+    private static StatusWithResult<Long> createLastChangeError(StoreItem item, String errorMsg)
     {
-        return DateStatus.createError("Cannot obtain last change time of the item " + item
-                + ". Reason: " + errorMsg);
+        return StatusWithResult.<Long> createError("Cannot obtain last change time of the item "
+                + item + ". Reason: " + errorMsg);
     }
 
     private String getRemoteFindExecutableOrDie()
@@ -282,7 +281,7 @@ public class FileStoreRemote extends AbstractFileStore
         }
     }
 
-    public final DateStatus lastChangedRelative(final StoreItem item,
+    public final StatusWithResult<Long> lastChangedRelative(final StoreItem item,
             final long stopWhenFindYoungerRelative)
     {
         return lastChanged(item);
diff --git a/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/store/FileStoreRemoteMounted.java b/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/store/FileStoreRemoteMounted.java
index 6d6f98c118b..2340c5da751 100644
--- a/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/store/FileStoreRemoteMounted.java
+++ b/datamover/source/java/ch/systemsx/cisd/datamover/filesystem/store/FileStoreRemoteMounted.java
@@ -20,7 +20,7 @@ import ch.systemsx.cisd.common.concurrent.MonitoringProxy;
 import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException;
 import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException;
 import ch.systemsx.cisd.common.exceptions.Status;
-import ch.systemsx.cisd.common.exceptions.StatusFlag;
+import ch.systemsx.cisd.common.exceptions.StatusWithResult;
 import ch.systemsx.cisd.common.highwatermark.HighwaterMarkWatcher;
 import ch.systemsx.cisd.common.highwatermark.HostAwareFileWithHighwaterMark;
 import ch.systemsx.cisd.common.logging.ISimpleLogger;
@@ -28,7 +28,6 @@ import ch.systemsx.cisd.common.logging.LogLevel;
 import ch.systemsx.cisd.common.utilities.StoreItem;
 import ch.systemsx.cisd.datamover.filesystem.intf.AbstractFileStore;
 import ch.systemsx.cisd.datamover.filesystem.intf.BooleanStatus;
-import ch.systemsx.cisd.datamover.filesystem.intf.DateStatus;
 import ch.systemsx.cisd.datamover.filesystem.intf.IExtendedFileStore;
 import ch.systemsx.cisd.datamover.filesystem.intf.IFileStore;
 import ch.systemsx.cisd.datamover.filesystem.intf.IFileSysOperationsFactory;
@@ -88,8 +87,7 @@ public final class FileStoreRemoteMounted extends AbstractFileStore
         final Status statusOrNull = localImplMonitored.delete(item);
         if (statusOrNull == null)
         {
-            return new Status(StatusFlag.RETRIABLE_ERROR, "Could not delete '" + item
-                    + "': time out.");
+            return Status.createRetriableError("Could not delete '" + item + "': time out.");
         }
         return statusOrNull;
     }
@@ -105,25 +103,25 @@ public final class FileStoreRemoteMounted extends AbstractFileStore
         return statusOrNull;
     }
 
-    public final DateStatus lastChanged(final StoreItem item, final long stopWhenFindYounger)
+    public final StatusWithResult<Long> lastChanged(final StoreItem item, final long stopWhenFindYounger)
     {
-        final DateStatus statusOrNull = localImplMonitored.lastChanged(item, stopWhenFindYounger);
+        final StatusWithResult<Long> statusOrNull = localImplMonitored.lastChanged(item, stopWhenFindYounger);
         if (statusOrNull == null)
         {
-            return DateStatus.createError(String.format(
+            return StatusWithResult.<Long>createError(String.format(
                     "Could not determine \"last changed time\" of %s: time out.", item));
         }
         return statusOrNull;
     }
 
-    public final DateStatus lastChangedRelative(final StoreItem item,
+    public final StatusWithResult<Long> lastChangedRelative(final StoreItem item,
             final long stopWhenFindYoungerRelative)
     {
-        final DateStatus statusOrNull =
+        final StatusWithResult<Long> statusOrNull =
                 localImplMonitored.lastChangedRelative(item, stopWhenFindYoungerRelative);
         if (statusOrNull == null)
         {
-            return DateStatus.createError(String.format(
+            return StatusWithResult.<Long>createError(String.format(
                     "Could not determine \"last changed time\" of %s: time out.", item));
         }
         return statusOrNull;
diff --git a/datamover/source/java/ch/systemsx/cisd/datamover/utils/QuietPeriodFileFilter.java b/datamover/source/java/ch/systemsx/cisd/datamover/utils/QuietPeriodFileFilter.java
index bdca11f031b..d4b0c8909f2 100644
--- a/datamover/source/java/ch/systemsx/cisd/datamover/utils/QuietPeriodFileFilter.java
+++ b/datamover/source/java/ch/systemsx/cisd/datamover/utils/QuietPeriodFileFilter.java
@@ -25,13 +25,13 @@ import org.apache.commons.lang.StringUtils;
 import org.apache.log4j.Logger;
 
 import ch.rinn.restrictions.Private;
+import ch.systemsx.cisd.common.exceptions.StatusWithResult;
 import ch.systemsx.cisd.common.logging.LogCategory;
 import ch.systemsx.cisd.common.logging.LogFactory;
 import ch.systemsx.cisd.common.utilities.ITimeProvider;
 import ch.systemsx.cisd.common.utilities.StoreItem;
 import ch.systemsx.cisd.common.utilities.SystemTimeProvider;
 import ch.systemsx.cisd.datamover.filesystem.intf.IFileStore;
-import ch.systemsx.cisd.datamover.filesystem.intf.DateStatus;
 import ch.systemsx.cisd.datamover.intf.ITimingParameters;
 
 /**
@@ -159,12 +159,12 @@ public class QuietPeriodFileFilter implements IStoreItemFilter
                 return false;
             }
             final long oldLastChanged = checkRecordOrNull.getTimeOfLastModification();
-            DateStatus newStatus = fileStore.lastChanged(item, oldLastChanged);
+            final StatusWithResult<Long> newStatus = fileStore.lastChanged(item, oldLastChanged);
             if (newStatus.isError())
             {
                 return false;
             }
-            final long newLastChanged = newStatus.getResult();
+            final long newLastChanged = newStatus.tryGetResult();
             if (newLastChanged != oldLastChanged)
             {
                 pathMap.put(item, new PathCheckRecord(now, newLastChanged));
@@ -198,10 +198,10 @@ public class QuietPeriodFileFilter implements IStoreItemFilter
 
     private void saveFirstModificationTime(final StoreItem item, final long now)
     {
-        DateStatus status = fileStore.lastChanged(item, 0L);
+        final StatusWithResult<Long> status = fileStore.lastChanged(item, 0L);
         if (status.isError() == false)
         {
-            pathMap.put(item, new PathCheckRecord(now, status.getResult()));
+            pathMap.put(item, new PathCheckRecord(now, status.tryGetResult()));
         }
     }
 
diff --git a/datamover/sourceTest/java/ch/systemsx/cisd/datamover/filesystem/remote/RemoteStoreCopyActivitySensorTest.java b/datamover/sourceTest/java/ch/systemsx/cisd/datamover/filesystem/remote/RemoteStoreCopyActivitySensorTest.java
index 102f48d3d9f..624bab75a16 100644
--- a/datamover/sourceTest/java/ch/systemsx/cisd/datamover/filesystem/remote/RemoteStoreCopyActivitySensorTest.java
+++ b/datamover/sourceTest/java/ch/systemsx/cisd/datamover/filesystem/remote/RemoteStoreCopyActivitySensorTest.java
@@ -26,9 +26,9 @@ import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 import ch.systemsx.cisd.common.concurrent.ConcurrencyUtilities;
+import ch.systemsx.cisd.common.exceptions.StatusWithResult;
 import ch.systemsx.cisd.common.logging.LogInitializer;
 import ch.systemsx.cisd.common.utilities.StoreItem;
-import ch.systemsx.cisd.datamover.filesystem.intf.DateStatus;
 import ch.systemsx.cisd.datamover.filesystem.intf.IFileStore;
 
 /**
@@ -45,6 +45,8 @@ public class RemoteStoreCopyActivitySensorTest
 
     private static final long MAX_DELTA = 5L;
 
+    private static final int MAX_ERRORS_TO_IGNORE = 1;
+
     private Mockery context;
 
     private IFileStore destinationStore;
@@ -65,7 +67,8 @@ public class RemoteStoreCopyActivitySensorTest
         context = new Mockery();
         destinationStore = context.mock(IFileStore.class);
         copyItem = new StoreItem(ITEM_NAME);
-        sensorUnderTest = new RemoteStoreCopyActivitySensor(destinationStore, copyItem);
+        sensorUnderTest =
+                new RemoteStoreCopyActivitySensor(destinationStore, copyItem, MAX_ERRORS_TO_IGNORE);
     }
 
     @AfterMethod
@@ -84,7 +87,7 @@ public class RemoteStoreCopyActivitySensorTest
             {
                 {
                     one(destinationStore).lastChangedRelative(copyItem, THRESHOLD);
-                    will(returnValue(DateStatus.create(lastChanged)));
+                    will(returnValue(StatusWithResult.<Long> create(lastChanged)));
                 }
             });
         final long delta =
@@ -106,13 +109,13 @@ public class RemoteStoreCopyActivitySensorTest
             {
                 {
                     one(destinationStore).lastChangedRelative(copyItem, THRESHOLD);
-                    will(returnValue(DateStatus.createError(errorMsg)));
+                    will(returnValue(StatusWithResult.<Long> createError(errorMsg)));
                 }
             });
         final long now = System.currentTimeMillis();
         final long delta = now - sensorUnderTest.getTimeOfLastActivityMoreRecentThan(THRESHOLD);
         assertTrue("Delta is " + delta, delta < MAX_DELTA);
-        assertEquals("Error [ERROR message]: Unable to determine the time of write activity on "
+        assertEquals("Error: Unable to determine the time of write activity on "
                 + "item 'I am probed' in store 'iFileStore'", sensorUnderTest
                 .describeInactivity(now));
     }
@@ -125,19 +128,20 @@ public class RemoteStoreCopyActivitySensorTest
             {
                 {
                     exactly(3).of(destinationStore).lastChangedRelative(copyItem, THRESHOLD);
-                    will(returnValue(DateStatus.createError(errorMsg)));
+                    will(returnValue(StatusWithResult.<Long> createError(errorMsg)));
                 }
             });
         ConcurrencyUtilities.sleep(10L);
         final long now = System.currentTimeMillis();
         final long lastActivity1 = sensorUnderTest.getTimeOfLastActivityMoreRecentThan(THRESHOLD);
-        assertEquals(now, lastActivity1);
+        final long delta = lastActivity1 - now;
+        assertTrue("Delta is " + delta, delta < MAX_DELTA);
         ConcurrencyUtilities.sleep(10L);
         final long lastActivity2 = sensorUnderTest.getTimeOfLastActivityMoreRecentThan(THRESHOLD);
-        assertEquals(now, lastActivity2);
+        assertEquals(lastActivity1, lastActivity2);
         ConcurrencyUtilities.sleep(10L);
         final long lastActivity3 = sensorUnderTest.getTimeOfLastActivityMoreRecentThan(THRESHOLD);
-        assertEquals(now, lastActivity3);
+        assertEquals(lastActivity1, lastActivity3);
     }
 
     @Test
@@ -148,9 +152,9 @@ public class RemoteStoreCopyActivitySensorTest
             {
                 {
                     exactly(3).of(destinationStore).lastChangedRelative(copyItem, THRESHOLD);
-                    will(onConsecutiveCalls(returnValue(DateStatus.create(17L)),
-                            returnValue(DateStatus.createError(errorMsg)), returnValue(DateStatus
-                                    .create(17L))));
+                    will(onConsecutiveCalls(returnValue(StatusWithResult.<Long> create(17L)),
+                            returnValue(StatusWithResult.<Long> createError(errorMsg)),
+                            returnValue(StatusWithResult.<Long> create(17L))));
                 }
             });
         ConcurrencyUtilities.sleep(10L);
diff --git a/datamover/sourceTest/java/ch/systemsx/cisd/datamover/utils/QuietPeriodFileFilterTest.java b/datamover/sourceTest/java/ch/systemsx/cisd/datamover/utils/QuietPeriodFileFilterTest.java
index 541650a17da..76420bb7f99 100644
--- a/datamover/sourceTest/java/ch/systemsx/cisd/datamover/utils/QuietPeriodFileFilterTest.java
+++ b/datamover/sourceTest/java/ch/systemsx/cisd/datamover/utils/QuietPeriodFileFilterTest.java
@@ -30,12 +30,12 @@ import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 import ch.rinn.restrictions.Friend;
+import ch.systemsx.cisd.common.exceptions.StatusWithResult;
 import ch.systemsx.cisd.common.logging.LogCategory;
 import ch.systemsx.cisd.common.test.LogMonitoringAppender;
 import ch.systemsx.cisd.common.utilities.ITimeProvider;
 import ch.systemsx.cisd.common.utilities.StoreItem;
 import ch.systemsx.cisd.datamover.filesystem.intf.IFileStore;
-import ch.systemsx.cisd.datamover.filesystem.intf.DateStatus;
 import ch.systemsx.cisd.datamover.intf.ITimingParameters;
 
 /**
@@ -154,7 +154,7 @@ public class QuietPeriodFileFilterTest
                     one(timeProvider).getTimeInMilliseconds();
                     will(returnValue(nowMillis));
                     one(fileStore).lastChanged(ITEM, 0L);
-                    will(returnValue(DateStatus.create(pathLastChangedMillis)));
+                    will(returnValue(StatusWithResult.<Long>create(pathLastChangedMillis)));
                     for (int i = 1; i <= 100; ++i)
                     {
                         one(timeProvider).getTimeInMilliseconds();
@@ -163,7 +163,7 @@ public class QuietPeriodFileFilterTest
                     // last call - will check only when last check is longer ago than the quiet
                     // period
                     one(fileStore).lastChanged(ITEM, pathLastChangedMillis);
-                    will(returnValue(DateStatus.create(pathLastChangedMillis)));
+                    will(returnValue(StatusWithResult.<Long>create(pathLastChangedMillis)));
                 }
             });
         for (int i = 0; i < 100; ++i)
@@ -237,7 +237,7 @@ public class QuietPeriodFileFilterTest
         long now = 0;
         for (int i = 0; i < errorRepetitions; i++)
         {
-            prepareLastChanged(now, 0L, DateStatus.createError());
+            prepareLastChanged(now, 0L, StatusWithResult.<Long>createError());
             now += QUIET_PERIOD_MILLIS;
         }
         for (int i = 0; i < errorRepetitions; i++)
@@ -245,12 +245,12 @@ public class QuietPeriodFileFilterTest
             assertNoAccept();
         }
         // first time we acquire modification time
-        DateStatus lastChange = DateStatus.create(0);
+        final StatusWithResult<Long> lastChange = StatusWithResult.<Long>create(0L);
         prepareLastChanged(now, 0L, lastChange);
         now += QUIET_PERIOD_MILLIS;
         assertNoAccept();
         // error again
-        prepareLastChanged(now, 0L, DateStatus.createError());
+        prepareLastChanged(now, 0L, StatusWithResult.<Long>createError());
         now += QUIET_PERIOD_MILLIS;
         assertNoAccept();
         // second time we acquire modification time - and nothing change during the quite period, so
@@ -274,11 +274,12 @@ public class QuietPeriodFileFilterTest
     private void prepareLastChanged(final long currentTime, final long stopWhenFindYounger,
             final long lastChanged)
     {
-        prepareLastChanged(currentTime, stopWhenFindYounger, DateStatus.create(lastChanged));
+        prepareLastChanged(currentTime, stopWhenFindYounger, StatusWithResult
+                .<Long> create(lastChanged));
     }
 
     private void prepareLastChanged(final long currentTime, final long stopWhenFindYounger,
-            final DateStatus lastChanged)
+            final StatusWithResult<Long> lastChanged)
     {
         context.checking(new Expectations()
             {
@@ -307,13 +308,13 @@ public class QuietPeriodFileFilterTest
                     one(timeProvider).getTimeInMilliseconds();
                     will(returnValue(nowMillis1));
                     allowing(fileStore).lastChanged(vanishingItem, 0L);
-                    will(returnValue(DateStatus.create(pathLastChangedMillis1)));
+                    will(returnValue(StatusWithResult.<Long>create(pathLastChangedMillis1)));
                     // calls to get the required number of calls for clean up
                     allowing(timeProvider).getTimeInMilliseconds();
                     will(returnValue(nowMillis2));
                     allowing(fileStore).lastChanged(with(same(ITEM)),
                             with(greaterThanOrEqualTo(0L)));
-                    will(returnValue(DateStatus.create(pathLastChangedMillis2)));
+                    will(returnValue(StatusWithResult.<Long>create(pathLastChangedMillis2)));
                 }
             });
         final LogMonitoringAppender appender =
-- 
GitLab