You are viewing a plain text version of this content. The canonical link for it is here.
Posted to server-dev@james.apache.org by ro...@apache.org on 2019/10/18 06:40:33 UTC

[james-project] branch master updated (fe0af0b -> a21e5ec)

This is an automated email from the ASF dual-hosted git repository.

rouazana pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git.


    from fe0af0b  JAMES-2813 Add Architecture Decision Record about Distributed Task Manager
     new b483682  JAMES-2813 handle UpdateAdditionalInformation command into the aggregate
     new ad2c55c  JAMES-2813 refactor TaskAggregate to prevent creation of the aggregate if History doesn't start with Created event
     new d74ecf0  JAMES-2813 refactor TaskAggregate to split infrastructure code from logic
     new 5da39ae  JAMES-2813 replace mocks in RabbitMQWorkQueueTest by a stub
     new 604c27d  JAMES-2813 rename fields and methods in RabbitMQWorkQueueTest
     new c58f80a  JAMES-2813 handle additional information in projection
     new e1e3e5d  JAMES-2813 worker now calls Listener.updated once per second
     new 12559bd  JAMES-2813 put executeWithSemaphore building blocks into submethods
     new 1b1ee83  JAMES-2813 wire UpdateAdditionalInformation command in the Event Sourcing system
     new 0f0b834  JAMES-2813 prevent cassandra cleanup to happen before countdown latch release
     new e99bc34  JAMES-2813 run AdditionalInformation polling into an elastic scheduler
     new 769d64e  JAMES-2813 check that AdditionalInformation instances are shared between nodes
     new e1ff8c5  JAMES-2813 change polling intervall and make it a parameter of the worker
     new 139e70c  JAMES-2813 add a timestamp on every AdditionalInformation
     new eb5f1be  JAMES-2813 delete rabbitmq queue after each test on distributed task manager
     new 1da890c  JAMES-2813 use timestamp in additional information to refuse stalled update event
     new a21e5ec  JAMES-2813 use immutable datastructure when building history in TaskAggregateTest

The 17 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../cassandra/migration/MigrationTask.java         |  13 +-
 .../MigrationTaskAdditionalInformationsDTO.java    |  16 +-
 .../migration/MigrationTaskSerializationTest.java  |   8 +-
 .../james/backends/rabbitmq/RabbitMQExtension.java |   4 +-
 .../apache/james/json/JsonGenericSerializer.java   |   2 +
 .../mail/migration/MailboxPathV2Migration.java     |  13 +-
 ...athV2MigrationTaskAdditionalInformationDTO.java |  18 ++-
 .../cassandra/mail/task/MailboxMergingTask.java    |  14 +-
 ...MailboxMergingTaskAdditionalInformationDTO.java |  18 ++-
 ...ailboxPathV2MigrationTaskSerializationTest.java |   6 +-
 .../mail/task/MailboxMergingTaskTest.java          |   6 +-
 .../blob/BlobStoreVaultGarbageCollectionTask.java  |  13 +-
 ...bageCollectionTaskAdditionalInformationDTO.java |  17 ++-
 ...aultGarbageCollectionTaskSerializationTest.java |   6 +-
 .../tools/indexer/MessageIdReIndexingTask.java     |  13 +-
 ...geIdReindexingTaskAdditionalInformationDTO.java |  22 ++-
 .../indexer/ReprocessingContextInformation.java    |  25 +++-
 .../indexer/ReprocessingContextInformationDTO.java |  40 +++--
 .../tools/indexer/SingleMailboxReindexingTask.java |   9 +-
 ...lboxReindexingTaskAdditionalInformationDTO.java |  23 ++-
 .../tools/indexer/SingleMessageReindexingTask.java |  13 +-
 ...sageReindexingTaskAdditionalInformationDTO.java |  25 +++-
 .../mailbox/tools/indexer/UserReindexingTask.java  |   9 +-
 ...UserReindexingTaskAdditionalInformationDTO.java |  22 ++-
 ...rorRecoveryIndexationTaskSerializationTest.java |   8 +-
 .../FullReindexingTaskSerializationTest.java       |   9 +-
 .../MessageIdReindexingTaskSerializationTest.java  |   9 +-
 ...ngleMailboxReindexingTaskSerializationTest.java |   9 +-
 ...ngleMessageReindexingTaskSerializationTest.java |   9 +-
 .../UserReindexingTaskSerializationTest.java       |   9 +-
 .../james/modules/TaskSerializationModule.java     |   4 +-
 .../migration/MappingsSourcesMigration.java        |  14 +-
 ...urcesMigrationTaskAdditionalInformationDTO.java |  25 +++-
 ...pingsSourcesMigrationTaskSerializationTest.java |   6 +-
 .../routes/DeletedMessagesVaultDeleteTask.java     |  13 +-
 ...gesVaultDeleteTaskAdditionalInformationDTO.java |  16 +-
 .../routes/DeletedMessagesVaultExportTask.java     |  14 +-
 ...gesVaultExportTaskAdditionalInformationDTO.java |  18 ++-
 .../routes/DeletedMessagesVaultRestoreTask.java    |  13 +-
 ...esVaultRestoreTaskAdditionalInformationDTO.java |  18 ++-
 ...edMessagesVaultDeleteTaskSerializationTest.java |   9 +-
 ...edMessagesVaultExportTaskSerializationTest.java |   7 +-
 ...dMessagesVaultRestoreTaskSerializationTest.java |   7 +-
 .../service/EventDeadLettersRedeliverAllTask.java  |   4 +-
 .../EventDeadLettersRedeliverGroupTask.java        |   4 +-
 .../service/EventDeadLettersRedeliverOneTask.java  |   7 +-
 ...LettersRedeliveryTaskAdditionalInformation.java |  15 +-
 ...tersRedeliveryTaskAdditionalInformationDTO.java |  40 +++--
 .../service/EventDeadLettersRedeliverTaskTest.java |  14 +-
 .../james/webadmin/service/ClearMailQueueTask.java |  13 +-
 ...ClearMailQueueTaskAdditionalInformationDTO.java |  18 ++-
 .../service/DeleteMailsFromMailQueueTask.java      |  13 +-
 ...sFromMailQueueTaskAdditionalInformationDTO.java |  17 ++-
 .../webadmin/service/ClearMailQueueTaskTest.java   |   8 +-
 .../service/DeleteMailsFromMailQueueTaskTest.java  |  12 +-
 .../webadmin/routes/MailRepositoriesRoutes.java    |   3 +-
 .../webadmin/service/ClearMailRepositoryTask.java  |  13 +-
 ...MailRepositoryTaskAdditionalInformationDTO.java |  17 ++-
 .../webadmin/service/ReprocessingAllMailsTask.java |  13 +-
 ...essingAllMailsTaskAdditionalInformationDTO.java |  16 +-
 .../webadmin/service/ReprocessingOneMailTask.java  |  19 ++-
 ...cessingOneMailTaskAdditionalInformationDTO.java |  16 +-
 .../service/ReprocessingOneMailTaskDTO.java        |  10 +-
 .../service/ClearMailRepositoryTaskTest.java       |  10 +-
 .../service/ReprocessingAllMailsTaskTest.java      |  11 +-
 .../service/ReprocessingOneMailTaskTest.java       |  22 ++-
 .../apache/james/task/TaskExecutionDetails.scala   |  25 +++-
 .../james/task/MemoryReferenceWithCounterTask.java |  27 +++-
 .../org/apache/james/task/TaskManagerContract.java |  41 +++++-
 .../distributed/RabbitMQWorkQueue.java             |   1 +
 .../distributed/TasksSerializationModule.java      |  11 +-
 .../distributed/RabbitMQWorkQueueSupplier.scala    |  13 +-
 .../eventsourcing/distributed/TaskEventDTO.scala   |  19 +++
 .../distributed/DistributedTaskManagerTest.java    |   7 +-
 .../distributed/RabbitMQWorkQueueTest.java         | 125 +++++++++-------
 .../distributed/TaskEventsSerializationTest.java   |  12 +-
 .../task/json/dto/AdditionalInformationDTO.java    |   4 +
 ...nceWithCounterTaskAdditionalInformationDTO.java |  17 ++-
 .../org/apache/james/task/MemoryTaskManager.java   |  10 +-
 .../apache/james/task/SerialTaskManagerWorker.java |  39 +++--
 .../org/apache/james/task/TaskManagerWorker.java   |   2 +
 .../james/task/eventsourcing/CommandHandlers.scala |  10 +-
 .../task/eventsourcing/DecisionProjection.scala    |  33 +++--
 .../eventsourcing/EventSourcingTaskManager.scala   |   3 +-
 .../apache/james/task/eventsourcing/Events.scala   |   2 +
 .../james/task/eventsourcing/TaskAggregate.scala   | 117 ++++++++-------
 .../james/task/eventsourcing/TaskCommand.scala     |   2 +
 .../TaskExecutionDetailsProjection.scala           |   2 +
 .../task/eventsourcing/WorkerStatusListener.scala  |   6 +-
 .../james/task/SerialTaskManagerWorkerTest.java    |  52 ++++++-
 .../EventSourcingTaskManagerTest.java              |   3 +-
 .../task/eventsourcing/TaskAggregateTest.java      | 164 +++++++++++++++++++++
 .../TaskExecutionDetailsProjectionContract.java    |   1 -
 .../james/task/TaskExecutionDetailsFixture.scala   |   8 +-
 94 files changed, 1270 insertions(+), 373 deletions(-)
 create mode 100644 server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/TaskAggregateTest.java


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[james-project] 15/17: JAMES-2813 delete rabbitmq queue after each test on distributed task manager

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rouazana pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit eb5f1be73c0806d7298315b4c247a45e2484463f
Author: Rémi KOWALSKI <rk...@linagora.com>
AuthorDate: Tue Oct 15 16:17:26 2019 +0200

    JAMES-2813 delete rabbitmq queue after each test on distributed task manager
---
 .../java/org/apache/james/backends/rabbitmq/RabbitMQExtension.java    | 4 ++--
 .../james/task/eventsourcing/distributed/RabbitMQWorkQueue.java       | 1 +
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/backends-common/rabbitmq/src/test/java/org/apache/james/backends/rabbitmq/RabbitMQExtension.java b/backends-common/rabbitmq/src/test/java/org/apache/james/backends/rabbitmq/RabbitMQExtension.java
index 577be12..668b808 100644
--- a/backends-common/rabbitmq/src/test/java/org/apache/james/backends/rabbitmq/RabbitMQExtension.java
+++ b/backends-common/rabbitmq/src/test/java/org/apache/james/backends/rabbitmq/RabbitMQExtension.java
@@ -118,10 +118,10 @@ public class RabbitMQExtension implements BeforeAllCallback, BeforeEachCallback,
     }
 
     @Override
-    public void afterEach(ExtensionContext context) {
+    public void afterEach(ExtensionContext context) throws Exception {
         simpleChannelPool.close();
         connectionPool.close();
-
+        rabbitMQ.reset();
         dockerRestartPolicy.afterEach(rabbitMQ);
     }
 
diff --git a/server/task/task-distributed/src/main/java/org/apache/james/task/eventsourcing/distributed/RabbitMQWorkQueue.java b/server/task/task-distributed/src/main/java/org/apache/james/task/eventsourcing/distributed/RabbitMQWorkQueue.java
index e736798..21a9880 100644
--- a/server/task/task-distributed/src/main/java/org/apache/james/task/eventsourcing/distributed/RabbitMQWorkQueue.java
+++ b/server/task/task-distributed/src/main/java/org/apache/james/task/eventsourcing/distributed/RabbitMQWorkQueue.java
@@ -203,6 +203,7 @@ public class RabbitMQWorkQueue implements WorkQueue, Startable {
     public void close() {
         Optional.ofNullable(receiverHandle).ifPresent(Disposable::dispose);
         Optional.ofNullable(receiver).ifPresent(RabbitMQExclusiveConsumer::close);
+        Optional.ofNullable(sender).ifPresent(Sender::close);
         Optional.ofNullable(sendCancelRequestsQueueHandle).ifPresent(Disposable::dispose);
         Optional.ofNullable(cancelRequestListenerHandle).ifPresent(Disposable::dispose);
         Optional.ofNullable(sender).ifPresent(Sender::close);


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[james-project] 14/17: JAMES-2813 add a timestamp on every AdditionalInformation

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rouazana pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 139e70c0a7569369616c2bf403c7c112aeb89320
Author: Rémi KOWALSKI <rk...@linagora.com>
AuthorDate: Thu Oct 17 10:56:42 2019 +0200

    JAMES-2813 add a timestamp on every AdditionalInformation
---
 .../cassandra/migration/MigrationTask.java         | 13 +++++--
 .../MigrationTaskAdditionalInformationsDTO.java    | 16 +++++++--
 .../migration/MigrationTaskSerializationTest.java  |  8 +++--
 .../apache/james/json/JsonGenericSerializer.java   |  2 ++
 .../mail/migration/MailboxPathV2Migration.java     | 13 +++++--
 ...athV2MigrationTaskAdditionalInformationDTO.java | 18 ++++++++--
 .../cassandra/mail/task/MailboxMergingTask.java    | 14 ++++++--
 ...MailboxMergingTaskAdditionalInformationDTO.java | 18 ++++++++--
 ...ailboxPathV2MigrationTaskSerializationTest.java |  6 ++--
 .../mail/task/MailboxMergingTaskTest.java          |  6 ++--
 .../blob/BlobStoreVaultGarbageCollectionTask.java  | 13 +++++--
 ...bageCollectionTaskAdditionalInformationDTO.java | 17 +++++++--
 ...aultGarbageCollectionTaskSerializationTest.java |  6 ++--
 .../tools/indexer/MessageIdReIndexingTask.java     | 13 +++++--
 ...geIdReindexingTaskAdditionalInformationDTO.java | 22 +++++++++---
 .../indexer/ReprocessingContextInformation.java    | 25 ++++++++++++--
 .../indexer/ReprocessingContextInformationDTO.java | 40 +++++++++++++++++-----
 .../tools/indexer/SingleMailboxReindexingTask.java |  9 +++--
 ...lboxReindexingTaskAdditionalInformationDTO.java | 23 ++++++++++---
 .../tools/indexer/SingleMessageReindexingTask.java | 13 +++++--
 ...sageReindexingTaskAdditionalInformationDTO.java | 25 +++++++++++---
 .../mailbox/tools/indexer/UserReindexingTask.java  |  9 +++--
 ...UserReindexingTaskAdditionalInformationDTO.java | 22 +++++++++---
 ...rorRecoveryIndexationTaskSerializationTest.java |  8 +++--
 .../FullReindexingTaskSerializationTest.java       |  9 +++--
 .../MessageIdReindexingTaskSerializationTest.java  |  9 +++--
 ...ngleMailboxReindexingTaskSerializationTest.java |  9 +++--
 ...ngleMessageReindexingTaskSerializationTest.java |  9 +++--
 .../UserReindexingTaskSerializationTest.java       |  9 +++--
 .../james/modules/TaskSerializationModule.java     |  4 ++-
 .../migration/MappingsSourcesMigration.java        | 14 ++++++--
 ...urcesMigrationTaskAdditionalInformationDTO.java | 25 ++++++++++----
 ...pingsSourcesMigrationTaskSerializationTest.java |  6 ++--
 .../routes/DeletedMessagesVaultDeleteTask.java     | 13 +++++--
 ...gesVaultDeleteTaskAdditionalInformationDTO.java | 16 +++++++--
 .../routes/DeletedMessagesVaultExportTask.java     | 14 ++++++--
 ...gesVaultExportTaskAdditionalInformationDTO.java | 18 ++++++++--
 .../routes/DeletedMessagesVaultRestoreTask.java    | 13 +++++--
 ...esVaultRestoreTaskAdditionalInformationDTO.java | 18 ++++++++--
 ...edMessagesVaultDeleteTaskSerializationTest.java |  9 +++--
 ...edMessagesVaultExportTaskSerializationTest.java |  7 ++--
 ...dMessagesVaultRestoreTaskSerializationTest.java |  7 ++--
 .../service/EventDeadLettersRedeliverAllTask.java  |  4 ++-
 .../EventDeadLettersRedeliverGroupTask.java        |  4 ++-
 .../service/EventDeadLettersRedeliverOneTask.java  |  7 ++--
 ...LettersRedeliveryTaskAdditionalInformation.java | 15 ++++++--
 ...tersRedeliveryTaskAdditionalInformationDTO.java | 40 ++++++++++++++++------
 .../service/EventDeadLettersRedeliverTaskTest.java | 14 ++++----
 .../james/webadmin/service/ClearMailQueueTask.java | 13 +++++--
 ...ClearMailQueueTaskAdditionalInformationDTO.java | 18 ++++++++--
 .../service/DeleteMailsFromMailQueueTask.java      | 13 +++++--
 ...sFromMailQueueTaskAdditionalInformationDTO.java | 17 +++++++--
 .../webadmin/service/ClearMailQueueTaskTest.java   |  8 +++--
 .../service/DeleteMailsFromMailQueueTaskTest.java  | 12 ++++---
 .../webadmin/routes/MailRepositoriesRoutes.java    |  3 +-
 .../webadmin/service/ClearMailRepositoryTask.java  | 13 +++++--
 ...MailRepositoryTaskAdditionalInformationDTO.java | 17 +++++++--
 .../webadmin/service/ReprocessingAllMailsTask.java | 13 +++++--
 ...essingAllMailsTaskAdditionalInformationDTO.java | 16 +++++++--
 .../webadmin/service/ReprocessingOneMailTask.java  | 19 ++++++++--
 ...cessingOneMailTaskAdditionalInformationDTO.java | 16 +++++++--
 .../service/ReprocessingOneMailTaskDTO.java        | 10 +++---
 .../service/ClearMailRepositoryTaskTest.java       | 10 ++++--
 .../service/ReprocessingAllMailsTaskTest.java      | 11 +++---
 .../service/ReprocessingOneMailTaskTest.java       | 22 +++++++-----
 .../apache/james/task/TaskExecutionDetails.scala   |  6 ++--
 .../james/task/MemoryReferenceWithCounterTask.java | 19 +++++++---
 .../distributed/TaskEventsSerializationTest.java   | 12 ++++---
 .../task/json/dto/AdditionalInformationDTO.java    |  4 +++
 ...nceWithCounterTaskAdditionalInformationDTO.java | 17 +++++++--
 .../org/apache/james/task/MemoryTaskManager.java   |  1 -
 .../EventSourcingTaskManagerTest.java              |  1 +
 .../task/eventsourcing/TaskAggregateTest.java      | 18 +++++-----
 .../TaskExecutionDetailsProjectionContract.java    |  1 -
 .../james/task/TaskExecutionDetailsFixture.scala   |  8 +++--
 75 files changed, 743 insertions(+), 227 deletions(-)

diff --git a/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/migration/MigrationTask.java b/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/migration/MigrationTask.java
index fafb144..8c6dcf6 100644
--- a/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/migration/MigrationTask.java
+++ b/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/migration/MigrationTask.java
@@ -21,6 +21,8 @@ package org.apache.james.backends.cassandra.migration;
 
 import static org.apache.james.backends.cassandra.versions.CassandraSchemaVersionManager.DEFAULT_VERSION;
 
+import java.time.Clock;
+import java.time.Instant;
 import java.util.Optional;
 
 import javax.inject.Inject;
@@ -68,14 +70,21 @@ public class MigrationTask implements Task {
     public static class AdditionalInformations implements TaskExecutionDetails.AdditionalInformation {
 
         private final SchemaVersion toVersion;
+        private final Instant timestamp;
 
-        public AdditionalInformations(SchemaVersion toVersion) {
+        public AdditionalInformations(SchemaVersion toVersion, Instant timestamp) {
             this.toVersion = toVersion;
+            this.timestamp = timestamp;
         }
 
         public int getToVersion() {
             return toVersion.getValue();
         }
+
+        @Override
+        public Instant timestamp() {
+            return timestamp;
+        }
     }
 
     private final CassandraSchemaVersionDAO schemaVersionDAO;
@@ -148,7 +157,7 @@ public class MigrationTask implements Task {
 
     @Override
     public Optional<TaskExecutionDetails.AdditionalInformation> details() {
-        return Optional.of(new AdditionalInformations(target));
+        return Optional.of(new AdditionalInformations(target, Clock.systemUTC().instant()));
     }
 
 }
diff --git a/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/migration/MigrationTaskAdditionalInformationsDTO.java b/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/migration/MigrationTaskAdditionalInformationsDTO.java
index de18a05..1b68a38 100644
--- a/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/migration/MigrationTaskAdditionalInformationsDTO.java
+++ b/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/migration/MigrationTaskAdditionalInformationsDTO.java
@@ -18,6 +18,8 @@
  ****************************************************************/
 package org.apache.james.backends.cassandra.migration;
 
+import java.time.Instant;
+
 import org.apache.james.backends.cassandra.versions.SchemaVersion;
 import org.apache.james.json.DTOModule;
 import org.apache.james.server.task.json.dto.AdditionalInformationDTO;
@@ -30,18 +32,22 @@ public class MigrationTaskAdditionalInformationsDTO implements AdditionalInforma
     public static final AdditionalInformationDTOModule<MigrationTask.AdditionalInformations, MigrationTaskAdditionalInformationsDTO> serializationModule() {
         return DTOModule.forDomainObject(MigrationTask.AdditionalInformations.class)
             .convertToDTO(MigrationTaskAdditionalInformationsDTO.class)
-            .toDomainObjectConverter(dto -> new MigrationTask.AdditionalInformations(new SchemaVersion(dto.getTargetVersion())))
-            .toDTOConverter((details, type) -> new MigrationTaskAdditionalInformationsDTO(type, details.getToVersion()))
+            .toDomainObjectConverter(dto -> new MigrationTask.AdditionalInformations(new SchemaVersion(dto.getTargetVersion()), dto.timestamp))
+            .toDTOConverter((details, type) -> new MigrationTaskAdditionalInformationsDTO(type, details.getToVersion(), details.timestamp()))
             .typeName(MigrationTask.CASSANDRA_MIGRATION.asString())
             .withFactory(AdditionalInformationDTOModule::new);
     }
 
     private final String type;
     private final int targetVersion;
+    private final Instant timestamp;
 
-    public MigrationTaskAdditionalInformationsDTO(@JsonProperty("type") String type, @JsonProperty("targetVersion") int targetVersion) {
+    public MigrationTaskAdditionalInformationsDTO(@JsonProperty("type") String type,
+                                                  @JsonProperty("targetVersion") int targetVersion,
+                                                  @JsonProperty("timestamp") Instant timestamp) {
         this.type = type;
         this.targetVersion = targetVersion;
+        this.timestamp = timestamp;
     }
 
     @Override
@@ -49,6 +55,10 @@ public class MigrationTaskAdditionalInformationsDTO implements AdditionalInforma
         return type;
     }
 
+    public Instant getTimestamp() {
+        return timestamp;
+    }
+
     public int getTargetVersion() {
         return targetVersion;
     }
diff --git a/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/migration/MigrationTaskSerializationTest.java b/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/migration/MigrationTaskSerializationTest.java
index 2e61141..8ce6df7 100644
--- a/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/migration/MigrationTaskSerializationTest.java
+++ b/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/migration/MigrationTaskSerializationTest.java
@@ -23,6 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
 
 import java.io.IOException;
+import java.time.Instant;
 
 import org.apache.james.backends.cassandra.versions.CassandraSchemaVersionDAO;
 import org.apache.james.backends.cassandra.versions.SchemaVersion;
@@ -36,7 +37,8 @@ class MigrationTaskSerializationTest {
 
     private static final int SCHEMA_VERSION = 12;
     private static final String SERIALIZED_TASK = "{\"type\": \"CassandraMigration\", \"targetVersion\": 12}";
-    private static final String SERIALIZED_ADDITIONAL_INFORMATION = "{\"type\": \"CassandraMigration\", \"targetVersion\": 12}";
+    private static final String SERIALIZED_ADDITIONAL_INFORMATION = "{\"type\": \"CassandraMigration\", \"targetVersion\": 12, \"timestamp\": \"2018-11-13T12:00:55Z\"}";
+    private static final Instant TIMESTAMP = Instant.parse("2018-11-13T12:00:55Z");
 
     private final CassandraSchemaVersionDAO cassandraSchemaVersionDAO = mock(CassandraSchemaVersionDAO.class);
     private final CassandraSchemaTransitions transitions = mock(CassandraSchemaTransitions.class);
@@ -59,13 +61,13 @@ class MigrationTaskSerializationTest {
 
     @Test
     void additionalInformationShouldBeSerializable() throws JsonProcessingException {
-        MigrationTask.AdditionalInformations details = new MigrationTask.AdditionalInformations(new SchemaVersion(SCHEMA_VERSION));
+        MigrationTask.AdditionalInformations details = new MigrationTask.AdditionalInformations(new SchemaVersion(SCHEMA_VERSION), TIMESTAMP);
         assertThatJson(jsonAdditionalInformationSerializer.serialize(details)).isEqualTo(SERIALIZED_ADDITIONAL_INFORMATION);
     }
 
     @Test
     void additonalInformationShouldBeDeserializable() throws IOException {
-        MigrationTask.AdditionalInformations details = new MigrationTask.AdditionalInformations(new SchemaVersion(SCHEMA_VERSION));
+        MigrationTask.AdditionalInformations details = new MigrationTask.AdditionalInformations(new SchemaVersion(SCHEMA_VERSION), TIMESTAMP);
         assertThat(jsonAdditionalInformationSerializer.deserialize(SERIALIZED_ADDITIONAL_INFORMATION))
             .isEqualToComparingFieldByField(details);
     }
diff --git a/json/src/main/java/org/apache/james/json/JsonGenericSerializer.java b/json/src/main/java/org/apache/james/json/JsonGenericSerializer.java
index 5ace727..d3b70fd 100644
--- a/json/src/main/java/org/apache/james/json/JsonGenericSerializer.java
+++ b/json/src/main/java/org/apache/james/json/JsonGenericSerializer.java
@@ -30,6 +30,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.DeserializationFeature;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
 import com.fasterxml.jackson.databind.exc.MismatchedInputException;
 import com.fasterxml.jackson.datatype.guava.GuavaModule;
 import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
@@ -70,6 +71,7 @@ public class JsonGenericSerializer<T, U extends DTO> {
         objectMapper.registerModule(new JavaTimeModule());
         objectMapper.registerModule(new GuavaModule());
         objectMapper.setSerializationInclusion(JsonInclude.Include.NON_ABSENT);
+        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
         objectMapper.enable(DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY);
         typeToModule = modules.stream()
             .collect(Guavate.toImmutableMap(
diff --git a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/migration/MailboxPathV2Migration.java b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/migration/MailboxPathV2Migration.java
index 7b46578..d127444 100644
--- a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/migration/MailboxPathV2Migration.java
+++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/migration/MailboxPathV2Migration.java
@@ -19,6 +19,8 @@
 
 package org.apache.james.mailbox.cassandra.mail.migration;
 
+import java.time.Clock;
+import java.time.Instant;
 import java.util.Optional;
 
 import javax.inject.Inject;
@@ -63,10 +65,12 @@ public class MailboxPathV2Migration implements Migration {
     public static class AdditionalInformation implements TaskExecutionDetails.AdditionalInformation {
         private final long remainingCount;
         private final long initialCount;
+        private final Instant timestamp;
 
-        public AdditionalInformation(long remainingCount, long initialCount) {
+        public AdditionalInformation(long remainingCount, long initialCount, Instant timestamp) {
             this.remainingCount = remainingCount;
             this.initialCount = initialCount;
+            this.timestamp = timestamp;
         }
 
         public long getRemainingCount() {
@@ -76,6 +80,11 @@ public class MailboxPathV2Migration implements Migration {
         public long getInitialCount() {
             return initialCount;
         }
+
+        @Override
+        public Instant timestamp() {
+            return timestamp;
+        }
     }
 
     public static final Logger LOGGER = LoggerFactory.getLogger(MailboxPathV2Migration.class);
@@ -117,7 +126,7 @@ public class MailboxPathV2Migration implements Migration {
     }
 
     AdditionalInformation getAdditionalInformation() {
-        return new AdditionalInformation(getCurrentCount(), initialCount);
+        return new AdditionalInformation(getCurrentCount(), initialCount, Clock.systemUTC().instant());
     }
 
     private Long getCurrentCount() {
diff --git a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/migration/MailboxPathV2MigrationTaskAdditionalInformationDTO.java b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/migration/MailboxPathV2MigrationTaskAdditionalInformationDTO.java
index dbe9890..5c6c62e 100644
--- a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/migration/MailboxPathV2MigrationTaskAdditionalInformationDTO.java
+++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/migration/MailboxPathV2MigrationTaskAdditionalInformationDTO.java
@@ -20,6 +20,8 @@
 
 package org.apache.james.mailbox.cassandra.mail.migration;
 
+import java.time.Instant;
+
 import org.apache.james.json.DTOModule;
 import org.apache.james.server.task.json.dto.AdditionalInformationDTO;
 import org.apache.james.server.task.json.dto.AdditionalInformationDTOModule;
@@ -32,7 +34,8 @@ public class MailboxPathV2MigrationTaskAdditionalInformationDTO implements Addit
         return new MailboxPathV2MigrationTaskAdditionalInformationDTO(
             type,
             additionalInformation.getRemainingCount(),
-            additionalInformation.getInitialCount()
+            additionalInformation.getInitialCount(),
+            additionalInformation.timestamp()
         );
     }
 
@@ -48,13 +51,16 @@ public class MailboxPathV2MigrationTaskAdditionalInformationDTO implements Addit
     private final String type;
     private final long remainingCount;
     private final long initialCount;
+    private final Instant timestamp;
 
     public MailboxPathV2MigrationTaskAdditionalInformationDTO(@JsonProperty("type") String type,
                                                               @JsonProperty("remainingCount") long remainingCount,
-                                                              @JsonProperty("initialCount") long initialCount) {
+                                                              @JsonProperty("initialCount") long initialCount,
+                                                              @JsonProperty("timestamp") Instant timestamp) {
         this.type = type;
         this.remainingCount = remainingCount;
         this.initialCount = initialCount;
+        this.timestamp = timestamp;
     }
 
     public long getRemainingCount() {
@@ -66,6 +72,11 @@ public class MailboxPathV2MigrationTaskAdditionalInformationDTO implements Addit
     }
 
     @Override
+    public Instant getTimestamp() {
+        return timestamp;
+    }
+
+    @Override
     public String getType() {
         return type;
     }
@@ -73,7 +84,8 @@ public class MailboxPathV2MigrationTaskAdditionalInformationDTO implements Addit
     private MailboxPathV2Migration.AdditionalInformation toDomainObject() {
         return new MailboxPathV2Migration.AdditionalInformation(
             remainingCount,
-            initialCount
+            initialCount,
+            timestamp
         );
     }
 }
diff --git a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/task/MailboxMergingTask.java b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/task/MailboxMergingTask.java
index 4a744c1..d11bd1c 100644
--- a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/task/MailboxMergingTask.java
+++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/task/MailboxMergingTask.java
@@ -19,6 +19,8 @@
 
 package org.apache.james.mailbox.cassandra.mail.task;
 
+import java.time.Clock;
+import java.time.Instant;
 import java.util.Optional;
 import java.util.concurrent.atomic.AtomicLong;
 
@@ -36,14 +38,16 @@ public class MailboxMergingTask implements Task {
         private final long totalMessageCount;
         private final long messageMovedCount;
         private final long messageFailedCount;
+        private final Instant timestamp;
 
 
-        public Details(CassandraId oldId, CassandraId newId, long totalMessageCount, long messageMovedCount, long messageFailedCount) {
+        public Details(CassandraId oldId, CassandraId newId, long totalMessageCount, long messageMovedCount, long messageFailedCount, Instant timestamp) {
             this.oldMailboxId = oldId;
             this.newMailboxId = newId;
             this.totalMessageCount = totalMessageCount;
             this.messageMovedCount = messageMovedCount;
             this.messageFailedCount = messageFailedCount;
+            this.timestamp = timestamp;
         }
 
         public String getOldMailboxId() {
@@ -65,6 +69,11 @@ public class MailboxMergingTask implements Task {
         public long getMessageFailedCount() {
             return messageFailedCount;
         }
+
+        @Override
+        public Instant timestamp() {
+            return timestamp;
+        }
     }
 
     public static class Context {
@@ -126,7 +135,8 @@ public class MailboxMergingTask implements Task {
         return Optional.of(new Details(oldMailboxId, newMailboxId,
             context.getTotalMessageCount(),
             context.getMessageMovedCount(),
-            context.getMessageFailedCount()));
+            context.getMessageFailedCount(),
+            Clock.systemUTC().instant()));
     }
 
     Context getContext() {
diff --git a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/task/MailboxMergingTaskAdditionalInformationDTO.java b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/task/MailboxMergingTaskAdditionalInformationDTO.java
index 3535627..df3dd79 100644
--- a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/task/MailboxMergingTaskAdditionalInformationDTO.java
+++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/task/MailboxMergingTaskAdditionalInformationDTO.java
@@ -20,6 +20,8 @@
 
 package org.apache.james.mailbox.cassandra.mail.task;
 
+import java.time.Instant;
+
 import org.apache.james.json.DTOModule;
 import org.apache.james.mailbox.cassandra.ids.CassandraId;
 import org.apache.james.server.task.json.dto.AdditionalInformationDTO;
@@ -37,7 +39,8 @@ public class MailboxMergingTaskAdditionalInformationDTO implements AdditionalInf
             details.getNewMailboxId(),
             details.getTotalMessageCount(),
             details.getMessageMovedCount(),
-            details.getMessageFailedCount()
+            details.getMessageFailedCount(),
+            details.timestamp()
         );
     }
 
@@ -55,19 +58,22 @@ public class MailboxMergingTaskAdditionalInformationDTO implements AdditionalInf
     private final long totalMessageCount;
     private final long messageMovedCount;
     private final long messageFailedCount;
+    private final Instant timestamp;
 
     public MailboxMergingTaskAdditionalInformationDTO(@JsonProperty("type") String type,
                                                       @JsonProperty("oldMailboxId") String oldMailboxId,
                                                       @JsonProperty("newMailboxId") String newMailboxId,
                                                       @JsonProperty("totalMessageCount") long totalMessageCount,
                                                       @JsonProperty("messageMovedCount") long messageMovedCount,
-                                                      @JsonProperty("messageFailedCount") long messageFailedCount) {
+                                                      @JsonProperty("messageFailedCount") long messageFailedCount,
+                                                      @JsonProperty("timestamp") Instant timestamp) {
         this.type = type;
         this.oldMailboxId = oldMailboxId;
         this.newMailboxId = newMailboxId;
         this.totalMessageCount = totalMessageCount;
         this.messageMovedCount = messageMovedCount;
         this.messageFailedCount = messageFailedCount;
+        this.timestamp = timestamp;
     }
 
     public String getOldMailboxId() {
@@ -95,13 +101,19 @@ public class MailboxMergingTaskAdditionalInformationDTO implements AdditionalInf
         return type;
     }
 
+    @Override
+    public Instant getTimestamp() {
+        return timestamp;
+    }
+
     private MailboxMergingTask.Details toDomainObject() {
         return new MailboxMergingTask.Details(
             ID_FACTORY.fromString(oldMailboxId),
             ID_FACTORY.fromString(newMailboxId),
             totalMessageCount,
             messageMovedCount,
-            messageFailedCount
+            messageFailedCount,
+            timestamp
         );
     }
 }
diff --git a/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/migration/MailboxPathV2MigrationTaskSerializationTest.java b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/migration/MailboxPathV2MigrationTaskSerializationTest.java
index f2a1b88..7ee513a 100644
--- a/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/migration/MailboxPathV2MigrationTaskSerializationTest.java
+++ b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/migration/MailboxPathV2MigrationTaskSerializationTest.java
@@ -25,6 +25,7 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
 
 import java.io.IOException;
+import java.time.Instant;
 
 import org.apache.james.server.task.json.JsonTaskAdditionalInformationsSerializer;
 import org.apache.james.server.task.json.JsonTaskSerializer;
@@ -34,11 +35,12 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 import org.junit.jupiter.api.Test;
 
 class MailboxPathV2MigrationTaskSerializationTest {
+    private static final Instant TIMESTAMP = Instant.parse("2018-11-13T12:00:55Z");
     private static final MailboxPathV2Migration MIGRATION = mock(MailboxPathV2Migration.class);
     private static final MailboxPathV2Migration.MailboxPathV2MigrationTask TASK = new MailboxPathV2Migration.MailboxPathV2MigrationTask(MIGRATION);
     private static final String SERIALIZED_TASK = "{\"type\": \"Cassandra_mailboxPathV2Migration\"}";
-    private static final MailboxPathV2Migration.AdditionalInformation DETAILS = new MailboxPathV2Migration.AdditionalInformation(42L, 10);
-    private static final String SERIALIZED_ADDITIONAL_INFORMATION = "{\"type\": \"Cassandra_mailboxPathV2Migration\", \"remainingCount\":42,\"initialCount\":10}";
+    private static final MailboxPathV2Migration.AdditionalInformation DETAILS = new MailboxPathV2Migration.AdditionalInformation(42L, 10, TIMESTAMP);
+    private static final String SERIALIZED_ADDITIONAL_INFORMATION = "{\"type\": \"Cassandra_mailboxPathV2Migration\", \"remainingCount\":42,\"initialCount\":10, \"timestamp\":\"2018-11-13T12:00:55Z\"}";
 
     private static final JsonTaskSerializer TASK_SERIALIZER = new JsonTaskSerializer(MailboxPathV2MigrationTaskDTO.MODULE.apply(MIGRATION));
     private static final JsonTaskAdditionalInformationsSerializer JSON_TASK_ADDITIONAL_INFORMATIONS_SERIALIZER = new JsonTaskAdditionalInformationsSerializer(MailboxPathV2MigrationTaskAdditionalInformationDTO.MODULE);
diff --git a/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/task/MailboxMergingTaskTest.java b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/task/MailboxMergingTaskTest.java
index fe175c2..575509c 100644
--- a/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/task/MailboxMergingTaskTest.java
+++ b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/task/MailboxMergingTaskTest.java
@@ -24,6 +24,7 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
 
 import java.io.IOException;
+import java.time.Instant;
 
 import org.apache.james.mailbox.cassandra.ids.CassandraId;
 import org.apache.james.server.task.json.JsonTaskAdditionalInformationsSerializer;
@@ -34,12 +35,13 @@ import net.javacrumbs.jsonunit.assertj.JsonAssertions;
 import org.junit.jupiter.api.Test;
 
 class MailboxMergingTaskTest {
+    private static final Instant TIMESTAMP = Instant.parse("2018-11-13T12:00:55Z");
     private static final CassandraId.Factory CASSANDRA_ID_FACTORY = new CassandraId.Factory();
     private static final String SERIALIZED = "{\"type\":\"mailboxMerging\",\"totalMessageCount\":0,\"oldMailboxId\":\"3b8e5f90-b94f-20f8-ce7b-3c4aad93b90c\",\"newMailboxId\":\"2c7f4081-aa30-11e9-bf6c-2d3b9e84aafd\"}";
-    private static final String SERIALIZED_ADDITIONAL_INFORMATION = "{\"type\":\"mailboxMerging\", \"oldMailboxId\":\"3b8e5f90-b94f-20f8-ce7b-3c4aad93b90c\",\"newMailboxId\":\"2c7f4081-aa30-11e9-bf6c-2d3b9e84aafd\",\"totalMessageCount\":10,\"messageMovedCount\":15,\"messageFailedCount\":20}";
+    private static final String SERIALIZED_ADDITIONAL_INFORMATION = "{\"type\":\"mailboxMerging\", \"oldMailboxId\":\"3b8e5f90-b94f-20f8-ce7b-3c4aad93b90c\",\"newMailboxId\":\"2c7f4081-aa30-11e9-bf6c-2d3b9e84aafd\",\"totalMessageCount\":10,\"messageMovedCount\":15,\"messageFailedCount\":20, \"timestamp\":\"2018-11-13T12:00:55Z\"}";
     private static final MailboxMergingTaskRunner TASK_RUNNER = mock(MailboxMergingTaskRunner.class);
     private static final MailboxMergingTask TASK = new MailboxMergingTask(TASK_RUNNER, 0L, CASSANDRA_ID_FACTORY.fromString("3b8e5f90-b94f-20f8-ce7b-3c4aad93b90c"), CASSANDRA_ID_FACTORY.fromString("2c7f4081-aa30-11e9-bf6c-2d3b9e84aafd"));
-    private static final MailboxMergingTask.Details DETAILS = new MailboxMergingTask.Details(CASSANDRA_ID_FACTORY.fromString("3b8e5f90-b94f-20f8-ce7b-3c4aad93b90c"), CASSANDRA_ID_FACTORY.fromString("2c7f4081-aa30-11e9-bf6c-2d3b9e84aafd"), 10, 15, 20);
+    private static final MailboxMergingTask.Details DETAILS = new MailboxMergingTask.Details(CASSANDRA_ID_FACTORY.fromString("3b8e5f90-b94f-20f8-ce7b-3c4aad93b90c"), CASSANDRA_ID_FACTORY.fromString("2c7f4081-aa30-11e9-bf6c-2d3b9e84aafd"), 10, 15, 20, TIMESTAMP);
     private static final JsonTaskAdditionalInformationsSerializer JSON_TASK_ADDITIONAL_INFORMATIONS_SERIALIZER = new JsonTaskAdditionalInformationsSerializer(MailboxMergingTaskAdditionalInformationDTO.SERIALIZATION_MODULE);
     private static final JsonTaskSerializer TESTEE = new JsonTaskSerializer(MailboxMergingTaskDTO.module(TASK_RUNNER));
 
diff --git a/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/blob/BlobStoreVaultGarbageCollectionTask.java b/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/blob/BlobStoreVaultGarbageCollectionTask.java
index 827658d..db85116 100644
--- a/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/blob/BlobStoreVaultGarbageCollectionTask.java
+++ b/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/blob/BlobStoreVaultGarbageCollectionTask.java
@@ -19,6 +19,8 @@
 
 package org.apache.james.vault.blob;
 
+import java.time.Clock;
+import java.time.Instant;
 import java.time.ZonedDateTime;
 import java.util.Collection;
 import java.util.List;
@@ -42,10 +44,12 @@ public class BlobStoreVaultGarbageCollectionTask implements Task {
 
         private final ZonedDateTime beginningOfRetentionPeriod;
         private final Collection<BucketName> deletedBuckets;
+        private final Instant timestamp;
 
-        AdditionalInformation(ZonedDateTime beginningOfRetentionPeriod, Collection<BucketName> deletedBuckets) {
+        AdditionalInformation(ZonedDateTime beginningOfRetentionPeriod, Collection<BucketName> deletedBuckets, Instant timestamp) {
             this.beginningOfRetentionPeriod = beginningOfRetentionPeriod;
             this.deletedBuckets = deletedBuckets;
+            this.timestamp = timestamp;
         }
 
         public ZonedDateTime getBeginningOfRetentionPeriod() {
@@ -57,6 +61,11 @@ public class BlobStoreVaultGarbageCollectionTask implements Task {
                 .map(BucketName::asString)
                 .collect(Guavate.toImmutableList());
         }
+
+        @Override
+        public Instant timestamp() {
+            return timestamp;
+        }
     }
 
     static final TaskType TYPE = TaskType.of("deletedMessages/blobStoreBasedGarbageCollection");
@@ -102,7 +111,7 @@ public class BlobStoreVaultGarbageCollectionTask implements Task {
 
     @Override
     public Optional<TaskExecutionDetails.AdditionalInformation> details() {
-        return Optional.of(new AdditionalInformation(beginningOfRetentionPeriod, deletedBuckets));
+        return Optional.of(new AdditionalInformation(beginningOfRetentionPeriod, deletedBuckets, Clock.systemUTC().instant()));
     }
 
     ZonedDateTime getBeginningOfRetentionPeriod() {
diff --git a/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/blob/BlobStoreVaultGarbageCollectionTaskAdditionalInformationDTO.java b/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/blob/BlobStoreVaultGarbageCollectionTaskAdditionalInformationDTO.java
index 87d236c..fff7526 100644
--- a/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/blob/BlobStoreVaultGarbageCollectionTaskAdditionalInformationDTO.java
+++ b/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/blob/BlobStoreVaultGarbageCollectionTaskAdditionalInformationDTO.java
@@ -20,6 +20,7 @@
 
 package org.apache.james.vault.blob;
 
+import java.time.Instant;
 import java.time.ZonedDateTime;
 import java.util.Collection;
 
@@ -36,7 +37,8 @@ public class BlobStoreVaultGarbageCollectionTaskAdditionalInformationDTO impleme
         return new BlobStoreVaultGarbageCollectionTaskAdditionalInformationDTO(
             type,
             additionalInformation.getBeginningOfRetentionPeriod().toString(),
-            additionalInformation.getDeletedBuckets()
+            additionalInformation.getDeletedBuckets(),
+            additionalInformation.timestamp()
         );
     }
 
@@ -52,14 +54,17 @@ public class BlobStoreVaultGarbageCollectionTaskAdditionalInformationDTO impleme
     private final String beginningOfRetentionPeriod;
     private final Collection<String> deletedBuckets;
     private final String type;
+    private final Instant timestamp;
 
     BlobStoreVaultGarbageCollectionTaskAdditionalInformationDTO(
         @JsonProperty("type") String type,
         @JsonProperty("beginningOfRetentionPeriod") String beginningOfRetentionPeriod,
-        @JsonProperty("deletedBuckets") Collection<String> deletedBuckets) {
+        @JsonProperty("deletedBuckets") Collection<String> deletedBuckets,
+        @JsonProperty("timestamp") Instant timestamp) {
         this.type = type;
         this.beginningOfRetentionPeriod = beginningOfRetentionPeriod;
         this.deletedBuckets = deletedBuckets;
+        this.timestamp = timestamp;
     }
 
     BlobStoreVaultGarbageCollectionTask.AdditionalInformation toDomainObject() {
@@ -68,7 +73,8 @@ public class BlobStoreVaultGarbageCollectionTaskAdditionalInformationDTO impleme
             deletedBuckets
                 .stream()
                 .map(BucketName::of)
-                .collect(Guavate.toImmutableList()));
+                .collect(Guavate.toImmutableList()),
+            timestamp);
     }
 
     public String getBeginningOfRetentionPeriod() {
@@ -83,4 +89,9 @@ public class BlobStoreVaultGarbageCollectionTaskAdditionalInformationDTO impleme
     public String getType() {
         return type;
     }
+
+    @Override
+    public Instant getTimestamp() {
+        return timestamp;
+    }
 }
diff --git a/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/blob/BlobStoreVaultGarbageCollectionTaskSerializationTest.java b/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/blob/BlobStoreVaultGarbageCollectionTaskSerializationTest.java
index 6c2a0b2..f49e7f5 100644
--- a/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/blob/BlobStoreVaultGarbageCollectionTaskSerializationTest.java
+++ b/mailbox/plugin/deleted-messages-vault/src/test/java/org/apache/james/vault/blob/BlobStoreVaultGarbageCollectionTaskSerializationTest.java
@@ -25,6 +25,7 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.ArgumentMatchers.any;
 
 import java.io.IOException;
+import java.time.Instant;
 import java.time.ZonedDateTime;
 
 import org.apache.james.blob.api.BucketName;
@@ -49,11 +50,12 @@ class BlobStoreVaultGarbageCollectionTaskSerializationTest {
     private static final ZonedDateTime BEGINNING_OF_RETENTION_PERIOD = ZonedDateTime.parse("2019-09-03T15:26:13.356+02:00[Europe/Paris]");
     private static final ImmutableList<BucketName> BUCKET_IDS = ImmutableList.of(BucketName.of("1"), BucketName.of("2"), BucketName.of("3"));
     private static final Flux<BucketName> RETENTION_OPERATION = Flux.fromIterable(BUCKET_IDS);
-    private static final BlobStoreVaultGarbageCollectionTask.AdditionalInformation DETAILS = new BlobStoreVaultGarbageCollectionTask.AdditionalInformation(BEGINNING_OF_RETENTION_PERIOD, BUCKET_IDS);
+    private static final Instant TIMESTAMP = Instant.parse("2018-11-13T12:00:55Z");
+    private static final BlobStoreVaultGarbageCollectionTask.AdditionalInformation DETAILS = new BlobStoreVaultGarbageCollectionTask.AdditionalInformation(BEGINNING_OF_RETENTION_PERIOD, BUCKET_IDS, TIMESTAMP);
     private static final BlobStoreVaultGarbageCollectionTask TASK = TASK_FACTORY.create();
 
     private static final String SERIALIZED_TASK = "{\"type\":\"deletedMessages/blobStoreBasedGarbageCollection\"}";
-    private static final String SERIALIZED_ADDITIONAL_INFORMATION_TASK = "{\"type\":\"deletedMessages/blobStoreBasedGarbageCollection\", \"beginningOfRetentionPeriod\":\"2019-09-03T15:26:13.356+02:00[Europe/Paris]\",\"deletedBuckets\":[\"1\", \"2\", \"3\"]}";
+    private static final String SERIALIZED_ADDITIONAL_INFORMATION_TASK = "{\"type\":\"deletedMessages/blobStoreBasedGarbageCollection\", \"beginningOfRetentionPeriod\":\"2019-09-03T15:26:13.356+02:00[Europe/Paris]\",\"deletedBuckets\":[\"1\", \"2\", \"3\"], \"timestamp\": \"2018-11-13T12:00:55Z\"}";
 
     private static final JsonTaskAdditionalInformationsSerializer JSON_TASK_ADDITIONAL_INFORMATIONS_SERIALIZER = new JsonTaskAdditionalInformationsSerializer(BlobStoreVaultGarbageCollectionTaskAdditionalInformationDTO.MODULE);
 
diff --git a/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/MessageIdReIndexingTask.java b/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/MessageIdReIndexingTask.java
index 74fc454..6cbba51 100644
--- a/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/MessageIdReIndexingTask.java
+++ b/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/MessageIdReIndexingTask.java
@@ -18,6 +18,8 @@
  ****************************************************************/
 package org.apache.mailbox.tools.indexer;
 
+import java.time.Clock;
+import java.time.Instant;
 import java.util.Optional;
 
 import javax.inject.Inject;
@@ -52,14 +54,21 @@ public class MessageIdReIndexingTask implements Task {
 
     public static final class AdditionalInformation implements TaskExecutionDetails.AdditionalInformation {
         private final MessageId messageId;
+        private final Instant timestamp;
 
-        AdditionalInformation(MessageId messageId) {
+        AdditionalInformation(MessageId messageId, Instant timestamp) {
             this.messageId = messageId;
+            this.timestamp = timestamp;
         }
 
         public String getMessageId() {
             return messageId.serialize();
         }
+
+        @Override
+        public Instant timestamp() {
+            return timestamp;
+        }
     }
 
 
@@ -70,7 +79,7 @@ public class MessageIdReIndexingTask implements Task {
     MessageIdReIndexingTask(ReIndexerPerformer reIndexerPerformer, MessageId messageId) {
         this.reIndexerPerformer = reIndexerPerformer;
         this.messageId = messageId;
-        this.additionalInformation = new AdditionalInformation(messageId);
+        this.additionalInformation = new AdditionalInformation(messageId, Clock.systemUTC().instant());
     }
 
     @Override
diff --git a/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/MessageIdReindexingTaskAdditionalInformationDTO.java b/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/MessageIdReindexingTaskAdditionalInformationDTO.java
index f2235c6..b417db8 100644
--- a/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/MessageIdReindexingTaskAdditionalInformationDTO.java
+++ b/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/MessageIdReindexingTaskAdditionalInformationDTO.java
@@ -18,6 +18,9 @@
  ****************************************************************/
 package org.apache.mailbox.tools.indexer;
 
+import java.time.Clock;
+import java.time.Instant;
+
 import org.apache.james.json.DTOModule;
 import org.apache.james.mailbox.model.MessageId;
 import org.apache.james.server.task.json.dto.AdditionalInformationDTO;
@@ -30,30 +33,41 @@ public class MessageIdReindexingTaskAdditionalInformationDTO implements Addition
     public static AdditionalInformationDTOModule<MessageIdReIndexingTask.AdditionalInformation, MessageIdReindexingTaskAdditionalInformationDTO> serializationModule(MessageId.Factory factory) {
         return DTOModule.forDomainObject(MessageIdReIndexingTask.AdditionalInformation.class)
             .convertToDTO(MessageIdReindexingTaskAdditionalInformationDTO.class)
-            .toDomainObjectConverter(dto -> new MessageIdReIndexingTask.AdditionalInformation(factory.fromString(dto.getMessageId())))
-            .toDTOConverter((details, type) -> new MessageIdReindexingTaskAdditionalInformationDTO(type, details.getMessageId()))
+            .toDomainObjectConverter(dto -> new MessageIdReIndexingTask.AdditionalInformation(factory.fromString(dto.getMessageId()), dto.timestamp))
+            .toDTOConverter((details, type) -> new MessageIdReindexingTaskAdditionalInformationDTO(type, details.getMessageId(), details.timestamp()))
             .typeName(MessageIdReIndexingTask.TYPE.asString())
             .withFactory(AdditionalInformationDTOModule::new);
     }
 
     private final String type;
     private final String messageId;
+    private final Instant timestamp;
 
-    private MessageIdReindexingTaskAdditionalInformationDTO(@JsonProperty("type") String type, @JsonProperty("messageId") String messageId) {
+    private MessageIdReindexingTaskAdditionalInformationDTO(@JsonProperty("type") String type,
+                                                            @JsonProperty("messageId") String messageId,
+                                                            @JsonProperty("timestamp") Instant timestamp) {
         this.type = type;
         this.messageId = messageId;
+        this.timestamp = timestamp;
     }
 
     public String getMessageId() {
         return messageId;
     }
 
+    public Instant getTimestamp() {
+        return timestamp;
+    }
+
     @Override
     public String getType() {
         return type;
     }
 
     public static MessageIdReindexingTaskAdditionalInformationDTO of(MessageIdReIndexingTask task) {
-        return new MessageIdReindexingTaskAdditionalInformationDTO(task.type().asString(), task.getMessageId().serialize());
+        return new MessageIdReindexingTaskAdditionalInformationDTO(
+            task.type().asString(),
+            task.getMessageId().serialize(),
+            Clock.systemUTC().instant());
     }
 }
diff --git a/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/ReprocessingContextInformation.java b/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/ReprocessingContextInformation.java
index 31c6839..2b6e656 100644
--- a/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/ReprocessingContextInformation.java
+++ b/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/ReprocessingContextInformation.java
@@ -19,6 +19,9 @@
 
 package org.apache.mailbox.tools.indexer;
 
+import java.time.Clock;
+import java.time.Instant;
+
 import org.apache.james.mailbox.indexer.IndexingDetailInformation;
 import org.apache.james.mailbox.indexer.ReIndexingExecutionFailures;
 import org.apache.james.task.TaskExecutionDetails;
@@ -31,21 +34,32 @@ import com.fasterxml.jackson.annotation.JsonProperty;
 public class ReprocessingContextInformation implements TaskExecutionDetails.AdditionalInformation, IndexingDetailInformation {
 
     public static ReprocessingContextInformationForErrorRecoveryIndexationTask forErrorRecoveryIndexationTask(ReprocessingContext reprocessingContext) {
-        return new ReprocessingContextInformationForErrorRecoveryIndexationTask(reprocessingContext.successfullyReprocessedMailCount(), reprocessingContext.failedReprocessingMailCount(), reprocessingContext.failures());
+        return new ReprocessingContextInformationForErrorRecoveryIndexationTask(
+            reprocessingContext.successfullyReprocessedMailCount(),
+            reprocessingContext.failedReprocessingMailCount(),
+            reprocessingContext.failures(),
+            Clock.systemUTC().instant());
     }
 
     public static ReprocessingContextInformationForFullReindexingTask forFullReindexingTask(ReprocessingContext reprocessingContext) {
-        return new ReprocessingContextInformationForFullReindexingTask(reprocessingContext.successfullyReprocessedMailCount(), reprocessingContext.failedReprocessingMailCount(), reprocessingContext.failures());
+        return new ReprocessingContextInformationForFullReindexingTask(
+            reprocessingContext.successfullyReprocessedMailCount(),
+            reprocessingContext.failedReprocessingMailCount(),
+            reprocessingContext.failures(),
+            Clock.systemUTC().instant());
     }
 
     private final int successfullyReprocessedMailCount;
     private final int failedReprocessedMailCount;
     private final ReIndexingExecutionFailures failures;
+    private final Instant timestamp;
 
-    ReprocessingContextInformation(int successfullyReprocessedMailCount, int failedReprocessedMailCount, ReIndexingExecutionFailures failures) {
+    ReprocessingContextInformation(int successfullyReprocessedMailCount, int failedReprocessedMailCount,
+                                   ReIndexingExecutionFailures failures, Instant timestamp) {
         this.successfullyReprocessedMailCount = successfullyReprocessedMailCount;
         this.failedReprocessedMailCount = failedReprocessedMailCount;
         this.failures = failures;
+        this.timestamp = timestamp;
     }
 
     @Override
@@ -68,4 +82,9 @@ public class ReprocessingContextInformation implements TaskExecutionDetails.Addi
     public SerializableReIndexingExecutionFailures failuresAsJson() {
         return SerializableReIndexingExecutionFailures.from(failures());
     }
+
+    @Override
+    public Instant timestamp() {
+        return timestamp;
+    }
 }
diff --git a/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/ReprocessingContextInformationDTO.java b/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/ReprocessingContextInformationDTO.java
index f7722d8..3ee9c7f 100644
--- a/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/ReprocessingContextInformationDTO.java
+++ b/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/ReprocessingContextInformationDTO.java
@@ -18,6 +18,7 @@
  ****************************************************************/
 package org.apache.mailbox.tools.indexer;
 
+import java.time.Instant;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -60,14 +61,23 @@ public class ReprocessingContextInformationDTO implements AdditionalInformationD
         public static final AdditionalInformationDTOModule<ReprocessingContextInformationForErrorRecoveryIndexationTask, ReprocessingContextInformationDTO> serializationModule(MailboxId.Factory mailboxIdFactory) {
             return DTOModule.forDomainObject(ReprocessingContextInformationForErrorRecoveryIndexationTask.class)
                 .convertToDTO(ReprocessingContextInformationDTO.class)
-                .toDomainObjectConverter(dto -> new ReprocessingContextInformationForErrorRecoveryIndexationTask(dto.successfullyReprocessedMailCount, dto.failedReprocessedMailCount, deserializeFailures(mailboxIdFactory, dto.failures)))
-                .toDTOConverter((details, type) -> new ReprocessingContextInformationDTO(type, details.getSuccessfullyReprocessedMailCount(), details.getFailedReprocessedMailCount(), serializeFailures(details.failures())))
+                .toDomainObjectConverter(dto -> new ReprocessingContextInformationForErrorRecoveryIndexationTask(
+                    dto.successfullyReprocessedMailCount,
+                    dto.failedReprocessedMailCount,
+                    deserializeFailures(mailboxIdFactory, dto.failures),
+                    dto.getTimestamp()))
+                .toDTOConverter((details, type) -> new ReprocessingContextInformationDTO(
+                    type,
+                    details.getSuccessfullyReprocessedMailCount(),
+                    details.getFailedReprocessedMailCount(),
+                    serializeFailures(details.failures()),
+                    details.timestamp()))
                 .typeName(ErrorRecoveryIndexationTask.PREVIOUS_FAILURES_INDEXING.asString())
                 .withFactory(AdditionalInformationDTOModule::new);
         }
 
-        ReprocessingContextInformationForErrorRecoveryIndexationTask(int successfullyReprocessedMailCount, int failedReprocessedMailCount, ReIndexingExecutionFailures failures) {
-            super(successfullyReprocessedMailCount, failedReprocessedMailCount, failures);
+        ReprocessingContextInformationForErrorRecoveryIndexationTask(int successfullyReprocessedMailCount, int failedReprocessedMailCount, ReIndexingExecutionFailures failures, Instant timestamp) {
+            super(successfullyReprocessedMailCount, failedReprocessedMailCount, failures, timestamp);
         }
     }
 
@@ -75,14 +85,19 @@ public class ReprocessingContextInformationDTO implements AdditionalInformationD
         public static final AdditionalInformationDTOModule<ReprocessingContextInformationForFullReindexingTask, ReprocessingContextInformationDTO> serializationModule(MailboxId.Factory mailboxIdFactory) {
             return DTOModule.forDomainObject(ReprocessingContextInformationForFullReindexingTask.class)
                 .convertToDTO(ReprocessingContextInformationDTO.class)
-                .toDomainObjectConverter(dto -> new ReprocessingContextInformationForFullReindexingTask(dto.successfullyReprocessedMailCount, dto.failedReprocessedMailCount, deserializeFailures(mailboxIdFactory, dto.failures)))
-                .toDTOConverter((details, type) -> new ReprocessingContextInformationDTO(type, details.getSuccessfullyReprocessedMailCount(), details.getFailedReprocessedMailCount(), serializeFailures(details.failures())))
+                .toDomainObjectConverter(dto -> new ReprocessingContextInformationForFullReindexingTask(dto.successfullyReprocessedMailCount, dto.failedReprocessedMailCount, deserializeFailures(mailboxIdFactory, dto.failures), dto.getTimestamp()))
+                .toDTOConverter((details, type) -> new ReprocessingContextInformationDTO(
+                    type,
+                    details.getSuccessfullyReprocessedMailCount(),
+                    details.getFailedReprocessedMailCount(),
+                    serializeFailures(details.failures()),
+                    details.timestamp()))
                 .typeName(FullReindexingTask.FULL_RE_INDEXING.asString())
                 .withFactory(AdditionalInformationDTOModule::new);
         }
 
-        ReprocessingContextInformationForFullReindexingTask(int successfullyReprocessedMailCount, int failedReprocessedMailCount, ReIndexingExecutionFailures failures) {
-            super(successfullyReprocessedMailCount, failedReprocessedMailCount, failures);
+        ReprocessingContextInformationForFullReindexingTask(int successfullyReprocessedMailCount, int failedReprocessedMailCount, ReIndexingExecutionFailures failures, Instant timestamp) {
+            super(successfullyReprocessedMailCount, failedReprocessedMailCount, failures, timestamp);
         }
     }
 
@@ -133,16 +148,19 @@ public class ReprocessingContextInformationDTO implements AdditionalInformationD
     private final int successfullyReprocessedMailCount;
     private final int failedReprocessedMailCount;
     private final List<ReindexingFailureDTO> failures;
+    private final Instant timestamp;
 
 
     ReprocessingContextInformationDTO(@JsonProperty("type") String type,
                                       @JsonProperty("successfullyReprocessedMailCount") int successfullyReprocessedMailCount,
                                       @JsonProperty("failedReprocessedMailCount") int failedReprocessedMailCount,
-                                      @JsonProperty("failures") List<ReindexingFailureDTO> failures) {
+                                      @JsonProperty("failures") List<ReindexingFailureDTO> failures,
+                                      @JsonProperty("timestamp") Instant timestamp) {
         this.type = type;
         this.successfullyReprocessedMailCount = successfullyReprocessedMailCount;
         this.failedReprocessedMailCount = failedReprocessedMailCount;
         this.failures = failures;
+        this.timestamp = timestamp;
     }
 
     public int getSuccessfullyReprocessedMailCount() {
@@ -157,6 +175,10 @@ public class ReprocessingContextInformationDTO implements AdditionalInformationD
         return failures;
     }
 
+    public Instant getTimestamp() {
+        return timestamp;
+    }
+
     @Override
     public String getType() {
         return type;
diff --git a/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/SingleMailboxReindexingTask.java b/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/SingleMailboxReindexingTask.java
index b29f000..a9e3a16 100644
--- a/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/SingleMailboxReindexingTask.java
+++ b/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/SingleMailboxReindexingTask.java
@@ -19,6 +19,8 @@
 
 package org.apache.mailbox.tools.indexer;
 
+import java.time.Clock;
+import java.time.Instant;
 import java.util.Optional;
 
 import javax.inject.Inject;
@@ -36,8 +38,8 @@ public class SingleMailboxReindexingTask implements Task {
     public static class AdditionalInformation extends ReprocessingContextInformation {
         private final MailboxId mailboxId;
 
-        AdditionalInformation(MailboxId mailboxId, int successfullyReprocessedMailCount, int failedReprocessedMailCount, ReIndexingExecutionFailures failures) {
-            super(successfullyReprocessedMailCount, failedReprocessedMailCount, failures);
+        AdditionalInformation(MailboxId mailboxId, int successfullyReprocessedMailCount, int failedReprocessedMailCount, ReIndexingExecutionFailures failures, Instant timestamp) {
+            super(successfullyReprocessedMailCount, failedReprocessedMailCount, failures, timestamp);
             this.mailboxId = mailboxId;
         }
 
@@ -99,7 +101,8 @@ public class SingleMailboxReindexingTask implements Task {
                 mailboxId,
                 reprocessingContext.successfullyReprocessedMailCount(),
                 reprocessingContext.failedReprocessingMailCount(),
-                reprocessingContext.failures())
+                reprocessingContext.failures(),
+                Clock.systemUTC().instant())
         );
     }
 
diff --git a/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/SingleMailboxReindexingTaskAdditionalInformationDTO.java b/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/SingleMailboxReindexingTaskAdditionalInformationDTO.java
index 8fb0733..7ac4084 100644
--- a/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/SingleMailboxReindexingTaskAdditionalInformationDTO.java
+++ b/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/SingleMailboxReindexingTaskAdditionalInformationDTO.java
@@ -18,6 +18,7 @@
  ****************************************************************/
 package org.apache.mailbox.tools.indexer;
 
+import java.time.Instant;
 import java.util.List;
 
 import org.apache.james.json.DTOModule;
@@ -36,9 +37,15 @@ public class SingleMailboxReindexingTaskAdditionalInformationDTO implements Addi
             .toDomainObjectConverter(dto -> new SingleMailboxReindexingTask.AdditionalInformation(factory.fromString(dto.getMailboxId()),
                 dto.getSuccessfullyReprocessedMailCount(),
                 dto.getFailedReprocessedMailCount(),
-                ReprocessingContextInformationDTO.deserializeFailures(factory, dto.getFailures())))
-            .toDTOConverter((details, type) -> new SingleMailboxReindexingTaskAdditionalInformationDTO(type, details.getMailboxId(), details.getSuccessfullyReprocessedMailCount(), details.getFailedReprocessedMailCount(),
-                ReprocessingContextInformationDTO.serializeFailures(details.failures())))
+                ReprocessingContextInformationDTO.deserializeFailures(factory, dto.getFailures()),
+                dto.getTimestamp()))
+            .toDTOConverter((details, type) -> new SingleMailboxReindexingTaskAdditionalInformationDTO(
+                type,
+                details.getMailboxId(),
+                details.getSuccessfullyReprocessedMailCount(),
+                details.getFailedReprocessedMailCount(),
+                ReprocessingContextInformationDTO.serializeFailures(details.failures()),
+                details.timestamp()))
             .typeName(SingleMailboxReindexingTask.MAILBOX_RE_INDEXING.asString())
             .withFactory(AdditionalInformationDTOModule::new);
     }
@@ -51,12 +58,14 @@ public class SingleMailboxReindexingTaskAdditionalInformationDTO implements Addi
                                                                 @JsonProperty("mailboxId") String mailboxId,
                                                                 @JsonProperty("successfullyReprocessedMailCount") int successfullyReprocessedMailCount,
                                                                 @JsonProperty("failedReprocessedMailCount") int failedReprocessedMailCount,
-                                                                @JsonProperty("failures") List<ReprocessingContextInformationDTO.ReindexingFailureDTO> failures) {
+                                                                @JsonProperty("failures") List<ReprocessingContextInformationDTO.ReindexingFailureDTO> failures,
+                                                                @JsonProperty("timestamp") Instant timestamp) {
         this.mailboxId = mailboxId;
         this.reprocessingContextInformationDTO = new ReprocessingContextInformationDTO(
             type,
             successfullyReprocessedMailCount,
-            failedReprocessedMailCount, failures);
+            failedReprocessedMailCount, failures,
+            timestamp);
     }
 
     @Override
@@ -64,6 +73,10 @@ public class SingleMailboxReindexingTaskAdditionalInformationDTO implements Addi
         return reprocessingContextInformationDTO.getType();
     }
 
+    public Instant getTimestamp() {
+        return reprocessingContextInformationDTO.getTimestamp();
+    }
+
     public String getMailboxId() {
         return mailboxId;
     }
diff --git a/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/SingleMessageReindexingTask.java b/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/SingleMessageReindexingTask.java
index 57d45ec..a5d3a52 100644
--- a/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/SingleMessageReindexingTask.java
+++ b/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/SingleMessageReindexingTask.java
@@ -19,6 +19,8 @@
 
 package org.apache.mailbox.tools.indexer;
 
+import java.time.Clock;
+import java.time.Instant;
 import java.util.Optional;
 
 import javax.inject.Inject;
@@ -40,10 +42,12 @@ public class SingleMessageReindexingTask implements Task {
     public static class AdditionalInformation implements TaskExecutionDetails.AdditionalInformation {
         private final MailboxId mailboxId;
         private final MessageUid uid;
+        private final Instant timestamp;
 
-        AdditionalInformation(MailboxId mailboxId, MessageUid uid) {
+        AdditionalInformation(MailboxId mailboxId, MessageUid uid, Instant timestamp) {
             this.mailboxId = mailboxId;
             this.uid = uid;
+            this.timestamp = timestamp;
         }
 
         public String getMailboxId() {
@@ -53,6 +57,11 @@ public class SingleMessageReindexingTask implements Task {
         public long getUid() {
             return uid.asLong();
         }
+
+        @Override
+        public Instant timestamp() {
+            return timestamp;
+        }
     }
 
     public static class Factory {
@@ -83,7 +92,7 @@ public class SingleMessageReindexingTask implements Task {
         this.reIndexerPerformer = reIndexerPerformer;
         this.mailboxId = mailboxId;
         this.uid = uid;
-        this.additionalInformation = new AdditionalInformation(mailboxId, uid);
+        this.additionalInformation = new AdditionalInformation(mailboxId, uid, Clock.systemUTC().instant());
     }
 
     @Override
diff --git a/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/SingleMessageReindexingTaskAdditionalInformationDTO.java b/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/SingleMessageReindexingTaskAdditionalInformationDTO.java
index c42d843..c783c63 100644
--- a/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/SingleMessageReindexingTaskAdditionalInformationDTO.java
+++ b/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/SingleMessageReindexingTaskAdditionalInformationDTO.java
@@ -18,6 +18,9 @@
  ****************************************************************/
 package org.apache.mailbox.tools.indexer;
 
+import java.time.Clock;
+import java.time.Instant;
+
 import org.apache.james.json.DTOModule;
 import org.apache.james.mailbox.MessageUid;
 import org.apache.james.mailbox.model.MailboxId;
@@ -31,8 +34,15 @@ public class SingleMessageReindexingTaskAdditionalInformationDTO implements Addi
     public static AdditionalInformationDTOModule<SingleMessageReindexingTask.AdditionalInformation, SingleMessageReindexingTaskAdditionalInformationDTO> serializationModule(MailboxId.Factory factory) {
         return DTOModule.forDomainObject(SingleMessageReindexingTask.AdditionalInformation.class)
             .convertToDTO(SingleMessageReindexingTaskAdditionalInformationDTO.class)
-            .toDomainObjectConverter(dto -> new SingleMessageReindexingTask.AdditionalInformation(factory.fromString(dto.mailboxId), MessageUid.of(dto.getUid())))
-            .toDTOConverter((details, type) -> new SingleMessageReindexingTaskAdditionalInformationDTO(type, details.getMailboxId(), details.getUid()))
+            .toDomainObjectConverter(dto -> new SingleMessageReindexingTask.AdditionalInformation(
+                factory.fromString(dto.mailboxId),
+                MessageUid.of(dto.getUid()),
+                dto.timestamp))
+            .toDTOConverter((details, type) -> new SingleMessageReindexingTaskAdditionalInformationDTO(
+                type,
+                details.getMailboxId(),
+                details.getUid(),
+                details.timestamp()))
             .typeName(SingleMessageReindexingTask.MESSAGE_RE_INDEXING.asString())
             .withFactory(AdditionalInformationDTOModule::new);
     }
@@ -40,13 +50,16 @@ public class SingleMessageReindexingTaskAdditionalInformationDTO implements Addi
     private final String type;
     private final String mailboxId;
     private final long uid;
+    private final Instant timestamp;
 
     private SingleMessageReindexingTaskAdditionalInformationDTO(@JsonProperty("type") String type,
                                                                 @JsonProperty("mailboxId") String mailboxId,
-                                                                @JsonProperty("uid") long uid) {
+                                                                @JsonProperty("uid") long uid,
+                                                                @JsonProperty("timestamp") Instant timestamp) {
         this.type = type;
         this.mailboxId = mailboxId;
         this.uid = uid;
+        this.timestamp = timestamp;
     }
 
     public String getMailboxId() {
@@ -62,7 +75,11 @@ public class SingleMessageReindexingTaskAdditionalInformationDTO implements Addi
         return type;
     }
 
+    public Instant getTimestamp() {
+        return timestamp;
+    }
+
     public static SingleMessageReindexingTaskAdditionalInformationDTO of(SingleMessageReindexingTask task) {
-        return new SingleMessageReindexingTaskAdditionalInformationDTO(task.type().asString(), task.getMailboxId().serialize(), task.getUid().asLong());
+        return new SingleMessageReindexingTaskAdditionalInformationDTO(task.type().asString(), task.getMailboxId().serialize(), task.getUid().asLong(), Clock.systemUTC().instant());
     }
 }
diff --git a/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/UserReindexingTask.java b/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/UserReindexingTask.java
index 7b69928..2be1147 100644
--- a/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/UserReindexingTask.java
+++ b/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/UserReindexingTask.java
@@ -19,6 +19,8 @@
 
 package org.apache.mailbox.tools.indexer;
 
+import java.time.Clock;
+import java.time.Instant;
 import java.util.Optional;
 
 import javax.inject.Inject;
@@ -37,8 +39,8 @@ public class UserReindexingTask implements Task {
     public static class AdditionalInformation extends ReprocessingContextInformation {
         private final User user;
 
-        AdditionalInformation(User user, int successfullyReprocessedMailCount, int failedReprocessedMailCount, ReIndexingExecutionFailures failures) {
-            super(successfullyReprocessedMailCount, failedReprocessedMailCount, failures);
+        AdditionalInformation(User user, int successfullyReprocessedMailCount, int failedReprocessedMailCount, ReIndexingExecutionFailures failures, Instant timestamp) {
+            super(successfullyReprocessedMailCount, failedReprocessedMailCount, failures, timestamp);
             this.user = user;
         }
 
@@ -97,7 +99,8 @@ public class UserReindexingTask implements Task {
         return Optional.of(new UserReindexingTask.AdditionalInformation(user,
             reprocessingContext.successfullyReprocessedMailCount(),
             reprocessingContext.failedReprocessingMailCount(),
-            reprocessingContext.failures())
+            reprocessingContext.failures(),
+            Clock.systemUTC().instant())
         );
     }
 }
diff --git a/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/UserReindexingTaskAdditionalInformationDTO.java b/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/UserReindexingTaskAdditionalInformationDTO.java
index 87e2b4e..38aa0ee 100644
--- a/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/UserReindexingTaskAdditionalInformationDTO.java
+++ b/mailbox/tools/indexer/src/main/java/org/apache/mailbox/tools/indexer/UserReindexingTaskAdditionalInformationDTO.java
@@ -18,6 +18,7 @@
  ****************************************************************/
 package org.apache.mailbox.tools.indexer;
 
+import java.time.Instant;
 import java.util.List;
 
 import org.apache.james.core.User;
@@ -37,9 +38,15 @@ public class UserReindexingTaskAdditionalInformationDTO implements AdditionalInf
             .toDomainObjectConverter(dto -> new UserReindexingTask.AdditionalInformation(User.fromUsername(dto.getUser()),
                 dto.getSuccessfullyReprocessedMailCount(),
                 dto.getFailedReprocessedMailCount(),
-                ReprocessingContextInformationDTO.deserializeFailures(factory, dto.getFailures())))
-            .toDTOConverter((details, type) -> new UserReindexingTaskAdditionalInformationDTO(type, details.getUser(), details.getSuccessfullyReprocessedMailCount(), details.getFailedReprocessedMailCount(),
-                ReprocessingContextInformationDTO.serializeFailures(details.failures())))
+                ReprocessingContextInformationDTO.deserializeFailures(factory, dto.getFailures()),
+                dto.getTimestamp()))
+            .toDTOConverter((details, type) -> new UserReindexingTaskAdditionalInformationDTO(
+                type,
+                details.getUser(),
+                details.getSuccessfullyReprocessedMailCount(),
+                details.getFailedReprocessedMailCount(),
+                ReprocessingContextInformationDTO.serializeFailures(details.failures()),
+                details.timestamp()))
             .typeName(UserReindexingTask.USER_RE_INDEXING.asString())
             .withFactory(AdditionalInformationDTOModule::new);
     }
@@ -52,12 +59,13 @@ public class UserReindexingTaskAdditionalInformationDTO implements AdditionalInf
                                                        @JsonProperty("user") String user,
                                                        @JsonProperty("successfullyReprocessedMailCount") int successfullyReprocessedMailCount,
                                                        @JsonProperty("failedReprocessedMailCount") int failedReprocessedMailCount,
-                                                       @JsonProperty("failures") List<ReprocessingContextInformationDTO.ReindexingFailureDTO> failures) {
+                                                       @JsonProperty("failures") List<ReprocessingContextInformationDTO.ReindexingFailureDTO> failures,
+                                                       @JsonProperty("timestamp") Instant timestamp) {
         this.user = user;
         this.reprocessingContextInformationDTO = new ReprocessingContextInformationDTO(
             type,
             successfullyReprocessedMailCount,
-            failedReprocessedMailCount, failures);
+            failedReprocessedMailCount, failures, timestamp);
     }
 
     @Override
@@ -65,6 +73,10 @@ public class UserReindexingTaskAdditionalInformationDTO implements AdditionalInf
         return reprocessingContextInformationDTO.getType();
     }
 
+    public Instant getTimestamp() {
+        return reprocessingContextInformationDTO.getTimestamp();
+    }
+
     public String getUser() {
         return user;
     }
diff --git a/mailbox/tools/indexer/src/test/java/org/apache/mailbox/tools/indexer/ErrorRecoveryIndexationTaskSerializationTest.java b/mailbox/tools/indexer/src/test/java/org/apache/mailbox/tools/indexer/ErrorRecoveryIndexationTaskSerializationTest.java
index 18c18a3..fa463a8 100644
--- a/mailbox/tools/indexer/src/test/java/org/apache/mailbox/tools/indexer/ErrorRecoveryIndexationTaskSerializationTest.java
+++ b/mailbox/tools/indexer/src/test/java/org/apache/mailbox/tools/indexer/ErrorRecoveryIndexationTaskSerializationTest.java
@@ -23,6 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
 
 import java.io.IOException;
+import java.time.Instant;
 import java.util.List;
 
 import org.apache.james.mailbox.MessageUid;
@@ -41,6 +42,7 @@ import com.google.common.collect.ImmutableList;
 
 class ErrorRecoveryIndexationTaskSerializationTest {
 
+    private static final Instant TIMESTAMP = Instant.parse("2018-11-13T12:00:55Z");
     private final TestId.Factory mailboxIdFactory = new TestId.Factory();
     private ReIndexerPerformer reIndexerPerformer;
     private JsonTaskSerializer taskSerializer;
@@ -52,7 +54,7 @@ class ErrorRecoveryIndexationTaskSerializationTest {
 
     private final String serializedErrorRecoveryReindexingTask = "{\"type\": \"ErrorRecoveryIndexation\"," +
         " \"previousFailures\" : [{\"mailboxId\":\"1\",\"uids\":[10]},{\"mailboxId\":\"2\",\"uids\":[20]}]}";
-    private final String SERIALIZED_ADDITIONAL_INFORMATION = "{\"type\": \"ErrorRecoveryIndexation\", \"successfullyReprocessedMailCount\":42,\"failedReprocessedMailCount\":2,\"failures\":[{\"mailboxId\":\"1\",\"uids\":[10]},{\"mailboxId\":\"2\",\"uids\":[20]}]}";
+    private final String SERIALIZED_ADDITIONAL_INFORMATION = "{\"type\": \"ErrorRecoveryIndexation\", \"successfullyReprocessedMailCount\":42,\"failedReprocessedMailCount\":2,\"failures\":[{\"mailboxId\":\"1\",\"uids\":[10]},{\"mailboxId\":\"2\",\"uids\":[20]}], \"timestamp\":\"2018-11-13T12:00:55Z\"}";
 
     private final TestId mailboxId = TestId.of(1L);
     private final MessageUid messageUid = MessageUid.of(10L);
@@ -99,13 +101,13 @@ class ErrorRecoveryIndexationTaskSerializationTest {
 
     @Test
     void additionalInformationShouldBeSerializable() throws JsonProcessingException {
-        ReprocessingContextInformation details = new ReprocessingContextInformationForErrorRecoveryIndexationTask(successfullyReprocessedMailCount, failedReprocessedMailCount, reIndexingExecutionFailures);
+        ReprocessingContextInformation details = new ReprocessingContextInformationForErrorRecoveryIndexationTask(successfullyReprocessedMailCount, failedReprocessedMailCount, reIndexingExecutionFailures, TIMESTAMP);
         assertThatJson(jsonAdditionalInformationSerializer.serialize(details)).isEqualTo(SERIALIZED_ADDITIONAL_INFORMATION);
     }
 
     @Test
     void additonalInformationShouldBeDeserializable() throws IOException {
-        ReprocessingContextInformation details = new ReprocessingContextInformation(successfullyReprocessedMailCount, failedReprocessedMailCount, reIndexingExecutionFailures);
+        ReprocessingContextInformation details = new ReprocessingContextInformationForErrorRecoveryIndexationTask(successfullyReprocessedMailCount, failedReprocessedMailCount, reIndexingExecutionFailures, TIMESTAMP);
         assertThat(jsonAdditionalInformationSerializer.deserialize(SERIALIZED_ADDITIONAL_INFORMATION))
             .isEqualToComparingFieldByField(details);
     }
diff --git a/mailbox/tools/indexer/src/test/java/org/apache/mailbox/tools/indexer/FullReindexingTaskSerializationTest.java b/mailbox/tools/indexer/src/test/java/org/apache/mailbox/tools/indexer/FullReindexingTaskSerializationTest.java
index 925a76c..6a54441 100644
--- a/mailbox/tools/indexer/src/test/java/org/apache/mailbox/tools/indexer/FullReindexingTaskSerializationTest.java
+++ b/mailbox/tools/indexer/src/test/java/org/apache/mailbox/tools/indexer/FullReindexingTaskSerializationTest.java
@@ -23,6 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
 
 import java.io.IOException;
+import java.time.Instant;
 
 import org.apache.james.mailbox.MessageUid;
 import org.apache.james.mailbox.indexer.ReIndexingExecutionFailures;
@@ -39,6 +40,8 @@ import com.google.common.collect.ImmutableList;
 
 class FullReindexingTaskSerializationTest {
 
+    private static final Instant TIMESTAMP = Instant.parse("2018-11-13T12:00:55Z");
+
     private ReIndexerPerformer reIndexerPerformer;
     private JsonTaskSerializer taskSerializer;
     private JsonTaskAdditionalInformationsSerializer jsonAdditionalInformationSerializer;
@@ -53,7 +56,7 @@ class FullReindexingTaskSerializationTest {
     private ReIndexingExecutionFailures reIndexingExecutionFailures;
 
     private final String serializedFullReindexingTask = "{\"type\": \"FullReIndexing\"}";
-    private final String SERIALIZED_ADDITIONAL_INFORMATION = "{\"type\": \"FullReIndexing\", \"successfullyReprocessedMailCount\":42,\"failedReprocessedMailCount\":2,\"failures\":[{\"mailboxId\":\"1\",\"uids\":[10]},{\"mailboxId\":\"2\",\"uids\":[20]}]}";
+    private final String SERIALIZED_ADDITIONAL_INFORMATION = "{\"type\": \"FullReIndexing\", \"successfullyReprocessedMailCount\":42,\"failedReprocessedMailCount\":2,\"failures\":[{\"mailboxId\":\"1\",\"uids\":[10]},{\"mailboxId\":\"2\",\"uids\":[20]}], \"timestamp\":\"2018-11-13T12:00:55Z\"}";
 
     @BeforeEach
     void setUp() {
@@ -86,13 +89,13 @@ class FullReindexingTaskSerializationTest {
 
     @Test
     void additionalInformationShouldBeSerializable() throws JsonProcessingException {
-        ReprocessingContextInformation details = new ReprocessingContextInformationForFullReindexingTask(successfullyReprocessedMailCount, failedReprocessedMailCount, reIndexingExecutionFailures);
+        ReprocessingContextInformation details = new ReprocessingContextInformationForFullReindexingTask(successfullyReprocessedMailCount, failedReprocessedMailCount, reIndexingExecutionFailures, TIMESTAMP);
         assertThatJson(jsonAdditionalInformationSerializer.serialize(details)).isEqualTo(SERIALIZED_ADDITIONAL_INFORMATION);
     }
 
     @Test
     void additonalInformationShouldBeDeserializable() throws IOException {
-        ReprocessingContextInformation details = new ReprocessingContextInformation(successfullyReprocessedMailCount, failedReprocessedMailCount, reIndexingExecutionFailures);
+        ReprocessingContextInformation details = new ReprocessingContextInformation(successfullyReprocessedMailCount, failedReprocessedMailCount, reIndexingExecutionFailures, TIMESTAMP);
         assertThat(jsonAdditionalInformationSerializer.deserialize(SERIALIZED_ADDITIONAL_INFORMATION))
             .isEqualToComparingFieldByField(details);
     }
diff --git a/mailbox/tools/indexer/src/test/java/org/apache/mailbox/tools/indexer/MessageIdReindexingTaskSerializationTest.java b/mailbox/tools/indexer/src/test/java/org/apache/mailbox/tools/indexer/MessageIdReindexingTaskSerializationTest.java
index 97fe1a5..5c276bf 100644
--- a/mailbox/tools/indexer/src/test/java/org/apache/mailbox/tools/indexer/MessageIdReindexingTaskSerializationTest.java
+++ b/mailbox/tools/indexer/src/test/java/org/apache/mailbox/tools/indexer/MessageIdReindexingTaskSerializationTest.java
@@ -23,6 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
 
 import java.io.IOException;
+import java.time.Instant;
 
 import org.apache.james.mailbox.model.MessageId;
 import org.apache.james.mailbox.model.TestMessageId;
@@ -35,11 +36,13 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 
 class MessageIdReindexingTaskSerializationTest {
 
+    private static final Instant TIMESTAMP = Instant.parse("2018-11-13T12:00:55Z");
+
     private ReIndexerPerformer reIndexerPerformer;
     private MessageId.Factory messageIdFactory;
     private JsonTaskSerializer taskSerializer;
     private final String serializedMessageIdReIndexingTask = "{\"type\": \"MessageIdReIndexingTask\", \"messageId\": \"1\"}";
-    private final String SERIALIZED_ADDITIONAL_INFORMATION = "{\"type\": \"MessageIdReIndexingTask\", \"messageId\": \"1\"}";
+    private final String SERIALIZED_ADDITIONAL_INFORMATION = "{\"type\": \"MessageIdReIndexingTask\", \"messageId\": \"1\", \"timestamp\":\"2018-11-13T12:00:55Z\"}";
 
     private JsonTaskAdditionalInformationsSerializer jsonAdditionalInformationSerializer;
 
@@ -74,14 +77,14 @@ class MessageIdReindexingTaskSerializationTest {
     @Test
     void additionalInformationShouldBeSerializable() throws JsonProcessingException {
         MessageId messageId = messageIdFactory.fromString("1");
-        MessageIdReIndexingTask.AdditionalInformation details = new MessageIdReIndexingTask.AdditionalInformation(messageId);
+        MessageIdReIndexingTask.AdditionalInformation details = new MessageIdReIndexingTask.AdditionalInformation(messageId, TIMESTAMP);
         assertThatJson(jsonAdditionalInformationSerializer.serialize(details)).isEqualTo(SERIALIZED_ADDITIONAL_INFORMATION);
     }
 
     @Test
     void additonalInformationShouldBeDeserializable() throws IOException {
         MessageId messageId = messageIdFactory.fromString("1");
-        MessageIdReIndexingTask.AdditionalInformation details = new MessageIdReIndexingTask.AdditionalInformation(messageId);
+        MessageIdReIndexingTask.AdditionalInformation details = new MessageIdReIndexingTask.AdditionalInformation(messageId, TIMESTAMP);
         assertThat(jsonAdditionalInformationSerializer.deserialize(SERIALIZED_ADDITIONAL_INFORMATION))
             .isEqualToComparingFieldByField(details);
     }
diff --git a/mailbox/tools/indexer/src/test/java/org/apache/mailbox/tools/indexer/SingleMailboxReindexingTaskSerializationTest.java b/mailbox/tools/indexer/src/test/java/org/apache/mailbox/tools/indexer/SingleMailboxReindexingTaskSerializationTest.java
index f3c0036..1be420f 100644
--- a/mailbox/tools/indexer/src/test/java/org/apache/mailbox/tools/indexer/SingleMailboxReindexingTaskSerializationTest.java
+++ b/mailbox/tools/indexer/src/test/java/org/apache/mailbox/tools/indexer/SingleMailboxReindexingTaskSerializationTest.java
@@ -23,6 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
 
 import java.io.IOException;
+import java.time.Instant;
 import java.util.List;
 
 import org.apache.james.mailbox.MessageUid;
@@ -38,6 +39,8 @@ import com.google.common.collect.ImmutableList;
 
 class SingleMailboxReindexingTaskSerializationTest {
 
+    private static final Instant TIMESTAMP = Instant.parse("2018-11-13T12:00:55Z");
+
     private ReIndexerPerformer reIndexerPerformer;
     private JsonTaskSerializer taskSerializer;
     private JsonTaskAdditionalInformationsSerializer jsonAdditionalInformationSerializer;
@@ -48,7 +51,7 @@ class SingleMailboxReindexingTaskSerializationTest {
 
     private final String serializedMailboxReindexingTask = "{\"type\": \"mailboxReIndexing\", \"mailboxId\": \"1\"}";
 
-    private final String SERIALIZED_ADDITIONAL_INFORMATION = "{\"type\": \"mailboxReIndexing\", \"mailboxId\": \"1\", \"successfullyReprocessedMailCount\":42,\"failedReprocessedMailCount\":2,\"failures\":[{\"mailboxId\":\"1\",\"uids\":[10, 20]}]}";
+    private final String SERIALIZED_ADDITIONAL_INFORMATION = "{\"type\": \"mailboxReIndexing\", \"mailboxId\": \"1\", \"successfullyReprocessedMailCount\":42,\"failedReprocessedMailCount\":2,\"failures\":[{\"mailboxId\":\"1\",\"uids\":[10, 20]}], \"timestamp\":\"2018-11-13T12:00:55Z\"}";
 
     private final TestId mailboxId = TestId.of(1L);
     private final MessageUid messageUid = MessageUid.of(10L);
@@ -92,13 +95,13 @@ class SingleMailboxReindexingTaskSerializationTest {
 
     @Test
     void additionalInformationShouldBeSerializable() throws JsonProcessingException {
-        SingleMailboxReindexingTask.AdditionalInformation details = new SingleMailboxReindexingTask.AdditionalInformation(mailboxId, successfullyReprocessedMailCount, failedReprocessedMailCount, reIndexingExecutionFailures);
+        SingleMailboxReindexingTask.AdditionalInformation details = new SingleMailboxReindexingTask.AdditionalInformation(mailboxId, successfullyReprocessedMailCount, failedReprocessedMailCount, reIndexingExecutionFailures, TIMESTAMP);
         assertThatJson(jsonAdditionalInformationSerializer.serialize(details)).isEqualTo(SERIALIZED_ADDITIONAL_INFORMATION);
     }
 
     @Test
     void additonalInformationShouldBeDeserializable() throws IOException {
-        SingleMailboxReindexingTask.AdditionalInformation details = new SingleMailboxReindexingTask.AdditionalInformation(mailboxId, successfullyReprocessedMailCount, failedReprocessedMailCount, reIndexingExecutionFailures);
+        SingleMailboxReindexingTask.AdditionalInformation details = new SingleMailboxReindexingTask.AdditionalInformation(mailboxId, successfullyReprocessedMailCount, failedReprocessedMailCount, reIndexingExecutionFailures, TIMESTAMP);
         assertThat(jsonAdditionalInformationSerializer.deserialize(SERIALIZED_ADDITIONAL_INFORMATION))
             .isEqualToComparingFieldByField(details);
     }
diff --git a/mailbox/tools/indexer/src/test/java/org/apache/mailbox/tools/indexer/SingleMessageReindexingTaskSerializationTest.java b/mailbox/tools/indexer/src/test/java/org/apache/mailbox/tools/indexer/SingleMessageReindexingTaskSerializationTest.java
index d6705c8..13984bc 100644
--- a/mailbox/tools/indexer/src/test/java/org/apache/mailbox/tools/indexer/SingleMessageReindexingTaskSerializationTest.java
+++ b/mailbox/tools/indexer/src/test/java/org/apache/mailbox/tools/indexer/SingleMessageReindexingTaskSerializationTest.java
@@ -23,6 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
 
 import java.io.IOException;
+import java.time.Instant;
 
 import org.apache.james.mailbox.MessageUid;
 import org.apache.james.mailbox.model.TestId;
@@ -35,11 +36,13 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 
 class SingleMessageReindexingTaskSerializationTest {
 
+    private static final Instant TIMESTAMP = Instant.parse("2018-11-13T12:00:55Z");
+
     private final TestId.Factory mailboxIdFactory = new TestId.Factory();
     private ReIndexerPerformer reIndexerPerformer;
     private JsonTaskSerializer taskSerializer;
     private final String serializedMessageReindexingTask = "{\"type\": \"messageReIndexing\", \"mailboxId\": \"1\", \"uid\": 10}";
-    private final String SERIALIZED_ADDITIONAL_INFORMATION = "{\"type\": \"messageReIndexing\", \"mailboxId\": \"1\", \"uid\": 10}";
+    private final String SERIALIZED_ADDITIONAL_INFORMATION = "{\"type\": \"messageReIndexing\", \"mailboxId\": \"1\", \"uid\": 10, \"timestamp\":\"2018-11-13T12:00:55Z\"}";
     private final TestId mailboxId = TestId.of(1L);
     private final MessageUid messageUid = MessageUid.of(10L);
     private JsonTaskAdditionalInformationsSerializer jsonAdditionalInformationSerializer = new JsonTaskAdditionalInformationsSerializer(
@@ -70,13 +73,13 @@ class SingleMessageReindexingTaskSerializationTest {
 
     @Test
     void additionalInformationShouldBeSerializable() throws JsonProcessingException {
-        SingleMessageReindexingTask.AdditionalInformation details = new SingleMessageReindexingTask.AdditionalInformation(mailboxId, messageUid);
+        SingleMessageReindexingTask.AdditionalInformation details = new SingleMessageReindexingTask.AdditionalInformation(mailboxId, messageUid, TIMESTAMP);
         assertThatJson(jsonAdditionalInformationSerializer.serialize(details)).isEqualTo(SERIALIZED_ADDITIONAL_INFORMATION);
     }
 
     @Test
     void additonalInformationShouldBeDeserializable() throws IOException {
-        SingleMessageReindexingTask.AdditionalInformation details = new SingleMessageReindexingTask.AdditionalInformation(mailboxId, messageUid);
+        SingleMessageReindexingTask.AdditionalInformation details = new SingleMessageReindexingTask.AdditionalInformation(mailboxId, messageUid, TIMESTAMP);
         assertThat(jsonAdditionalInformationSerializer.deserialize(SERIALIZED_ADDITIONAL_INFORMATION))
             .isEqualToComparingFieldByField(details);
     }
diff --git a/mailbox/tools/indexer/src/test/java/org/apache/mailbox/tools/indexer/UserReindexingTaskSerializationTest.java b/mailbox/tools/indexer/src/test/java/org/apache/mailbox/tools/indexer/UserReindexingTaskSerializationTest.java
index 618fa70..cd73ac8 100644
--- a/mailbox/tools/indexer/src/test/java/org/apache/mailbox/tools/indexer/UserReindexingTaskSerializationTest.java
+++ b/mailbox/tools/indexer/src/test/java/org/apache/mailbox/tools/indexer/UserReindexingTaskSerializationTest.java
@@ -23,6 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
 
 import java.io.IOException;
+import java.time.Instant;
 import java.util.List;
 
 import org.apache.james.core.User;
@@ -39,6 +40,8 @@ import com.google.common.collect.ImmutableList;
 
 class UserReindexingTaskSerializationTest {
 
+    private static final Instant TIMESTAMP = Instant.parse("2018-11-13T12:00:55Z");
+
     private ReIndexerPerformer reIndexerPerformer;
     private JsonTaskSerializer taskSerializer;
     private JsonTaskAdditionalInformationsSerializer jsonAdditionalInformationSerializer;
@@ -49,7 +52,7 @@ class UserReindexingTaskSerializationTest {
     private ReIndexingExecutionFailures reIndexingExecutionFailures;
     private final String serializedUserReindexingTask = "{\"type\": \"userReIndexing\", \"username\": \"foo@apache.org\"}";
 
-    private final String SERIALIZED_ADDITIONAL_INFORMATION = "{\"type\": \"userReIndexing\", \"user\": \"foo@apache.org\", \"successfullyReprocessedMailCount\":42,\"failedReprocessedMailCount\":2,\"failures\":[{\"mailboxId\":\"1\",\"uids\":[10]},{\"mailboxId\":\"2\",\"uids\":[20]}]}";
+    private final String SERIALIZED_ADDITIONAL_INFORMATION = "{\"type\": \"userReIndexing\", \"user\": \"foo@apache.org\", \"successfullyReprocessedMailCount\":42,\"failedReprocessedMailCount\":2,\"failures\":[{\"mailboxId\":\"1\",\"uids\":[10]},{\"mailboxId\":\"2\",\"uids\":[20]}], \"timestamp\":\"2018-11-13T12:00:55Z\"}";
 
     private final TestId mailboxId = TestId.of(1L);
     private final MessageUid messageUid = MessageUid.of(10L);
@@ -95,13 +98,13 @@ class UserReindexingTaskSerializationTest {
 
     @Test
     void additionalInformationShouldBeSerializable() throws JsonProcessingException {
-        UserReindexingTask.AdditionalInformation details = new UserReindexingTask.AdditionalInformation(user, successfullyReprocessedMailCount, failedReprocessedMailCount, reIndexingExecutionFailures);
+        UserReindexingTask.AdditionalInformation details = new UserReindexingTask.AdditionalInformation(user, successfullyReprocessedMailCount, failedReprocessedMailCount, reIndexingExecutionFailures, TIMESTAMP);
         assertThatJson(jsonAdditionalInformationSerializer.serialize(details)).isEqualTo(SERIALIZED_ADDITIONAL_INFORMATION);
     }
 
     @Test
     void additionalInformationShouldBeDeserializable() throws IOException {
-        UserReindexingTask.AdditionalInformation details = new UserReindexingTask.AdditionalInformation(user, successfullyReprocessedMailCount, failedReprocessedMailCount, reIndexingExecutionFailures);
+        UserReindexingTask.AdditionalInformation details = new UserReindexingTask.AdditionalInformation(user, successfullyReprocessedMailCount, failedReprocessedMailCount, reIndexingExecutionFailures, TIMESTAMP);
         assertThat(jsonAdditionalInformationSerializer.deserialize(SERIALIZED_ADDITIONAL_INFORMATION))
             .isEqualToComparingFieldByField(details);
     }
diff --git a/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/TaskSerializationModule.java b/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/TaskSerializationModule.java
index 31e1120..39f38b0 100644
--- a/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/TaskSerializationModule.java
+++ b/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/TaskSerializationModule.java
@@ -18,6 +18,8 @@
  ****************************************************************/
 package org.apache.james.modules;
 
+import java.time.Clock;
+
 import org.apache.james.backends.cassandra.migration.MigrationTask;
 import org.apache.james.backends.cassandra.migration.MigrationTaskAdditionalInformationsDTO;
 import org.apache.james.backends.cassandra.migration.MigrationTaskDTO;
@@ -205,7 +207,7 @@ public class TaskSerializationModule extends AbstractModule {
 
     @ProvidesIntoSet
     public TaskDTOModule<?, ?> reprocessingOneMailsTask(ReprocessingService reprocessingService) {
-        return ReprocessingOneMailTaskDTO.module(reprocessingService);
+        return ReprocessingOneMailTaskDTO.module(Clock.systemUTC(), reprocessingService);
     }
 
     @ProvidesIntoSet
diff --git a/server/data/data-cassandra/src/main/java/org/apache/james/rrt/cassandra/migration/MappingsSourcesMigration.java b/server/data/data-cassandra/src/main/java/org/apache/james/rrt/cassandra/migration/MappingsSourcesMigration.java
index dd61851..cc55a70 100644
--- a/server/data/data-cassandra/src/main/java/org/apache/james/rrt/cassandra/migration/MappingsSourcesMigration.java
+++ b/server/data/data-cassandra/src/main/java/org/apache/james/rrt/cassandra/migration/MappingsSourcesMigration.java
@@ -19,6 +19,8 @@
 
 package org.apache.james.rrt.cassandra.migration;
 
+import java.time.Clock;
+import java.time.Instant;
 import java.util.Optional;
 import java.util.concurrent.atomic.AtomicLong;
 
@@ -71,10 +73,12 @@ public class MappingsSourcesMigration implements Migration {
     public static class AdditionalInformation implements TaskExecutionDetails.AdditionalInformation {
         private final long successfulMappingsCount;
         private final long errorMappingsCount;
+        private final Instant timestamp;
 
-        AdditionalInformation(long successfulMappingsCount, long errorMappingsCount) {
+        AdditionalInformation(long successfulMappingsCount, long errorMappingsCount, Instant timestamp) {
             this.successfulMappingsCount = successfulMappingsCount;
             this.errorMappingsCount = errorMappingsCount;
+            this.timestamp = timestamp;
         }
 
         public long getSuccessfulMappingsCount() {
@@ -84,6 +88,11 @@ public class MappingsSourcesMigration implements Migration {
         public long getErrorMappingsCount() {
             return errorMappingsCount;
         }
+
+        @Override
+        public Instant timestamp() {
+            return timestamp;
+        }
     }
 
     private final CassandraRecipientRewriteTableDAO cassandraRecipientRewriteTableDAO;
@@ -133,6 +142,7 @@ public class MappingsSourcesMigration implements Migration {
     AdditionalInformation createAdditionalInformation() {
         return new AdditionalInformation(
             successfulMappingsCount.get(),
-            errorMappingsCount.get());
+            errorMappingsCount.get(),
+            Clock.systemUTC().instant());
     }
 }
diff --git a/server/data/data-cassandra/src/main/java/org/apache/james/rrt/cassandra/migration/MappingsSourcesMigrationTaskAdditionalInformationDTO.java b/server/data/data-cassandra/src/main/java/org/apache/james/rrt/cassandra/migration/MappingsSourcesMigrationTaskAdditionalInformationDTO.java
index 45cf96b..5ddf19b 100644
--- a/server/data/data-cassandra/src/main/java/org/apache/james/rrt/cassandra/migration/MappingsSourcesMigrationTaskAdditionalInformationDTO.java
+++ b/server/data/data-cassandra/src/main/java/org/apache/james/rrt/cassandra/migration/MappingsSourcesMigrationTaskAdditionalInformationDTO.java
@@ -20,6 +20,8 @@
 
 package org.apache.james.rrt.cassandra.migration;
 
+import java.time.Instant;
+
 import org.apache.james.json.DTOModule;
 import org.apache.james.server.task.json.dto.AdditionalInformationDTO;
 import org.apache.james.server.task.json.dto.AdditionalInformationDTOModule;
@@ -33,7 +35,8 @@ public class MappingsSourcesMigrationTaskAdditionalInformationDTO implements Add
         return new MappingsSourcesMigrationTaskAdditionalInformationDTO(
             type,
             additionalInformation.getSuccessfulMappingsCount(),
-            additionalInformation.getErrorMappingsCount()
+            additionalInformation.getErrorMappingsCount(),
+            additionalInformation.timestamp()
         );
     }
 
@@ -49,22 +52,25 @@ public class MappingsSourcesMigrationTaskAdditionalInformationDTO implements Add
 
     private final String type;
     private final long successfulMappingsCount;
-    private final long errorMappinsCount;
+    private final long errorMappingsCount;
+    private final Instant timestamp;
 
     public MappingsSourcesMigrationTaskAdditionalInformationDTO(@JsonProperty("type") String type,
                                                                 @JsonProperty("successfulMappingsCount") long successfulMappingsCount,
-                                                                @JsonProperty("errorMappinsCount") long errorMappinsCount) {
+                                                                @JsonProperty("errorMappingsCount") long errorMappingsCount,
+                                                                @JsonProperty("timestamp") Instant timestamp) {
         this.type = type;
         this.successfulMappingsCount = successfulMappingsCount;
-        this.errorMappinsCount = errorMappinsCount;
+        this.errorMappingsCount = errorMappingsCount;
+        this.timestamp = timestamp;
     }
 
     public long getSuccessfulMappingsCount() {
         return successfulMappingsCount;
     }
 
-    public long getErrorMappinsCount() {
-        return errorMappinsCount;
+    public long getErrorMappingsCount() {
+        return errorMappingsCount;
     }
 
     @Override
@@ -72,10 +78,15 @@ public class MappingsSourcesMigrationTaskAdditionalInformationDTO implements Add
         return type;
     }
 
+    public Instant getTimestamp() {
+        return timestamp;
+    }
+
     private MappingsSourcesMigration.AdditionalInformation toDomainObject() {
         return new MappingsSourcesMigration.AdditionalInformation(
             successfulMappingsCount,
-            errorMappinsCount
+            errorMappingsCount,
+            timestamp
         );
     }
 }
diff --git a/server/data/data-cassandra/src/test/java/org/apache/james/rrt/cassandra/migration/MappingsSourcesMigrationTaskSerializationTest.java b/server/data/data-cassandra/src/test/java/org/apache/james/rrt/cassandra/migration/MappingsSourcesMigrationTaskSerializationTest.java
index b2af55d..dfbdca2 100644
--- a/server/data/data-cassandra/src/test/java/org/apache/james/rrt/cassandra/migration/MappingsSourcesMigrationTaskSerializationTest.java
+++ b/server/data/data-cassandra/src/test/java/org/apache/james/rrt/cassandra/migration/MappingsSourcesMigrationTaskSerializationTest.java
@@ -25,6 +25,7 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
 
 import java.io.IOException;
+import java.time.Instant;
 
 import org.apache.james.server.task.json.JsonTaskAdditionalInformationsSerializer;
 import org.apache.james.server.task.json.JsonTaskSerializer;
@@ -33,11 +34,12 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 import org.junit.jupiter.api.Test;
 
 class MappingsSourcesMigrationTaskSerializationTest {
+    private static final Instant TIMESTAMP = Instant.parse("2018-11-13T12:00:55Z");
     private static final MappingsSourcesMigration MIGRATION = mock(MappingsSourcesMigration.class);
     private static final MappingsSourcesMigration.MappingsSourcesMigrationTask TASK = new MappingsSourcesMigration.MappingsSourcesMigrationTask(MIGRATION);
     private static final String SERIALIZED_TASK = "{\"type\": \"mappingsSourcesMigration\"}";
-    private static final MappingsSourcesMigration.AdditionalInformation DETAILS = new MappingsSourcesMigration.AdditionalInformation(42L, 10);
-    private static final String SERIALIZED_ADDITIONAL_INFORMATION = "{\"type\": \"mappingsSourcesMigration\", \"successfulMappingsCount\":42,\"errorMappinsCount\":10}";
+    private static final MappingsSourcesMigration.AdditionalInformation DETAILS = new MappingsSourcesMigration.AdditionalInformation(42L, 10, TIMESTAMP);
+    private static final String SERIALIZED_ADDITIONAL_INFORMATION = "{\"type\": \"mappingsSourcesMigration\", \"successfulMappingsCount\":42,\"errorMappingsCount\":10,\"timestamp\":\"2018-11-13T12:00:55Z\"}";
 
     private static final JsonTaskSerializer TASK_SERIALIZER = new JsonTaskSerializer(MappingsSourcesMigrationTaskDTO.MODULE.apply(MIGRATION));
     private static final JsonTaskAdditionalInformationsSerializer JSON_TASK_ADDITIONAL_INFORMATIONS_SERIALIZER = new JsonTaskAdditionalInformationsSerializer(MappingsSourcesMigrationTaskAdditionalInformationDTO.serializationModule(MappingsSourcesMigration.TYPE));
diff --git a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultDeleteTask.java b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultDeleteTask.java
index bb5f4e6..0ab9b08 100644
--- a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultDeleteTask.java
+++ b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultDeleteTask.java
@@ -19,6 +19,8 @@
 
 package org.apache.james.webadmin.vault.routes;
 
+import java.time.Clock;
+import java.time.Instant;
 import java.util.Optional;
 
 import javax.inject.Inject;
@@ -58,10 +60,12 @@ public class DeletedMessagesVaultDeleteTask implements Task {
 
         private final User user;
         private final MessageId deleteMessageId;
+        private final Instant timestamp;
 
-        AdditionalInformation(User user, MessageId deleteMessageId) {
+        AdditionalInformation(User user, MessageId deleteMessageId, Instant timestamp) {
             this.user = user;
             this.deleteMessageId = deleteMessageId;
+            this.timestamp = timestamp;
         }
 
         public String getUser() {
@@ -71,6 +75,11 @@ public class DeletedMessagesVaultDeleteTask implements Task {
         public String getDeleteMessageId() {
             return deleteMessageId.serialize();
         }
+
+        @Override
+        public Instant timestamp() {
+            return timestamp;
+        }
     }
 
     private final DeletedMessageVault vault;
@@ -107,7 +116,7 @@ public class DeletedMessagesVaultDeleteTask implements Task {
 
     @Override
     public Optional<TaskExecutionDetails.AdditionalInformation> details() {
-        return Optional.of(new AdditionalInformation(user, messageId));
+        return Optional.of(new AdditionalInformation(user, messageId, Clock.systemUTC().instant()));
     }
 
 }
diff --git a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultDeleteTaskAdditionalInformationDTO.java b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultDeleteTaskAdditionalInformationDTO.java
index ae55cbf..d5b1445 100644
--- a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultDeleteTaskAdditionalInformationDTO.java
+++ b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultDeleteTaskAdditionalInformationDTO.java
@@ -18,6 +18,8 @@
  ****************************************************************/
 package org.apache.james.webadmin.vault.routes;
 
+import java.time.Instant;
+
 import org.apache.james.core.User;
 import org.apache.james.json.DTOModule;
 import org.apache.james.mailbox.model.MessageId;
@@ -31,8 +33,8 @@ public class DeletedMessagesVaultDeleteTaskAdditionalInformationDTO implements A
     public static AdditionalInformationDTOModule<DeletedMessagesVaultDeleteTask.AdditionalInformation, DeletedMessagesVaultDeleteTaskAdditionalInformationDTO> serializationModule(MessageId.Factory factory) {
         return DTOModule.forDomainObject(DeletedMessagesVaultDeleteTask.AdditionalInformation.class)
             .convertToDTO(DeletedMessagesVaultDeleteTaskAdditionalInformationDTO.class)
-            .toDomainObjectConverter(dto -> new DeletedMessagesVaultDeleteTask.AdditionalInformation(User.fromUsername(dto.userName), factory.fromString(dto.getMessageId())))
-            .toDTOConverter((details, type) -> new DeletedMessagesVaultDeleteTaskAdditionalInformationDTO(type, details.getUser(), details.getDeleteMessageId()))
+            .toDomainObjectConverter(dto -> new DeletedMessagesVaultDeleteTask.AdditionalInformation(User.fromUsername(dto.userName), factory.fromString(dto.getMessageId()), dto.getTimestamp()))
+            .toDTOConverter((details, type) -> new DeletedMessagesVaultDeleteTaskAdditionalInformationDTO(type, details.getUser(), details.getDeleteMessageId(), details.timestamp()))
             .typeName(DeletedMessagesVaultDeleteTask.TYPE.asString())
             .withFactory(AdditionalInformationDTOModule::new);
     }
@@ -40,13 +42,16 @@ public class DeletedMessagesVaultDeleteTaskAdditionalInformationDTO implements A
     private final String type;
     private final String userName;
     private final String messageId;
+    private final Instant timestamp;
 
     public DeletedMessagesVaultDeleteTaskAdditionalInformationDTO(@JsonProperty("type") String type,
                                                                   @JsonProperty("userName") String userName,
-                                                                  @JsonProperty("messageId") String messageId) {
+                                                                  @JsonProperty("messageId") String messageId,
+                                                                  @JsonProperty("timestamp") Instant timestamp) {
         this.type = type;
         this.userName = userName;
         this.messageId = messageId;
+        this.timestamp = timestamp;
     }
 
     public String getMessageId() {
@@ -58,6 +63,11 @@ public class DeletedMessagesVaultDeleteTaskAdditionalInformationDTO implements A
     }
 
     @Override
+    public Instant getTimestamp() {
+        return timestamp;
+    }
+
+    @Override
     public String getType() {
         return type;
     }
diff --git a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultExportTask.java b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultExportTask.java
index 37357f6..3ca6b47 100644
--- a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultExportTask.java
+++ b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultExportTask.java
@@ -20,6 +20,8 @@
 package org.apache.james.webadmin.vault.routes;
 
 import java.io.IOException;
+import java.time.Clock;
+import java.time.Instant;
 import java.util.Optional;
 import java.util.concurrent.atomic.AtomicLong;
 
@@ -29,7 +31,6 @@ import org.apache.james.task.Task;
 import org.apache.james.task.TaskExecutionDetails;
 import org.apache.james.task.TaskType;
 import org.apache.james.vault.search.Query;
-
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -44,11 +45,13 @@ public class DeletedMessagesVaultExportTask implements Task {
         private final User userExportFrom;
         private final MailAddress exportTo;
         private final long totalExportedMessages;
+        private final Instant timestamp;
 
-        public AdditionalInformation(User userExportFrom, MailAddress exportTo, long totalExportedMessages) {
+        public AdditionalInformation(User userExportFrom, MailAddress exportTo, long totalExportedMessages, Instant timestamp) {
             this.userExportFrom = userExportFrom;
             this.exportTo = exportTo;
             this.totalExportedMessages = totalExportedMessages;
+            this.timestamp = timestamp;
         }
 
         public String getUserExportFrom() {
@@ -62,6 +65,11 @@ public class DeletedMessagesVaultExportTask implements Task {
         public long getTotalExportedMessages() {
             return totalExportedMessages;
         }
+
+        @Override
+        public Instant timestamp() {
+            return timestamp;
+        }
     }
 
     private static final Logger LOGGER = LoggerFactory.getLogger(DeletedMessagesVaultExportTask.class);
@@ -100,7 +108,7 @@ public class DeletedMessagesVaultExportTask implements Task {
 
     @Override
     public Optional<TaskExecutionDetails.AdditionalInformation> details() {
-        return Optional.of(new AdditionalInformation(userExportFrom, exportTo, totalExportedMessages.get()));
+        return Optional.of(new AdditionalInformation(userExportFrom, exportTo, totalExportedMessages.get(), Clock.systemUTC().instant()));
     }
 
     User getUserExportFrom() {
diff --git a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultExportTaskAdditionalInformationDTO.java b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultExportTaskAdditionalInformationDTO.java
index 2e56836..76b3c23 100644
--- a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultExportTaskAdditionalInformationDTO.java
+++ b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultExportTaskAdditionalInformationDTO.java
@@ -20,6 +20,8 @@
 
 package org.apache.james.webadmin.vault.routes;
 
+import java.time.Instant;
+
 import javax.mail.internet.AddressException;
 
 import org.apache.james.core.MailAddress;
@@ -37,7 +39,8 @@ public class DeletedMessagesVaultExportTaskAdditionalInformationDTO implements A
             type,
             additionalInformation.getUserExportFrom(),
             additionalInformation.getExportTo(),
-            additionalInformation.getTotalExportedMessages()
+            additionalInformation.getTotalExportedMessages(),
+            additionalInformation.timestamp()
         );
     }
 
@@ -54,15 +57,18 @@ public class DeletedMessagesVaultExportTaskAdditionalInformationDTO implements A
     private final String userExportFrom;
     private final String exportTo;
     private final Long totalExportedMessages;
+    private final Instant timestamp;
 
     public DeletedMessagesVaultExportTaskAdditionalInformationDTO(@JsonProperty("type") String type,
                                                                   @JsonProperty("user") String userExportFrom,
                                                                   @JsonProperty("exportTo") String exportTo,
-                                                                  @JsonProperty("errorRestoreCount") Long totalExportedMessages) {
+                                                                  @JsonProperty("errorRestoreCount") Long totalExportedMessages,
+                                                                  @JsonProperty("timestamp") Instant timestamp) {
         this.type = type;
         this.userExportFrom = userExportFrom;
         this.exportTo = exportTo;
         this.totalExportedMessages = totalExportedMessages;
+        this.timestamp = timestamp;
     }
 
     public String getUserExportFrom() {
@@ -82,7 +88,8 @@ public class DeletedMessagesVaultExportTaskAdditionalInformationDTO implements A
             return new DeletedMessagesVaultExportTask.AdditionalInformation(
                 User.fromUsername(userExportFrom),
                 new MailAddress(exportTo),
-                totalExportedMessages
+                totalExportedMessages,
+                timestamp
             );
         } catch (AddressException e) {
             throw new RuntimeException(e);
@@ -93,4 +100,9 @@ public class DeletedMessagesVaultExportTaskAdditionalInformationDTO implements A
     public String getType() {
         return type;
     }
+
+    @Override
+    public Instant getTimestamp() {
+        return timestamp;
+    }
 }
diff --git a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRestoreTask.java b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRestoreTask.java
index 4b06e4f..4b35ee4 100644
--- a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRestoreTask.java
+++ b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRestoreTask.java
@@ -21,6 +21,8 @@ package org.apache.james.webadmin.vault.routes;
 
 import static org.apache.james.webadmin.vault.routes.RestoreService.RestoreResult.RESTORE_SUCCEED;
 
+import java.time.Clock;
+import java.time.Instant;
 import java.util.Optional;
 import java.util.concurrent.atomic.AtomicLong;
 
@@ -43,11 +45,13 @@ public class DeletedMessagesVaultRestoreTask implements Task {
         private final User user;
         private final long successfulRestoreCount;
         private final long errorRestoreCount;
+        private final Instant timestamp;
 
-        AdditionalInformation(User user, long successfulRestoreCount, long errorRestoreCount) {
+        AdditionalInformation(User user, long successfulRestoreCount, long errorRestoreCount, Instant timestamp) {
             this.user = user;
             this.successfulRestoreCount = successfulRestoreCount;
             this.errorRestoreCount = errorRestoreCount;
+            this.timestamp = timestamp;
         }
 
         public long getSuccessfulRestoreCount() {
@@ -61,6 +65,11 @@ public class DeletedMessagesVaultRestoreTask implements Task {
         public String getUser() {
             return user.asString();
         }
+
+        @Override
+        public Instant timestamp() {
+            return timestamp;
+        }
     }
 
     private static final Logger LOGGER = LoggerFactory.getLogger(DeletedMessagesVaultRestoreTask.class);
@@ -127,7 +136,7 @@ public class DeletedMessagesVaultRestoreTask implements Task {
 
     @Override
     public Optional<TaskExecutionDetails.AdditionalInformation> details() {
-        return Optional.of(new AdditionalInformation(userToRestore, successfulRestoreCount.get(), errorRestoreCount.get()));
+        return Optional.of(new AdditionalInformation(userToRestore, successfulRestoreCount.get(), errorRestoreCount.get(), Clock.systemUTC().instant()));
     }
 
     User getUserToRestore() {
diff --git a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRestoreTaskAdditionalInformationDTO.java b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRestoreTaskAdditionalInformationDTO.java
index 6582a61..5f0d630 100644
--- a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRestoreTaskAdditionalInformationDTO.java
+++ b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRestoreTaskAdditionalInformationDTO.java
@@ -20,6 +20,8 @@
 
 package org.apache.james.webadmin.vault.routes;
 
+import java.time.Instant;
+
 import org.apache.james.core.User;
 import org.apache.james.json.DTOModule;
 import org.apache.james.server.task.json.dto.AdditionalInformationDTO;
@@ -34,7 +36,8 @@ public class DeletedMessagesVaultRestoreTaskAdditionalInformationDTO implements
             type,
             additionalInformation.getUser(),
             additionalInformation.getSuccessfulRestoreCount(),
-            additionalInformation.getErrorRestoreCount()
+            additionalInformation.getErrorRestoreCount(),
+            additionalInformation.timestamp()
         );
     }
 
@@ -51,15 +54,18 @@ public class DeletedMessagesVaultRestoreTaskAdditionalInformationDTO implements
     private final String user;
     private final Long successfulRestoreCount;
     private final Long errorRestoreCount;
+    private final Instant timestamp;
 
     public DeletedMessagesVaultRestoreTaskAdditionalInformationDTO(@JsonProperty("type") String type,
                                                                    @JsonProperty("user") String user,
                                                                    @JsonProperty("successfulRestoreCount") Long successfulRestoreCount,
-                                                                   @JsonProperty("errorRestoreCount") Long errorRestoreCount) {
+                                                                   @JsonProperty("errorRestoreCount") Long errorRestoreCount,
+                                                                   @JsonProperty("timestamp") Instant timestamp) {
         this.type = type;
         this.user = user;
         this.successfulRestoreCount = successfulRestoreCount;
         this.errorRestoreCount = errorRestoreCount;
+        this.timestamp = timestamp;
     }
 
     public String getUser() {
@@ -78,11 +84,17 @@ public class DeletedMessagesVaultRestoreTaskAdditionalInformationDTO implements
         return new DeletedMessagesVaultRestoreTask.AdditionalInformation(
             User.fromUsername(user),
             successfulRestoreCount,
-            errorRestoreCount
+            errorRestoreCount,
+            timestamp
         );
     }
 
     @Override
+    public Instant getTimestamp() {
+        return timestamp;
+    }
+
+    @Override
     public String getType() {
         return type;
     }
diff --git a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultDeleteTaskSerializationTest.java b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultDeleteTaskSerializationTest.java
index 349cda7..bba39d9 100644
--- a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultDeleteTaskSerializationTest.java
+++ b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultDeleteTaskSerializationTest.java
@@ -23,6 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
 
 import java.io.IOException;
+import java.time.Instant;
 
 import org.apache.james.core.User;
 import org.apache.james.mailbox.model.MessageId;
@@ -37,6 +38,8 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 
 class DeletedMessagesVaultDeleteTaskSerializationTest {
 
+    private static final Instant TIMESTAMP = Instant.parse("2018-11-13T12:00:55Z");
+
     private DeletedMessageVault deletedMessageVault;
     private JsonTaskSerializer taskSerializer;
     private final String username = "james";
@@ -46,7 +49,7 @@ class DeletedMessagesVaultDeleteTaskSerializationTest {
     private final MessageId messageId = messageIdFactory.generate();
 
     private final String serializedDeleteMessagesVaultDeleteTask = "{\"type\": \"deletedMessages/delete\", \"userName\":\"james\", \"messageId\": \"" + messageId.serialize() + "\"}";
-    private final String serializedAdditionalInformation = "{\"type\": \"deletedMessages/delete\", \"userName\":\"james\", \"messageId\": \"" + messageId.serialize() + "\"}";
+    private final String serializedAdditionalInformation = "{\"type\": \"deletedMessages/delete\", \"userName\":\"james\", \"messageId\": \"" + messageId.serialize() + "\", \"timestamp\":\"2018-11-13T12:00:55Z\"}";
 
     private JsonTaskAdditionalInformationsSerializer jsonAdditionalInformationSerializer;
 
@@ -77,13 +80,13 @@ class DeletedMessagesVaultDeleteTaskSerializationTest {
 
     @Test
     void additionalInformationShouldBeSerializable() throws JsonProcessingException {
-        DeletedMessagesVaultDeleteTask.AdditionalInformation details = new DeletedMessagesVaultDeleteTask.AdditionalInformation(user, messageId);
+        DeletedMessagesVaultDeleteTask.AdditionalInformation details = new DeletedMessagesVaultDeleteTask.AdditionalInformation(user, messageId, TIMESTAMP);
         assertThatJson(jsonAdditionalInformationSerializer.serialize(details)).isEqualTo(serializedAdditionalInformation);
     }
 
     @Test
     void additonalInformationShouldBeDeserializable() throws IOException {
-        DeletedMessagesVaultDeleteTask.AdditionalInformation details = new DeletedMessagesVaultDeleteTask.AdditionalInformation(user, messageId);
+        DeletedMessagesVaultDeleteTask.AdditionalInformation details = new DeletedMessagesVaultDeleteTask.AdditionalInformation(user, messageId, TIMESTAMP);
         assertThat(jsonAdditionalInformationSerializer.deserialize(serializedAdditionalInformation))
             .isEqualToComparingFieldByField(details);
     }
diff --git a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultExportTaskSerializationTest.java b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultExportTaskSerializationTest.java
index ab01f44..e62a7b2 100644
--- a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultExportTaskSerializationTest.java
+++ b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultExportTaskSerializationTest.java
@@ -24,6 +24,7 @@ import static org.assertj.core.api.Assertions.assertThatCode;
 import static org.mockito.Mockito.mock;
 
 import java.io.IOException;
+import java.time.Instant;
 
 import javax.mail.internet.AddressException;
 
@@ -44,6 +45,8 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 
 class DeletedMessagesVaultExportTaskSerializationTest {
 
+    private static final Instant TIMESTAMP = Instant.parse("2018-11-13T12:00:55Z");
+
     private ExportService exportService;
     private final TestId.Factory mailboxIdFactory = new TestId.Factory();
     private final QueryTranslator queryTranslator = new QueryTranslator(mailboxIdFactory);
@@ -60,14 +63,14 @@ class DeletedMessagesVaultExportTaskSerializationTest {
         "\"userExportFrom\":\"james\"," +
         "\"exportQuery\":{\"combinator\":\"and\",\"criteria\":[{\"fieldName\":\"hasAttachment\",\"operator\":\"equals\",\"value\":\"true\"}]}," +
         "\"exportTo\":\"james@apache.org\"}\n";
-    private static final String SERIALIZED_ADDITIONAL_INFORMATION_TASK = "{\"type\":\"deletedMessages/export\", \"exportTo\":\"james@apache.org\",\"userExportFrom\":\"james\",\"totalExportedMessages\":42}";
+    private static final String SERIALIZED_ADDITIONAL_INFORMATION_TASK = "{\"type\":\"deletedMessages/export\", \"exportTo\":\"james@apache.org\",\"userExportFrom\":\"james\",\"totalExportedMessages\":42, \"timestamp\":\"2018-11-13T12:00:55Z\"}";
 
     private static final JsonTaskAdditionalInformationsSerializer JSON_TASK_ADDITIONAL_INFORMATIONS_SERIALIZER = new JsonTaskAdditionalInformationsSerializer(DeletedMessagesVaultExportTaskAdditionalInformationDTO.MODULE);
 
     @BeforeAll
     static void init() throws AddressException {
         exportTo = new MailAddress("james@apache.org");
-        details = new DeletedMessagesVaultExportTask.AdditionalInformation(USER_EXPORT_FROM, exportTo, 42);
+        details = new DeletedMessagesVaultExportTask.AdditionalInformation(USER_EXPORT_FROM, exportTo, 42, TIMESTAMP);
     }
 
     @BeforeEach
diff --git a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRestoreTaskSerializationTest.java b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRestoreTaskSerializationTest.java
index 6304f96..50c3810 100644
--- a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRestoreTaskSerializationTest.java
+++ b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRestoreTaskSerializationTest.java
@@ -23,6 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
 
 import java.io.IOException;
+import java.time.Instant;
 
 import org.apache.james.core.User;
 import org.apache.james.mailbox.model.TestId;
@@ -39,6 +40,8 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 
 class DeletedMessagesVaultRestoreTaskSerializationTest {
 
+    private static final Instant TIMESTAMP = Instant.parse("2018-11-13T12:00:55Z");
+
     private RestoreService exportService;
     private final TestId.Factory mailboxIdFactory = new TestId.Factory();
     private final QueryTranslator queryTranslator = new QueryTranslator(mailboxIdFactory);
@@ -48,13 +51,13 @@ class DeletedMessagesVaultRestoreTaskSerializationTest {
     private static final String USERNAME = "james";
     private static final User USER_TO_RESTORE = User.fromUsername(USERNAME);
     private static final Query QUERY = Query.of(CriterionFactory.hasAttachment(true));
-    private static final DeletedMessagesVaultRestoreTask.AdditionalInformation DETAILS = new DeletedMessagesVaultRestoreTask.AdditionalInformation(USER_TO_RESTORE,42, 10);
+    private static final DeletedMessagesVaultRestoreTask.AdditionalInformation DETAILS = new DeletedMessagesVaultRestoreTask.AdditionalInformation(USER_TO_RESTORE,42, 10, TIMESTAMP);
 
     private static final String SERIALIZED_DELETE_MESSAGES_VAULT_RESTORE_TASK = "{\"type\":\"deletedMessages/restore\"," +
         "\"userToRestore\":\"james\"," +
         "\"query\":{\"combinator\":\"and\",\"criteria\":[{\"fieldName\":\"hasAttachment\",\"operator\":\"equals\",\"value\":\"true\"}]}" +
         "}";
-    private static final String SERIALIZED_ADDITIONAL_INFORMATION_TASK = "{\"type\":\"deletedMessages/restore\", \"user\":\"james\",\"successfulRestoreCount\":42,\"errorRestoreCount\":10}";
+    private static final String SERIALIZED_ADDITIONAL_INFORMATION_TASK = "{\"type\":\"deletedMessages/restore\", \"user\":\"james\",\"successfulRestoreCount\":42,\"errorRestoreCount\":10, \"timestamp\":\"2018-11-13T12:00:55Z\"}";
 
     private static final JsonTaskAdditionalInformationsSerializer JSON_TASK_ADDITIONAL_INFORMATIONS_SERIALIZER = new JsonTaskAdditionalInformationsSerializer(DeletedMessagesVaultRestoreTaskAdditionalInformationDTO.MODULE);
 
diff --git a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/EventDeadLettersRedeliverAllTask.java b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/EventDeadLettersRedeliverAllTask.java
index d23f0d4..9087e0b 100644
--- a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/EventDeadLettersRedeliverAllTask.java
+++ b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/EventDeadLettersRedeliverAllTask.java
@@ -19,6 +19,7 @@
 
 package org.apache.james.webadmin.service;
 
+import java.time.Clock;
 import java.util.Optional;
 import java.util.concurrent.atomic.AtomicLong;
 
@@ -81,6 +82,7 @@ public class EventDeadLettersRedeliverAllTask implements Task {
     EventDeadLettersRedeliveryTaskAdditionalInformation createAdditionalInformation() {
         return new EventDeadLettersRedeliveryTaskAdditionalInformationForAll(
             successfulRedeliveriesCount.get(),
-            failedRedeliveriesCount.get());
+            failedRedeliveriesCount.get(),
+            Clock.systemUTC().instant());
     }
 }
diff --git a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/EventDeadLettersRedeliverGroupTask.java b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/EventDeadLettersRedeliverGroupTask.java
index 678e3cd..71958fb 100644
--- a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/EventDeadLettersRedeliverGroupTask.java
+++ b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/EventDeadLettersRedeliverGroupTask.java
@@ -19,6 +19,7 @@
 
 package org.apache.james.webadmin.service;
 
+import java.time.Clock;
 import java.util.Optional;
 import java.util.concurrent.atomic.AtomicLong;
 
@@ -89,6 +90,7 @@ public class EventDeadLettersRedeliverGroupTask implements Task {
         return new EventDeadLettersRedeliveryTaskAdditionalInformationForGroup(
             successfulRedeliveriesCount.get(),
             failedRedeliveriesCount.get(),
-            eventRetriever.forGroup());
+            eventRetriever.forGroup(),
+            Clock.systemUTC().instant());
     }
 }
diff --git a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/EventDeadLettersRedeliverOneTask.java b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/EventDeadLettersRedeliverOneTask.java
index d1a0973..8b15859 100644
--- a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/EventDeadLettersRedeliverOneTask.java
+++ b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/EventDeadLettersRedeliverOneTask.java
@@ -19,6 +19,7 @@
 
 package org.apache.james.webadmin.service;
 
+import java.time.Clock;
 import java.util.Optional;
 import java.util.concurrent.atomic.AtomicLong;
 
@@ -27,7 +28,6 @@ import org.apache.james.mailbox.events.Group;
 import org.apache.james.task.Task;
 import org.apache.james.task.TaskExecutionDetails;
 import org.apache.james.task.TaskType;
-import org.apache.james.webadmin.service.EventDeadLettersRedeliveryTaskAdditionalInformationDTO.EventDeadLettersRedeliveryTaskAdditionalInformationForOne;
 
 public class EventDeadLettersRedeliverOneTask implements Task {
     public static final TaskType TYPE = TaskType.of("eventDeadLettersRedeliverOneTask");
@@ -93,10 +93,11 @@ public class EventDeadLettersRedeliverOneTask implements Task {
     }
 
     EventDeadLettersRedeliveryTaskAdditionalInformation createAdditionalInformation() {
-        return new EventDeadLettersRedeliveryTaskAdditionalInformationForOne(
+        return new EventDeadLettersRedeliveryTaskAdditionalInformationDTO.EventDeadLettersRedeliveryTaskAdditionalInformationForOne(
             successfulRedeliveriesCount.get(),
             failedRedeliveriesCount.get(),
             eventRetriever.forGroup(),
-            eventRetriever.forEvent());
+            eventRetriever.forEvent(),
+            Clock.systemUTC().instant());
     }
 }
diff --git a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/EventDeadLettersRedeliveryTaskAdditionalInformation.java b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/EventDeadLettersRedeliveryTaskAdditionalInformation.java
index a6028ac..95efa3f 100644
--- a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/EventDeadLettersRedeliveryTaskAdditionalInformation.java
+++ b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/EventDeadLettersRedeliveryTaskAdditionalInformation.java
@@ -18,6 +18,7 @@
  ****************************************************************/
 package org.apache.james.webadmin.service;
 
+import java.time.Instant;
 import java.util.Optional;
 
 import org.apache.james.mailbox.events.EventDeadLetters;
@@ -31,13 +32,18 @@ public class EventDeadLettersRedeliveryTaskAdditionalInformation implements Task
     private final long failedRedeliveriesCount;
     private final Optional<Group> group;
     private final Optional<EventDeadLetters.InsertionId> insertionId;
+    private final Instant timestamp;
 
-    EventDeadLettersRedeliveryTaskAdditionalInformation(long successfulRedeliveriesCount, long failedRedeliveriesCount,
-                                                        Optional<Group> group, Optional<EventDeadLetters.InsertionId> insertionId) {
+    EventDeadLettersRedeliveryTaskAdditionalInformation(long successfulRedeliveriesCount,
+                                                        long failedRedeliveriesCount,
+                                                        Optional<Group> group,
+                                                        Optional<EventDeadLetters.InsertionId> insertionId,
+                                                        Instant timestamp) {
         this.successfulRedeliveriesCount = successfulRedeliveriesCount;
         this.failedRedeliveriesCount = failedRedeliveriesCount;
         this.group = group;
         this.insertionId = insertionId;
+        this.timestamp = timestamp;
     }
 
     public long getSuccessfulRedeliveriesCount() {
@@ -57,4 +63,9 @@ public class EventDeadLettersRedeliveryTaskAdditionalInformation implements Task
     public Optional<String> getInsertionId() {
         return insertionId.map(insertionId -> insertionId.getId().toString());
     }
+
+    @Override
+    public Instant timestamp() {
+        return timestamp;
+    }
 }
diff --git a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/EventDeadLettersRedeliveryTaskAdditionalInformationDTO.java b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/EventDeadLettersRedeliveryTaskAdditionalInformationDTO.java
index f3ee084..46dce06 100644
--- a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/EventDeadLettersRedeliveryTaskAdditionalInformationDTO.java
+++ b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/EventDeadLettersRedeliveryTaskAdditionalInformationDTO.java
@@ -1,5 +1,6 @@
 package org.apache.james.webadmin.service;
 
+import java.time.Instant;
 import java.util.Optional;
 
 import org.apache.james.json.DTOModule;
@@ -23,8 +24,8 @@ public class EventDeadLettersRedeliveryTaskAdditionalInformationDTO implements A
                 .withFactory(AdditionalInformationDTOModule::new);
 
 
-        EventDeadLettersRedeliveryTaskAdditionalInformationForAll(long successfulRedeliveriesCount, long failedRedeliveriesCount) {
-            super(successfulRedeliveriesCount, failedRedeliveriesCount, Optional.empty(), Optional.empty());
+        EventDeadLettersRedeliveryTaskAdditionalInformationForAll(long successfulRedeliveriesCount, long failedRedeliveriesCount, Instant timestamp) {
+            super(successfulRedeliveriesCount, failedRedeliveriesCount, Optional.empty(), Optional.empty(), timestamp);
         }
     }
 
@@ -39,8 +40,8 @@ public class EventDeadLettersRedeliveryTaskAdditionalInformationDTO implements A
                 .withFactory(AdditionalInformationDTOModule::new);
 
 
-        EventDeadLettersRedeliveryTaskAdditionalInformationForGroup(long successfulRedeliveriesCount, long failedRedeliveriesCount, Optional<Group> group) {
-            super(successfulRedeliveriesCount, failedRedeliveriesCount, group, Optional.empty());
+        EventDeadLettersRedeliveryTaskAdditionalInformationForGroup(long successfulRedeliveriesCount, long failedRedeliveriesCount, Optional<Group> group, Instant timestamp) {
+            super(successfulRedeliveriesCount, failedRedeliveriesCount, group, Optional.empty(), timestamp);
         }
     }
 
@@ -55,8 +56,13 @@ public class EventDeadLettersRedeliveryTaskAdditionalInformationDTO implements A
                 .withFactory(AdditionalInformationDTOModule::new);
 
 
-        EventDeadLettersRedeliveryTaskAdditionalInformationForOne(long successfulRedeliveriesCount, long failedRedeliveriesCount, Optional<Group> group, Optional<EventDeadLetters.InsertionId> insertionId) {
-            super(successfulRedeliveriesCount, failedRedeliveriesCount, group, insertionId);
+        EventDeadLettersRedeliveryTaskAdditionalInformationForOne(
+            long successfulRedeliveriesCount,
+            long failedRedeliveriesCount,
+            Optional<Group> group,
+            Optional<EventDeadLetters.InsertionId> insertionId,
+            Instant timestamp) {
+            super(successfulRedeliveriesCount, failedRedeliveriesCount, group, insertionId, timestamp);
         }
     }
 
@@ -66,20 +72,23 @@ public class EventDeadLettersRedeliveryTaskAdditionalInformationDTO implements A
             domainObject.getSuccessfulRedeliveriesCount(),
             domainObject.getFailedRedeliveriesCount(),
             domainObject.getGroup(),
-            domainObject.getInsertionId());
+            domainObject.getInsertionId(),
+            domainObject.timestamp());
     }
 
     private static EventDeadLettersRedeliveryTaskAdditionalInformationForAll fromAll(EventDeadLettersRedeliveryTaskAdditionalInformationDTO dto) {
         return new EventDeadLettersRedeliveryTaskAdditionalInformationForAll(
             dto.successfulRedeliveriesCount,
-            dto.failedRedeliveriesCount);
+            dto.failedRedeliveriesCount,
+            dto.timestamp);
     }
 
     private static EventDeadLettersRedeliveryTaskAdditionalInformationForGroup fromGroup(EventDeadLettersRedeliveryTaskAdditionalInformationDTO dto) {
         return new EventDeadLettersRedeliveryTaskAdditionalInformationForGroup(
             dto.successfulRedeliveriesCount,
             dto.failedRedeliveriesCount,
-            dto.group.map(Throwing.function(Group::deserialize).sneakyThrow()));
+            dto.group.map(Throwing.function(Group::deserialize).sneakyThrow()),
+            dto.timestamp);
     }
 
     private static EventDeadLettersRedeliveryTaskAdditionalInformationForOne fromOne(EventDeadLettersRedeliveryTaskAdditionalInformationDTO dto) {
@@ -87,7 +96,8 @@ public class EventDeadLettersRedeliveryTaskAdditionalInformationDTO implements A
             dto.successfulRedeliveriesCount,
             dto.failedRedeliveriesCount,
             dto.group.map(Throwing.function(Group::deserialize).sneakyThrow()),
-            dto.insertionId.map(EventDeadLetters.InsertionId::of));
+            dto.insertionId.map(EventDeadLetters.InsertionId::of),
+            dto.timestamp);
     }
 
     private final String type;
@@ -95,18 +105,21 @@ public class EventDeadLettersRedeliveryTaskAdditionalInformationDTO implements A
     private final long failedRedeliveriesCount;
     private final Optional<String> group;
     private final Optional<String> insertionId;
+    private final Instant timestamp;
 
     public EventDeadLettersRedeliveryTaskAdditionalInformationDTO(@JsonProperty("type") String type,
                                                                   @JsonProperty("successfulRedeliveriesCount") long successfulRedeliveriesCount,
                                                                   @JsonProperty("failedRedeliveriesCount") long failedRedeliveriesCount,
                                                                   @JsonProperty("group") Optional<String> group,
-                                                                  @JsonProperty("insertionId") Optional<String> insertionId
+                                                                  @JsonProperty("insertionId") Optional<String> insertionId,
+                                                                  @JsonProperty("timestamp") Instant timestamp
     ) {
         this.type = type;
         this.successfulRedeliveriesCount = successfulRedeliveriesCount;
         this.failedRedeliveriesCount = failedRedeliveriesCount;
         this.group = group;
         this.insertionId = insertionId;
+        this.timestamp = timestamp;
     }
 
 
@@ -127,6 +140,11 @@ public class EventDeadLettersRedeliveryTaskAdditionalInformationDTO implements A
     }
 
     @Override
+    public Instant getTimestamp() {
+        return timestamp;
+    }
+
+    @Override
     public String getType() {
         return type;
     }
diff --git a/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/service/EventDeadLettersRedeliverTaskTest.java b/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/service/EventDeadLettersRedeliverTaskTest.java
index 2e4d451..b7cda3c 100644
--- a/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/service/EventDeadLettersRedeliverTaskTest.java
+++ b/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/service/EventDeadLettersRedeliverTaskTest.java
@@ -24,6 +24,7 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
 
 import java.io.IOException;
+import java.time.Instant;
 import java.util.Optional;
 import java.util.stream.Stream;
 
@@ -46,12 +47,13 @@ import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
 
 class EventDeadLettersRedeliverTaskTest {
+    private static final Instant TIMESTAMP = Instant.parse("2018-11-13T12:00:55Z");
     private static final String SERIALIZED_ALL = "{\"type\":\"eventDeadLettersRedeliverAllTask\"}";
     private static final String SERIALIZED_GROUP = "{\"type\":\"eventDeadLettersRedeliverGroupTask\",\"group\":\"org.apache.james.mailbox.events.GenericGroup-abc\"}";
     private static final String SERIALIZED_ONE = "{\"type\":\"eventDeadLettersRedeliverOneTask\",\"group\":\"org.apache.james.mailbox.events.GenericGroup-abc\",\"insertionId\":\"fcbc3c92-e9a0-4ece-94ed-6e6b45045258\"}";
-    private static final String SERIALIZED_TASK_ADDITIONAL_INFORMATION_ALL = "{\"type\":\"eventDeadLettersRedeliverAllTask\",\"successfulRedeliveriesCount\":10,\"failedRedeliveriesCount\":4}";
-    private static final String SERIALIZED_TASK_ADDITIONAL_INFORMATION_GROUP = "{\"type\":\"eventDeadLettersRedeliverGroupTask\",\"successfulRedeliveriesCount\":10,\"failedRedeliveriesCount\":4,\"group\":\"org.apache.james.mailbox.events.GenericGroup-foo\"}";
-    private static final String SERIALIZED_TASK_ADDITIONAL_INFORMATION_ONE = "{\"type\":\"eventDeadLettersRedeliverOneTask\",\"successfulRedeliveriesCount\":10,\"failedRedeliveriesCount\":4,\"group\":\"org.apache.james.mailbox.events.GenericGroup-foo\",\"insertionId\":\"53db3dd9-80eb-476f-b25a-722ad364905a\"}";
+    private static final String SERIALIZED_TASK_ADDITIONAL_INFORMATION_ALL = "{\"type\":\"eventDeadLettersRedeliverAllTask\",\"successfulRedeliveriesCount\":10,\"failedRedeliveriesCount\":4, \"timestamp\":\"2018-11-13T12:00:55Z\"}";
+    private static final String SERIALIZED_TASK_ADDITIONAL_INFORMATION_GROUP = "{\"type\":\"eventDeadLettersRedeliverGroupTask\",\"successfulRedeliveriesCount\":10,\"failedRedeliveriesCount\":4,\"group\":\"org.apache.james.mailbox.events.GenericGroup-foo\", \"timestamp\":\"2018-11-13T12:00:55Z\"}";
+    private static final String SERIALIZED_TASK_ADDITIONAL_INFORMATION_ONE = "{\"type\":\"eventDeadLettersRedeliverOneTask\",\"successfulRedeliveriesCount\":10,\"failedRedeliveriesCount\":4,\"group\":\"org.apache.james.mailbox.events.GenericGroup-foo\",\"insertionId\":\"53db3dd9-80eb-476f-b25a-722ad364905a\", \"timestamp\":\"2018-11-13T12:00:55Z\"}";
     private static final EventDeadLettersRedeliverService SERVICE = mock(EventDeadLettersRedeliverService.class);
     private static final EventDeadLettersRedeliverAllTask TASK_ALL = new EventDeadLettersRedeliverAllTask(SERVICE);
     private static final EventDeadLettersRedeliverGroupTask TASK_GROUP = new EventDeadLettersRedeliverGroupTask(SERVICE, new GenericGroup("abc"));
@@ -104,15 +106,15 @@ class EventDeadLettersRedeliverTaskTest {
     static Stream<Arguments> additionalInformation() {
         return Stream.of(
             Arguments.of(
-                new EventDeadLettersRedeliveryTaskAdditionalInformationForAll(SUCCESSFUL_REDELIVERY_COUNT, FAILED_REDELIVERY_COUNT),
+                new EventDeadLettersRedeliveryTaskAdditionalInformationForAll(SUCCESSFUL_REDELIVERY_COUNT, FAILED_REDELIVERY_COUNT, TIMESTAMP),
                 SERIALIZED_TASK_ADDITIONAL_INFORMATION_ALL,
                 "eventDeadLettersRedeliverAllTask"),
             Arguments.of(
-                new EventDeadLettersRedeliveryTaskAdditionalInformationForGroup(SUCCESSFUL_REDELIVERY_COUNT, FAILED_REDELIVERY_COUNT, SOME_GROUP),
+                new EventDeadLettersRedeliveryTaskAdditionalInformationForGroup(SUCCESSFUL_REDELIVERY_COUNT, FAILED_REDELIVERY_COUNT, SOME_GROUP, TIMESTAMP),
                 SERIALIZED_TASK_ADDITIONAL_INFORMATION_GROUP,
                 "eventDeadLettersRedeliverGroupTask"),
             Arguments.of(
-                new EventDeadLettersRedeliveryTaskAdditionalInformationForOne(SUCCESSFUL_REDELIVERY_COUNT, FAILED_REDELIVERY_COUNT, SOME_GROUP, SOME_INSERTION_ID),
+                new EventDeadLettersRedeliveryTaskAdditionalInformationForOne(SUCCESSFUL_REDELIVERY_COUNT, FAILED_REDELIVERY_COUNT, SOME_GROUP, SOME_INSERTION_ID, TIMESTAMP),
                 SERIALIZED_TASK_ADDITIONAL_INFORMATION_ONE,
                 "eventDeadLettersRedeliverOneTask")
         );
diff --git a/server/protocols/webadmin/webadmin-mailqueue/src/main/java/org/apache/james/webadmin/service/ClearMailQueueTask.java b/server/protocols/webadmin/webadmin-mailqueue/src/main/java/org/apache/james/webadmin/service/ClearMailQueueTask.java
index f8db79f..ff4d777 100644
--- a/server/protocols/webadmin/webadmin-mailqueue/src/main/java/org/apache/james/webadmin/service/ClearMailQueueTask.java
+++ b/server/protocols/webadmin/webadmin-mailqueue/src/main/java/org/apache/james/webadmin/service/ClearMailQueueTask.java
@@ -19,6 +19,8 @@
 
 package org.apache.james.webadmin.service;
 
+import java.time.Clock;
+import java.time.Instant;
 import java.util.Optional;
 
 import org.apache.james.queue.api.MailQueue;
@@ -35,11 +37,13 @@ public class ClearMailQueueTask implements Task {
         private final String mailQueueName;
         private final long initialCount;
         private final long remainingCount;
+        private final Instant timestamp;
 
-        public AdditionalInformation(String mailQueueName, long initialCount, long remainingCount) {
+        public AdditionalInformation(String mailQueueName, long initialCount, long remainingCount, Instant timestamp) {
             this.mailQueueName = mailQueueName;
             this.initialCount = initialCount;
             this.remainingCount = remainingCount;
+            this.timestamp = timestamp;
         }
 
         public String getMailQueueName() {
@@ -53,6 +57,11 @@ public class ClearMailQueueTask implements Task {
         public long getRemainingCount() {
             return remainingCount;
         }
+
+        @Override
+        public Instant timestamp() {
+            return timestamp;
+        }
     }
 
     public static class UnknownSerializedQueue extends RuntimeException {
@@ -91,7 +100,7 @@ public class ClearMailQueueTask implements Task {
 
     @Override
     public Optional<TaskExecutionDetails.AdditionalInformation> details() {
-        return Optional.of(new AdditionalInformation(queue.getName(), initialCount, getRemainingSize()));
+        return Optional.of(new AdditionalInformation(queue.getName(), initialCount, getRemainingSize(), Clock.systemUTC().instant()));
     }
 
     ManageableMailQueue getQueue() {
diff --git a/server/protocols/webadmin/webadmin-mailqueue/src/main/java/org/apache/james/webadmin/service/ClearMailQueueTaskAdditionalInformationDTO.java b/server/protocols/webadmin/webadmin-mailqueue/src/main/java/org/apache/james/webadmin/service/ClearMailQueueTaskAdditionalInformationDTO.java
index 0c240bd..94763a1 100644
--- a/server/protocols/webadmin/webadmin-mailqueue/src/main/java/org/apache/james/webadmin/service/ClearMailQueueTaskAdditionalInformationDTO.java
+++ b/server/protocols/webadmin/webadmin-mailqueue/src/main/java/org/apache/james/webadmin/service/ClearMailQueueTaskAdditionalInformationDTO.java
@@ -18,6 +18,8 @@
  ****************************************************************/
 package org.apache.james.webadmin.service;
 
+import java.time.Instant;
+
 import org.apache.james.json.DTOModule;
 import org.apache.james.server.task.json.dto.AdditionalInformationDTO;
 import org.apache.james.server.task.json.dto.AdditionalInformationDTOModule;
@@ -32,13 +34,15 @@ public class ClearMailQueueTaskAdditionalInformationDTO implements AdditionalInf
             .toDomainObjectConverter(dto -> new ClearMailQueueTask.AdditionalInformation(
                 dto.mailQueueName,
                 dto.initialCount,
-                dto.remainingCount
+                dto.remainingCount,
+                dto.timestamp
             ))
             .toDTOConverter((details, type) -> new ClearMailQueueTaskAdditionalInformationDTO(
                 type,
                 details.getMailQueueName(),
                 details.getInitialCount(),
-                details.getRemainingCount()))
+                details.getRemainingCount(),
+                details.timestamp()))
             .typeName(ClearMailQueueTask.TYPE.asString())
             .withFactory(AdditionalInformationDTOModule::new);
 
@@ -46,15 +50,18 @@ public class ClearMailQueueTaskAdditionalInformationDTO implements AdditionalInf
     private final String type;
     private final long initialCount;
     private final long remainingCount;
+    private final Instant timestamp;
 
     public ClearMailQueueTaskAdditionalInformationDTO(@JsonProperty("type") String type,
                                                       @JsonProperty("mailQueueName") String mailQueueName,
                                                       @JsonProperty("initialCount") long initialCount,
-                                                      @JsonProperty("remainingCount") long remainingCount) {
+                                                      @JsonProperty("remainingCount") long remainingCount,
+                                                      @JsonProperty("timestamp") Instant timestamp) {
         this.type = type;
         this.mailQueueName = mailQueueName;
         this.initialCount = initialCount;
         this.remainingCount = remainingCount;
+        this.timestamp = timestamp;
     }
 
     public String getMailQueueName() {
@@ -73,4 +80,9 @@ public class ClearMailQueueTaskAdditionalInformationDTO implements AdditionalInf
     public String getType() {
         return type;
     }
+
+    @Override
+    public Instant getTimestamp() {
+        return timestamp;
+    }
 }
diff --git a/server/protocols/webadmin/webadmin-mailqueue/src/main/java/org/apache/james/webadmin/service/DeleteMailsFromMailQueueTask.java b/server/protocols/webadmin/webadmin-mailqueue/src/main/java/org/apache/james/webadmin/service/DeleteMailsFromMailQueueTask.java
index ec5a14c..e55b7d6 100644
--- a/server/protocols/webadmin/webadmin-mailqueue/src/main/java/org/apache/james/webadmin/service/DeleteMailsFromMailQueueTask.java
+++ b/server/protocols/webadmin/webadmin-mailqueue/src/main/java/org/apache/james/webadmin/service/DeleteMailsFromMailQueueTask.java
@@ -19,6 +19,8 @@
 
 package org.apache.james.webadmin.service;
 
+import java.time.Clock;
+import java.time.Instant;
 import java.util.Optional;
 
 import org.apache.james.core.MailAddress;
@@ -42,10 +44,11 @@ public class DeleteMailsFromMailQueueTask implements Task {
         private final Optional<String> sender;
         private final Optional<String> name;
         private final Optional<String> recipient;
+        private final Instant timestamp;
 
         public AdditionalInformation(String mailQueueName, long initialCount, long remainingCount,
                                      Optional<MailAddress> maybeSender, Optional<String> maybeName,
-                                     Optional<MailAddress> maybeRecipient) {
+                                     Optional<MailAddress> maybeRecipient, Instant timestamp) {
             this.mailQueueName = mailQueueName;
             this.initialCount = initialCount;
             this.remainingCount = remainingCount;
@@ -53,6 +56,7 @@ public class DeleteMailsFromMailQueueTask implements Task {
             sender = maybeSender.map(MailAddress::asString);
             name = maybeName;
             recipient = maybeRecipient.map(MailAddress::asString);
+            this.timestamp = timestamp;
         }
 
         public String getMailQueueName() {
@@ -78,6 +82,11 @@ public class DeleteMailsFromMailQueueTask implements Task {
         public Optional<String> getRecipient() {
             return recipient;
         }
+
+        @Override
+        public Instant timestamp() {
+            return timestamp;
+        }
     }
 
     public static class UnknownSerializedQueue extends RuntimeException {
@@ -146,7 +155,7 @@ public class DeleteMailsFromMailQueueTask implements Task {
         return Optional.of(new AdditionalInformation(queue.getName(),
             initialCount,
             getRemainingSize(), maybeSender,
-            maybeName, maybeRecipient));
+            maybeName, maybeRecipient, Clock.systemUTC().instant()));
     }
 
     public long getRemainingSize() {
diff --git a/server/protocols/webadmin/webadmin-mailqueue/src/main/java/org/apache/james/webadmin/service/DeleteMailsFromMailQueueTaskAdditionalInformationDTO.java b/server/protocols/webadmin/webadmin-mailqueue/src/main/java/org/apache/james/webadmin/service/DeleteMailsFromMailQueueTaskAdditionalInformationDTO.java
index 74e44c6..f4c2928 100644
--- a/server/protocols/webadmin/webadmin-mailqueue/src/main/java/org/apache/james/webadmin/service/DeleteMailsFromMailQueueTaskAdditionalInformationDTO.java
+++ b/server/protocols/webadmin/webadmin-mailqueue/src/main/java/org/apache/james/webadmin/service/DeleteMailsFromMailQueueTaskAdditionalInformationDTO.java
@@ -1,5 +1,6 @@
 package org.apache.james.webadmin.service;
 
+import java.time.Instant;
 import java.util.Optional;
 
 import org.apache.james.core.MailAddress;
@@ -29,7 +30,8 @@ public class DeleteMailsFromMailQueueTaskAdditionalInformationDTO implements Add
             domainObject.getName(),
             domainObject.getRecipient(),
             domainObject.getInitialCount(),
-            domainObject.getRemainingCount());
+            domainObject.getRemainingCount(),
+            domainObject.timestamp());
     }
 
     private static DeleteMailsFromMailQueueTask.AdditionalInformation fromDTO(DeleteMailsFromMailQueueTaskAdditionalInformationDTO dto) {
@@ -39,7 +41,8 @@ public class DeleteMailsFromMailQueueTaskAdditionalInformationDTO implements Add
             dto.getRemainingCount(),
             dto.sender.map(Throwing.<String, MailAddress>function(MailAddress::new).sneakyThrow()),
             dto.name,
-            dto.recipient.map(Throwing.<String, MailAddress>function(MailAddress::new).sneakyThrow()));
+            dto.recipient.map(Throwing.<String, MailAddress>function(MailAddress::new).sneakyThrow()),
+            dto.timestamp);
     }
 
 
@@ -50,6 +53,7 @@ public class DeleteMailsFromMailQueueTaskAdditionalInformationDTO implements Add
     private final Optional<String> recipient;
     private final long initialCount;
     private final long remainingCount;
+    private final Instant timestamp;
 
     public DeleteMailsFromMailQueueTaskAdditionalInformationDTO(@JsonProperty("type") String type,
                                                                 @JsonProperty("queue") String queue,
@@ -57,7 +61,8 @@ public class DeleteMailsFromMailQueueTaskAdditionalInformationDTO implements Add
                                                                 @JsonProperty("name") Optional<String> name,
                                                                 @JsonProperty("recipient") Optional<String> recipient,
                                                                 @JsonProperty("initialCount") long initialCount,
-                                                                @JsonProperty("remainingCount") long remainingCount
+                                                                @JsonProperty("remainingCount") long remainingCount,
+                                                                @JsonProperty("timestamp") Instant timestamp
     ) {
         this.type = type;
         this.queue = queue;
@@ -66,6 +71,7 @@ public class DeleteMailsFromMailQueueTaskAdditionalInformationDTO implements Add
         this.recipient = recipient;
         this.initialCount = initialCount;
         this.remainingCount = remainingCount;
+        this.timestamp = timestamp;
     }
 
 
@@ -94,6 +100,11 @@ public class DeleteMailsFromMailQueueTaskAdditionalInformationDTO implements Add
     }
 
     @Override
+    public Instant getTimestamp() {
+        return timestamp;
+    }
+
+    @Override
     public String getType() {
         return type;
     }
diff --git a/server/protocols/webadmin/webadmin-mailqueue/src/test/java/org/apache/james/webadmin/service/ClearMailQueueTaskTest.java b/server/protocols/webadmin/webadmin-mailqueue/src/test/java/org/apache/james/webadmin/service/ClearMailQueueTaskTest.java
index 4dbd21f..bb6cd92 100644
--- a/server/protocols/webadmin/webadmin-mailqueue/src/test/java/org/apache/james/webadmin/service/ClearMailQueueTaskTest.java
+++ b/server/protocols/webadmin/webadmin-mailqueue/src/test/java/org/apache/james/webadmin/service/ClearMailQueueTaskTest.java
@@ -25,6 +25,7 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import java.io.IOException;
+import java.time.Instant;
 import java.util.Optional;
 
 import org.apache.james.queue.api.MailQueueFactory;
@@ -38,13 +39,14 @@ import net.javacrumbs.jsonunit.assertj.JsonAssertions;
 
 class ClearMailQueueTaskTest {
 
+    private static final Instant TIMESTAMP = Instant.parse("2018-11-13T12:00:55Z");
     private static final String SERIALIZED = "{\"type\": \"clear-mail-queue\", \"queue\": \"anyQueue\"}";
     private static final String QUEUE_NAME = "anyQueue";
     private static final long INITIAL_COUNT = 0L;
     private static final long REMAINING_COUNT = 10L;
     private JsonTaskAdditionalInformationsSerializer jsonAdditionalInformationSerializer = new JsonTaskAdditionalInformationsSerializer(
         ClearMailQueueTaskAdditionalInformationDTO.SERIALIZATION_MODULE);
-    private static final String SERIALIZED_TASK_ADDITIONAL_INFORMATION = "{\"type\": \"clear-mail-queue\", \"mailQueueName\":\"anyQueue\",\"initialCount\":0,\"remainingCount\":10}";
+    private static final String SERIALIZED_TASK_ADDITIONAL_INFORMATION = "{\"type\": \"clear-mail-queue\", \"mailQueueName\":\"anyQueue\",\"initialCount\":0,\"remainingCount\":10, \"timestamp\":\"2018-11-13T12:00:55Z\"}";
 
     @Test
     void taskShouldBeSerializable() throws Exception {
@@ -85,13 +87,13 @@ class ClearMailQueueTaskTest {
 
     @Test
     void additionalInformationShouldBeSerializable() throws JsonProcessingException {
-        ClearMailQueueTask.AdditionalInformation details = new ClearMailQueueTask.AdditionalInformation(QUEUE_NAME, INITIAL_COUNT, REMAINING_COUNT);
+        ClearMailQueueTask.AdditionalInformation details = new ClearMailQueueTask.AdditionalInformation(QUEUE_NAME, INITIAL_COUNT, REMAINING_COUNT, TIMESTAMP);
         assertThatJson(jsonAdditionalInformationSerializer.serialize(details)).isEqualTo(SERIALIZED_TASK_ADDITIONAL_INFORMATION);
     }
 
     @Test
     void additionalInformationShouldBeDeserializable() throws IOException {
-        ClearMailQueueTask.AdditionalInformation details = new ClearMailQueueTask.AdditionalInformation(QUEUE_NAME, INITIAL_COUNT, REMAINING_COUNT);
+        ClearMailQueueTask.AdditionalInformation details = new ClearMailQueueTask.AdditionalInformation(QUEUE_NAME, INITIAL_COUNT, REMAINING_COUNT, TIMESTAMP);
         assertThat(jsonAdditionalInformationSerializer.deserialize(SERIALIZED_TASK_ADDITIONAL_INFORMATION))
             .isEqualToComparingFieldByField(details);
     }
diff --git a/server/protocols/webadmin/webadmin-mailqueue/src/test/java/org/apache/james/webadmin/service/DeleteMailsFromMailQueueTaskTest.java b/server/protocols/webadmin/webadmin-mailqueue/src/test/java/org/apache/james/webadmin/service/DeleteMailsFromMailQueueTaskTest.java
index adee47e..33092c5 100644
--- a/server/protocols/webadmin/webadmin-mailqueue/src/test/java/org/apache/james/webadmin/service/DeleteMailsFromMailQueueTaskTest.java
+++ b/server/protocols/webadmin/webadmin-mailqueue/src/test/java/org/apache/james/webadmin/service/DeleteMailsFromMailQueueTaskTest.java
@@ -25,6 +25,7 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import java.io.IOException;
+import java.time.Instant;
 import java.util.Optional;
 import java.util.stream.Stream;
 
@@ -45,6 +46,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 
 class DeleteMailsFromMailQueueTaskTest {
 
+    private static final Instant TIMESTAMP = Instant.parse("2018-11-13T12:00:55Z");
     private MailQueueFactory<ManageableMailQueue> mailQueueFactory;
     private ManageableMailQueue mockedQueue;
     private final static String queueName = "anyQueue";
@@ -117,9 +119,9 @@ class DeleteMailsFromMailQueueTaskTest {
 
     private static Stream<Arguments> validAdditionalInformations() throws Exception {
         return Stream.of(
-            Arguments.of(10L, 5L, Optional.of(new MailAddress("a@b.c")), Optional.empty(), Optional.empty(), "{\"type\": \"delete-mails-from-mail-queue\", \"queue\": \"anyQueue\", \"sender\": \"a@b.c\", \"initialCount\" : 10, \"remainingCount\":5}"),
-            Arguments.of(1L, 0L, Optional.empty(), Optional.of("name"), Optional.empty(), "{\"type\": \"delete-mails-from-mail-queue\", \"queue\": \"anyQueue\", \"name\": \"name\", \"initialCount\" : 1, \"remainingCount\":0}"),
-            Arguments.of(6L, 6L, Optional.empty(), Optional.empty(), Optional.of(new MailAddress("d@e.f")), "{\"type\": \"delete-mails-from-mail-queue\", \"queue\": \"anyQueue\", \"recipient\": \"d@e.f\", \"initialCount\" : 6, \"remainingCount\":6}")
+            Arguments.of(10L, 5L, Optional.of(new MailAddress("a@b.c")), Optional.empty(), Optional.empty(), "{\"type\": \"delete-mails-from-mail-queue\", \"queue\": \"anyQueue\", \"sender\": \"a@b.c\", \"initialCount\" : 10, \"remainingCount\":5, \"timestamp\": \"2018-11-13T12:00:55Z\"}"),
+            Arguments.of(1L, 0L, Optional.empty(), Optional.of("name"), Optional.empty(), "{\"type\": \"delete-mails-from-mail-queue\", \"queue\": \"anyQueue\", \"name\": \"name\", \"initialCount\" : 1, \"remainingCount\":0, \"timestamp\": \"2018-11-13T12:00:55Z\"}"),
+            Arguments.of(6L, 6L, Optional.empty(), Optional.empty(), Optional.of(new MailAddress("d@e.f")), "{\"type\": \"delete-mails-from-mail-queue\", \"queue\": \"anyQueue\", \"recipient\": \"d@e.f\", \"initialCount\" : 6, \"remainingCount\":6, \"timestamp\": \"2018-11-13T12:00:55Z\"}")
         );
     }
 
@@ -133,7 +135,7 @@ class DeleteMailsFromMailQueueTaskTest {
                                                    String serializedAdditionalInformationJson) throws JsonProcessingException {
 
         ManageableMailQueue queue = mailQueueFactory.getQueue(queueName).get();
-        DeleteMailsFromMailQueueTask.AdditionalInformation details = new DeleteMailsFromMailQueueTask.AdditionalInformation(queue.getName(), initialCount, remainingCount, sender, name, recipient);
+        DeleteMailsFromMailQueueTask.AdditionalInformation details = new DeleteMailsFromMailQueueTask.AdditionalInformation(queue.getName(), initialCount, remainingCount, sender, name, recipient, TIMESTAMP);
 
         assertThatJson(jsonAdditionalInformationsSerializer.serialize(details)).isEqualTo(serializedAdditionalInformationJson);
     }
@@ -150,7 +152,7 @@ class DeleteMailsFromMailQueueTaskTest {
                                                      Optional<String> name,
                                                      Optional<MailAddress> recipient,
                                                      String serializedAdditionalInformationJson) throws IOException {
-        DeleteMailsFromMailQueueTask.AdditionalInformation details = new DeleteMailsFromMailQueueTask.AdditionalInformation(queueName, initialCount, remainingCount, sender, name, recipient);
+        DeleteMailsFromMailQueueTask.AdditionalInformation details = new DeleteMailsFromMailQueueTask.AdditionalInformation(queueName, initialCount, remainingCount, sender, name, recipient, TIMESTAMP);
 
         assertThat(jsonAdditionalInformationsSerializer.deserialize(serializedAdditionalInformationJson))
             .isEqualToComparingFieldByField(details);
diff --git a/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/routes/MailRepositoriesRoutes.java b/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/routes/MailRepositoriesRoutes.java
index 7a956a1..c9fc45e 100644
--- a/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/routes/MailRepositoriesRoutes.java
+++ b/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/routes/MailRepositoriesRoutes.java
@@ -22,6 +22,7 @@ package org.apache.james.webadmin.routes;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
+import java.time.Clock;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
@@ -499,7 +500,7 @@ public class MailRepositoriesRoutes implements Routes {
         Optional<String> targetProcessor = Optional.ofNullable(request.queryParams("processor"));
         String targetQueue = Optional.ofNullable(request.queryParams("queue")).orElse(MailQueueFactory.SPOOL);
 
-        return new ReprocessingOneMailTask(reprocessingService, path, targetQueue, key, targetProcessor);
+        return new ReprocessingOneMailTask(reprocessingService, path, targetQueue, key, targetProcessor, Clock.systemUTC());
     }
 
     private void enforceActionParameter(Request request) {
diff --git a/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/ClearMailRepositoryTask.java b/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/ClearMailRepositoryTask.java
index 824a83e..268f518 100644
--- a/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/ClearMailRepositoryTask.java
+++ b/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/ClearMailRepositoryTask.java
@@ -19,6 +19,8 @@
 
 package org.apache.james.webadmin.service;
 
+import java.time.Clock;
+import java.time.Instant;
 import java.util.List;
 import java.util.Optional;
 
@@ -58,11 +60,13 @@ public class ClearMailRepositoryTask implements Task {
         private final MailRepositoryPath repositoryPath;
         private final long initialCount;
         private final long remainingCount;
+        private final Instant timestamp;
 
-        public AdditionalInformation(MailRepositoryPath repositoryPath, long initialCount, long remainingCount) {
+        public AdditionalInformation(MailRepositoryPath repositoryPath, long initialCount, long remainingCount, Instant timestamp) {
             this.repositoryPath = repositoryPath;
             this.initialCount = initialCount;
             this.remainingCount = remainingCount;
+            this.timestamp = timestamp;
         }
 
         public String getRepositoryPath() {
@@ -76,6 +80,11 @@ public class ClearMailRepositoryTask implements Task {
         public long getInitialCount() {
             return initialCount;
         }
+
+        @Override
+        public Instant timestamp() {
+            return timestamp;
+        }
     }
 
     public static class UrlEncodingFailureSerializationException extends RuntimeException {
@@ -128,7 +137,7 @@ public class ClearMailRepositoryTask implements Task {
 
     @Override
     public Optional<TaskExecutionDetails.AdditionalInformation> details() {
-        return Optional.of(new AdditionalInformation(mailRepositoryPath, initialCount, getRemainingSize()));
+        return Optional.of(new AdditionalInformation(mailRepositoryPath, initialCount, getRemainingSize(), Clock.systemUTC().instant()));
     }
 
     public long getRemainingSize() {
diff --git a/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/ClearMailRepositoryTaskAdditionalInformationDTO.java b/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/ClearMailRepositoryTaskAdditionalInformationDTO.java
index bc08a14..2b9d9f7 100644
--- a/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/ClearMailRepositoryTaskAdditionalInformationDTO.java
+++ b/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/ClearMailRepositoryTaskAdditionalInformationDTO.java
@@ -18,6 +18,8 @@
  ****************************************************************/
 package org.apache.james.webadmin.service;
 
+import java.time.Instant;
+
 import org.apache.james.json.DTOModule;
 import org.apache.james.mailrepository.api.MailRepositoryPath;
 import org.apache.james.server.task.json.dto.AdditionalInformationDTO;
@@ -33,13 +35,15 @@ public class ClearMailRepositoryTaskAdditionalInformationDTO implements Addition
             .toDomainObjectConverter(dto -> new ClearMailRepositoryTask.AdditionalInformation(
                 MailRepositoryPath.from(dto.mailRepositoryPath),
                 dto.initialCount,
-                dto.remainingCount
+                dto.remainingCount,
+                dto.timestamp
             ))
             .toDTOConverter((details, type) -> new ClearMailRepositoryTaskAdditionalInformationDTO(
                 type,
                 details.getRepositoryPath(),
                 details.getInitialCount(),
-                details.getRemainingCount()))
+                details.getRemainingCount(),
+                details.timestamp()))
             .typeName(ClearMailRepositoryTask.TYPE.asString())
             .withFactory(AdditionalInformationDTOModule::new);
 
@@ -47,15 +51,18 @@ public class ClearMailRepositoryTaskAdditionalInformationDTO implements Addition
     private final String type;
     private final long initialCount;
     private final long remainingCount;
+    private final Instant timestamp;
 
     public ClearMailRepositoryTaskAdditionalInformationDTO(@JsonProperty("type") String type,
                                                            @JsonProperty("mailRepositoryPath") String mailRepositoryPath,
                                                            @JsonProperty("initialCount") long initialCount,
-                                                           @JsonProperty("remainingCount") long remainingCount) {
+                                                           @JsonProperty("remainingCount") long remainingCount,
+                                                           @JsonProperty("timestamp") Instant timestamp) {
         this.type = type;
         this.mailRepositoryPath = mailRepositoryPath;
         this.initialCount = initialCount;
         this.remainingCount = remainingCount;
+        this.timestamp = timestamp;
     }
 
     public String getMailRepositoryPath() {
@@ -70,6 +77,10 @@ public class ClearMailRepositoryTaskAdditionalInformationDTO implements Addition
         return remainingCount;
     }
 
+    public Instant getTimestamp() {
+        return timestamp;
+    }
+
     @Override
     public String getType() {
         return type;
diff --git a/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/ReprocessingAllMailsTask.java b/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/ReprocessingAllMailsTask.java
index f524027..fc731ea 100644
--- a/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/ReprocessingAllMailsTask.java
+++ b/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/ReprocessingAllMailsTask.java
@@ -19,6 +19,8 @@
 
 package org.apache.james.webadmin.service;
 
+import java.time.Clock;
+import java.time.Instant;
 import java.util.Optional;
 import java.util.concurrent.atomic.AtomicLong;
 
@@ -41,13 +43,15 @@ public class ReprocessingAllMailsTask implements Task {
         private final Optional<String> targetProcessor;
         private final long initialCount;
         private final long remainingCount;
+        private final Instant timestamp;
 
-        public AdditionalInformation(MailRepositoryPath repositoryPath, String targetQueue, Optional<String> targetProcessor, long initialCount, long remainingCount) {
+        public AdditionalInformation(MailRepositoryPath repositoryPath, String targetQueue, Optional<String> targetProcessor, long initialCount, long remainingCount, Instant timestamp) {
             this.repositoryPath = repositoryPath;
             this.targetQueue = targetQueue;
             this.targetProcessor = targetProcessor;
             this.initialCount = initialCount;
             this.remainingCount = remainingCount;
+            this.timestamp = timestamp;
         }
 
         public String getTargetQueue() {
@@ -70,6 +74,10 @@ public class ReprocessingAllMailsTask implements Task {
             return initialCount;
         }
 
+        @Override
+        public Instant timestamp() {
+            return timestamp;
+        }
     }
 
     public static class UrlEncodingFailureSerializationException extends RuntimeException {
@@ -142,7 +150,8 @@ public class ReprocessingAllMailsTask implements Task {
     @Override
     public Optional<TaskExecutionDetails.AdditionalInformation> details() {
         return Optional.of(new AdditionalInformation(
-            repositoryPath, targetQueue, targetProcessor, repositorySize, repositorySize - processedCount.get()));
+            repositoryPath, targetQueue, targetProcessor, repositorySize, repositorySize - processedCount.get(),
+            Clock.systemUTC().instant()));
     }
 
 }
diff --git a/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/ReprocessingAllMailsTaskAdditionalInformationDTO.java b/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/ReprocessingAllMailsTaskAdditionalInformationDTO.java
index 6f773c2..15b8cee 100644
--- a/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/ReprocessingAllMailsTaskAdditionalInformationDTO.java
+++ b/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/ReprocessingAllMailsTaskAdditionalInformationDTO.java
@@ -18,6 +18,7 @@
  ****************************************************************/
 package org.apache.james.webadmin.service;
 
+import java.time.Instant;
 import java.util.Optional;
 
 import org.apache.james.json.DTOModule;
@@ -37,7 +38,8 @@ public class ReprocessingAllMailsTaskAdditionalInformationDTO implements Additio
                 dto.targetQueue,
                 dto.targetProcessor,
                 dto.initialCount,
-                dto.remainingCount
+                dto.remainingCount,
+                dto.timestamp
             ))
             .toDTOConverter((details, type) -> new ReprocessingAllMailsTaskAdditionalInformationDTO(
                 type,
@@ -45,7 +47,8 @@ public class ReprocessingAllMailsTaskAdditionalInformationDTO implements Additio
                 details.getTargetQueue(),
                 details.getTargetProcessor(),
                 details.getInitialCount(),
-                details.getRemainingCount()))
+                details.getRemainingCount(),
+                details.timestamp()))
             .typeName(ReprocessingAllMailsTask.TYPE.asString())
             .withFactory(AdditionalInformationDTOModule::new);
 
@@ -55,6 +58,7 @@ public class ReprocessingAllMailsTaskAdditionalInformationDTO implements Additio
     private final Optional<String> targetProcessor;
     private final long initialCount;
     private final long remainingCount;
+    private final Instant timestamp;
 
     public ReprocessingAllMailsTaskAdditionalInformationDTO(
         @JsonProperty("type") String type,
@@ -62,13 +66,15 @@ public class ReprocessingAllMailsTaskAdditionalInformationDTO implements Additio
         @JsonProperty("targetQueue") String targetQueue,
         @JsonProperty("targetProcessor") Optional<String> targetProcessor,
         @JsonProperty("initialCount") long initialCount,
-        @JsonProperty("remainingCount") long remainingCount) {
+        @JsonProperty("remainingCount") long remainingCount,
+        @JsonProperty("timestamp") Instant timestamp) {
         this.type = type;
         this.repositoryPath = repositoryPath;
         this.targetQueue = targetQueue;
         this.targetProcessor = targetProcessor;
         this.initialCount = initialCount;
         this.remainingCount = remainingCount;
+        this.timestamp = timestamp;
     }
 
     @Override
@@ -92,6 +98,10 @@ public class ReprocessingAllMailsTaskAdditionalInformationDTO implements Additio
         return targetQueue;
     }
 
+    public Instant getTimestamp() {
+        return timestamp;
+    }
+
     public Optional<String> getTargetProcessor() {
         return targetProcessor;
     }
diff --git a/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/ReprocessingOneMailTask.java b/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/ReprocessingOneMailTask.java
index b4e260b..c470bcd 100644
--- a/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/ReprocessingOneMailTask.java
+++ b/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/ReprocessingOneMailTask.java
@@ -19,6 +19,8 @@
 
 package org.apache.james.webadmin.service;
 
+import java.time.Clock;
+import java.time.Instant;
 import java.util.Optional;
 
 import javax.mail.MessagingException;
@@ -39,12 +41,14 @@ public class ReprocessingOneMailTask implements Task {
         private final String targetQueue;
         private final MailKey mailKey;
         private final Optional<String> targetProcessor;
+        private final Instant timestamp;
 
-        public AdditionalInformation(MailRepositoryPath repositoryPath, String targetQueue, MailKey mailKey, Optional<String> targetProcessor) {
+        public AdditionalInformation(MailRepositoryPath repositoryPath, String targetQueue, MailKey mailKey, Optional<String> targetProcessor, Instant timestamp) {
             this.repositoryPath = repositoryPath;
             this.targetQueue = targetQueue;
             this.mailKey = mailKey;
             this.targetProcessor = targetProcessor;
+            this.timestamp = timestamp;
         }
 
         public String getMailKey() {
@@ -62,6 +66,11 @@ public class ReprocessingOneMailTask implements Task {
         public String getRepositoryPath() {
             return repositoryPath.asString();
         }
+
+        @Override
+        public Instant timestamp() {
+            return timestamp;
+        }
     }
 
     public static class UrlEncodingFailureSerializationException extends RuntimeException {
@@ -86,13 +95,17 @@ public class ReprocessingOneMailTask implements Task {
     private final AdditionalInformation additionalInformation;
 
     public ReprocessingOneMailTask(ReprocessingService reprocessingService,
-                                   MailRepositoryPath repositoryPath, String targetQueue, MailKey mailKey, Optional<String> targetProcessor) {
+                                   MailRepositoryPath repositoryPath,
+                                   String targetQueue,
+                                   MailKey mailKey,
+                                   Optional<String> targetProcessor,
+                                   Clock clock) {
         this.reprocessingService = reprocessingService;
         this.repositoryPath = repositoryPath;
         this.targetQueue = targetQueue;
         this.mailKey = mailKey;
         this.targetProcessor = targetProcessor;
-        this.additionalInformation = new AdditionalInformation(repositoryPath, targetQueue, mailKey, targetProcessor);
+        this.additionalInformation = new AdditionalInformation(repositoryPath, targetQueue, mailKey, targetProcessor, clock.instant());
     }
 
     @Override
diff --git a/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/ReprocessingOneMailTaskAdditionalInformationDTO.java b/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/ReprocessingOneMailTaskAdditionalInformationDTO.java
index 3133dd5..528caf5 100644
--- a/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/ReprocessingOneMailTaskAdditionalInformationDTO.java
+++ b/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/ReprocessingOneMailTaskAdditionalInformationDTO.java
@@ -18,6 +18,7 @@
  ****************************************************************/
 package org.apache.james.webadmin.service;
 
+import java.time.Instant;
 import java.util.Optional;
 
 import org.apache.james.json.DTOModule;
@@ -37,14 +38,16 @@ public class ReprocessingOneMailTaskAdditionalInformationDTO implements Addition
                 MailRepositoryPath.from(dto.repositoryPath),
                 dto.targetQueue,
                 new MailKey(dto.mailKey),
-                dto.targetProcessor
+                dto.targetProcessor,
+                dto.timestamp
             ))
             .toDTOConverter((details, type) -> new ReprocessingOneMailTaskAdditionalInformationDTO(
                 type,
                 details.getRepositoryPath(),
                 details.getTargetQueue(),
                 details.getMailKey(),
-                details.getTargetProcessor()))
+                details.getTargetProcessor(),
+                details.timestamp()))
             .typeName(ReprocessingOneMailTask.TYPE.asString())
             .withFactory(AdditionalInformationDTOModule::new);
 
@@ -53,17 +56,20 @@ public class ReprocessingOneMailTaskAdditionalInformationDTO implements Addition
     private final String targetQueue;
     private final String mailKey;
     private final Optional<String> targetProcessor;
+    private final Instant timestamp;
 
     public ReprocessingOneMailTaskAdditionalInformationDTO(@JsonProperty("type") String type,
                                                            @JsonProperty("repositoryPath") String repositoryPath,
                                                            @JsonProperty("targetQueue") String targetQueue,
                                                            @JsonProperty("mailKey") String mailKey,
-                                                           @JsonProperty("targetProcessor") Optional<String> targetProcessor) {
+                                                           @JsonProperty("targetProcessor") Optional<String> targetProcessor,
+                                                           @JsonProperty("timestamp") Instant timestamp) {
         this.type = type;
         this.repositoryPath = repositoryPath;
         this.targetQueue = targetQueue;
         this.mailKey = mailKey;
         this.targetProcessor = targetProcessor;
+        this.timestamp = timestamp;
     }
 
     @Override
@@ -83,6 +89,10 @@ public class ReprocessingOneMailTaskAdditionalInformationDTO implements Addition
         return mailKey;
     }
 
+    public Instant getTimestamp() {
+        return timestamp;
+    }
+
     public Optional<String> getTargetProcessor() {
         return targetProcessor;
     }
diff --git a/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/ReprocessingOneMailTaskDTO.java b/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/ReprocessingOneMailTaskDTO.java
index 58c2fad..462e598 100644
--- a/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/ReprocessingOneMailTaskDTO.java
+++ b/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/ReprocessingOneMailTaskDTO.java
@@ -18,6 +18,7 @@
  ****************************************************************/
 package org.apache.james.webadmin.service;
 
+import java.time.Clock;
 import java.util.Optional;
 
 import org.apache.james.json.DTOModule;
@@ -30,11 +31,11 @@ import com.fasterxml.jackson.annotation.JsonProperty;
 
 public class ReprocessingOneMailTaskDTO implements TaskDTO {
 
-    public static final TaskDTOModule<ReprocessingOneMailTask, ReprocessingOneMailTaskDTO> module(ReprocessingService reprocessingService) {
+    public static TaskDTOModule<ReprocessingOneMailTask, ReprocessingOneMailTaskDTO> module(Clock clock, ReprocessingService reprocessingService) {
         return DTOModule
             .forDomainObject(ReprocessingOneMailTask.class)
             .convertToDTO(ReprocessingOneMailTaskDTO.class)
-            .toDomainObjectConverter(dto -> dto.fromDTO(reprocessingService))
+            .toDomainObjectConverter(dto -> dto.fromDTO(reprocessingService, clock))
             .toDTOConverter(ReprocessingOneMailTaskDTO::toDTO)
             .typeName(ReprocessingOneMailTask.TYPE.asString())
             .withFactory(TaskDTOModule::new);
@@ -72,13 +73,14 @@ public class ReprocessingOneMailTaskDTO implements TaskDTO {
         this.targetProcessor = targetProcessor;
     }
 
-    public ReprocessingOneMailTask fromDTO(ReprocessingService reprocessingService) {
+    public ReprocessingOneMailTask fromDTO(ReprocessingService reprocessingService, Clock clock) {
         return new ReprocessingOneMailTask(
             reprocessingService,
             getMailRepositoryPath(),
             targetQueue,
             new MailKey(mailKey),
-            targetProcessor
+            targetProcessor,
+            clock
         );
     }
 
diff --git a/server/protocols/webadmin/webadmin-mailrepository/src/test/java/org/apache/james/webadmin/service/ClearMailRepositoryTaskTest.java b/server/protocols/webadmin/webadmin-mailrepository/src/test/java/org/apache/james/webadmin/service/ClearMailRepositoryTaskTest.java
index 5647635..1505b0a 100644
--- a/server/protocols/webadmin/webadmin-mailrepository/src/test/java/org/apache/james/webadmin/service/ClearMailRepositoryTaskTest.java
+++ b/server/protocols/webadmin/webadmin-mailrepository/src/test/java/org/apache/james/webadmin/service/ClearMailRepositoryTaskTest.java
@@ -25,6 +25,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.Mockito.mock;
 
 import java.io.IOException;
+import java.time.Instant;
 
 import org.apache.james.mailrepository.api.MailRepository;
 import org.apache.james.mailrepository.api.MailRepositoryPath;
@@ -38,8 +39,11 @@ import com.google.common.collect.ImmutableList;
 import net.javacrumbs.jsonunit.assertj.JsonAssertions;
 
 class ClearMailRepositoryTaskTest {
+
+    private static final Instant TIMESTAMP = Instant.parse("2018-11-13T12:00:55Z");
+
     private static final String SERIALIZED = "{\"type\":\"clearMailRepository\",\"mailRepositoryPath\":\"a\"}";
-    private static final String SERIALIZED_TASK_ADDITIONAL_INFORMATION = "{\"type\":\"clearMailRepository\", \"mailRepositoryPath\":\"a\", \"initialCount\": 0, \"remainingCount\": 10}";
+    private static final String SERIALIZED_TASK_ADDITIONAL_INFORMATION = "{\"type\":\"clearMailRepository\", \"mailRepositoryPath\":\"a\", \"initialCount\": 0, \"remainingCount\": 10, \"timestamp\":\"2018-11-13T12:00:55Z\"}";
     private static final ClearMailRepositoryTask.Factory FACTORY = new ClearMailRepositoryTask.Factory(mock(MailRepositoryStore.class));
     private static final ImmutableList<MailRepository> MAIL_REPOSITORIES = ImmutableList.of();
     private static final MailRepositoryPath MAIL_REPOSITORY_PATH = MailRepositoryPath.from("a");
@@ -74,13 +78,13 @@ class ClearMailRepositoryTaskTest {
 
     @Test
     void additionalInformationShouldBeSerializable() throws JsonProcessingException {
-        ClearMailRepositoryTask.AdditionalInformation details = new ClearMailRepositoryTask.AdditionalInformation(MAIL_REPOSITORY_PATH, INITIAL_COUNT, REMAINING_COUNT);
+        ClearMailRepositoryTask.AdditionalInformation details = new ClearMailRepositoryTask.AdditionalInformation(MAIL_REPOSITORY_PATH, INITIAL_COUNT, REMAINING_COUNT, TIMESTAMP);
         assertThatJson(jsonAdditionalInformationSerializer.serialize(details)).isEqualTo(SERIALIZED_TASK_ADDITIONAL_INFORMATION);
     }
 
     @Test
     void additionalInformationShouldBeDeserializable() throws IOException {
-        ClearMailRepositoryTask.AdditionalInformation details = new ClearMailRepositoryTask.AdditionalInformation(MAIL_REPOSITORY_PATH, INITIAL_COUNT, REMAINING_COUNT);
+        ClearMailRepositoryTask.AdditionalInformation details = new ClearMailRepositoryTask.AdditionalInformation(MAIL_REPOSITORY_PATH, INITIAL_COUNT, REMAINING_COUNT, TIMESTAMP);
         assertThat(jsonAdditionalInformationSerializer.deserialize(SERIALIZED_TASK_ADDITIONAL_INFORMATION))
             .isEqualToComparingFieldByField(details);
     }
diff --git a/server/protocols/webadmin/webadmin-mailrepository/src/test/java/org/apache/james/webadmin/service/ReprocessingAllMailsTaskTest.java b/server/protocols/webadmin/webadmin-mailrepository/src/test/java/org/apache/james/webadmin/service/ReprocessingAllMailsTaskTest.java
index 10c9153..1910472 100644
--- a/server/protocols/webadmin/webadmin-mailrepository/src/test/java/org/apache/james/webadmin/service/ReprocessingAllMailsTaskTest.java
+++ b/server/protocols/webadmin/webadmin-mailrepository/src/test/java/org/apache/james/webadmin/service/ReprocessingAllMailsTaskTest.java
@@ -25,6 +25,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.Mockito.mock;
 
 import java.io.IOException;
+import java.time.Instant;
 import java.util.Optional;
 import java.util.stream.Stream;
 
@@ -40,6 +41,8 @@ import org.junit.jupiter.params.provider.ValueSource;
 import com.fasterxml.jackson.core.JsonProcessingException;
 
 class ReprocessingAllMailsTaskTest {
+
+    private static final Instant TIMESTAMP = Instant.parse("2018-11-13T12:00:55Z");
     private static final ReprocessingService REPROCESSING_SERVICE = mock(ReprocessingService.class);
     private JsonTaskAdditionalInformationsSerializer jsonAdditionalInformationSerializer = new JsonTaskAdditionalInformationsSerializer(ReprocessingAllMailsTaskAdditionalInformationDTO.SERIALIZATION_MODULE);
     private static final long REPOSITORY_SIZE = 5L;
@@ -50,8 +53,8 @@ class ReprocessingAllMailsTaskTest {
     private static final long REMAINING_COUNT = 3L;
     private static final String SERIALIZED_TASK_WITH_TARGET_PROCESSOR = "{\"type\":\"reprocessingAllTask\",\"repositorySize\":5,\"repositoryPath\":\"a\",\"targetQueue\":\"queue\",\"targetProcessor\":\"targetProcessor\"}";
     private static final String SERIALIZED_TASK_WITHOUT_TARGET_PROCESSOR = "{\"type\":\"reprocessingAllTask\",\"repositorySize\":5,\"repositoryPath\":\"a\",\"targetQueue\":\"queue\"}";
-    private static final String SERIALIZED_TASK_ADDITIONAL_INFORMATION_WITH_TARGET_PROCESSOR = "{\"type\":\"reprocessingAllTask\", \"repositoryPath\":\"a\",\"targetQueue\":\"queue\",\"targetProcessor\":\"targetProcessor\",\"initialCount\":5,\"remainingCount\":3}";
-    private static final String SERIALIZED_TASK_ADDITIONAL_INFORMATION_WITHOUT_TARGET_PROCESSOR = "{\"type\":\"reprocessingAllTask\", \"repositoryPath\":\"a\",\"targetQueue\":\"queue\", \"initialCount\":5,\"remainingCount\":3}";
+    private static final String SERIALIZED_TASK_ADDITIONAL_INFORMATION_WITH_TARGET_PROCESSOR = "{\"type\":\"reprocessingAllTask\", \"repositoryPath\":\"a\",\"targetQueue\":\"queue\",\"targetProcessor\":\"targetProcessor\",\"initialCount\":5,\"remainingCount\":3, \"timestamp\":\"2018-11-13T12:00:55Z\"}";
+    private static final String SERIALIZED_TASK_ADDITIONAL_INFORMATION_WITHOUT_TARGET_PROCESSOR = "{\"type\":\"reprocessingAllTask\", \"repositoryPath\":\"a\",\"targetQueue\":\"queue\", \"initialCount\":5,\"remainingCount\":3, \"timestamp\":\"2018-11-13T12:00:55Z\"}";
 
     @ParameterizedTest
     @MethodSource
@@ -120,7 +123,7 @@ class ReprocessingAllMailsTaskTest {
                                                    long repositorySize,
                                                    long remainingCount,
                                                    String serialized) throws JsonProcessingException {
-        ReprocessingAllMailsTask.AdditionalInformation details = new ReprocessingAllMailsTask.AdditionalInformation(repositoryPath, targetQueue, targetProcessor, repositorySize, remainingCount);
+        ReprocessingAllMailsTask.AdditionalInformation details = new ReprocessingAllMailsTask.AdditionalInformation(repositoryPath, targetQueue, targetProcessor, repositorySize, remainingCount, TIMESTAMP);
         assertThatJson(jsonAdditionalInformationSerializer.serialize(details)).isEqualTo(serialized);
     }
 
@@ -136,7 +139,7 @@ class ReprocessingAllMailsTaskTest {
                                                      long repositorySize,
                                                      long remainingCount,
                                                      String serialized) throws IOException {
-        ReprocessingAllMailsTask.AdditionalInformation details = new ReprocessingAllMailsTask.AdditionalInformation(repositoryPath, targetQueue, targetProcessor, repositorySize, remainingCount);
+        ReprocessingAllMailsTask.AdditionalInformation details = new ReprocessingAllMailsTask.AdditionalInformation(repositoryPath, targetQueue, targetProcessor, repositorySize, remainingCount, TIMESTAMP);
         assertThat(jsonAdditionalInformationSerializer.deserialize(serialized))
             .isEqualToComparingFieldByField(details);
     }
diff --git a/server/protocols/webadmin/webadmin-mailrepository/src/test/java/org/apache/james/webadmin/service/ReprocessingOneMailTaskTest.java b/server/protocols/webadmin/webadmin-mailrepository/src/test/java/org/apache/james/webadmin/service/ReprocessingOneMailTaskTest.java
index 7743e24..51248b8 100644
--- a/server/protocols/webadmin/webadmin-mailrepository/src/test/java/org/apache/james/webadmin/service/ReprocessingOneMailTaskTest.java
+++ b/server/protocols/webadmin/webadmin-mailrepository/src/test/java/org/apache/james/webadmin/service/ReprocessingOneMailTaskTest.java
@@ -25,6 +25,9 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.Mockito.mock;
 
 import java.io.IOException;
+import java.time.Clock;
+import java.time.Instant;
+import java.time.ZoneId;
 import java.util.Optional;
 import java.util.stream.Stream;
 
@@ -42,9 +45,12 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 import net.javacrumbs.jsonunit.assertj.JsonAssertions;
 
 class ReprocessingOneMailTaskTest {
+
+    private static final Instant TIMESTAMP = Instant.parse("2018-11-13T12:00:55Z");
+    private static final Clock CLOCK = Clock.fixed(TIMESTAMP, ZoneId.of("UTC"));
     private static final ReprocessingService REPROCESSING_SERVICE = mock(ReprocessingService.class);
     private static final String SERIALIZED_TASK_1 = "{\"type\":\"reprocessingOneTask\",\"repositoryPath\":\"a\",\"targetQueue\":\"queue\",\"mailKey\": \"myMail\",\"targetProcessor\":\"targetProcessor\"}";
-    private static final String SERIALIZED_TASK_1_ADDITIONAL_INFORMATION = "{\"type\":\"reprocessingOneTask\", \"repositoryPath\":\"a\",\"targetQueue\":\"queue\",\"mailKey\": \"myMail\",\"targetProcessor\":\"targetProcessor\"}";
+    private static final String SERIALIZED_TASK_1_ADDITIONAL_INFORMATION = "{\"type\":\"reprocessingOneTask\", \"repositoryPath\":\"a\",\"targetQueue\":\"queue\",\"mailKey\": \"myMail\",\"targetProcessor\":\"targetProcessor\", \"timestamp\":\"2018-11-13T12:00:55Z\"}";
     private static final MailRepositoryPath REPOSITORY_PATH = MailRepositoryPath.from("a");
     private static final String TARGET_QUEUE = "queue";
     private static final MailKey MAIL_KEY = new MailKey("myMail");
@@ -58,8 +64,8 @@ class ReprocessingOneMailTaskTest {
                                   MailKey mailKey,
                                   Optional<String> targetProcessor,
                                   String serialized) throws JsonProcessingException {
-        JsonTaskSerializer testee = new JsonTaskSerializer(ReprocessingOneMailTaskDTO.module(REPROCESSING_SERVICE));
-        ReprocessingOneMailTask task = new ReprocessingOneMailTask(REPROCESSING_SERVICE, repositoryPath, targetQueue, mailKey, targetProcessor);
+        JsonTaskSerializer testee = new JsonTaskSerializer(ReprocessingOneMailTaskDTO.module(CLOCK, REPROCESSING_SERVICE));
+        ReprocessingOneMailTask task = new ReprocessingOneMailTask(REPROCESSING_SERVICE, repositoryPath, targetQueue, mailKey, targetProcessor, CLOCK);
         JsonAssertions.assertThatJson(testee.serialize(task))
             .isEqualTo(serialized);
     }
@@ -75,8 +81,8 @@ class ReprocessingOneMailTaskTest {
                                     MailKey mailKey,
                                     Optional<String> targetProcessor,
                                     String serialized) throws IOException {
-        JsonTaskSerializer testee = new JsonTaskSerializer(ReprocessingOneMailTaskDTO.module(REPROCESSING_SERVICE));
-        ReprocessingOneMailTask task = new ReprocessingOneMailTask(REPROCESSING_SERVICE, repositoryPath, targetQueue, mailKey, targetProcessor);
+        JsonTaskSerializer testee = new JsonTaskSerializer(ReprocessingOneMailTaskDTO.module(CLOCK, REPROCESSING_SERVICE));
+        ReprocessingOneMailTask task = new ReprocessingOneMailTask(REPROCESSING_SERVICE, repositoryPath, targetQueue, mailKey, targetProcessor, CLOCK);
 
         assertThat(testee.deserialize(serialized))
             .isEqualToComparingFieldByFieldRecursively(task);
@@ -96,7 +102,7 @@ class ReprocessingOneMailTaskTest {
     @ParameterizedTest
     @ValueSource(strings = {"{\"type\":\"reprocessingOneTask\",\"repositoryPath\":\"%\",\"targetQueue\":\"queue\",\"mailKey\": \"myMail\",\"targetProcessor\":\"targetProcessor\"}", "{\"type\":\"reprocessingOneTask\",\"repositoryPath\":\"%\",\"targetQueue\":\"queue\",\"mailKey\": \"myMail\"}"})
     void taskShouldThrowOnDeserializationUrlDecodingError(String serialized) {
-        JsonTaskSerializer testee = new JsonTaskSerializer(ReprocessingOneMailTaskDTO.module(REPROCESSING_SERVICE));
+        JsonTaskSerializer testee = new JsonTaskSerializer(ReprocessingOneMailTaskDTO.module(CLOCK, REPROCESSING_SERVICE));
 
         assertThatThrownBy(() -> testee.deserialize(serialized))
             .isInstanceOf(ReprocessingOneMailTask.InvalidMailRepositoryPathDeserializationException.class);
@@ -104,13 +110,13 @@ class ReprocessingOneMailTaskTest {
 
     @Test
     void additionalInformationShouldBeSerializable() throws JsonProcessingException {
-        ReprocessingOneMailTask.AdditionalInformation details = new ReprocessingOneMailTask.AdditionalInformation(REPOSITORY_PATH, TARGET_QUEUE, MAIL_KEY, TARGET_PROCESSOR);
+        ReprocessingOneMailTask.AdditionalInformation details = new ReprocessingOneMailTask.AdditionalInformation(REPOSITORY_PATH, TARGET_QUEUE, MAIL_KEY, TARGET_PROCESSOR, TIMESTAMP);
         assertThatJson(jsonAdditionalInformationSerializer.serialize(details)).isEqualTo(SERIALIZED_TASK_1_ADDITIONAL_INFORMATION);
     }
 
     @Test
     void additonalInformationShouldBeDeserializable() throws IOException {
-        ReprocessingOneMailTask.AdditionalInformation details = new ReprocessingOneMailTask.AdditionalInformation(REPOSITORY_PATH, TARGET_QUEUE, MAIL_KEY, TARGET_PROCESSOR);
+        ReprocessingOneMailTask.AdditionalInformation details = new ReprocessingOneMailTask.AdditionalInformation(REPOSITORY_PATH, TARGET_QUEUE, MAIL_KEY, TARGET_PROCESSOR, TIMESTAMP);
         assertThat(jsonAdditionalInformationSerializer.deserialize(SERIALIZED_TASK_1_ADDITIONAL_INFORMATION))
             .isEqualToComparingFieldByField(details);
     }
diff --git a/server/task/task-api/src/main/scala/org/apache/james/task/TaskExecutionDetails.scala b/server/task/task-api/src/main/scala/org/apache/james/task/TaskExecutionDetails.scala
index b331907..c0fd884 100644
--- a/server/task/task-api/src/main/scala/org/apache/james/task/TaskExecutionDetails.scala
+++ b/server/task/task-api/src/main/scala/org/apache/james/task/TaskExecutionDetails.scala
@@ -19,7 +19,7 @@
 
 package org.apache.james.task
 
-import java.time.ZonedDateTime
+import java.time.{Instant, ZonedDateTime}
 import java.util.{Objects, Optional}
 
 import com.google.common.base.MoreObjects
@@ -28,7 +28,9 @@ import org.apache.james.task.TaskManager.Status._
 
 object TaskExecutionDetails {
 
-  trait AdditionalInformation {}
+  trait AdditionalInformation {
+    def timestamp: Instant
+  }
 
   def from(task: Task, id: TaskId, hostname: Hostname) = new TaskExecutionDetails(id, task.`type`, WAITING, submittedDate = ZonedDateTime.now, submittedNode = hostname, () => task.details)
 }
diff --git a/server/task/task-api/src/test/java/org/apache/james/task/MemoryReferenceWithCounterTask.java b/server/task/task-api/src/test/java/org/apache/james/task/MemoryReferenceWithCounterTask.java
index 819737d..eff246c 100644
--- a/server/task/task-api/src/test/java/org/apache/james/task/MemoryReferenceWithCounterTask.java
+++ b/server/task/task-api/src/test/java/org/apache/james/task/MemoryReferenceWithCounterTask.java
@@ -18,6 +18,8 @@
  ****************************************************************/
 package org.apache.james.task;
 
+import java.time.Clock;
+import java.time.Instant;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.concurrent.atomic.AtomicLong;
@@ -42,9 +44,11 @@ public class MemoryReferenceWithCounterTask implements Task {
 
     public static class AdditionalInformation implements TaskExecutionDetails.AdditionalInformation {
         private final long count;
+        private final Instant timestamp;
 
-        public AdditionalInformation(long count) {
+        public AdditionalInformation(long count, Instant timestamp) {
             this.count = count;
+            this.timestamp = timestamp;
         }
 
         public long getCount() {
@@ -52,22 +56,29 @@ public class MemoryReferenceWithCounterTask implements Task {
         }
 
         @Override
+        public Instant timestamp() {
+            return timestamp;
+        }
+
+        @Override
         public boolean equals(Object that) {
             if(that instanceof MemoryReferenceWithCounterTask.AdditionalInformation) {
-                return Objects.equals(this.count, ((AdditionalInformation) that).getCount());
+                return Objects.equals(this.count, ((AdditionalInformation) that).getCount()) &&
+                    Objects.equals(this.timestamp, ((AdditionalInformation) that).timestamp);
             }
             return false;
         }
 
         @Override
         public int hashCode() {
-            return Objects.hashCode(this.count);
+            return Objects.hash(this.count, this.timestamp);
         }
 
         @Override
         public String toString() {
             return MoreObjects.toStringHelper(this)
                 .add("count", count)
+                .add("timestamp", timestamp)
                 .toString();
         }
     }
@@ -95,6 +106,6 @@ public class MemoryReferenceWithCounterTask implements Task {
 
     @Override
     public Optional<TaskExecutionDetails.AdditionalInformation> details() {
-        return Optional.of(new MemoryReferenceWithCounterTask.AdditionalInformation(counter.get()));
+        return Optional.of(new MemoryReferenceWithCounterTask.AdditionalInformation(counter.get(), Clock.systemUTC().instant()));
     }
 }
diff --git a/server/task/task-distributed/src/test/java/org/apache/james/task/eventsourcing/distributed/TaskEventsSerializationTest.java b/server/task/task-distributed/src/test/java/org/apache/james/task/eventsourcing/distributed/TaskEventsSerializationTest.java
index e714622..63901b9 100644
--- a/server/task/task-distributed/src/test/java/org/apache/james/task/eventsourcing/distributed/TaskEventsSerializationTest.java
+++ b/server/task/task-distributed/src/test/java/org/apache/james/task/eventsourcing/distributed/TaskEventsSerializationTest.java
@@ -21,6 +21,7 @@ package org.apache.james.task.eventsourcing.distributed;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
+import java.time.Instant;
 import java.util.List;
 import java.util.stream.Stream;
 
@@ -54,6 +55,7 @@ import net.javacrumbs.jsonunit.assertj.JsonAssertions;
 import scala.Option;
 
 class TaskEventsSerializationTest {
+    private static final Instant TIMESTAMP = Instant.parse("2018-11-13T12:00:55Z");
     private static final JsonTaskSerializer TASK_SERIALIZER = new JsonTaskSerializer(TestTaskDTOModules.COMPLETED_TASK_MODULE,
         TestTaskDTOModules.MEMORY_REFERENCE_WITH_COUNTER_TASK_MODULE.apply(new MemoryReferenceWithCounterTaskStore()));
     private static final JsonTaskAdditionalInformationsSerializer TASK_ADDITIONNAL_INFORMATION_SERIALIZER = new JsonTaskAdditionalInformationsSerializer(MemoryReferenceWithCounterTaskAdditionalInformationDTO.SERIALIZATION_MODULE);
@@ -63,7 +65,7 @@ class TaskEventsSerializationTest {
     private static final EventId EVENT_ID = EventId.fromSerialized(42);
     private static final Task TASK = new CompletedTask();
     private static final Hostname HOSTNAME = new Hostname("foo");
-    private static final MemoryReferenceWithCounterTask.AdditionalInformation counterAdditionalInformation = new MemoryReferenceWithCounterTask.AdditionalInformation(3);
+    private static final MemoryReferenceWithCounterTask.AdditionalInformation counterAdditionalInformation = new MemoryReferenceWithCounterTask.AdditionalInformation(3, TIMESTAMP);
 
     @ParameterizedTest
     @MethodSource
@@ -95,10 +97,10 @@ class TaskEventsSerializationTest {
             Arguments.of(new Failed(AGGREGATE_ID, EVENT_ID, Option.empty(), Option.empty(), Option.empty()), "{\"aggregate\":\"2c7f4081-aa30-11e9-bf6c-2d3b9e84aafd\",\"event\":42,\"type\":\"task-manager-failed\"}"),
             Arguments.of(new Failed(AGGREGATE_ID, EVENT_ID, Option.empty(), Option.apply("contextual message"), Option.apply("my exception")), "{\"aggregate\":\"2c7f4081-aa30-11e9-bf6c-2d3b9e84aafd\",\"event\":42,\"type\":\"task-manager-failed\", \"errorMessage\": \"contextual message\", \"exception\": \"my exception\"}"),
             Arguments.of(new Cancelled(AGGREGATE_ID, EVENT_ID, Option.empty()), "{\"aggregate\":\"2c7f4081-aa30-11e9-bf6c-2d3b9e84aafd\",\"event\":42,\"type\":\"task-manager-cancelled\"}"),
-            Arguments.of(new Completed(AGGREGATE_ID, EVENT_ID, Task.Result.COMPLETED, Option.apply(counterAdditionalInformation)), "{\"result\":\"COMPLETED\",\"aggregate\":\"2c7f4081-aa30-11e9-bf6c-2d3b9e84aafd\",\"event\":42,\"type\":\"task-manager-completed\",\"additionalInformation\":\"{\\\"type\\\":\\\"memory-reference-task-with-counter\\\",\\\"count\\\":3}\"}"),
-            Arguments.of(new Completed(AGGREGATE_ID, EVENT_ID, Task.Result.PARTIAL, Option.apply(counterAdditionalInformation)), "{\"result\":\"PARTIAL\",\"aggregate\":\"2c7f4081-aa30-11e9-bf6c-2d3b9e84aafd\",\"event\":42,\"type\":\"task-manager-completed\",\"additionalInformation\":\"{\\\"type\\\":\\\"memory-reference-task-with-counter\\\",\\\"count\\\":3}\"}"),
-            Arguments.of(new Failed(AGGREGATE_ID, EVENT_ID, Option.apply(counterAdditionalInformation), Option.empty(), Option.empty()), "{\"aggregate\":\"2c7f4081-aa30-11e9-bf6c-2d3b9e84aafd\",\"event\":42,\"type\":\"task-manager-failed\",\"additionalInformation\":\"{\\\"type\\\":\\\"memory-reference-task-with-counter\\\",\\\"count\\\":3}\"}"),
-            Arguments.of(new Cancelled(AGGREGATE_ID, EVENT_ID, Option.apply(counterAdditionalInformation)), "{\"aggregate\":\"2c7f4081-aa30-11e9-bf6c-2d3b9e84aafd\",\"event\":42,\"type\":\"task-manager-cancelled\",\"additionalInformation\":\"{\\\"type\\\":\\\"memory-reference-task-with-counter\\\",\\\"count\\\":3}\"}")
+            Arguments.of(new Completed(AGGREGATE_ID, EVENT_ID, Task.Result.COMPLETED, Option.apply(counterAdditionalInformation)), "{\"result\":\"COMPLETED\",\"aggregate\":\"2c7f4081-aa30-11e9-bf6c-2d3b9e84aafd\",\"event\":42,\"type\":\"task-manager-completed\",\"additionalInformation\":\"{\\\"type\\\":\\\"memory-reference-task-with-counter\\\",\\\"count\\\":3,\\\"timestamp\\\":\\\"2018-11-13T12:00:55Z\\\"}\"}"),
+            Arguments.of(new Completed(AGGREGATE_ID, EVENT_ID, Task.Result.PARTIAL, Option.apply(counterAdditionalInformation)), "{\"result\":\"PARTIAL\",\"aggregate\":\"2c7f4081-aa30-11e9-bf6c-2d3b9e84aafd\",\"event\":42,\"type\":\"task-manager-completed\",\"additionalInformation\":\"{\\\"type\\\":\\\"memory-reference-task-with-counter\\\",\\\"count\\\":3,\\\"timestamp\\\":\\\"2018-11-13T12:00:55Z\\\"}\"}"),
+            Arguments.of(new Failed(AGGREGATE_ID, EVENT_ID, Option.apply(counterAdditionalInformation), Option.empty(), Option.empty()), "{\"aggregate\":\"2c7f4081-aa30-11e9-bf6c-2d3b9e84aafd\",\"event\":42,\"type\":\"task-manager-failed\",\"additionalInformation\":\"{\\\"type\\\":\\\"memory-reference-task-with-counter\\\",\\\"count\\\":3,\\\"timestamp\\\":\\\"2018-11-13T12:00:55Z\\\"}\"}"),
+            Arguments.of(new Cancelled(AGGREGATE_ID, EVENT_ID, Option.apply(counterAdditionalInformation)), "{\"aggregate\":\"2c7f4081-aa30-11e9-bf6c-2d3b9e84aafd\",\"event\":42,\"type\":\"task-manager-cancelled\",\"additionalInformation\":\"{\\\"type\\\":\\\"memory-reference-task-with-counter\\\",\\\"count\\\":3,\\\"timestamp\\\":\\\"2018-11-13T12:00:55Z\\\"}\"}")
         );
     }
 
diff --git a/server/task/task-json/src/main/java/org/apache/james/server/task/json/dto/AdditionalInformationDTO.java b/server/task/task-json/src/main/java/org/apache/james/server/task/json/dto/AdditionalInformationDTO.java
index 548cf1c..e1bdde3 100644
--- a/server/task/task-json/src/main/java/org/apache/james/server/task/json/dto/AdditionalInformationDTO.java
+++ b/server/task/task-json/src/main/java/org/apache/james/server/task/json/dto/AdditionalInformationDTO.java
@@ -18,8 +18,12 @@
  ****************************************************************/
 package org.apache.james.server.task.json.dto;
 
+import java.time.Instant;
+
 import org.apache.james.json.DTO;
 
 public interface AdditionalInformationDTO extends DTO {
     String getType();
+
+    Instant getTimestamp();
 }
diff --git a/server/task/task-json/src/test/java/org/apache/james/server/task/json/dto/MemoryReferenceWithCounterTaskAdditionalInformationDTO.java b/server/task/task-json/src/test/java/org/apache/james/server/task/json/dto/MemoryReferenceWithCounterTaskAdditionalInformationDTO.java
index 011799d..dc78329 100644
--- a/server/task/task-json/src/test/java/org/apache/james/server/task/json/dto/MemoryReferenceWithCounterTaskAdditionalInformationDTO.java
+++ b/server/task/task-json/src/test/java/org/apache/james/server/task/json/dto/MemoryReferenceWithCounterTaskAdditionalInformationDTO.java
@@ -19,6 +19,8 @@
 
 package org.apache.james.server.task.json.dto;
 
+import java.time.Instant;
+
 import org.apache.james.json.DTOModule;
 import org.apache.james.task.MemoryReferenceWithCounterTask;
 
@@ -30,19 +32,23 @@ public class MemoryReferenceWithCounterTaskAdditionalInformationDTO implements A
         DTOModule.forDomainObject(MemoryReferenceWithCounterTask.AdditionalInformation.class)
             .convertToDTO(MemoryReferenceWithCounterTaskAdditionalInformationDTO.class)
             .toDomainObjectConverter(dto -> new MemoryReferenceWithCounterTask.AdditionalInformation(
-                dto.count
+                dto.count, dto.timestamp
             ))
             .toDTOConverter((details, type) -> new MemoryReferenceWithCounterTaskAdditionalInformationDTO(
-                type, details.getCount()))
+                type, details.getCount(), details.timestamp()))
             .typeName(MemoryReferenceWithCounterTask.TYPE.asString())
             .withFactory(AdditionalInformationDTOModule::new);
 
     private final String type;
     private final long count;
+    private final Instant timestamp;
 
-    public MemoryReferenceWithCounterTaskAdditionalInformationDTO(@JsonProperty("type") String type, @JsonProperty("count") long count) {
+    public MemoryReferenceWithCounterTaskAdditionalInformationDTO(@JsonProperty("type") String type,
+                                                                  @JsonProperty("count") long count,
+                                                                  @JsonProperty("timestamp") Instant timestamp) {
         this.type = type;
         this.count = count;
+        this.timestamp = timestamp;
     }
 
     public long getCount() {
@@ -50,6 +56,11 @@ public class MemoryReferenceWithCounterTaskAdditionalInformationDTO implements A
     }
 
     @Override
+    public Instant getTimestamp() {
+        return timestamp;
+    }
+
+    @Override
     public String getType() {
         return type;
     }
diff --git a/server/task/task-memory/src/main/java/org/apache/james/task/MemoryTaskManager.java b/server/task/task-memory/src/main/java/org/apache/james/task/MemoryTaskManager.java
index 64f60e9..752c1e6 100644
--- a/server/task/task-memory/src/main/java/org/apache/james/task/MemoryTaskManager.java
+++ b/server/task/task-memory/src/main/java/org/apache/james/task/MemoryTaskManager.java
@@ -32,7 +32,6 @@ import javax.inject.Inject;
 
 import com.github.steveash.guavate.Guavate;
 import com.google.common.collect.ImmutableList;
-
 import reactor.core.publisher.Flux;
 import reactor.core.scheduler.Schedulers;
 
diff --git a/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/EventSourcingTaskManagerTest.java b/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/EventSourcingTaskManagerTest.java
index 05ef373..edf2d7c 100644
--- a/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/EventSourcingTaskManagerTest.java
+++ b/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/EventSourcingTaskManagerTest.java
@@ -48,6 +48,7 @@ class EventSourcingTaskManagerTest implements TaskManagerContract {
         .and().pollDelay(ONE_HUNDRED_MILLISECONDS)
         .await();
 
+
     private static final Hostname HOSTNAME = new Hostname("foo");
     private EventSourcingTaskManager taskManager;
     private EventStore eventStore;
diff --git a/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/TaskAggregateTest.java b/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/TaskAggregateTest.java
index 075deda..bb98b26 100644
--- a/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/TaskAggregateTest.java
+++ b/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/TaskAggregateTest.java
@@ -21,6 +21,7 @@ package org.apache.james.task.eventsourcing;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.function.Function;
 import java.util.stream.Collectors;
@@ -42,6 +43,7 @@ class TaskAggregateTest {
 
     static final Hostname HOSTNAME = Hostname.apply("foo");
     static final TaskAggregateId ID = TaskAggregateId.apply(TaskId.generateTaskId());
+    static final Instant timestamp = Instant.parse("2018-11-13T12:00:55Z");
 
     History buildHistory(Function<EventId, Event>... events) {
         return History.of(
@@ -69,7 +71,7 @@ class TaskAggregateTest {
             eventId -> Created.apply(ID, eventId, new MemoryReferenceWithCounterTask((counter) -> Task.Result.COMPLETED), HOSTNAME)
         );
         TaskAggregate aggregate = TaskAggregate.fromHistory(ID, history);
-        assertThat(aggregate.update(new MemoryReferenceWithCounterTask.AdditionalInformation(3))).isEmpty();
+        assertThat(aggregate.update(new MemoryReferenceWithCounterTask.AdditionalInformation(3, timestamp))).isEmpty();
     }
 
     @Test
@@ -79,8 +81,8 @@ class TaskAggregateTest {
             eventId -> Started.apply(ID, eventId, HOSTNAME)
         );
         TaskAggregate aggregate = TaskAggregate.fromHistory(ID, history);
-        assertThat(aggregate.update(new MemoryReferenceWithCounterTask.AdditionalInformation(3)))
-            .containsExactly(AdditionalInformationUpdated.apply(ID, history.getNextEventId(), new MemoryReferenceWithCounterTask.AdditionalInformation(3)));
+        assertThat(aggregate.update(new MemoryReferenceWithCounterTask.AdditionalInformation(3, timestamp)))
+            .containsExactly(AdditionalInformationUpdated.apply(ID, history.getNextEventId(), new MemoryReferenceWithCounterTask.AdditionalInformation(3, timestamp)));
     }
 
     @Test
@@ -91,8 +93,8 @@ class TaskAggregateTest {
             eventId -> CancelRequested.apply(ID, eventId, HOSTNAME)
         );
         TaskAggregate aggregate = TaskAggregate.fromHistory(ID, history);
-        assertThat(aggregate.update(new MemoryReferenceWithCounterTask.AdditionalInformation(3)))
-            .containsExactly(AdditionalInformationUpdated.apply(ID, history.getNextEventId(), new MemoryReferenceWithCounterTask.AdditionalInformation(3)));
+        assertThat(aggregate.update(new MemoryReferenceWithCounterTask.AdditionalInformation(3, timestamp)))
+            .containsExactly(AdditionalInformationUpdated.apply(ID, history.getNextEventId(), new MemoryReferenceWithCounterTask.AdditionalInformation(3, timestamp)));
     }
 
     @Test
@@ -104,7 +106,7 @@ class TaskAggregateTest {
             eventId -> Completed.apply(ID, eventId, Task.Result.COMPLETED, Option.empty())
         );
         TaskAggregate aggregate = TaskAggregate.fromHistory(ID, history);
-        assertThat(aggregate.update(new MemoryReferenceWithCounterTask.AdditionalInformation(3))).isEmpty();
+        assertThat(aggregate.update(new MemoryReferenceWithCounterTask.AdditionalInformation(3, timestamp))).isEmpty();
     }
 
     @Test
@@ -116,7 +118,7 @@ class TaskAggregateTest {
             eventId -> Failed.apply(ID, eventId, Option.empty(), Option.empty(), Option.empty())
         );
         TaskAggregate aggregate = TaskAggregate.fromHistory(ID, history);
-        assertThat(aggregate.update(new MemoryReferenceWithCounterTask.AdditionalInformation(3))).isEmpty();
+        assertThat(aggregate.update(new MemoryReferenceWithCounterTask.AdditionalInformation(3, timestamp))).isEmpty();
     }
 
     @Test
@@ -128,6 +130,6 @@ class TaskAggregateTest {
             eventId -> Cancelled.apply(ID, eventId, Option.empty())
         );
         TaskAggregate aggregate = TaskAggregate.fromHistory(ID, history);
-        assertThat(aggregate.update(new MemoryReferenceWithCounterTask.AdditionalInformation(3))).isEmpty();
+        assertThat(aggregate.update(new MemoryReferenceWithCounterTask.AdditionalInformation(3, timestamp))).isEmpty();
     }
 }
diff --git a/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/TaskExecutionDetailsProjectionContract.java b/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/TaskExecutionDetailsProjectionContract.java
index 04fe89a..0d0fefb 100644
--- a/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/TaskExecutionDetailsProjectionContract.java
+++ b/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/TaskExecutionDetailsProjectionContract.java
@@ -31,7 +31,6 @@ import java.util.List;
 import java.util.Optional;
 
 import org.apache.james.task.TaskExecutionDetails;
-
 import org.junit.jupiter.api.Test;
 
 import scala.collection.JavaConverters;
diff --git a/server/task/task-memory/src/test/scala/org/apache/james/task/TaskExecutionDetailsFixture.scala b/server/task/task-memory/src/test/scala/org/apache/james/task/TaskExecutionDetailsFixture.scala
index 7ba491e..036b6e1 100644
--- a/server/task/task-memory/src/test/scala/org/apache/james/task/TaskExecutionDetailsFixture.scala
+++ b/server/task/task-memory/src/test/scala/org/apache/james/task/TaskExecutionDetailsFixture.scala
@@ -18,7 +18,7 @@
  * ***************************************************************/
 package org.apache.james.task
 
-import java.time.{LocalDateTime, ZoneId, ZonedDateTime}
+import java.time.{Clock, Instant, LocalDateTime, ZoneId, ZonedDateTime}
 import java.util.Optional
 
 import org.apache.james.task.TaskExecutionDetails.AdditionalInformation
@@ -37,10 +37,12 @@ object TaskExecutionDetailsFixture {
   val TASK_EXECUTION_DETAILS_2 = new TaskExecutionDetails(TASK_ID_2, TYPE, TaskManager.Status.COMPLETED, SUBMITTED_DATE, SUBMITTED_NODE, EMPTY_ADDITIONAL_INFORMATION)
   val TASK_EXECUTION_DETAILS_UPDATED = new TaskExecutionDetails(TASK_ID, TYPE, TaskManager.Status.FAILED, SUBMITTED_DATE, SUBMITTED_NODE, EMPTY_ADDITIONAL_INFORMATION)
 
+  val DATE: Instant = Instant.parse("2007-07-03T10:15:30.00Z")
+  val CLOCK: Clock = Clock.fixed(DATE, ZoneId.of("UTC"))
 
-  val ADDITIONAL_INFORMATION: () => Optional[AdditionalInformation] = () => Optional.of(new MemoryReferenceWithCounterTask.AdditionalInformation(5))
+  val ADDITIONAL_INFORMATION: () => Optional[AdditionalInformation] = () => Optional.of(new MemoryReferenceWithCounterTask.AdditionalInformation(5, CLOCK.instant()))
   val TASK_EXECUTION_DETAILS_WITH_ADDITIONAL_INFORMATION = new TaskExecutionDetails(TASK_ID, MemoryReferenceWithCounterTask.TYPE, TaskManager.Status.COMPLETED, SUBMITTED_DATE_2, SUBMITTED_NODE_2, ADDITIONAL_INFORMATION)
 
 }
 
-case class CustomAdditionalInformation(value: String) extends AdditionalInformation
+case class CustomAdditionalInformation(value: String, timestamp: Instant) extends AdditionalInformation


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[james-project] 05/17: JAMES-2813 rename fields and methods in RabbitMQWorkQueueTest

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rouazana pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 604c27dfbc1969e96869f11061b942548f2dc322
Author: Matthieu Baechler <ma...@apache.org>
AuthorDate: Tue Oct 8 15:39:58 2019 +0200

    JAMES-2813 rename fields and methods in RabbitMQWorkQueueTest
---
 .../distributed/RabbitMQWorkQueueTest.java         | 44 +++++++++++-----------
 1 file changed, 22 insertions(+), 22 deletions(-)

diff --git a/server/task/task-distributed/src/test/java/org/apache/james/task/eventsourcing/distributed/RabbitMQWorkQueueTest.java b/server/task/task-distributed/src/test/java/org/apache/james/task/eventsourcing/distributed/RabbitMQWorkQueueTest.java
index 2a109cd..91af0e2 100644
--- a/server/task/task-distributed/src/test/java/org/apache/james/task/eventsourcing/distributed/RabbitMQWorkQueueTest.java
+++ b/server/task/task-distributed/src/test/java/org/apache/james/task/eventsourcing/distributed/RabbitMQWorkQueueTest.java
@@ -66,8 +66,8 @@ class RabbitMQWorkQueueTest {
 
 
     private RabbitMQWorkQueue testee;
-    private ImmediateWorker taskManagerWorker;
-    private JsonTaskSerializer taskSerializer;
+    private ImmediateWorker worker;
+    private JsonTaskSerializer serializer;
 
     private static class ImmediateWorker implements TaskManagerWorker {
 
@@ -99,9 +99,9 @@ class RabbitMQWorkQueueTest {
 
     @BeforeEach
     void setUp() {
-        taskManagerWorker = spy(new ImmediateWorker());
-        taskSerializer = new JsonTaskSerializer(TestTaskDTOModules.COMPLETED_TASK_MODULE);
-        testee = new RabbitMQWorkQueue(taskManagerWorker, rabbitMQExtension.getRabbitConnectionPool(), taskSerializer);
+        worker = spy(new ImmediateWorker());
+        serializer = new JsonTaskSerializer(TestTaskDTOModules.COMPLETED_TASK_MODULE);
+        testee = new RabbitMQWorkQueue(worker, rabbitMQExtension.getRabbitConnectionPool(), serializer);
         testee.start();
     }
 
@@ -111,20 +111,20 @@ class RabbitMQWorkQueueTest {
     }
 
     @Test
-    void workerShouldConsumeSubmittedTask() {
+    void workQueueShouldConsumeSubmittedTask() {
         testee.submit(TASK_WITH_ID);
-        await().atMost(FIVE_HUNDRED_MILLISECONDS).until(() -> !taskManagerWorker.results.isEmpty());
-        assertThat(taskManagerWorker.tasks).containsExactly(TASK_WITH_ID);
-        assertThat(taskManagerWorker.results).containsExactly(Task.Result.COMPLETED);
+        await().atMost(FIVE_HUNDRED_MILLISECONDS).until(() -> !worker.results.isEmpty());
+        assertThat(worker.tasks).containsExactly(TASK_WITH_ID);
+        assertThat(worker.results).containsExactly(Task.Result.COMPLETED);
     }
 
     @Test
-    void workerShouldConsumeTwoSubmittedTask() {
+    void workQueueShouldConsumeTwoSubmittedTasks() {
         testee.submit(TASK_WITH_ID);
         testee.submit(TASK_WITH_ID_2);
-        await().atMost(FIVE_HUNDRED_MILLISECONDS).until(() -> taskManagerWorker.results.size() == 2);
-        assertThat(taskManagerWorker.tasks).containsExactly(TASK_WITH_ID, TASK_WITH_ID_2);
-        assertThat(taskManagerWorker.results).allSatisfy(result -> assertThat(result).isEqualTo(Task.Result.COMPLETED));
+        await().atMost(FIVE_HUNDRED_MILLISECONDS).until(() -> worker.results.size() == 2);
+        assertThat(worker.tasks).containsExactly(TASK_WITH_ID, TASK_WITH_ID_2);
+        assertThat(worker.results).allSatisfy(result -> assertThat(result).isEqualTo(Task.Result.COMPLETED));
     }
 
     @Test
@@ -132,13 +132,13 @@ class RabbitMQWorkQueueTest {
         testee.submit(TASK_WITH_ID);
 
         ImmediateWorker otherTaskManagerWorker = new ImmediateWorker();
-        try (RabbitMQWorkQueue otherWorkQueue = new RabbitMQWorkQueue(otherTaskManagerWorker, rabbitMQExtension.getRabbitConnectionPool(), taskSerializer)) {
+        try (RabbitMQWorkQueue otherWorkQueue = new RabbitMQWorkQueue(otherTaskManagerWorker, rabbitMQExtension.getRabbitConnectionPool(), serializer)) {
             otherWorkQueue.start();
 
             IntStream.range(0, 9)
                 .forEach(ignoredIndex -> testee.submit(TASK_WITH_ID_2));
 
-            await().atMost(FIVE_HUNDRED_MILLISECONDS).until(() -> taskManagerWorker.results.size() == 10);
+            await().atMost(FIVE_HUNDRED_MILLISECONDS).until(() -> worker.results.size() == 10);
             assertThat(otherTaskManagerWorker.tasks).isEmpty();
         }
     }
@@ -158,12 +158,12 @@ class RabbitMQWorkQueueTest {
 
             otherWorkQueue.submit(taskWithId);
 
-            await().atMost(FIVE_HUNDRED_MILLISECONDS).until(() -> taskManagerWorker.failedTasks.size() == 1);
-            assertThat(taskManagerWorker.failedTasks).containsExactly(taskWithId.getId());
+            await().atMost(FIVE_HUNDRED_MILLISECONDS).until(() -> worker.failedTasks.size() == 1);
+            assertThat(worker.failedTasks).containsExactly(taskWithId.getId());
 
             testee.submit(TASK_WITH_ID);
-            await().atMost(FIVE_HUNDRED_MILLISECONDS).until(() -> taskManagerWorker.results.size() == 1);
-            assertThat(taskManagerWorker.tasks).containsExactly(TASK_WITH_ID);
+            await().atMost(FIVE_HUNDRED_MILLISECONDS).until(() -> worker.results.size() == 1);
+            assertThat(worker.tasks).containsExactly(TASK_WITH_ID);
         }
     }
 
@@ -177,7 +177,7 @@ class RabbitMQWorkQueueTest {
         TaskId taskId2 = TaskId.fromString("2222d082-aa30-22e9-bf6c-2d3b9e84aafd");
         TaskWithId taskWithId2 = new TaskWithId(taskId2, task2);
 
-        when(taskManagerWorker.executeTask(taskWithId1)).then(answer -> {
+        when(worker.executeTask(taskWithId1)).then(answer -> {
             TimeUnit.MINUTES.sleep(2);
             return Mono.just(Task.Result.COMPLETED);
         });
@@ -185,7 +185,7 @@ class RabbitMQWorkQueueTest {
         testee.submit(taskWithId1);
         testee.submit(taskWithId2);
 
-        verify(taskManagerWorker, timeout(100)).executeTask(taskWithId1);
-        verifyNoMoreInteractions(taskManagerWorker);
+        verify(worker, timeout(100)).executeTask(taskWithId1);
+        verifyNoMoreInteractions(worker);
     }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[james-project] 08/17: JAMES-2813 put executeWithSemaphore building blocks into submethods

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rouazana pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 12559bdf8638c692950c97c115905d9c5b1983c4
Author: Matthieu Baechler <ma...@apache.org>
AuthorDate: Wed Oct 9 11:58:12 2019 +0200

    JAMES-2813 put executeWithSemaphore building blocks into submethods
---
 .../apache/james/task/SerialTaskManagerWorker.java | 31 +++++++++++-----------
 1 file changed, 16 insertions(+), 15 deletions(-)

diff --git a/server/task/task-memory/src/main/java/org/apache/james/task/SerialTaskManagerWorker.java b/server/task/task-memory/src/main/java/org/apache/james/task/SerialTaskManagerWorker.java
index b954aa2..8fb0e15 100644
--- a/server/task/task-memory/src/main/java/org/apache/james/task/SerialTaskManagerWorker.java
+++ b/server/task/task-memory/src/main/java/org/apache/james/task/SerialTaskManagerWorker.java
@@ -38,7 +38,6 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.common.collect.Sets;
-import reactor.core.Disposable;
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 import reactor.util.function.Tuple2;
@@ -89,19 +88,12 @@ public class SerialTaskManagerWorker implements TaskManagerWorker {
                 CompletableFuture<Task.Result> future = CompletableFuture.supplyAsync(() -> runWithMdc(taskWithId, listener), taskExecutor);
                 runningTask.set(Tuples.of(taskWithId.getId(), future));
 
-                Disposable informationPolling = pollAdditionalInformation(taskWithId)
-                    .doOnNext(information -> listener.updated(taskWithId.getId(), information))
-                    .subscribe();
-                return Mono.fromFuture(future)
-                        .doOnError(exception -> {
-                            if (exception instanceof CancellationException) {
-                                listener.cancelled(taskWithId.getId(), taskWithId.getTask().details());
-                            } else {
-                                listener.failed(taskWithId.getId(), taskWithId.getTask().details(), exception);
-                            }
-                        })
-                        .onErrorReturn(Task.Result.PARTIAL)
-                        .doOnTerminate(informationPolling::dispose);
+                return Mono.using(
+                    () -> pollAdditionalInformation(taskWithId).subscribe(),
+                    ignored -> Mono.fromFuture(future)
+                            .doOnError(exception -> handleExecutionError(taskWithId, listener, exception))
+                            .onErrorReturn(Task.Result.PARTIAL),
+                    polling -> polling.dispose());
             } else {
                 listener.cancelled(taskWithId.getId(), taskWithId.getTask().details());
                 return Mono.empty();
@@ -109,11 +101,20 @@ public class SerialTaskManagerWorker implements TaskManagerWorker {
         };
     }
 
+    private void handleExecutionError(TaskWithId taskWithId, Listener listener, Throwable exception) {
+        if (exception instanceof CancellationException) {
+            listener.cancelled(taskWithId.getId(), taskWithId.getTask().details());
+        } else {
+            listener.failed(taskWithId.getId(), taskWithId.getTask().details(), exception);
+        }
+    }
+
     private Flux<TaskExecutionDetails.AdditionalInformation> pollAdditionalInformation(TaskWithId taskWithId) {
         return Mono.fromCallable(() -> taskWithId.getTask().details())
             .delayElement(Duration.ofSeconds(1))
             .repeat()
-            .flatMap(Mono::justOrEmpty);
+            .flatMap(Mono::justOrEmpty)
+            .doOnNext(information -> listener.updated(taskWithId.getId(), taskWithId.getTask().type(), information));
     }
 
 


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[james-project] 04/17: JAMES-2813 replace mocks in RabbitMQWorkQueueTest by a stub

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rouazana pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 5da39aee831a23a855ba201fc1ed42095b841174
Author: Matthieu Baechler <ma...@apache.org>
AuthorDate: Tue Oct 8 15:37:20 2019 +0200

    JAMES-2813 replace mocks in RabbitMQWorkQueueTest by a stub
---
 .../distributed/RabbitMQWorkQueueTest.java         | 109 ++++++++++++---------
 1 file changed, 63 insertions(+), 46 deletions(-)

diff --git a/server/task/task-distributed/src/test/java/org/apache/james/task/eventsourcing/distributed/RabbitMQWorkQueueTest.java b/server/task/task-distributed/src/test/java/org/apache/james/task/eventsourcing/distributed/RabbitMQWorkQueueTest.java
index 50950e3..2a109cd 100644
--- a/server/task/task-distributed/src/test/java/org/apache/james/task/eventsourcing/distributed/RabbitMQWorkQueueTest.java
+++ b/server/task/task-distributed/src/test/java/org/apache/james/task/eventsourcing/distributed/RabbitMQWorkQueueTest.java
@@ -20,16 +20,17 @@
 package org.apache.james.task.eventsourcing.distributed;
 
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
+import static org.awaitility.Awaitility.await;
+import static org.awaitility.Duration.FIVE_HUNDRED_MILLISECONDS;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
+import java.io.IOException;
 import java.util.Optional;
+import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.IntStream;
 
@@ -39,6 +40,7 @@ import org.apache.james.server.task.json.TestTask;
 import org.apache.james.server.task.json.dto.TestTaskDTOModules;
 import org.apache.james.task.CompletedTask;
 import org.apache.james.task.Task;
+import org.apache.james.task.TaskExecutionDetails;
 import org.apache.james.task.TaskId;
 import org.apache.james.task.TaskManagerWorker;
 import org.apache.james.task.TaskWithId;
@@ -46,10 +48,9 @@ import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
-import org.mockito.ArgumentCaptor;
-import org.mockito.verification.Timeout;
 
 import reactor.core.publisher.Mono;
+import reactor.core.scheduler.Schedulers;
 
 class RabbitMQWorkQueueTest {
     private static final TaskId TASK_ID = TaskId.fromString("2c7f4081-aa30-11e9-bf6c-2d3b9e84aafd");
@@ -65,14 +66,40 @@ class RabbitMQWorkQueueTest {
 
 
     private RabbitMQWorkQueue testee;
-    private TaskManagerWorker taskManagerWorker;
+    private ImmediateWorker taskManagerWorker;
     private JsonTaskSerializer taskSerializer;
 
+    private static class ImmediateWorker implements TaskManagerWorker {
+
+        ConcurrentLinkedQueue<TaskWithId> tasks = new ConcurrentLinkedQueue<>();
+        ConcurrentLinkedQueue<Task.Result> results = new ConcurrentLinkedQueue<>();
+        ConcurrentLinkedQueue<TaskId> failedTasks = new ConcurrentLinkedQueue<>();
+
+        @Override
+        public Mono<Task.Result> executeTask(TaskWithId taskWithId) {
+            tasks.add(taskWithId);
+            return Mono.fromCallable(() -> taskWithId.getTask().run())
+                .doOnNext(result -> results.add(result))
+                .subscribeOn(Schedulers.boundedElastic());
+        }
+
+        @Override
+        public void cancelTask(TaskId taskId) {
+        }
+
+        @Override
+        public void fail(TaskId taskId, Optional<TaskExecutionDetails.AdditionalInformation> additionalInformation,String errorMessage, Throwable reason) {
+            failedTasks.add(taskId);
+        }
+
+        @Override
+        public void close() throws IOException {
+        }
+    }
+
     @BeforeEach
     void setUp() {
-        taskManagerWorker = mock(TaskManagerWorker.class);
-        when(taskManagerWorker.executeTask(TASK_WITH_ID)).thenReturn(Mono.just(Task.Result.COMPLETED));
-        when(taskManagerWorker.executeTask(TASK_WITH_ID_2)).thenReturn(Mono.just(Task.Result.COMPLETED));
+        taskManagerWorker = spy(new ImmediateWorker());
         taskSerializer = new JsonTaskSerializer(TestTaskDTOModules.COMPLETED_TASK_MODULE);
         testee = new RabbitMQWorkQueue(taskManagerWorker, rabbitMQExtension.getRabbitConnectionPool(), taskSerializer);
         testee.start();
@@ -86,46 +113,34 @@ class RabbitMQWorkQueueTest {
     @Test
     void workerShouldConsumeSubmittedTask() {
         testee.submit(TASK_WITH_ID);
-
-        ArgumentCaptor<TaskWithId> taskWithIdCaptor = ArgumentCaptor.forClass(TaskWithId.class);
-        verify(taskManagerWorker, timeout(1000)).executeTask(taskWithIdCaptor.capture());
-
-        TaskWithId actualTaskWithId = taskWithIdCaptor.getValue();
-        assertThat(actualTaskWithId.getId()).isEqualTo(TASK_ID);
-        assertThat(actualTaskWithId.getTask().type()).isEqualTo(TASK.type());
+        await().atMost(FIVE_HUNDRED_MILLISECONDS).until(() -> !taskManagerWorker.results.isEmpty());
+        assertThat(taskManagerWorker.tasks).containsExactly(TASK_WITH_ID);
+        assertThat(taskManagerWorker.results).containsExactly(Task.Result.COMPLETED);
     }
 
     @Test
     void workerShouldConsumeTwoSubmittedTask() {
         testee.submit(TASK_WITH_ID);
         testee.submit(TASK_WITH_ID_2);
-
-        ArgumentCaptor<TaskWithId> taskWithIdCaptor = ArgumentCaptor.forClass(TaskWithId.class);
-        verify(taskManagerWorker, new Timeout(1000, times(2))).executeTask(taskWithIdCaptor.capture());
-
-        TaskWithId actualTaskWithId = taskWithIdCaptor.getAllValues().get(0);
-        assertThat(actualTaskWithId.getId()).isEqualTo(TASK_ID);
-        assertThat(actualTaskWithId.getTask().type()).isEqualTo(TASK.type());
-
-        TaskWithId actualSecondTaskWithId = taskWithIdCaptor.getAllValues().get(1);
-        assertThat(actualSecondTaskWithId.getId()).isEqualTo(TASK_ID_2);
-        assertThat(actualSecondTaskWithId.getTask().type()).isEqualTo(TASK2.type());
+        await().atMost(FIVE_HUNDRED_MILLISECONDS).until(() -> taskManagerWorker.results.size() == 2);
+        assertThat(taskManagerWorker.tasks).containsExactly(TASK_WITH_ID, TASK_WITH_ID_2);
+        assertThat(taskManagerWorker.results).allSatisfy(result -> assertThat(result).isEqualTo(Task.Result.COMPLETED));
     }
 
     @Test
     void givenTwoWorkQueuesOnlyTheFirstOneIsConsumingTasks() {
         testee.submit(TASK_WITH_ID);
 
-        TaskManagerWorker otherTaskManagerWorker = mock(TaskManagerWorker.class);
-        RabbitMQWorkQueue otherWorkQueue = new RabbitMQWorkQueue(otherTaskManagerWorker, rabbitMQExtension.getRabbitConnectionPool(), taskSerializer);
-        otherWorkQueue.start();
-
-        IntStream.range(0, 9)
-            .forEach(ignoredIndex -> testee.submit(TASK_WITH_ID_2));
+        ImmediateWorker otherTaskManagerWorker = new ImmediateWorker();
+        try (RabbitMQWorkQueue otherWorkQueue = new RabbitMQWorkQueue(otherTaskManagerWorker, rabbitMQExtension.getRabbitConnectionPool(), taskSerializer)) {
+            otherWorkQueue.start();
 
-        verify(taskManagerWorker, new Timeout(1000, times(10))).executeTask(any());
+            IntStream.range(0, 9)
+                .forEach(ignoredIndex -> testee.submit(TASK_WITH_ID_2));
 
-        verify(otherTaskManagerWorker, new Timeout(1000, times(0))).executeTask(any());
+            await().atMost(FIVE_HUNDRED_MILLISECONDS).until(() -> taskManagerWorker.results.size() == 10);
+            assertThat(otherTaskManagerWorker.tasks).isEmpty();
+        }
     }
 
     @Test
@@ -134,20 +149,22 @@ class RabbitMQWorkQueueTest {
         TaskId taskId = TaskId.fromString("4bf6d081-aa30-11e9-bf6c-2d3b9e84aafd");
         TaskWithId taskWithId = new TaskWithId(taskId, task);
 
-        TaskManagerWorker otherTaskManagerWorker = mock(TaskManagerWorker.class);
+        ImmediateWorker otherTaskManagerWorker = new ImmediateWorker();
         JsonTaskSerializer otherTaskSerializer = new JsonTaskSerializer(TestTaskDTOModules.TEST_TYPE);
-        RabbitMQWorkQueue otherWorkQueue = new RabbitMQWorkQueue(otherTaskManagerWorker, rabbitMQExtension.getRabbitConnectionPool(), otherTaskSerializer);
-        //wait to be sur that the first workqueue has subscribed as an exclusive consumer of the RabbitMQ queue.
-        Thread.sleep(200);
-        otherWorkQueue.start();
+        try (RabbitMQWorkQueue otherWorkQueue = new RabbitMQWorkQueue(otherTaskManagerWorker, rabbitMQExtension.getRabbitConnectionPool(), otherTaskSerializer)) {
+            //wait to be sur that the first workqueue has subscribed as an exclusive consumer of the RabbitMQ queue.
+            Thread.sleep(200);
+            otherWorkQueue.start();
 
-        otherWorkQueue.submit(taskWithId);
+            otherWorkQueue.submit(taskWithId);
 
-        verify(taskManagerWorker, new Timeout(100, times(0))).executeTask(any());
-        verify(taskManagerWorker, timeout(100)).fail(eq(taskId), eq(Optional.empty()), any(), any());
+            await().atMost(FIVE_HUNDRED_MILLISECONDS).until(() -> taskManagerWorker.failedTasks.size() == 1);
+            assertThat(taskManagerWorker.failedTasks).containsExactly(taskWithId.getId());
 
-        testee.submit(TASK_WITH_ID);
-        verify(taskManagerWorker, timeout(100)).executeTask(any());
+            testee.submit(TASK_WITH_ID);
+            await().atMost(FIVE_HUNDRED_MILLISECONDS).until(() -> taskManagerWorker.results.size() == 1);
+            assertThat(taskManagerWorker.tasks).containsExactly(TASK_WITH_ID);
+        }
     }
 
     @Test


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[james-project] 02/17: JAMES-2813 refactor TaskAggregate to prevent creation of the aggregate if History doesn't start with Created event

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rouazana pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit ad2c55cd985fd4f55fb21f67d78941a0ed169fbb
Author: Matthieu Baechler <ma...@apache.org>
AuthorDate: Tue Oct 8 10:25:30 2019 +0200

    JAMES-2813 refactor TaskAggregate to prevent creation of the aggregate if History doesn't start with Created event
---
 .../james/task/eventsourcing/CommandHandlers.scala |  2 +-
 .../james/task/eventsourcing/TaskAggregate.scala   | 68 ++++++++++++----------
 .../task/eventsourcing/TaskAggregateTest.java      | 21 +++++--
 3 files changed, 55 insertions(+), 36 deletions(-)

diff --git a/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/CommandHandlers.scala b/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/CommandHandlers.scala
index ddda76a..1184988 100644
--- a/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/CommandHandlers.scala
+++ b/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/CommandHandlers.scala
@@ -37,7 +37,7 @@ class CreateCommandHandler(private val loadHistory: TaskAggregateId => History,
   override def handledClass: Class[Create] = classOf[Create]
 
   override def handle(command: Create): util.List[_ <: Event] = {
-    loadAggregate(loadHistory, command.id).create(command.task, hostname)
+    TaskAggregate.create(TaskAggregateId(command.id), command.task, hostname)
   }
 }
 
diff --git a/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/TaskAggregate.scala b/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/TaskAggregate.scala
index 0c158ba..1a2094b 100644
--- a/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/TaskAggregate.scala
+++ b/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/TaskAggregate.scala
@@ -31,63 +31,67 @@ import scala.collection.JavaConverters._
 
 class TaskAggregate private(val aggregateId: TaskAggregateId, private val history: History) {
 
-  private val currentStatus: Option[Status] = history
+  history.getEvents.asScala.headOption match {
+    case Some(Created(_, _, _, _)) =>
+    case _ => throw new IllegalArgumentException("History must start with Created event")
+  }
+
+  private val currentStatus: Status = history
     .getEvents
     .asScala
     .foldLeft(DecisionProjection.empty)((decision, event) => decision.update(event))
     .status
-
-
-  def create(task: Task, hostname: Hostname): util.List[Event] = {
-    if (currentStatus.isEmpty) {
-      createEventWithId(Created(aggregateId, _, task, hostname))
-    } else Nil.asJava
-  }
+    .get
 
   private[eventsourcing] def start(hostname: Hostname): util.List[Event] = {
-    currentStatus match {
-      case Some(Status.WAITING) => createEventWithId(Started(aggregateId, _, hostname))
-      case _ => Nil.asJava
+    if (!currentStatus.isFinished) {
+      createEventWithId(Started(aggregateId, _, hostname))
+    } else {
+      Nil.asJava
     }
   }
 
   def requestCancel(hostname: Hostname): util.List[Event] = {
-    currentStatus match {
-      case Some(status) if !status.isFinished => createEventWithId(CancelRequested(aggregateId, _, hostname))
-      case _ => Nil.asJava
+    if (!currentStatus.isFinished) {
+      createEventWithId(CancelRequested(aggregateId, _, hostname))
+    } else {
+      Nil.asJava
     }
   }
 
   private[eventsourcing] def update(additionalInformation: AdditionalInformation): util.List[Event] = {
     currentStatus match {
-      case Some(Status.IN_PROGRESS) => createEventWithId(AdditionalInformationUpdated(aggregateId, _, additionalInformation))
-      case Some(Status.CANCEL_REQUESTED) => createEventWithId(AdditionalInformationUpdated(aggregateId, _, additionalInformation))
-      case Some(Status.COMPLETED) => Nil.asJava
-      case Some(Status.FAILED) => Nil.asJava
-      case Some(Status.WAITING) => Nil.asJava
-      case Some(Status.CANCELLED) => Nil.asJava
+      case Status.IN_PROGRESS => createEventWithId(AdditionalInformationUpdated(aggregateId, _, additionalInformation))
+      case Status.CANCEL_REQUESTED => createEventWithId(AdditionalInformationUpdated(aggregateId, _, additionalInformation))
+      case Status.COMPLETED => Nil.asJava
+      case Status.FAILED => Nil.asJava
+      case Status.WAITING => Nil.asJava
+      case Status.CANCELLED => Nil.asJava
       case _ => Nil.asJava
     }
   }
 
   private[eventsourcing] def complete(result: Result, additionalInformation: Option[AdditionalInformation]): util.List[Event] = {
-    currentStatus match {
-      case Some(status) if !status.isFinished => createEventWithId(Completed(aggregateId, _, result, additionalInformation))
-      case _ => Nil.asJava
+    if (!currentStatus.isFinished) {
+      createEventWithId(Completed(aggregateId, _, result, additionalInformation))
+    } else {
+      Nil.asJava
     }
   }
 
   private[eventsourcing] def fail(additionalInformation: Option[AdditionalInformation], errorMessage: Option[String], exception: Option[String]): util.List[Event] = {
-    currentStatus match {
-      case Some(status) if !status.isFinished => createEventWithId(Failed(aggregateId, _, additionalInformation, errorMessage, exception))
-      case _ => Nil.asJava
+    if (!currentStatus.isFinished) {
+      createEventWithId(Failed(aggregateId, _, additionalInformation, errorMessage, exception))
+    } else {
+      Nil.asJava
     }
   }
 
   private[eventsourcing] def cancel(additionalInformation: Option[AdditionalInformation]): util.List[Event] = {
-    currentStatus match {
-      case Some(status) if !status.isFinished => createEventWithId(Cancelled(aggregateId, _, additionalInformation))
-      case _ => Nil.asJava
+    if (!currentStatus.isFinished) {
+      createEventWithId(Cancelled(aggregateId, _, additionalInformation))
+    } else {
+      Nil.asJava
     }
   }
 
@@ -98,5 +102,9 @@ class TaskAggregate private(val aggregateId: TaskAggregateId, private val histor
 }
 
 object TaskAggregate {
-  def fromHistory(aggregateId: TaskAggregateId, history: History) = new TaskAggregate(aggregateId, history)
+  def fromHistory(aggregateId: TaskAggregateId, history: History): TaskAggregate = new TaskAggregate(aggregateId, history)
+
+  def create(aggregateId: TaskAggregateId, task: Task, hostname: Hostname): util.List[Event] = {
+    List[Event](Created(aggregateId, EventId.first(), task, hostname)).asJava
+  }
 }
diff --git a/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/TaskAggregateTest.java b/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/TaskAggregateTest.java
index 03797da..075deda 100644
--- a/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/TaskAggregateTest.java
+++ b/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/TaskAggregateTest.java
@@ -19,6 +19,7 @@
 package org.apache.james.task.eventsourcing;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 import java.util.Arrays;
 import java.util.function.Function;
@@ -35,8 +36,7 @@ import org.apache.james.task.TaskId;
 import org.junit.jupiter.api.Test;
 
 import com.google.common.collect.Streams;
-import scala.None;
-import scala.None$;
+import scala.Option;
 
 class TaskAggregateTest {
 
@@ -53,6 +53,17 @@ class TaskAggregateTest {
     }
 
     @Test
+    void TaskAggregateShouldThrowWhenHistoryDoesntStartWithCreatedEvent() {
+        assertThatThrownBy(() -> TaskAggregate.fromHistory(ID, buildHistory(eventId -> Started.apply(ID, eventId, HOSTNAME))))
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    void TaskAggregateShouldThrowWhenEmptyHistory() {
+        assertThatThrownBy(() -> TaskAggregate.fromHistory(ID, History.empty())).isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
     void givenNoStartedTaskEmitNoEventWhenUpdateAdditionalInformationCommand() {
         History history = buildHistory(
             eventId -> Created.apply(ID, eventId, new MemoryReferenceWithCounterTask((counter) -> Task.Result.COMPLETED), HOSTNAME)
@@ -90,7 +101,7 @@ class TaskAggregateTest {
         History history = buildHistory(
             eventId -> Created.apply(ID, eventId, task, HOSTNAME),
             eventId -> Started.apply(ID, eventId, HOSTNAME),
-            eventId -> Completed.apply(ID, eventId, Task.Result.COMPLETED, task.type(), None$.empty())
+            eventId -> Completed.apply(ID, eventId, Task.Result.COMPLETED, Option.empty())
         );
         TaskAggregate aggregate = TaskAggregate.fromHistory(ID, history);
         assertThat(aggregate.update(new MemoryReferenceWithCounterTask.AdditionalInformation(3))).isEmpty();
@@ -102,7 +113,7 @@ class TaskAggregateTest {
         History history = buildHistory(
             eventId -> Created.apply(ID, eventId, task, HOSTNAME),
             eventId -> Started.apply(ID, eventId, HOSTNAME),
-            eventId -> Failed.apply(ID, eventId, task.type(), None$.empty())
+            eventId -> Failed.apply(ID, eventId, Option.empty(), Option.empty(), Option.empty())
         );
         TaskAggregate aggregate = TaskAggregate.fromHistory(ID, history);
         assertThat(aggregate.update(new MemoryReferenceWithCounterTask.AdditionalInformation(3))).isEmpty();
@@ -114,7 +125,7 @@ class TaskAggregateTest {
         History history = buildHistory(
             eventId -> Created.apply(ID, eventId, task, HOSTNAME),
             eventId -> Started.apply(ID, eventId, HOSTNAME),
-            eventId -> Cancelled.apply(ID, eventId, task.type(), None$.empty())
+            eventId -> Cancelled.apply(ID, eventId, Option.empty())
         );
         TaskAggregate aggregate = TaskAggregate.fromHistory(ID, history);
         assertThat(aggregate.update(new MemoryReferenceWithCounterTask.AdditionalInformation(3))).isEmpty();


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[james-project] 03/17: JAMES-2813 refactor TaskAggregate to split infrastructure code from logic

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rouazana pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit d74ecf0796ee3dc6d53d48c4827d0758e45aa05d
Author: Matthieu Baechler <ma...@apache.org>
AuthorDate: Tue Oct 8 11:21:50 2019 +0200

    JAMES-2813 refactor TaskAggregate to split infrastructure code from logic
---
 .../james/task/eventsourcing/TaskAggregate.scala   | 110 +++++++++------------
 1 file changed, 45 insertions(+), 65 deletions(-)

diff --git a/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/TaskAggregate.scala b/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/TaskAggregate.scala
index 1a2094b..1a4bbbb 100644
--- a/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/TaskAggregate.scala
+++ b/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/TaskAggregate.scala
@@ -1,21 +1,21 @@
 /** **************************************************************
-  * 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.                                           *
-  * ***************************************************************/
+ * 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.james.task.eventsourcing
 
 import java.util
@@ -43,62 +43,42 @@ class TaskAggregate private(val aggregateId: TaskAggregateId, private val histor
     .status
     .get
 
-  private[eventsourcing] def start(hostname: Hostname): util.List[Event] = {
-    if (!currentStatus.isFinished) {
-      createEventWithId(Started(aggregateId, _, hostname))
-    } else {
-      Nil.asJava
-    }
-  }
+  private def optionToJavaList[T](element: Option[T]): util.List[T] = element.toList.asJava
 
-  def requestCancel(hostname: Hostname): util.List[Event] = {
+  private def createEventIfNotFinished(event: EventId => Event): Option[Event] = {
     if (!currentStatus.isFinished) {
-      createEventWithId(CancelRequested(aggregateId, _, hostname))
-    } else {
-      Nil.asJava
-    }
+      Some(event(history.getNextEventId))
+    } else
+      None
   }
 
-  private[eventsourcing] def update(additionalInformation: AdditionalInformation): util.List[Event] = {
-    currentStatus match {
-      case Status.IN_PROGRESS => createEventWithId(AdditionalInformationUpdated(aggregateId, _, additionalInformation))
-      case Status.CANCEL_REQUESTED => createEventWithId(AdditionalInformationUpdated(aggregateId, _, additionalInformation))
-      case Status.COMPLETED => Nil.asJava
-      case Status.FAILED => Nil.asJava
-      case Status.WAITING => Nil.asJava
-      case Status.CANCELLED => Nil.asJava
-      case _ => Nil.asJava
-    }
-  }
+  private def createEvent(event: EventId => Event): Option[Event] = Some(event(history.getNextEventId))
 
-  private[eventsourcing] def complete(result: Result, additionalInformation: Option[AdditionalInformation]): util.List[Event] = {
-    if (!currentStatus.isFinished) {
-      createEventWithId(Completed(aggregateId, _, result, additionalInformation))
-    } else {
-      Nil.asJava
-    }
-  }
+  private def createEventIfNotFinishedAsJavaList(event: EventId => Event): util.List[Event] = optionToJavaList(createEventIfNotFinished(event))
 
-  private[eventsourcing] def fail(additionalInformation: Option[AdditionalInformation], errorMessage: Option[String], exception: Option[String]): util.List[Event] = {
-    if (!currentStatus.isFinished) {
-      createEventWithId(Failed(aggregateId, _, additionalInformation, errorMessage, exception))
-    } else {
-      Nil.asJava
-    }
-  }
+  private[eventsourcing] def start(hostname: Hostname): util.List[Event] =
+    createEventIfNotFinishedAsJavaList(Started(aggregateId, _, hostname))
 
-  private[eventsourcing] def cancel(additionalInformation: Option[AdditionalInformation]): util.List[Event] = {
-    if (!currentStatus.isFinished) {
-      createEventWithId(Cancelled(aggregateId, _, additionalInformation))
-    } else {
-      Nil.asJava
-    }
-  }
+  private[eventsourcing] def requestCancel(hostname: Hostname): util.List[Event] =
+    createEventIfNotFinishedAsJavaList(CancelRequested(aggregateId, _, hostname))
+
+  private[eventsourcing] def update(additionalInformation: AdditionalInformation): util.List[Event] =
+    (currentStatus match {
+      case Status.IN_PROGRESS => createEvent(AdditionalInformationUpdated(aggregateId, _, additionalInformation))
+      case Status.CANCEL_REQUESTED => createEvent(AdditionalInformationUpdated(aggregateId, _, additionalInformation))
+      case _ => None
+    }).toList.asJava
+
+  private[eventsourcing] def complete(result: Result, additionalInformation: Option[AdditionalInformation]): util.List[Event] =
+    createEventIfNotFinishedAsJavaList(Completed(aggregateId, _, result, additionalInformation))
+
+
+  private[eventsourcing] def fail(additionalInformation: Option[AdditionalInformation], errorMessage: Option[String], exception: Option[String]): util.List[Event] =
+    createEventIfNotFinishedAsJavaList(Failed(aggregateId, _, additionalInformation, errorMessage, exception))
+
+  private[eventsourcing] def cancel(additionalInformation: Option[AdditionalInformation]): util.List[Event] =
+    createEventIfNotFinishedAsJavaList(Cancelled(aggregateId, _, additionalInformation))
 
-  private def createEventWithId(event: EventId => Event): util.List[Event] =
-    List(history.getNextEventId)
-      .map({ eventId => event(eventId) })
-      .asJava
 }
 
 object TaskAggregate {


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[james-project] 01/17: JAMES-2813 handle UpdateAdditionalInformation command into the aggregate

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rouazana pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit b483682befaed94e483e9137546e93ae9bc1bdbe
Author: Matthieu Baechler <ma...@apache.org>
AuthorDate: Mon Oct 7 17:23:54 2019 +0200

    JAMES-2813 handle UpdateAdditionalInformation command into the aggregate
---
 .../apache/james/task/eventsourcing/Events.scala   |   2 +
 .../james/task/eventsourcing/TaskAggregate.scala   |  12 ++
 .../james/task/eventsourcing/TaskCommand.scala     |   2 +
 .../task/eventsourcing/TaskAggregateTest.java      | 122 +++++++++++++++++++++
 4 files changed, 138 insertions(+)

diff --git a/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/Events.scala b/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/Events.scala
index f31c33a..42b4cc0 100644
--- a/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/Events.scala
+++ b/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/Events.scala
@@ -33,6 +33,8 @@ case class Created(aggregateId: TaskAggregateId, override val eventId: EventId,
 
 case class Started(aggregateId: TaskAggregateId, override val eventId: EventId, hostname: Hostname) extends TaskEvent(aggregateId, eventId)
 
+case class AdditionalInformationUpdated(aggregateId: TaskAggregateId, override val eventId: EventId, additionalInformation: AdditionalInformation) extends TaskEvent(aggregateId, eventId)
+
 case class CancelRequested(aggregateId: TaskAggregateId, override val eventId: EventId, hostname: Hostname) extends TaskEvent(aggregateId, eventId)
 
 case class Completed(aggregateId: TaskAggregateId, override val eventId: EventId, result: Result, additionalInformation: Option[AdditionalInformation]) extends TerminalTaskEvent(aggregateId, eventId, additionalInformation)
diff --git a/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/TaskAggregate.scala b/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/TaskAggregate.scala
index 9b902d2..0c158ba 100644
--- a/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/TaskAggregate.scala
+++ b/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/TaskAggregate.scala
@@ -58,6 +58,18 @@ class TaskAggregate private(val aggregateId: TaskAggregateId, private val histor
     }
   }
 
+  private[eventsourcing] def update(additionalInformation: AdditionalInformation): util.List[Event] = {
+    currentStatus match {
+      case Some(Status.IN_PROGRESS) => createEventWithId(AdditionalInformationUpdated(aggregateId, _, additionalInformation))
+      case Some(Status.CANCEL_REQUESTED) => createEventWithId(AdditionalInformationUpdated(aggregateId, _, additionalInformation))
+      case Some(Status.COMPLETED) => Nil.asJava
+      case Some(Status.FAILED) => Nil.asJava
+      case Some(Status.WAITING) => Nil.asJava
+      case Some(Status.CANCELLED) => Nil.asJava
+      case _ => Nil.asJava
+    }
+  }
+
   private[eventsourcing] def complete(result: Result, additionalInformation: Option[AdditionalInformation]): util.List[Event] = {
     currentStatus match {
       case Some(status) if !status.isFinished => createEventWithId(Completed(aggregateId, _, result, additionalInformation))
diff --git a/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/TaskCommand.scala b/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/TaskCommand.scala
index 6c8b20e..15abba5 100644
--- a/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/TaskCommand.scala
+++ b/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/TaskCommand.scala
@@ -31,6 +31,8 @@ object TaskCommand {
 
   case class Start(id: TaskId) extends TaskCommand
 
+  case class UpdateAdditionalInformation(id: TaskId, additionalInformation: AdditionalInformation) extends TaskCommand
+
   case class Complete(id: TaskId, result: Result, additionalInformation: Option[AdditionalInformation]) extends TaskCommand
 
   case class RequestCancel(id: TaskId) extends TaskCommand
diff --git a/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/TaskAggregateTest.java b/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/TaskAggregateTest.java
new file mode 100644
index 0000000..03797da
--- /dev/null
+++ b/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/TaskAggregateTest.java
@@ -0,0 +1,122 @@
+/****************************************************************
+ * 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.james.task.eventsourcing;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Arrays;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.james.eventsourcing.Event;
+import org.apache.james.eventsourcing.EventId;
+import org.apache.james.eventsourcing.eventstore.History;
+import org.apache.james.task.Hostname;
+import org.apache.james.task.MemoryReferenceWithCounterTask;
+import org.apache.james.task.Task;
+import org.apache.james.task.TaskId;
+import org.junit.jupiter.api.Test;
+
+import com.google.common.collect.Streams;
+import scala.None;
+import scala.None$;
+
+class TaskAggregateTest {
+
+    static final Hostname HOSTNAME = Hostname.apply("foo");
+    static final TaskAggregateId ID = TaskAggregateId.apply(TaskId.generateTaskId());
+
+    History buildHistory(Function<EventId, Event>... events) {
+        return History.of(
+            Streams.zip(
+                    Stream.iterate(EventId.first(), EventId::next),
+                    Arrays.stream(events),
+                    (id, event) -> event.apply(id))
+                .collect(Collectors.toList()));
+    }
+
+    @Test
+    void givenNoStartedTaskEmitNoEventWhenUpdateAdditionalInformationCommand() {
+        History history = buildHistory(
+            eventId -> Created.apply(ID, eventId, new MemoryReferenceWithCounterTask((counter) -> Task.Result.COMPLETED), HOSTNAME)
+        );
+        TaskAggregate aggregate = TaskAggregate.fromHistory(ID, history);
+        assertThat(aggregate.update(new MemoryReferenceWithCounterTask.AdditionalInformation(3))).isEmpty();
+    }
+
+    @Test
+    void givenInProgressTaskEmitEventWhenUpdateAdditionalInformationCommand() {
+        History history = buildHistory(
+            eventId -> Created.apply(ID, eventId, new MemoryReferenceWithCounterTask((counter) -> Task.Result.COMPLETED), HOSTNAME),
+            eventId -> Started.apply(ID, eventId, HOSTNAME)
+        );
+        TaskAggregate aggregate = TaskAggregate.fromHistory(ID, history);
+        assertThat(aggregate.update(new MemoryReferenceWithCounterTask.AdditionalInformation(3)))
+            .containsExactly(AdditionalInformationUpdated.apply(ID, history.getNextEventId(), new MemoryReferenceWithCounterTask.AdditionalInformation(3)));
+    }
+
+    @Test
+    void givenCancelRequestedTaskEmitEventWhenUpdateAdditionalInformationCommand() {
+        History history = buildHistory(
+            eventId -> Created.apply(ID, eventId, new MemoryReferenceWithCounterTask((counter) -> Task.Result.COMPLETED), HOSTNAME),
+            eventId -> Started.apply(ID, eventId, HOSTNAME),
+            eventId -> CancelRequested.apply(ID, eventId, HOSTNAME)
+        );
+        TaskAggregate aggregate = TaskAggregate.fromHistory(ID, history);
+        assertThat(aggregate.update(new MemoryReferenceWithCounterTask.AdditionalInformation(3)))
+            .containsExactly(AdditionalInformationUpdated.apply(ID, history.getNextEventId(), new MemoryReferenceWithCounterTask.AdditionalInformation(3)));
+    }
+
+    @Test
+    void givenCompletedTaskEmitNoEventWhenUpdateAdditionalInformationCommand() {
+        MemoryReferenceWithCounterTask task = new MemoryReferenceWithCounterTask((counter) -> Task.Result.COMPLETED);
+        History history = buildHistory(
+            eventId -> Created.apply(ID, eventId, task, HOSTNAME),
+            eventId -> Started.apply(ID, eventId, HOSTNAME),
+            eventId -> Completed.apply(ID, eventId, Task.Result.COMPLETED, task.type(), None$.empty())
+        );
+        TaskAggregate aggregate = TaskAggregate.fromHistory(ID, history);
+        assertThat(aggregate.update(new MemoryReferenceWithCounterTask.AdditionalInformation(3))).isEmpty();
+    }
+
+    @Test
+    void givenFailedTaskEmitNoEventWhenUpdateAdditionalInformationCommand() {
+        MemoryReferenceWithCounterTask task = new MemoryReferenceWithCounterTask((counter) -> Task.Result.COMPLETED);
+        History history = buildHistory(
+            eventId -> Created.apply(ID, eventId, task, HOSTNAME),
+            eventId -> Started.apply(ID, eventId, HOSTNAME),
+            eventId -> Failed.apply(ID, eventId, task.type(), None$.empty())
+        );
+        TaskAggregate aggregate = TaskAggregate.fromHistory(ID, history);
+        assertThat(aggregate.update(new MemoryReferenceWithCounterTask.AdditionalInformation(3))).isEmpty();
+    }
+
+    @Test
+    void givenCancelTaskEmitNoEventWhenUpdateAdditionalInformationCommand() {
+        MemoryReferenceWithCounterTask task = new MemoryReferenceWithCounterTask((counter) -> Task.Result.COMPLETED);
+        History history = buildHistory(
+            eventId -> Created.apply(ID, eventId, task, HOSTNAME),
+            eventId -> Started.apply(ID, eventId, HOSTNAME),
+            eventId -> Cancelled.apply(ID, eventId, task.type(), None$.empty())
+        );
+        TaskAggregate aggregate = TaskAggregate.fromHistory(ID, history);
+        assertThat(aggregate.update(new MemoryReferenceWithCounterTask.AdditionalInformation(3))).isEmpty();
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[james-project] 09/17: JAMES-2813 wire UpdateAdditionalInformation command in the Event Sourcing system

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rouazana pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 1b1ee833b339a80ac949f6350e6c0a6d8e847e1b
Author: Matthieu Baechler <ma...@apache.org>
AuthorDate: Wed Oct 9 15:21:47 2019 +0200

    JAMES-2813 wire UpdateAdditionalInformation command in the Event Sourcing system
---
 .../distributed/TasksSerializationModule.java      | 11 ++++++++-
 .../eventsourcing/distributed/TaskEventDTO.scala   | 19 +++++++++++++++
 .../apache/james/task/SerialTaskManagerWorker.java |  2 +-
 .../james/task/eventsourcing/CommandHandlers.scala |  8 +++++++
 .../task/eventsourcing/DecisionProjection.scala    | 27 +++++++++++-----------
 .../eventsourcing/EventSourcingTaskManager.scala   |  3 ++-
 .../task/eventsourcing/WorkerStatusListener.scala  |  6 ++---
 .../james/task/SerialTaskManagerWorkerTest.java    |  5 ++--
 8 files changed, 59 insertions(+), 22 deletions(-)

diff --git a/server/task/task-distributed/src/main/java/org/apache/james/task/eventsourcing/distributed/TasksSerializationModule.java b/server/task/task-distributed/src/main/java/org/apache/james/task/eventsourcing/distributed/TasksSerializationModule.java
index 07cadec..ed2a80a 100644
--- a/server/task/task-distributed/src/main/java/org/apache/james/task/eventsourcing/distributed/TasksSerializationModule.java
+++ b/server/task/task-distributed/src/main/java/org/apache/james/task/eventsourcing/distributed/TasksSerializationModule.java
@@ -26,6 +26,7 @@ import java.util.stream.Stream;
 import org.apache.james.eventsourcing.eventstore.cassandra.dto.EventDTOModule;
 import org.apache.james.server.task.json.JsonTaskAdditionalInformationsSerializer;
 import org.apache.james.server.task.json.JsonTaskSerializer;
+import org.apache.james.task.eventsourcing.AdditionalInformationUpdated;
 import org.apache.james.task.eventsourcing.CancelRequested;
 import org.apache.james.task.eventsourcing.Cancelled;
 import org.apache.james.task.eventsourcing.Completed;
@@ -90,8 +91,16 @@ public interface TasksSerializationModule {
         .typeName("task-manager-cancelled")
         .withFactory(EventDTOModule::new);
 
+    TaskSerializationModuleFactory<AdditionalInformationUpdated, AdditionalInformationUpdatedDTO> UPDATED = (jsonTaskSerializer, jsonTaskAdditionalInformationsSerializer) -> EventDTOModule
+        .forEvent(AdditionalInformationUpdated.class)
+        .convertToDTO(AdditionalInformationUpdatedDTO.class)
+        .toDomainObjectConverter(dto -> dto.toDomainObject(jsonTaskAdditionalInformationsSerializer))
+        .toDTOConverter((event, typeName) -> AdditionalInformationUpdatedDTO.fromDomainObject(jsonTaskAdditionalInformationsSerializer, event, typeName))
+        .typeName("task-manager-updated")
+        .withFactory(EventDTOModule::new);
+
     BiFunction<JsonTaskSerializer, JsonTaskAdditionalInformationsSerializer, List<EventDTOModule<?, ?>>> MODULES = (jsonTaskSerializer, jsonTaskAdditionalInformationsSerializer) -> Stream
-        .of(CREATED, STARTED, CANCEL_REQUESTED, CANCELLED, COMPLETED, FAILED)
+        .of(CREATED, STARTED, CANCEL_REQUESTED, CANCELLED, COMPLETED, FAILED, UPDATED)
         .map(moduleFactory -> moduleFactory.create(jsonTaskSerializer, jsonTaskAdditionalInformationsSerializer))
         .collect(Guavate.toImmutableList());
 }
diff --git a/server/task/task-distributed/src/main/scala/org/apache/james/task/eventsourcing/distributed/TaskEventDTO.scala b/server/task/task-distributed/src/main/scala/org/apache/james/task/eventsourcing/distributed/TaskEventDTO.scala
index 978d08e..4730a4c 100644
--- a/server/task/task-distributed/src/main/scala/org/apache/james/task/eventsourcing/distributed/TaskEventDTO.scala
+++ b/server/task/task-distributed/src/main/scala/org/apache/james/task/eventsourcing/distributed/TaskEventDTO.scala
@@ -27,6 +27,7 @@ import org.apache.james.eventsourcing.eventstore.cassandra.dto.EventDTO
 import org.apache.james.server.task.json.{JsonTaskAdditionalInformationsSerializer, JsonTaskSerializer}
 import org.apache.james.task.eventsourcing._
 import org.apache.james.task.{Hostname, Task, TaskId}
+
 import scala.compat.java8.OptionConverters._
 
 sealed abstract class TaskEventDTO(val getType: String, val getAggregate: String, val getEvent: Int) extends EventDTO {
@@ -140,3 +141,21 @@ object CancelledDTO {
     CancelledDTO(typeName, event.aggregateId.taskId.asString(), event.eventId.serialize(), serializedAdditionalInformations)
   }
 }
+
+case class AdditionalInformationUpdatedDTO(@JsonProperty("type") typeName: String,
+                     @JsonProperty("aggregate") aggregateId: String,
+                     @JsonProperty("event") eventId: Int,
+                     @JsonProperty("additionalInformation") getAdditionalInformation: String)
+  extends TaskEventDTO(typeName, aggregateId, eventId) {
+  def toDomainObject(jsonTaskAdditionalInformationsSerializer: JsonTaskAdditionalInformationsSerializer): AdditionalInformationUpdated = {
+    val deserializedAdditionalInformation = jsonTaskAdditionalInformationsSerializer.deserialize(getAdditionalInformation)
+    AdditionalInformationUpdated(domainAggregateId, domainEventId, deserializedAdditionalInformation)
+  }
+}
+
+object AdditionalInformationUpdatedDTO {
+  def fromDomainObject(jsonTaskAdditionalInformationsSerializer: JsonTaskAdditionalInformationsSerializer)(event: AdditionalInformationUpdated, typeName: String): AdditionalInformationUpdatedDTO = {
+    val serializedAdditionalInformations = jsonTaskAdditionalInformationsSerializer.serialize(event.additionalInformation)
+    AdditionalInformationUpdatedDTO(typeName, event.aggregateId.taskId.asString(), event.eventId.serialize(), serializedAdditionalInformations)
+  }
+}
diff --git a/server/task/task-memory/src/main/java/org/apache/james/task/SerialTaskManagerWorker.java b/server/task/task-memory/src/main/java/org/apache/james/task/SerialTaskManagerWorker.java
index 8fb0e15..60b5a16 100644
--- a/server/task/task-memory/src/main/java/org/apache/james/task/SerialTaskManagerWorker.java
+++ b/server/task/task-memory/src/main/java/org/apache/james/task/SerialTaskManagerWorker.java
@@ -114,7 +114,7 @@ public class SerialTaskManagerWorker implements TaskManagerWorker {
             .delayElement(Duration.ofSeconds(1))
             .repeat()
             .flatMap(Mono::justOrEmpty)
-            .doOnNext(information -> listener.updated(taskWithId.getId(), taskWithId.getTask().type(), information));
+            .doOnNext(information -> listener.updated(taskWithId.getId(), information));
     }
 
 
diff --git a/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/CommandHandlers.scala b/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/CommandHandlers.scala
index 1184988..9db161e 100644
--- a/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/CommandHandlers.scala
+++ b/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/CommandHandlers.scala
@@ -81,4 +81,12 @@ class FailCommandHandler(private val loadHistory: TaskAggregateId => History) ex
   override def handle(command: Fail): util.List[_ <: Event] = {
     loadAggregate(loadHistory, command.id).fail(command.additionalInformation, command.errorMessage, command.exception)
   }
+}
+
+class UpdateCommandHandler(private val loadHistory: TaskAggregateId => History) extends TaskCommandHandler[UpdateAdditionalInformation] {
+  override def handledClass: Class[UpdateAdditionalInformation] = classOf[UpdateAdditionalInformation]
+
+  override def handle(command: UpdateAdditionalInformation): util.List[_ <: Event] = {
+    loadAggregate(loadHistory, command.id).update(command.additionalInformation)
+  }
 }
\ No newline at end of file
diff --git a/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/DecisionProjection.scala b/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/DecisionProjection.scala
index d5394bb..b5a460f 100644
--- a/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/DecisionProjection.scala
+++ b/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/DecisionProjection.scala
@@ -22,22 +22,23 @@ import org.apache.james.eventsourcing.Event
 import org.apache.james.task.TaskManager.Status
 
 case class DecisionProjection(status: Option[Status]) {
-  val update: Event => DecisionProjection =
-    DecisionProjection.create
+  def update(event: Event): DecisionProjection = {
+    DecisionProjection(
+      event match {
+        case event: Created => Some(Status.WAITING)
+        case event: Started => Some(Status.IN_PROGRESS)
+        case event: CancelRequested => Some(Status.CANCEL_REQUESTED)
+        case event: Cancelled => Some(Status.CANCELLED)
+        case event: Completed => Some(Status.COMPLETED)
+        case event: Failed => Some(Status.FAILED)
+        case event: AdditionalInformationUpdated => status
+      }
+    )
+  }
+
 }
 
 object DecisionProjection {
-  def create(event: Event): DecisionProjection = DecisionProjection(Some(fromEvent(event)))
-
   def empty: DecisionProjection = DecisionProjection(None)
-
-  private def fromEvent(event: Event): Status = event match {
-    case event: Created => Status.WAITING
-    case event: Started => Status.IN_PROGRESS
-    case event: CancelRequested => Status.CANCEL_REQUESTED
-    case event: Cancelled => Status.CANCELLED
-    case event: Completed => Status.COMPLETED
-    case event: Failed => Status.FAILED
-  }
 }
 
diff --git a/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/EventSourcingTaskManager.scala b/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/EventSourcingTaskManager.scala
index 20cf21d..35856b0 100644
--- a/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/EventSourcingTaskManager.scala
+++ b/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/EventSourcingTaskManager.scala
@@ -59,7 +59,8 @@ class EventSourcingTaskManager @Inject @VisibleForTesting private[eventsourcing]
       new RequestCancelCommandHandler(loadHistory, hostname),
       new CompleteCommandHandler(loadHistory),
       new CancelCommandHandler(loadHistory),
-      new FailCommandHandler(loadHistory)),
+      new FailCommandHandler(loadHistory),
+      new UpdateCommandHandler(loadHistory)),
     subscribers = Set(
       executionDetailsProjection.asSubscriber(hostname),
       workDispatcher,
diff --git a/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/WorkerStatusListener.scala b/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/WorkerStatusListener.scala
index b6a3f80..aa93d82 100644
--- a/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/WorkerStatusListener.scala
+++ b/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/WorkerStatusListener.scala
@@ -21,13 +21,12 @@ package org.apache.james.task.eventsourcing
 
 import java.util.Optional
 
+import com.google.common.base.Throwables
 import org.apache.james.eventsourcing.EventSourcingSystem
 import org.apache.james.task.Task.Result
 import org.apache.james.task.eventsourcing.TaskCommand._
 import org.apache.james.task.{TaskExecutionDetails, TaskId, TaskManagerWorker}
 
-import com.google.common.base.Throwables
-
 import scala.compat.java8.OptionConverters._
 
 case class WorkerStatusListener(eventSourcingSystem: EventSourcingSystem) extends TaskManagerWorker.Listener {
@@ -49,5 +48,6 @@ case class WorkerStatusListener(eventSourcingSystem: EventSourcingSystem) extend
   override def cancelled(taskId: TaskId, additionalInformation: Optional[TaskExecutionDetails.AdditionalInformation]): Unit =
     eventSourcingSystem.dispatch(Cancel(taskId, additionalInformation.asScala ))
 
-  override def updated(taskId: TaskId, additionalInformation: TaskExecutionDetails.AdditionalInformation): Unit = ???
+  override def updated(taskId: TaskId, additionalInformation: TaskExecutionDetails.AdditionalInformation): Unit =
+    eventSourcingSystem.dispatch(UpdateAdditionalInformation(taskId, additionalInformation))
 }
\ No newline at end of file
diff --git a/server/task/task-memory/src/test/java/org/apache/james/task/SerialTaskManagerWorkerTest.java b/server/task/task-memory/src/test/java/org/apache/james/task/SerialTaskManagerWorkerTest.java
index 7448a20..b693347 100644
--- a/server/task/task-memory/src/test/java/org/apache/james/task/SerialTaskManagerWorkerTest.java
+++ b/server/task/task-memory/src/test/java/org/apache/james/task/SerialTaskManagerWorkerTest.java
@@ -100,10 +100,9 @@ class SerialTaskManagerWorkerTest {
 
         worker.executeTask(taskWithId).block();
 
-        verify(listener, atMost(2)).updated(eq(taskWithId.getId()), notNull());
+        verify(listener, atMost(3)).updated(eq(taskWithId.getId()), notNull());
     }
 
-
     @Test
     void aRunningTaskShouldEmitAtMostOneInformationPerSecond() {
         TaskWithId taskWithId = new TaskWithId(TaskId.generateTaskId(), new MemoryReferenceWithCounterTask((counter) ->
@@ -115,7 +114,7 @@ class SerialTaskManagerWorkerTest {
 
         worker.executeTask(taskWithId).block();
 
-        verify(listener, times(2)).updated(eq(taskWithId.getId()), notNull());
+        verify(listener, atMost(3)).updated(eq(taskWithId.getId()), notNull());
     }
 
     @Test


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[james-project] 17/17: JAMES-2813 use immutable datastructure when building history in TaskAggregateTest

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rouazana pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit a21e5ecc3048acb88c13b79159d829af964c44f0
Author: Rémi KOWALSKI <rk...@linagora.com>
AuthorDate: Thu Oct 17 11:48:20 2019 +0200

    JAMES-2813 use immutable datastructure when building history in TaskAggregateTest
---
 .../java/org/apache/james/task/eventsourcing/TaskAggregateTest.java   | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/TaskAggregateTest.java b/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/TaskAggregateTest.java
index e12f8bc..b9bdbb1 100644
--- a/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/TaskAggregateTest.java
+++ b/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/TaskAggregateTest.java
@@ -24,7 +24,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import java.time.Instant;
 import java.util.Arrays;
 import java.util.function.Function;
-import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import org.apache.james.eventsourcing.Event;
@@ -36,6 +35,7 @@ import org.apache.james.task.Task;
 import org.apache.james.task.TaskId;
 import org.junit.jupiter.api.Test;
 
+import com.github.steveash.guavate.Guavate;
 import com.google.common.collect.Streams;
 import scala.Option;
 
@@ -51,7 +51,7 @@ class TaskAggregateTest {
                     Stream.iterate(EventId.first(), EventId::next),
                     Arrays.stream(events),
                     (id, event) -> event.apply(id))
-                .collect(Collectors.toList()));
+                .collect(Guavate.toImmutableList()));
     }
 
     @Test


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[james-project] 13/17: JAMES-2813 change polling intervall and make it a parameter of the worker

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rouazana pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit e1ff8c54a13a4609c1d42cae9c001da2e1ef36c0
Author: Rémi KOWALSKI <rk...@linagora.com>
AuthorDate: Thu Oct 17 10:55:50 2019 +0200

    JAMES-2813 change polling intervall and make it a parameter of the worker
---
 .../java/org/apache/james/task/TaskManagerContract.java     |  2 +-
 .../distributed/RabbitMQWorkQueueSupplier.scala             | 13 +++++++++++--
 .../distributed/DistributedTaskManagerTest.java             |  3 +--
 .../main/java/org/apache/james/task/MemoryTaskManager.java  |  3 ++-
 .../java/org/apache/james/task/SerialTaskManagerWorker.java |  6 ++++--
 .../org/apache/james/task/SerialTaskManagerWorkerTest.java  |  8 +++++---
 .../task/eventsourcing/EventSourcingTaskManagerTest.java    |  2 +-
 7 files changed, 25 insertions(+), 12 deletions(-)

diff --git a/server/task/task-api/src/test/java/org/apache/james/task/TaskManagerContract.java b/server/task/task-api/src/test/java/org/apache/james/task/TaskManagerContract.java
index d482eed..406671f 100644
--- a/server/task/task-api/src/test/java/org/apache/james/task/TaskManagerContract.java
+++ b/server/task/task-api/src/test/java/org/apache/james/task/TaskManagerContract.java
@@ -38,7 +38,7 @@ import org.hamcrest.Matchers;
 import org.junit.jupiter.api.Test;
 
 public interface TaskManagerContract {
-
+    java.time.Duration UPDATE_INFORMATION_POLLING_INTERVAL = java.time.Duration.ofSeconds(1);
     Duration slowPacedPollInterval = ONE_HUNDRED_MILLISECONDS;
     ConditionFactory calmlyAwait = Awaitility.with()
         .pollInterval(slowPacedPollInterval)
diff --git a/server/task/task-distributed/src/main/scala/org/apache/james/task/eventsourcing/distributed/RabbitMQWorkQueueSupplier.scala b/server/task/task-distributed/src/main/scala/org/apache/james/task/eventsourcing/distributed/RabbitMQWorkQueueSupplier.scala
index a6c468c..f86be44 100644
--- a/server/task/task-distributed/src/main/scala/org/apache/james/task/eventsourcing/distributed/RabbitMQWorkQueueSupplier.scala
+++ b/server/task/task-distributed/src/main/scala/org/apache/james/task/eventsourcing/distributed/RabbitMQWorkQueueSupplier.scala
@@ -18,8 +18,10 @@
  * ***************************************************************/
 package org.apache.james.task.eventsourcing.distributed
 
-import javax.inject.Inject
+import java.time.Duration
 
+import com.google.common.annotations.VisibleForTesting
+import javax.inject.Inject
 import org.apache.james.backends.rabbitmq.SimpleConnectionPool
 import org.apache.james.eventsourcing.EventSourcingSystem
 import org.apache.james.server.task.json.JsonTaskSerializer
@@ -28,9 +30,16 @@ import org.apache.james.task.eventsourcing.{WorkQueueSupplier, WorkerStatusListe
 
 class RabbitMQWorkQueueSupplier @Inject()(private val rabbitMQConnectionPool: SimpleConnectionPool,
                                 private val jsonTaskSerializer: JsonTaskSerializer) extends WorkQueueSupplier {
+
+  val DEFAULT_ADDITIONAL_INFORMATION_POLLING_INTERVAL =  Duration.ofSeconds(30)
   override def apply(eventSourcingSystem: EventSourcingSystem): RabbitMQWorkQueue = {
+     apply(eventSourcingSystem, DEFAULT_ADDITIONAL_INFORMATION_POLLING_INTERVAL)
+  }
+
+  @VisibleForTesting
+  def apply(eventSourcingSystem: EventSourcingSystem, additionalInformationPollingInterval: Duration): RabbitMQWorkQueue = {
     val listener = WorkerStatusListener(eventSourcingSystem)
-    val worker = new SerialTaskManagerWorker(listener)
+    val worker = new SerialTaskManagerWorker(listener, additionalInformationPollingInterval)
     val rabbitMQWorkQueue = new RabbitMQWorkQueue(worker, rabbitMQConnectionPool, jsonTaskSerializer)
     rabbitMQWorkQueue.start()
     rabbitMQWorkQueue
diff --git a/server/task/task-distributed/src/test/java/org/apache/james/task/eventsourcing/distributed/DistributedTaskManagerTest.java b/server/task/task-distributed/src/test/java/org/apache/james/task/eventsourcing/distributed/DistributedTaskManagerTest.java
index 6774b67..3f4be63 100644
--- a/server/task/task-distributed/src/test/java/org/apache/james/task/eventsourcing/distributed/DistributedTaskManagerTest.java
+++ b/server/task/task-distributed/src/test/java/org/apache/james/task/eventsourcing/distributed/DistributedTaskManagerTest.java
@@ -68,7 +68,6 @@ import org.awaitility.Duration;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.jupiter.api.extension.RegisterExtension;
 
 import com.github.steveash.guavate.Guavate;
@@ -86,7 +85,7 @@ class DistributedTaskManagerTest implements TaskManagerContract {
 
         @Override
         public WorkQueue apply(EventSourcingSystem eventSourcingSystem) {
-            RabbitMQWorkQueue workQueue = supplier.apply(eventSourcingSystem);
+            RabbitMQWorkQueue workQueue = supplier.apply(eventSourcingSystem, UPDATE_INFORMATION_POLLING_INTERVAL);
             workQueues.add(workQueue);
             return workQueue;
         }
diff --git a/server/task/task-memory/src/main/java/org/apache/james/task/MemoryTaskManager.java b/server/task/task-memory/src/main/java/org/apache/james/task/MemoryTaskManager.java
index 56ffd2d..64f60e9 100644
--- a/server/task/task-memory/src/main/java/org/apache/james/task/MemoryTaskManager.java
+++ b/server/task/task-memory/src/main/java/org/apache/james/task/MemoryTaskManager.java
@@ -93,6 +93,7 @@ public class MemoryTaskManager implements TaskManager {
         }
     }
 
+    private static final Duration UPDATE_INFORMATION_POLLING_DURATION = Duration.ofSeconds(5);
     private static final Duration AWAIT_POLLING_DURATION = Duration.ofMillis(500);
     public static final Duration NOW = Duration.ZERO;
 
@@ -105,7 +106,7 @@ public class MemoryTaskManager implements TaskManager {
     public MemoryTaskManager(Hostname hostname) {
         this.hostname = hostname;
         this.idToExecutionDetails = new ConcurrentHashMap<>();
-        this.worker = new SerialTaskManagerWorker(updater());
+        this.worker = new SerialTaskManagerWorker(updater(), UPDATE_INFORMATION_POLLING_DURATION);
         workQueue = new MemoryWorkQueue(worker);
     }
 
diff --git a/server/task/task-memory/src/main/java/org/apache/james/task/SerialTaskManagerWorker.java b/server/task/task-memory/src/main/java/org/apache/james/task/SerialTaskManagerWorker.java
index c296bf1..34f4548 100644
--- a/server/task/task-memory/src/main/java/org/apache/james/task/SerialTaskManagerWorker.java
+++ b/server/task/task-memory/src/main/java/org/apache/james/task/SerialTaskManagerWorker.java
@@ -52,8 +52,10 @@ public class SerialTaskManagerWorker implements TaskManagerWorker {
     private final AtomicReference<Tuple2<TaskId, Future<?>>> runningTask;
     private final Semaphore semaphore;
     private final Set<TaskId> cancelledTasks;
+    private final Duration pollingInterval;
 
-    public SerialTaskManagerWorker(Listener listener) {
+    public SerialTaskManagerWorker(Listener listener, Duration pollingInterval) {
+        this.pollingInterval = pollingInterval;
         this.taskExecutor = Executors.newSingleThreadExecutor(NamedThreadFactory.withName("task executor"));
         this.listener = listener;
         this.cancelledTasks = Sets.newConcurrentHashSet();
@@ -112,7 +114,7 @@ public class SerialTaskManagerWorker implements TaskManagerWorker {
 
     private Flux<TaskExecutionDetails.AdditionalInformation> pollAdditionalInformation(TaskWithId taskWithId) {
         return Mono.fromCallable(() -> taskWithId.getTask().details())
-            .delayElement(Duration.ofSeconds(1), Schedulers.boundedElastic())
+            .delayElement(pollingInterval, Schedulers.boundedElastic())
             .repeat()
             .flatMap(Mono::justOrEmpty)
             .doOnNext(information -> listener.updated(taskWithId.getId(), information));
diff --git a/server/task/task-memory/src/test/java/org/apache/james/task/SerialTaskManagerWorkerTest.java b/server/task/task-memory/src/test/java/org/apache/james/task/SerialTaskManagerWorkerTest.java
index b693347..a83392f 100644
--- a/server/task/task-memory/src/test/java/org/apache/james/task/SerialTaskManagerWorkerTest.java
+++ b/server/task/task-memory/src/test/java/org/apache/james/task/SerialTaskManagerWorkerTest.java
@@ -44,6 +44,8 @@ import org.junit.jupiter.api.Test;
 import reactor.core.publisher.Mono;
 
 class SerialTaskManagerWorkerTest {
+    private  static final Duration UPDATE_INFORMATION_POLLING_DURATION = Duration.ofSeconds(1);
+
     private TaskManagerWorker.Listener listener;
     private SerialTaskManagerWorker worker;
 
@@ -54,7 +56,7 @@ class SerialTaskManagerWorkerTest {
     @BeforeEach
     void beforeEach() {
         listener = mock(TaskManagerWorker.Listener.class);
-        worker = new SerialTaskManagerWorker(listener);
+        worker = new SerialTaskManagerWorker(listener, UPDATE_INFORMATION_POLLING_DURATION);
     }
 
     @AfterEach
@@ -94,13 +96,13 @@ class SerialTaskManagerWorkerTest {
         TaskWithId taskWithId = new TaskWithId(TaskId.generateTaskId(), new MemoryReferenceWithCounterTask((counter) ->
             Mono.fromCallable(counter::incrementAndGet)
                 .delayElement(Duration.ofSeconds(1))
-                .repeat(2)
+                .repeat(3)
                 .then(Mono.just(Task.Result.COMPLETED))
                 .block()));
 
         worker.executeTask(taskWithId).block();
 
-        verify(listener, atMost(3)).updated(eq(taskWithId.getId()), notNull());
+        verify(listener, atMost(4)).updated(eq(taskWithId.getId()), notNull());
     }
 
     @Test
diff --git a/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/EventSourcingTaskManagerTest.java b/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/EventSourcingTaskManagerTest.java
index d3d9653..05ef373 100644
--- a/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/EventSourcingTaskManagerTest.java
+++ b/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/EventSourcingTaskManagerTest.java
@@ -58,7 +58,7 @@ class EventSourcingTaskManagerTest implements TaskManagerContract {
         TaskExecutionDetailsProjection executionDetailsProjection = new MemoryTaskExecutionDetailsProjection();
         WorkQueueSupplier workQueueSupplier = eventSourcingSystem -> {
             WorkerStatusListener listener = new WorkerStatusListener(eventSourcingSystem);
-            TaskManagerWorker worker = new SerialTaskManagerWorker(listener);
+            TaskManagerWorker worker = new SerialTaskManagerWorker(listener, UPDATE_INFORMATION_POLLING_INTERVAL);
             return new MemoryWorkQueue(worker);
         };
         taskManager = new EventSourcingTaskManager(workQueueSupplier, eventStore, executionDetailsProjection, HOSTNAME, new MemoryTerminationSubscriber());


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[james-project] 06/17: JAMES-2813 handle additional information in projection

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rouazana pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit c58f80a721424504f6300ac3e799883ee49e619f
Author: Matthieu Baechler <ma...@apache.org>
AuthorDate: Wed Oct 9 11:12:13 2019 +0200

    JAMES-2813 handle additional information in projection
---
 .../org/apache/james/task/TaskExecutionDetails.scala  | 19 +++++++++++++++++++
 .../TaskExecutionDetailsProjection.scala              |  2 ++
 2 files changed, 21 insertions(+)

diff --git a/server/task/task-api/src/main/scala/org/apache/james/task/TaskExecutionDetails.scala b/server/task/task-api/src/main/scala/org/apache/james/task/TaskExecutionDetails.scala
index 499e9d1..b331907 100644
--- a/server/task/task-api/src/main/scala/org/apache/james/task/TaskExecutionDetails.scala
+++ b/server/task/task-api/src/main/scala/org/apache/james/task/TaskExecutionDetails.scala
@@ -100,6 +100,12 @@ class TaskExecutionDetails(val taskId: TaskId,
     case _ => this
   }
 
+  def updateInformation(information: AdditionalInformation): TaskExecutionDetails = status match {
+    case IN_PROGRESS => update(information)
+    case CANCEL_REQUESTED => update(information)
+    case _ => this
+  }
+
   def canEqual(other: Any): Boolean = other.isInstanceOf[TaskExecutionDetails]
 
   override def equals(other: Any): Boolean = other match {
@@ -145,6 +151,7 @@ class TaskExecutionDetails(val taskId: TaskId,
     additionalInformation = additionalInformation,
     startedDate = Optional.of(ZonedDateTime.now),
     ranNode = Optional.of(hostname))
+
   private def complete(finalAdditionalInformation: Optional[AdditionalInformation]) = new TaskExecutionDetails(taskId, `type`, TaskManager.Status.COMPLETED,
     submittedDate = submittedDate,
     submittedNode = submittedNode,
@@ -153,6 +160,7 @@ class TaskExecutionDetails(val taskId: TaskId,
     ranNode = ranNode,
     cancelRequestedNode = cancelRequestedNode,
     completedDate = Optional.of(ZonedDateTime.now))
+
   private def fail(finalAdditionalInformation: Optional[AdditionalInformation]) = new TaskExecutionDetails(taskId, `type`, TaskManager.Status.FAILED,
     submittedDate = submittedDate,
     submittedNode = submittedNode,
@@ -161,6 +169,7 @@ class TaskExecutionDetails(val taskId: TaskId,
     ranNode = ranNode,
     cancelRequestedNode = cancelRequestedNode,
     failedDate = Optional.of(ZonedDateTime.now))
+
   private def requestCancel(hostname: Hostname) = new TaskExecutionDetails(taskId, `type`, TaskManager.Status.CANCEL_REQUESTED,
     submittedDate = submittedDate,
     submittedNode = submittedNode,
@@ -169,6 +178,16 @@ class TaskExecutionDetails(val taskId: TaskId,
     ranNode = ranNode,
     cancelRequestedNode = Optional.of(hostname),
     canceledDate = Optional.of(ZonedDateTime.now))
+
+  private def update(updatedInformation: AdditionalInformation) = new TaskExecutionDetails(taskId, `type`, status,
+    submittedDate = submittedDate,
+    submittedNode = submittedNode,
+    additionalInformation = () => Optional.of(updatedInformation),
+    startedDate = startedDate,
+    ranNode = ranNode,
+    cancelRequestedNode = cancelRequestedNode,
+    canceledDate = canceledDate)
+
   private def cancel(finalAdditionalInformation: Optional[AdditionalInformation]) = new TaskExecutionDetails(taskId, `type`, TaskManager.Status.CANCELLED,
     submittedDate = submittedDate,
     submittedNode = submittedNode,
diff --git a/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/TaskExecutionDetailsProjection.scala b/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/TaskExecutionDetailsProjection.scala
index 0af572e..df28da4 100644
--- a/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/TaskExecutionDetailsProjection.scala
+++ b/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/TaskExecutionDetailsProjection.scala
@@ -40,6 +40,8 @@ trait TaskExecutionDetailsProjection {
       update(failed.aggregateId.taskId)(_.failed(failed.additionalInformation.asJava))
     case canceled: Cancelled =>
       update(canceled.aggregateId.taskId)(_.cancelEffectively(canceled.additionalInformation.asJava))
+    case updated: AdditionalInformationUpdated =>
+      update(updated.aggregateId.taskId)(_.updateInformation(updated.additionalInformation))
   }
 
   private def update(taskId: TaskId)(updater: TaskExecutionDetails => TaskExecutionDetails): Unit =


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[james-project] 10/17: JAMES-2813 prevent cassandra cleanup to happen before countdown latch release

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rouazana pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 0f0b834878c2fb79b5357efd290c0d6ebcb6c8bd
Author: Matthieu Baechler <ma...@apache.org>
AuthorDate: Wed Oct 9 15:22:29 2019 +0200

    JAMES-2813 prevent cassandra cleanup to happen before countdown latch release
---
 .../task/eventsourcing/distributed/DistributedTaskManagerTest.java    | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/server/task/task-distributed/src/test/java/org/apache/james/task/eventsourcing/distributed/DistributedTaskManagerTest.java b/server/task/task-distributed/src/test/java/org/apache/james/task/eventsourcing/distributed/DistributedTaskManagerTest.java
index 4017965..6774b67 100644
--- a/server/task/task-distributed/src/test/java/org/apache/james/task/eventsourcing/distributed/DistributedTaskManagerTest.java
+++ b/server/task/task-distributed/src/test/java/org/apache/james/task/eventsourcing/distributed/DistributedTaskManagerTest.java
@@ -73,7 +73,6 @@ import org.junit.jupiter.api.extension.RegisterExtension;
 
 import com.github.steveash.guavate.Guavate;
 
-@ExtendWith(CountDownLatchExtension.class)
 class DistributedTaskManagerTest implements TaskManagerContract {
 
     private static class TrackedRabbitMQWorkQueueSupplier implements WorkQueueSupplier {
@@ -127,6 +126,9 @@ class DistributedTaskManagerTest implements TaskManagerContract {
     @RegisterExtension
     static CassandraEventStoreExtension eventStoreExtension = new CassandraEventStoreExtension(cassandraCluster, MODULES);
 
+    @RegisterExtension
+    static CountDownLatchExtension countDownLatchExtension = new CountDownLatchExtension();
+
     private final CassandraCluster cassandra = cassandraCluster.getCassandraCluster();
     private final CassandraTaskExecutionDetailsProjectionDAO cassandraTaskExecutionDetailsProjectionDAO = new CassandraTaskExecutionDetailsProjectionDAO(cassandra.getConf(), cassandra.getTypesProvider(), jsonTaskAdditionalInformationsSerializer);
     private final TaskExecutionDetailsProjection executionDetailsProjection = new CassandraTaskExecutionDetailsProjection(cassandraTaskExecutionDetailsProjectionDAO);


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[james-project] 12/17: JAMES-2813 check that AdditionalInformation instances are shared between nodes

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rouazana pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 769d64edf8dbc22a5f27d5ce504f0c372f6ec803
Author: Matthieu Baechler <ma...@apache.org>
AuthorDate: Wed Oct 9 15:30:43 2019 +0200

    JAMES-2813 check that AdditionalInformation instances are shared between nodes
---
 .../org/apache/james/task/TaskManagerContract.java | 39 ++++++++++++++++++++++
 1 file changed, 39 insertions(+)

diff --git a/server/task/task-api/src/test/java/org/apache/james/task/TaskManagerContract.java b/server/task/task-api/src/test/java/org/apache/james/task/TaskManagerContract.java
index 2420a83..d482eed 100644
--- a/server/task/task-api/src/test/java/org/apache/james/task/TaskManagerContract.java
+++ b/server/task/task-api/src/test/java/org/apache/james/task/TaskManagerContract.java
@@ -275,6 +275,45 @@ public interface TaskManagerContract {
     }
 
     @Test
+    default void additionalInformationShouldBeUpdatedDuringExecution(CountDownLatch countDownLatch) {
+        TaskManager taskManager = taskManager();
+        TaskId id = taskManager.submit(new MemoryReferenceWithCounterTask((counter) -> {
+            counter.incrementAndGet();
+            countDownLatch.await();
+            return Task.Result.COMPLETED;
+        }));
+
+        awaitUntilTaskHasStatus(id, TaskManager.Status.IN_PROGRESS, taskManager);
+
+        calmlyAwait.atMost(FIVE_SECONDS).untilAsserted(() ->
+            assertThat(getAdditionalInformation(taskManager, id).getCount()).isEqualTo(1L));
+    }
+
+    @Test
+    default void additionalInformationShouldBeAvailableOnAnyTaskManagerDuringExecution(CountDownLatch countDownLatch) {
+        TaskManager taskManager = taskManager();
+        TaskManager otherTaskManager = taskManager();
+        TaskId id = taskManager.submit(new MemoryReferenceWithCounterTask((counter) -> {
+            counter.incrementAndGet();
+            countDownLatch.await();
+            return Task.Result.COMPLETED;
+        }));
+
+        awaitUntilTaskHasStatus(id, TaskManager.Status.IN_PROGRESS, taskManager);
+
+        calmlyAwait.atMost(FIVE_SECONDS).untilAsserted(() ->
+            assertThat(getAdditionalInformation(taskManager, id).getCount()).isEqualTo(1L));
+        assertThat(getAdditionalInformation(otherTaskManager, id).getCount()).isEqualTo(1L);
+    }
+
+    default MemoryReferenceWithCounterTask.AdditionalInformation getAdditionalInformation(TaskManager taskManager, TaskId id) {
+        return (MemoryReferenceWithCounterTask.AdditionalInformation) taskManager
+            .getExecutionDetails(id)
+            .getAdditionalInformation()
+            .get();
+    }
+
+    @Test
     default void getStatusShouldReturnFailedWhenRunPartially() {
         TaskManager taskManager = taskManager();
         TaskId taskId = taskManager.submit(


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[james-project] 16/17: JAMES-2813 use timestamp in additional information to refuse stalled update event

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rouazana pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 1da890c47c4cc0eae8686a304bf2a923bec60819
Author: Rémi KOWALSKI <rk...@linagora.com>
AuthorDate: Tue Oct 15 17:54:38 2019 +0200

    JAMES-2813 use timestamp in additional information to refuse stalled update event
---
 .../task/eventsourcing/DecisionProjection.scala    | 24 +++++++------
 .../james/task/eventsourcing/TaskAggregate.scala   | 21 ++++++-----
 .../task/eventsourcing/TaskAggregateTest.java      | 41 ++++++++++++++++++----
 3 files changed, 58 insertions(+), 28 deletions(-)

diff --git a/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/DecisionProjection.scala b/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/DecisionProjection.scala
index b5a460f..ce2f6d2 100644
--- a/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/DecisionProjection.scala
+++ b/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/DecisionProjection.scala
@@ -18,27 +18,29 @@
   * ***************************************************************/
 package org.apache.james.task.eventsourcing
 
+import java.time.Instant
+
 import org.apache.james.eventsourcing.Event
 import org.apache.james.task.TaskManager.Status
 
-case class DecisionProjection(status: Option[Status]) {
+case class DecisionProjection(status: Status, latestUpdateAdditionalInformationUpdate : Option[Instant]) {
   def update(event: Event): DecisionProjection = {
-    DecisionProjection(
       event match {
-        case event: Created => Some(Status.WAITING)
-        case event: Started => Some(Status.IN_PROGRESS)
-        case event: CancelRequested => Some(Status.CANCEL_REQUESTED)
-        case event: Cancelled => Some(Status.CANCELLED)
-        case event: Completed => Some(Status.COMPLETED)
-        case event: Failed => Some(Status.FAILED)
-        case event: AdditionalInformationUpdated => status
+        case _: Created => this
+        case _: Started => DecisionProjection(Status.IN_PROGRESS, None)
+        case _: CancelRequested => DecisionProjection(Status.CANCEL_REQUESTED,latestUpdateAdditionalInformationUpdate)
+        case event: Cancelled => DecisionProjection(Status.CANCELLED, event.additionalInformation.map(_.timestamp))
+        case event: Completed => DecisionProjection(Status.COMPLETED, event.additionalInformation.map(_.timestamp))
+        case event: Failed => DecisionProjection(Status.FAILED, event.additionalInformation.map(_.timestamp))
+        case event: AdditionalInformationUpdated => DecisionProjection(status, Some(event.additionalInformation.timestamp))
       }
-    )
   }
 
+  def additionalInformationIsOlderThan(timestamp: Instant) : Boolean = latestUpdateAdditionalInformationUpdate.forall(timestamp.isAfter)
+
 }
 
 object DecisionProjection {
-  def empty: DecisionProjection = DecisionProjection(None)
+  def initial(created : Created): DecisionProjection = DecisionProjection(Status.WAITING, None)
 }
 
diff --git a/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/TaskAggregate.scala b/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/TaskAggregate.scala
index 1a4bbbb..d73cf65 100644
--- a/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/TaskAggregate.scala
+++ b/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/TaskAggregate.scala
@@ -31,22 +31,21 @@ import scala.collection.JavaConverters._
 
 class TaskAggregate private(val aggregateId: TaskAggregateId, private val history: History) {
 
-  history.getEvents.asScala.headOption match {
-    case Some(Created(_, _, _, _)) =>
+  val initialEvent = history.getEvents.asScala.headOption match {
+    case Some(created @ Created(_, _, _, _)) => created
     case _ => throw new IllegalArgumentException("History must start with Created event")
   }
 
-  private val currentStatus: Status = history
+  private val currentDecisionProjection: DecisionProjection = history
     .getEvents
     .asScala
-    .foldLeft(DecisionProjection.empty)((decision, event) => decision.update(event))
-    .status
-    .get
+    .tail
+    .foldLeft(DecisionProjection.initial(initialEvent))((decision, event) => decision.update(event))
 
   private def optionToJavaList[T](element: Option[T]): util.List[T] = element.toList.asJava
 
   private def createEventIfNotFinished(event: EventId => Event): Option[Event] = {
-    if (!currentStatus.isFinished) {
+    if (!currentDecisionProjection.status.isFinished) {
       Some(event(history.getNextEventId))
     } else
       None
@@ -63,11 +62,11 @@ class TaskAggregate private(val aggregateId: TaskAggregateId, private val histor
     createEventIfNotFinishedAsJavaList(CancelRequested(aggregateId, _, hostname))
 
   private[eventsourcing] def update(additionalInformation: AdditionalInformation): util.List[Event] =
-    (currentStatus match {
-      case Status.IN_PROGRESS => createEvent(AdditionalInformationUpdated(aggregateId, _, additionalInformation))
-      case Status.CANCEL_REQUESTED => createEvent(AdditionalInformationUpdated(aggregateId, _, additionalInformation))
+    optionToJavaList(currentDecisionProjection.status match {
+      case Status.IN_PROGRESS if currentDecisionProjection.additionalInformationIsOlderThan(additionalInformation.timestamp) => createEvent(AdditionalInformationUpdated(aggregateId, _, additionalInformation))
+      case Status.CANCEL_REQUESTED if currentDecisionProjection.additionalInformationIsOlderThan(additionalInformation.timestamp) => createEvent(AdditionalInformationUpdated(aggregateId, _, additionalInformation))
       case _ => None
-    }).toList.asJava
+    })
 
   private[eventsourcing] def complete(result: Result, additionalInformation: Option[AdditionalInformation]): util.List[Event] =
     createEventIfNotFinishedAsJavaList(Completed(aggregateId, _, result, additionalInformation))
diff --git a/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/TaskAggregateTest.java b/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/TaskAggregateTest.java
index bb98b26..e12f8bc 100644
--- a/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/TaskAggregateTest.java
+++ b/server/task/task-memory/src/test/java/org/apache/james/task/eventsourcing/TaskAggregateTest.java
@@ -66,7 +66,7 @@ class TaskAggregateTest {
     }
 
     @Test
-    void givenNoStartedTaskEmitNoEventWhenUpdateAdditionalInformationCommand() {
+    void givenNoStartedTaskShouldEmitNoEventWhenUpdateAdditionalInformationCommand() {
         History history = buildHistory(
             eventId -> Created.apply(ID, eventId, new MemoryReferenceWithCounterTask((counter) -> Task.Result.COMPLETED), HOSTNAME)
         );
@@ -75,7 +75,7 @@ class TaskAggregateTest {
     }
 
     @Test
-    void givenInProgressTaskEmitEventWhenUpdateAdditionalInformationCommand() {
+    void givenInProgressTaskShouldEmitEventWhenUpdateAdditionalInformationCommand() {
         History history = buildHistory(
             eventId -> Created.apply(ID, eventId, new MemoryReferenceWithCounterTask((counter) -> Task.Result.COMPLETED), HOSTNAME),
             eventId -> Started.apply(ID, eventId, HOSTNAME)
@@ -86,7 +86,36 @@ class TaskAggregateTest {
     }
 
     @Test
-    void givenCancelRequestedTaskEmitEventWhenUpdateAdditionalInformationCommand() {
+    void givenInProgressTaskWithOneNewerUpdateShouldEmitEventWhenUpdateAdditionalInformationCommand() {
+        History history = buildHistory(
+            eventId -> Created.apply(ID, eventId, new MemoryReferenceWithCounterTask((counter) -> Task.Result.COMPLETED), HOSTNAME),
+            eventId -> Started.apply(ID, eventId, HOSTNAME),
+            eventId -> AdditionalInformationUpdated.apply(ID, eventId, new MemoryReferenceWithCounterTask.AdditionalInformation(1, timestamp))
+        );
+        TaskAggregate aggregate = TaskAggregate.fromHistory(ID, history);
+        Instant newEventTime = TaskAggregateTest.timestamp.plusSeconds(3);
+        MemoryReferenceWithCounterTask.AdditionalInformation youngerAdditionalInformation = new MemoryReferenceWithCounterTask.AdditionalInformation(3, newEventTime);
+        assertThat(aggregate.update(youngerAdditionalInformation))
+            .isNotEmpty()
+            .anySatisfy(event -> assertThat(event)
+                .isInstanceOfSatisfying(AdditionalInformationUpdated.class,
+                    additionalInformationUpdated -> assertThat(additionalInformationUpdated.additionalInformation().timestamp()).isEqualTo(newEventTime)));
+    }
+
+    @Test
+    void givenInProgressTaskWithOneStalledUpdateShouldEmitEventWhenUpdateAdditionalInformationCommand() {
+        History history = buildHistory(
+            eventId -> Created.apply(ID, eventId, new MemoryReferenceWithCounterTask((counter) -> Task.Result.COMPLETED), HOSTNAME),
+            eventId -> Started.apply(ID, eventId, HOSTNAME),
+            eventId -> AdditionalInformationUpdated.apply(ID, eventId, new MemoryReferenceWithCounterTask.AdditionalInformation(1, timestamp))
+        );
+        TaskAggregate aggregate = TaskAggregate.fromHistory(ID, history);
+        MemoryReferenceWithCounterTask.AdditionalInformation olderAdditionalInformation = new MemoryReferenceWithCounterTask.AdditionalInformation(3, timestamp.minusSeconds(3));
+        assertThat(aggregate.update(olderAdditionalInformation)).isEmpty();
+    }
+
+    @Test
+    void givenCancelRequestedTaskShouldEmitEventWhenUpdateAdditionalInformationCommand() {
         History history = buildHistory(
             eventId -> Created.apply(ID, eventId, new MemoryReferenceWithCounterTask((counter) -> Task.Result.COMPLETED), HOSTNAME),
             eventId -> Started.apply(ID, eventId, HOSTNAME),
@@ -98,7 +127,7 @@ class TaskAggregateTest {
     }
 
     @Test
-    void givenCompletedTaskEmitNoEventWhenUpdateAdditionalInformationCommand() {
+    void givenCompletedTaskShouldEmitNoEventWhenUpdateAdditionalInformationCommand() {
         MemoryReferenceWithCounterTask task = new MemoryReferenceWithCounterTask((counter) -> Task.Result.COMPLETED);
         History history = buildHistory(
             eventId -> Created.apply(ID, eventId, task, HOSTNAME),
@@ -110,7 +139,7 @@ class TaskAggregateTest {
     }
 
     @Test
-    void givenFailedTaskEmitNoEventWhenUpdateAdditionalInformationCommand() {
+    void givenFailedTaskShouldEmitNoEventWhenUpdateAdditionalInformationCommand() {
         MemoryReferenceWithCounterTask task = new MemoryReferenceWithCounterTask((counter) -> Task.Result.COMPLETED);
         History history = buildHistory(
             eventId -> Created.apply(ID, eventId, task, HOSTNAME),
@@ -122,7 +151,7 @@ class TaskAggregateTest {
     }
 
     @Test
-    void givenCancelTaskEmitNoEventWhenUpdateAdditionalInformationCommand() {
+    void givenCancelTaskShouldEmitNoEventWhenUpdateAdditionalInformationCommand() {
         MemoryReferenceWithCounterTask task = new MemoryReferenceWithCounterTask((counter) -> Task.Result.COMPLETED);
         History history = buildHistory(
             eventId -> Created.apply(ID, eventId, task, HOSTNAME),


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[james-project] 07/17: JAMES-2813 worker now calls Listener.updated once per second

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rouazana pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit e1e3e5ddd60f25310d0887092b83f92fbb3c59ce
Author: Matthieu Baechler <ma...@apache.org>
AuthorDate: Wed Oct 9 11:13:19 2019 +0200

    JAMES-2813 worker now calls Listener.updated once per second
---
 .../james/task/MemoryReferenceWithCounterTask.java |  8 ++++
 .../org/apache/james/task/MemoryTaskManager.java   |  6 +++
 .../apache/james/task/SerialTaskManagerWorker.java | 17 +++++++-
 .../org/apache/james/task/TaskManagerWorker.java   |  2 +
 .../task/eventsourcing/WorkerStatusListener.scala  |  2 +
 .../james/task/SerialTaskManagerWorkerTest.java    | 49 ++++++++++++++++++++++
 6 files changed, 83 insertions(+), 1 deletion(-)

diff --git a/server/task/task-api/src/test/java/org/apache/james/task/MemoryReferenceWithCounterTask.java b/server/task/task-api/src/test/java/org/apache/james/task/MemoryReferenceWithCounterTask.java
index 0a43f4c..819737d 100644
--- a/server/task/task-api/src/test/java/org/apache/james/task/MemoryReferenceWithCounterTask.java
+++ b/server/task/task-api/src/test/java/org/apache/james/task/MemoryReferenceWithCounterTask.java
@@ -23,6 +23,7 @@ import java.util.Optional;
 import java.util.concurrent.atomic.AtomicLong;
 
 import com.github.fge.lambdas.functions.ThrowingFunction;
+import com.google.common.base.MoreObjects;
 
 /**
  * This class is used for unit testing.
@@ -62,6 +63,13 @@ public class MemoryReferenceWithCounterTask implements Task {
         public int hashCode() {
             return Objects.hashCode(this.count);
         }
+
+        @Override
+        public String toString() {
+            return MoreObjects.toStringHelper(this)
+                .add("count", count)
+                .toString();
+        }
     }
 
     private final ThrowingFunction<AtomicLong, Result> task;
diff --git a/server/task/task-memory/src/main/java/org/apache/james/task/MemoryTaskManager.java b/server/task/task-memory/src/main/java/org/apache/james/task/MemoryTaskManager.java
index bffc24e..56ffd2d 100644
--- a/server/task/task-memory/src/main/java/org/apache/james/task/MemoryTaskManager.java
+++ b/server/task/task-memory/src/main/java/org/apache/james/task/MemoryTaskManager.java
@@ -85,6 +85,12 @@ public class MemoryTaskManager implements TaskManager {
             updaterFactory.apply(taskId)
                 .accept(details -> details.cancelEffectively(additionalInformation));
         }
+
+        @Override
+        public void updated(TaskId taskId, TaskExecutionDetails.AdditionalInformation additionalInformation) {
+            //The memory task manager doesn't use polling to update its additionalInformation.
+            throw new IllegalStateException();
+        }
     }
 
     private static final Duration AWAIT_POLLING_DURATION = Duration.ofMillis(500);
diff --git a/server/task/task-memory/src/main/java/org/apache/james/task/SerialTaskManagerWorker.java b/server/task/task-memory/src/main/java/org/apache/james/task/SerialTaskManagerWorker.java
index c7ac921..b954aa2 100644
--- a/server/task/task-memory/src/main/java/org/apache/james/task/SerialTaskManagerWorker.java
+++ b/server/task/task-memory/src/main/java/org/apache/james/task/SerialTaskManagerWorker.java
@@ -19,6 +19,7 @@
 package org.apache.james.task;
 
 import java.io.IOException;
+import java.time.Duration;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.Callable;
@@ -37,6 +38,8 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.common.collect.Sets;
+import reactor.core.Disposable;
+import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 import reactor.util.function.Tuple2;
 import reactor.util.function.Tuples;
@@ -86,6 +89,9 @@ public class SerialTaskManagerWorker implements TaskManagerWorker {
                 CompletableFuture<Task.Result> future = CompletableFuture.supplyAsync(() -> runWithMdc(taskWithId, listener), taskExecutor);
                 runningTask.set(Tuples.of(taskWithId.getId(), future));
 
+                Disposable informationPolling = pollAdditionalInformation(taskWithId)
+                    .doOnNext(information -> listener.updated(taskWithId.getId(), information))
+                    .subscribe();
                 return Mono.fromFuture(future)
                         .doOnError(exception -> {
                             if (exception instanceof CancellationException) {
@@ -94,7 +100,8 @@ public class SerialTaskManagerWorker implements TaskManagerWorker {
                                 listener.failed(taskWithId.getId(), taskWithId.getTask().details(), exception);
                             }
                         })
-                        .onErrorReturn(Task.Result.PARTIAL);
+                        .onErrorReturn(Task.Result.PARTIAL)
+                        .doOnTerminate(informationPolling::dispose);
             } else {
                 listener.cancelled(taskWithId.getId(), taskWithId.getTask().details());
                 return Mono.empty();
@@ -102,6 +109,14 @@ public class SerialTaskManagerWorker implements TaskManagerWorker {
         };
     }
 
+    private Flux<TaskExecutionDetails.AdditionalInformation> pollAdditionalInformation(TaskWithId taskWithId) {
+        return Mono.fromCallable(() -> taskWithId.getTask().details())
+            .delayElement(Duration.ofSeconds(1))
+            .repeat()
+            .flatMap(Mono::justOrEmpty);
+    }
+
+
     private Task.Result runWithMdc(TaskWithId taskWithId, Listener listener) {
         return MDCBuilder.withMdc(
             MDCBuilder.create()
diff --git a/server/task/task-memory/src/main/java/org/apache/james/task/TaskManagerWorker.java b/server/task/task-memory/src/main/java/org/apache/james/task/TaskManagerWorker.java
index d24206b..c6475e0 100644
--- a/server/task/task-memory/src/main/java/org/apache/james/task/TaskManagerWorker.java
+++ b/server/task/task-memory/src/main/java/org/apache/james/task/TaskManagerWorker.java
@@ -37,6 +37,8 @@ public interface TaskManagerWorker extends Closeable {
         void failed(TaskId taskId, Optional<TaskExecutionDetails.AdditionalInformation> additionalInformation);
 
         void cancelled(TaskId taskId, Optional<TaskExecutionDetails.AdditionalInformation> additionalInformation);
+
+        void updated(TaskId taskId, TaskExecutionDetails.AdditionalInformation additionalInformation);
     }
 
     Mono<Task.Result> executeTask(TaskWithId taskWithId);
diff --git a/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/WorkerStatusListener.scala b/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/WorkerStatusListener.scala
index b184c39..b6a3f80 100644
--- a/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/WorkerStatusListener.scala
+++ b/server/task/task-memory/src/main/scala/org/apache/james/task/eventsourcing/WorkerStatusListener.scala
@@ -48,4 +48,6 @@ case class WorkerStatusListener(eventSourcingSystem: EventSourcingSystem) extend
 
   override def cancelled(taskId: TaskId, additionalInformation: Optional[TaskExecutionDetails.AdditionalInformation]): Unit =
     eventSourcingSystem.dispatch(Cancel(taskId, additionalInformation.asScala ))
+
+  override def updated(taskId: TaskId, additionalInformation: TaskExecutionDetails.AdditionalInformation): Unit = ???
 }
\ No newline at end of file
diff --git a/server/task/task-memory/src/test/java/org/apache/james/task/SerialTaskManagerWorkerTest.java b/server/task/task-memory/src/test/java/org/apache/james/task/SerialTaskManagerWorkerTest.java
index 527d1d2..7448a20 100644
--- a/server/task/task-memory/src/test/java/org/apache/james/task/SerialTaskManagerWorkerTest.java
+++ b/server/task/task-memory/src/test/java/org/apache/james/task/SerialTaskManagerWorkerTest.java
@@ -21,8 +21,11 @@ package org.apache.james.task;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.notNull;
 import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.atMost;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 
@@ -30,6 +33,7 @@ import java.io.IOException;
 import java.time.Duration;
 import java.util.Optional;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import org.awaitility.Awaitility;
@@ -70,6 +74,51 @@ class SerialTaskManagerWorkerTest {
     }
 
     @Test
+    void aRunningTaskShouldProvideInformationUpdatesDuringExecution() throws InterruptedException {
+        TaskWithId taskWithId = new TaskWithId(TaskId.generateTaskId(), new MemoryReferenceWithCounterTask((counter) ->
+            Mono.fromCallable(counter::incrementAndGet)
+                .delayElement(Duration.ofSeconds(2))
+                .repeat(10)
+                .then(Mono.just(Task.Result.COMPLETED))
+                .block()));
+
+        worker.executeTask(taskWithId).subscribe();
+
+        TimeUnit.SECONDS.sleep(2);
+
+        verify(listener, atLeastOnce()).updated(eq(taskWithId.getId()), notNull());
+    }
+
+    @Test
+    void aRunningTaskShouldHaveAFiniteNumberOfInformation() throws InterruptedException {
+        TaskWithId taskWithId = new TaskWithId(TaskId.generateTaskId(), new MemoryReferenceWithCounterTask((counter) ->
+            Mono.fromCallable(counter::incrementAndGet)
+                .delayElement(Duration.ofSeconds(1))
+                .repeat(2)
+                .then(Mono.just(Task.Result.COMPLETED))
+                .block()));
+
+        worker.executeTask(taskWithId).block();
+
+        verify(listener, atMost(2)).updated(eq(taskWithId.getId()), notNull());
+    }
+
+
+    @Test
+    void aRunningTaskShouldEmitAtMostOneInformationPerSecond() {
+        TaskWithId taskWithId = new TaskWithId(TaskId.generateTaskId(), new MemoryReferenceWithCounterTask((counter) ->
+            Mono.fromCallable(counter::incrementAndGet)
+                .delayElement(Duration.ofMillis(10))
+                .repeat(200)
+                .then(Mono.just(Task.Result.COMPLETED))
+                .block()));
+
+        worker.executeTask(taskWithId).block();
+
+        verify(listener, times(2)).updated(eq(taskWithId.getId()), notNull());
+    }
+
+    @Test
     void aFailedTaskShouldCompleteWithFailedStatus() {
         TaskWithId taskWithId = new TaskWithId(TaskId.generateTaskId(), failedTask);
 


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


[james-project] 11/17: JAMES-2813 run AdditionalInformation polling into an elastic scheduler

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rouazana pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit e99bc34eaf358fb887d485d6657293006e58d9fc
Author: Matthieu Baechler <ma...@apache.org>
AuthorDate: Wed Oct 9 15:23:12 2019 +0200

    JAMES-2813 run AdditionalInformation polling into an elastic scheduler
---
 .../src/main/java/org/apache/james/task/SerialTaskManagerWorker.java   | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/server/task/task-memory/src/main/java/org/apache/james/task/SerialTaskManagerWorker.java b/server/task/task-memory/src/main/java/org/apache/james/task/SerialTaskManagerWorker.java
index 60b5a16..c296bf1 100644
--- a/server/task/task-memory/src/main/java/org/apache/james/task/SerialTaskManagerWorker.java
+++ b/server/task/task-memory/src/main/java/org/apache/james/task/SerialTaskManagerWorker.java
@@ -40,6 +40,7 @@ import org.slf4j.LoggerFactory;
 import com.google.common.collect.Sets;
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
+import reactor.core.scheduler.Schedulers;
 import reactor.util.function.Tuple2;
 import reactor.util.function.Tuples;
 
@@ -111,7 +112,7 @@ public class SerialTaskManagerWorker implements TaskManagerWorker {
 
     private Flux<TaskExecutionDetails.AdditionalInformation> pollAdditionalInformation(TaskWithId taskWithId) {
         return Mono.fromCallable(() -> taskWithId.getTask().details())
-            .delayElement(Duration.ofSeconds(1))
+            .delayElement(Duration.ofSeconds(1), Schedulers.boundedElastic())
             .repeat()
             .flatMap(Mono::justOrEmpty)
             .doOnNext(information -> listener.updated(taskWithId.getId(), information));


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org