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 bt...@apache.org on 2020/03/09 16:07:34 UTC

[james-project] 03/17: JAMES-3105 Add a service for recomputing mailbox counters

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

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

commit 23e7dd605febdbd1262df908bd9a800d2d53ae28
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Fri Mar 6 22:36:42 2020 +0700

    JAMES-3105 Add a service for recomputing mailbox counters
    
    Algorithm:
    
     - List existing mailboxes
     - List their messages
     - Check them against their source of truth
     - Compute mailbox counter values
     - And reset the value of the counter if needed
    
    Limitations:
    
    This won't be friendly in the face of concurrent operations, which
    would be ignored during the recomputation of the counter of the
    given mailbox. However the source of truth is unaffected hence,
    upon rerunning the algorithm, the result will be eventually correct.
    
    We rely on the "listing messages by mailbox" projection (that we
    recheck). Missing entries in there will be ignored until the given
    projection is healed (currently unsupported).
---
 .../mailbox/cassandra/ids/CassandraMessageId.java  |   2 +-
 .../mail/task/RecomputeMailboxCountersService.java | 198 ++++++++++++++
 .../task/RecomputeMailboxCountersServiceTest.java  | 287 +++++++++++++++++++++
 3 files changed, 486 insertions(+), 1 deletion(-)

diff --git a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/ids/CassandraMessageId.java b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/ids/CassandraMessageId.java
index be359a4..cf7cb63 100644
--- a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/ids/CassandraMessageId.java
+++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/ids/CassandraMessageId.java
@@ -41,7 +41,7 @@ public class CassandraMessageId implements MessageId {
         }
 
         @Override
-        public MessageId fromString(String serialized) {
+        public CassandraMessageId fromString(String serialized) {
             return of(UUID.fromString(serialized));
         }
     }
diff --git a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/task/RecomputeMailboxCountersService.java b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/task/RecomputeMailboxCountersService.java
new file mode 100644
index 0000000..d45cb30
--- /dev/null
+++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/task/RecomputeMailboxCountersService.java
@@ -0,0 +1,198 @@
+/****************************************************************
+ * 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.mailbox.cassandra.mail.task;
+
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentLinkedDeque;
+import java.util.concurrent.atomic.AtomicLong;
+
+import javax.inject.Inject;
+import javax.mail.Flags;
+
+import org.apache.james.mailbox.cassandra.ids.CassandraId;
+import org.apache.james.mailbox.cassandra.ids.CassandraMessageId;
+import org.apache.james.mailbox.cassandra.mail.CassandraMailboxCounterDAO;
+import org.apache.james.mailbox.cassandra.mail.CassandraMailboxDAO;
+import org.apache.james.mailbox.cassandra.mail.CassandraMessageIdDAO;
+import org.apache.james.mailbox.cassandra.mail.CassandraMessageIdToImapUidDAO;
+import org.apache.james.mailbox.model.ComposedMessageIdWithMetaData;
+import org.apache.james.mailbox.model.Mailbox;
+import org.apache.james.mailbox.model.MailboxCounters;
+import org.apache.james.mailbox.model.MessageRange;
+import org.apache.james.task.Task;
+import org.apache.james.task.Task.Result;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+public class RecomputeMailboxCountersService {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(RecomputeMailboxCountersService.class);
+
+    private static class Counter {
+        private final CassandraId mailboxId;
+        private final AtomicLong total;
+        private final AtomicLong unseen;
+
+        private Counter(CassandraId mailboxId) {
+            this.mailboxId = mailboxId;
+            unseen = new AtomicLong();
+            total = new AtomicLong();
+        }
+
+        void process(ComposedMessageIdWithMetaData metadata) {
+            total.incrementAndGet();
+            if (!metadata.getFlags().contains(Flags.Flag.SEEN)) {
+                unseen.incrementAndGet();
+            }
+        }
+
+        MailboxCounters snapshot() {
+            return MailboxCounters.builder()
+                .mailboxId(mailboxId)
+                .count(total.get())
+                .unseen(unseen.get())
+                .build();
+        }
+    }
+
+    static class Context {
+        static class Snapshot {
+            private final long processedMailboxCount;
+            private final ImmutableList<CassandraId> failedMailboxes;
+
+            private Snapshot(long processedMailboxCount, ImmutableList<CassandraId> failedMailboxes) {
+                this.processedMailboxCount = processedMailboxCount;
+                this.failedMailboxes = failedMailboxes;
+            }
+
+            long getProcessedMailboxCount() {
+                return processedMailboxCount;
+            }
+
+            ImmutableList<CassandraId> getFailedMailboxes() {
+                return failedMailboxes;
+            }
+
+            @Override
+            public final boolean equals(Object o) {
+                if (o instanceof Snapshot) {
+                    Snapshot snapshot = (Snapshot) o;
+
+                    return Objects.equals(this.processedMailboxCount, snapshot.processedMailboxCount)
+                        && Objects.equals(this.failedMailboxes, snapshot.failedMailboxes);
+                }
+                return false;
+            }
+
+            @Override
+            public final int hashCode() {
+                return Objects.hash(processedMailboxCount, failedMailboxes);
+            }
+
+            @Override
+            public String toString() {
+                return MoreObjects.toStringHelper(this)
+                    .add("processedMailboxCount", processedMailboxCount)
+                    .add("failedMailboxes", failedMailboxes)
+                    .toString();
+            }
+        }
+
+        private final AtomicLong processedMailboxCount;
+        private final ConcurrentLinkedDeque<CassandraId> failedMailboxes;
+
+        Context() {
+            processedMailboxCount = new AtomicLong();
+            failedMailboxes = new ConcurrentLinkedDeque<>();
+        }
+
+        void incrementProcessed() {
+            processedMailboxCount.incrementAndGet();
+        }
+
+        void addToFailedMailboxes(CassandraId cassandraId) {
+            failedMailboxes.add(cassandraId);
+        }
+
+        Snapshot snapshot() {
+            return new Snapshot(processedMailboxCount.get(),
+                ImmutableList.copyOf(failedMailboxes));
+        }
+    }
+
+    private final CassandraMailboxDAO mailboxDAO;
+    private final CassandraMessageIdDAO imapUidToMessageIdDAO;
+    private final CassandraMessageIdToImapUidDAO messageIdToImapUidDAO;
+    private final CassandraMailboxCounterDAO counterDAO;
+
+    @Inject
+    RecomputeMailboxCountersService(CassandraMailboxDAO mailboxDAO,
+                                    CassandraMessageIdDAO imapUidToMessageIdDAO,
+                                    CassandraMessageIdToImapUidDAO messageIdToImapUidDAO,
+                                    CassandraMailboxCounterDAO counterDAO) {
+        this.mailboxDAO = mailboxDAO;
+        this.imapUidToMessageIdDAO = imapUidToMessageIdDAO;
+        this.messageIdToImapUidDAO = messageIdToImapUidDAO;
+        this.counterDAO = counterDAO;
+    }
+
+    Mono<Result> recomputeMailboxCounters(Context context) {
+        return mailboxDAO.retrieveAllMailboxes()
+            .flatMap(mailbox -> recomputeMailboxCounter(context, mailbox))
+            .reduce(Result.COMPLETED, Task::combine)
+            .onErrorResume(e -> {
+                LOGGER.error("Error listing mailboxes", e);
+                return Mono.just(Result.PARTIAL);
+            });
+    }
+
+    private Mono<Result> recomputeMailboxCounter(Context context, Mailbox mailbox) {
+        CassandraId mailboxId = (CassandraId) mailbox.getMailboxId();
+        Counter counter = new Counter(mailboxId);
+
+        return imapUidToMessageIdDAO.retrieveMessages(mailboxId, MessageRange.all())
+            .flatMap(message -> latestMetadata(mailboxId, message))
+            .doOnNext(counter::process)
+            .then(Mono.defer(() -> counterDAO.resetCounters(counter.snapshot())))
+            .then(Mono.just(Result.COMPLETED))
+            .doOnNext(any -> {
+                LOGGER.info("Counters recomputed for {}", mailboxId.serialize());
+                context.incrementProcessed();
+            })
+            .onErrorResume(e -> {
+                context.addToFailedMailboxes(mailboxId);
+                LOGGER.error("Error while recomputing counters for {}", mailboxId.serialize(), e);
+                return Mono.just(Result.PARTIAL);
+            });
+    }
+
+    private Flux<ComposedMessageIdWithMetaData> latestMetadata(CassandraId mailboxId, ComposedMessageIdWithMetaData message) {
+        CassandraMessageId messageId = (CassandraMessageId) message.getComposedMessageId().getMessageId();
+
+        return messageIdToImapUidDAO.retrieve(messageId, Optional.of(mailboxId));
+    }
+}
diff --git a/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/task/RecomputeMailboxCountersServiceTest.java b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/task/RecomputeMailboxCountersServiceTest.java
new file mode 100644
index 0000000..334f7b4
--- /dev/null
+++ b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/task/RecomputeMailboxCountersServiceTest.java
@@ -0,0 +1,287 @@
+/****************************************************************
+ * 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.mailbox.cassandra.mail.task;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.UUID;
+
+import javax.mail.Flags;
+
+import org.apache.james.backends.cassandra.CassandraCluster;
+import org.apache.james.backends.cassandra.CassandraClusterExtension;
+import org.apache.james.backends.cassandra.components.CassandraModule;
+import org.apache.james.backends.cassandra.versions.CassandraSchemaVersionModule;
+import org.apache.james.core.Username;
+import org.apache.james.mailbox.MessageUid;
+import org.apache.james.mailbox.ModSeq;
+import org.apache.james.mailbox.cassandra.ids.CassandraId;
+import org.apache.james.mailbox.cassandra.ids.CassandraMessageId;
+import org.apache.james.mailbox.cassandra.mail.CassandraMailboxCounterDAO;
+import org.apache.james.mailbox.cassandra.mail.CassandraMailboxDAO;
+import org.apache.james.mailbox.cassandra.mail.CassandraMessageIdDAO;
+import org.apache.james.mailbox.cassandra.mail.CassandraMessageIdToImapUidDAO;
+import org.apache.james.mailbox.cassandra.mail.task.RecomputeMailboxCountersService.Context;
+import org.apache.james.mailbox.cassandra.modules.CassandraAclModule;
+import org.apache.james.mailbox.cassandra.modules.CassandraMailboxCounterModule;
+import org.apache.james.mailbox.cassandra.modules.CassandraMailboxModule;
+import org.apache.james.mailbox.cassandra.modules.CassandraMessageModule;
+import org.apache.james.mailbox.model.ComposedMessageId;
+import org.apache.james.mailbox.model.ComposedMessageIdWithMetaData;
+import org.apache.james.mailbox.model.Mailbox;
+import org.apache.james.mailbox.model.MailboxCounters;
+import org.apache.james.mailbox.model.MailboxPath;
+import org.apache.james.mailbox.model.UidValidity;
+import org.apache.james.task.Task.Result;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+class RecomputeMailboxCountersServiceTest {
+    private static final UidValidity UID_VALIDITY_1 = UidValidity.ofValid(145);
+    private static final Username USER = Username.of("user");
+    private static final MailboxPath MAILBOX_PATH = MailboxPath.forUser(USER, "abc");
+    private static final CassandraMessageId.Factory MESSAGE_ID_FACTORY = new CassandraMessageId.Factory();
+    private static final CassandraMessageId MESSAGE_ID_1 = MESSAGE_ID_FACTORY.fromString("40ff9e30-6022-11ea-9a94-d300cbf968c0");
+    private static CassandraId CASSANDRA_ID_1 = CassandraId.of(UUID.fromString("16d681e0-6023-11ea-a7f2-0f94ad804b0d"));
+    private static final ComposedMessageIdWithMetaData METADATA_UNSEEN = new ComposedMessageIdWithMetaData(new ComposedMessageId(CASSANDRA_ID_1, MESSAGE_ID_1, MessageUid.of(45)), new Flags(), ModSeq.of(45));
+    private static final ComposedMessageIdWithMetaData METADATA_SEEN = new ComposedMessageIdWithMetaData(new ComposedMessageId(CASSANDRA_ID_1, MESSAGE_ID_1, MessageUid.of(45)), new Flags(Flags.Flag.SEEN), ModSeq.of(45));
+    private static final Mailbox MAILBOX = new Mailbox(MAILBOX_PATH, UID_VALIDITY_1, CASSANDRA_ID_1);
+
+    @RegisterExtension
+    static CassandraClusterExtension cassandraCluster = new CassandraClusterExtension(
+        CassandraModule.aggregateModules(
+            CassandraSchemaVersionModule.MODULE,
+            CassandraMailboxModule.MODULE,
+            CassandraMessageModule.MODULE,
+            CassandraMailboxCounterModule.MODULE,
+            CassandraAclModule.MODULE));
+
+    CassandraMailboxDAO mailboxDAO;
+    CassandraMessageIdDAO imapUidToMessageIdDAO;
+    CassandraMessageIdToImapUidDAO messageIdToImapUidDAO;
+    CassandraMailboxCounterDAO counterDAO;
+    RecomputeMailboxCountersService testee;
+
+    @BeforeEach
+    void setUp(CassandraCluster cassandra) {
+        mailboxDAO = new CassandraMailboxDAO(cassandra.getConf(), cassandra.getTypesProvider());
+        imapUidToMessageIdDAO = new CassandraMessageIdDAO(cassandra.getConf(), MESSAGE_ID_FACTORY);
+        messageIdToImapUidDAO = new CassandraMessageIdToImapUidDAO(cassandra.getConf(), MESSAGE_ID_FACTORY);
+        counterDAO = new CassandraMailboxCounterDAO(cassandra.getConf());
+        testee = new RecomputeMailboxCountersService(mailboxDAO, imapUidToMessageIdDAO, messageIdToImapUidDAO, counterDAO);
+    }
+
+    @Test
+    void recomputeMailboxCountersShouldReturnCompletedWhenNoMailboxes() {
+        assertThat(testee.recomputeMailboxCounters(new Context()).block())
+            .isEqualTo(Result.COMPLETED);
+    }
+
+    @Test
+    void recomputeMailboxCountersShouldReturnCompletedWhenMailboxWithNoMessages() {
+        mailboxDAO.save(MAILBOX).block();
+
+        assertThat(testee.recomputeMailboxCounters(new Context()).block())
+            .isEqualTo(Result.COMPLETED);
+    }
+
+    @Test
+    void recomputeMailboxCountersShouldReturnCompletedWhenMailboxWithMessages() {
+        mailboxDAO.save(MAILBOX).block();
+        imapUidToMessageIdDAO.insert(METADATA_UNSEEN).block();
+        messageIdToImapUidDAO.insert(METADATA_UNSEEN).block();
+        counterDAO.incrementUnseen(CASSANDRA_ID_1).block();
+        counterDAO.incrementCount(CASSANDRA_ID_1).block();
+
+        testee.recomputeMailboxCounters(new Context()).block();
+
+        assertThat(testee.recomputeMailboxCounters(new Context()).block())
+            .isEqualTo(Result.COMPLETED);
+    }
+
+    @Test
+    void recomputeMailboxCountersShouldReturnCompletedWhenMessageDenormalizationIssue() {
+        mailboxDAO.save(MAILBOX).block();
+        imapUidToMessageIdDAO.insert(METADATA_UNSEEN).block();
+        messageIdToImapUidDAO.insert(METADATA_SEEN).block();
+        counterDAO.incrementUnseen(CASSANDRA_ID_1).block();
+        counterDAO.incrementCount(CASSANDRA_ID_1).block();
+
+        testee.recomputeMailboxCounters(new Context()).block();
+
+        assertThat(testee.recomputeMailboxCounters(new Context()).block())
+            .isEqualTo(Result.COMPLETED);
+    }
+
+    @Test
+    void recomputeMailboxCountersShouldReturnCountersAreIncorrect() {
+        mailboxDAO.save(MAILBOX).block();
+        imapUidToMessageIdDAO.insert(METADATA_UNSEEN).block();
+        messageIdToImapUidDAO.insert(METADATA_UNSEEN).block();
+
+        testee.recomputeMailboxCounters(new Context()).block();
+
+        assertThat(testee.recomputeMailboxCounters(new Context()).block())
+            .isEqualTo(Result.COMPLETED);
+    }
+
+    @Test
+    void recomputeMailboxCountersShouldReturnCompletedWhenOrphanMailboxRegistration() {
+        mailboxDAO.save(MAILBOX).block();
+        imapUidToMessageIdDAO.insert(METADATA_UNSEEN).block();
+        counterDAO.incrementUnseen(CASSANDRA_ID_1).block();
+        counterDAO.incrementCount(CASSANDRA_ID_1).block();
+
+        testee.recomputeMailboxCounters(new Context()).block();
+
+        assertThat(testee.recomputeMailboxCounters(new Context()).block())
+            .isEqualTo(Result.COMPLETED);
+    }
+
+    @Test
+    void recomputeMailboxCountersShouldReturnCompletedWhenMailboxListReferenceIsMissing() {
+        mailboxDAO.save(MAILBOX).block();
+        messageIdToImapUidDAO.insert(METADATA_UNSEEN).block();
+        counterDAO.incrementUnseen(CASSANDRA_ID_1).block();
+        counterDAO.incrementCount(CASSANDRA_ID_1).block();
+
+        testee.recomputeMailboxCounters(new Context()).block();
+
+        assertThat(testee.recomputeMailboxCounters(new Context()).block())
+            .isEqualTo(Result.COMPLETED);
+    }
+
+    @Test
+    void recomputeMailboxCountersShouldNoopWhenMailboxWithoutMessage() {
+        mailboxDAO.save(MAILBOX).block();
+
+        testee.recomputeMailboxCounters(new Context()).block();
+
+        assertThat(counterDAO.retrieveMailboxCounters(CASSANDRA_ID_1).blockOptional())
+            .isEmpty();
+    }
+
+    @Test
+    void recomputeMailboxCountersShouldNoopWhenValidCounters() {
+        mailboxDAO.save(MAILBOX).block();
+        imapUidToMessageIdDAO.insert(METADATA_UNSEEN).block();
+        messageIdToImapUidDAO.insert(METADATA_UNSEEN).block();
+        counterDAO.incrementUnseen(CASSANDRA_ID_1).block();
+        counterDAO.incrementCount(CASSANDRA_ID_1).block();
+
+        testee.recomputeMailboxCounters(new Context()).block();
+
+        assertThat(counterDAO.retrieveMailboxCounters(CASSANDRA_ID_1).blockOptional())
+            .contains(MailboxCounters.builder()
+                .mailboxId(CASSANDRA_ID_1)
+                .count(1)
+                .unseen(1)
+                .build());
+    }
+
+    @Test
+    void recomputeMailboxCountersShouldRecreateMissingCounters() {
+        mailboxDAO.save(MAILBOX).block();
+        imapUidToMessageIdDAO.insert(METADATA_UNSEEN).block();
+        messageIdToImapUidDAO.insert(METADATA_UNSEEN).block();
+
+        testee.recomputeMailboxCounters(new Context()).block();
+
+        assertThat(counterDAO.retrieveMailboxCounters(CASSANDRA_ID_1).blockOptional())
+            .contains(MailboxCounters.builder()
+                .mailboxId(CASSANDRA_ID_1)
+                .count(1)
+                .unseen(1)
+                .build());
+    }
+
+    @Test
+    void recomputeMailboxCountersShouldResetIncorrectCounters() {
+        mailboxDAO.save(MAILBOX).block();
+        imapUidToMessageIdDAO.insert(METADATA_UNSEEN).block();
+        messageIdToImapUidDAO.insert(METADATA_UNSEEN).block();
+        counterDAO.incrementCount(CASSANDRA_ID_1).block();
+
+        testee.recomputeMailboxCounters(new Context()).block();
+
+        assertThat(counterDAO.retrieveMailboxCounters(CASSANDRA_ID_1).blockOptional())
+            .contains(MailboxCounters.builder()
+                .mailboxId(CASSANDRA_ID_1)
+                .count(1)
+                .unseen(1)
+                .build());
+    }
+
+    @Test
+    void recomputeMailboxCountersShouldTakeSeenIntoAccount() {
+        mailboxDAO.save(MAILBOX).block();
+        imapUidToMessageIdDAO.insert(METADATA_SEEN).block();
+        messageIdToImapUidDAO.insert(METADATA_SEEN).block();
+        counterDAO.incrementCount(CASSANDRA_ID_1).block();
+
+        testee.recomputeMailboxCounters(new Context()).block();
+
+        assertThat(counterDAO.retrieveMailboxCounters(CASSANDRA_ID_1).blockOptional())
+            .contains(MailboxCounters.builder()
+                .mailboxId(CASSANDRA_ID_1)
+                .count(1)
+                .unseen(0)
+                .build());
+    }
+
+    @Test
+    void recomputeMailboxCountersShouldUseSourceOfTruthForComputation() {
+        mailboxDAO.save(MAILBOX).block();
+        imapUidToMessageIdDAO.insert(METADATA_SEEN).block();
+        messageIdToImapUidDAO.insert(METADATA_UNSEEN).block();
+
+        testee.recomputeMailboxCounters(new Context()).block();
+
+        assertThat(counterDAO.retrieveMailboxCounters(CASSANDRA_ID_1).blockOptional())
+            .contains(MailboxCounters.builder()
+                .mailboxId(CASSANDRA_ID_1)
+                .count(1)
+                .unseen(1)
+                .build());
+    }
+
+    @Test
+    void recomputeMailboxCountersShouldIgnoreMissingMailboxListReferences() {
+        mailboxDAO.save(MAILBOX).block();
+        imapUidToMessageIdDAO.insert(METADATA_SEEN).block();
+
+        testee.recomputeMailboxCounters(new Context()).block();
+
+        assertThat(counterDAO.retrieveMailboxCounters(CASSANDRA_ID_1).blockOptional())
+            .isEmpty();
+    }
+
+    @Test
+    void recomputeMailboxCountersShouldIgnoreOrphanMailboxListReference() {
+        mailboxDAO.save(MAILBOX).block();
+        imapUidToMessageIdDAO.insert(METADATA_UNSEEN).block();
+
+        testee.recomputeMailboxCounters(new Context()).block();
+
+        assertThat(counterDAO.retrieveMailboxCounters(CASSANDRA_ID_1).blockOptional())
+            .isEmpty();
+    }
+}
\ No newline at end of file


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