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/04/21 08:17:36 UTC

[james-project] branch master updated (457452d -> 3b512f1)

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

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


    from 457452d  JAMES-3009 ADR for using scala in the event sourcing modules
     new bbe0866  JAMES-3135 Write a CassandraDumbBlobStoreCache
     new 3b512f1  JAMES-3059 Remove a debug statement

The 2 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:
 .../apache/james/backends/cassandra/Scenario.java  |   4 +-
 .../apache/james/blob/cassandra/BlobTables.java    |   7 ++
 .../james/blob/cassandra/CassandraBlobStore.java   |   4 +-
 .../cache/CassandraCacheConfiguration.java         |  95 ++++++++++++++
 .../cache/CassandraDumbBlobCacheModule.java}       |  30 +++--
 .../cache/CassandraDumbBlobStoreCache.java         | 138 +++++++++++++++++++++
 .../blob/cassandra/cache/DumbBlobStoreCache.java}  |  13 +-
 .../cache/CassandraCacheConfigurationTest.java     | 130 +++++++++++++++++++
 .../CassandraDumbBlobStoreCacheTest.java}          |  47 +++----
 .../cache/DumbBlobStoreCacheContract.java          | 111 +++++++++++++++++
 10 files changed, 534 insertions(+), 45 deletions(-)
 create mode 100644 server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/cache/CassandraCacheConfiguration.java
 copy server/{data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/vacation/CassandraNotificationRegistryModule.java => blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/cache/CassandraDumbBlobCacheModule.java} (62%)
 create mode 100644 server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/cache/CassandraDumbBlobStoreCache.java
 copy server/{data/data-api/src/main/java/org/apache/james/dlp/api/DLPConfigurationLoader.java => blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/cache/DumbBlobStoreCache.java} (82%)
 create mode 100644 server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/cache/CassandraCacheConfigurationTest.java
 copy server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/{CassandraDumbBlobStoreTest.java => cache/CassandraDumbBlobStoreCacheTest.java} (59%)
 create mode 100644 server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/cache/DumbBlobStoreCacheContract.java


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


[james-project] 02/02: JAMES-3059 Remove a debug statement

Posted by bt...@apache.org.
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 3b512f1d9e6bf0635cc341cfe8d61e7d0c0d0ed8
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Apr 20 17:27:01 2020 +0700

    JAMES-3059 Remove a debug statement
---
 .../src/test/java/org/apache/james/backends/cassandra/Scenario.java   | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/Scenario.java b/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/Scenario.java
index efbf6a8..0d01c3e 100644
--- a/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/Scenario.java
+++ b/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/Scenario.java
@@ -36,9 +36,7 @@ public class Scenario {
     @FunctionalInterface
     interface Behavior {
         Behavior THROW = (session, statement) -> {
-            RuntimeException injected_failure = new RuntimeException("Injected failure");
-            injected_failure.printStackTrace();
-            throw injected_failure;
+            throw new RuntimeException("Injected failure");
         };
 
         Behavior EXECUTE_NORMALLY = Session::executeAsync;


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


[james-project] 01/02: JAMES-3135 Write a CassandraDumbBlobStoreCache

Posted by bt...@apache.org.
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 bbe0866f4d3cea603aca01a14e0f3fcdeb91248a
Author: ducnv <du...@gmail.com>
AuthorDate: Tue Apr 7 17:25:47 2020 +0700

    JAMES-3135 Write a CassandraDumbBlobStoreCache
---
 .../apache/james/blob/cassandra/BlobTables.java    |   7 ++
 .../james/blob/cassandra/CassandraBlobStore.java   |   4 +-
 .../cache/CassandraCacheConfiguration.java         |  95 ++++++++++++++
 .../CassandraDumbBlobCacheModule.java}             |  48 ++++---
 .../cache/CassandraDumbBlobStoreCache.java         | 138 +++++++++++++++++++++
 .../DumbBlobStoreCache.java}                       |  35 ++----
 .../cache/CassandraCacheConfigurationTest.java     | 130 +++++++++++++++++++
 .../cache/CassandraDumbBlobStoreCacheTest.java     |  62 +++++++++
 .../cache/DumbBlobStoreCacheContract.java          | 111 +++++++++++++++++
 9 files changed, 574 insertions(+), 56 deletions(-)

diff --git a/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/BlobTables.java b/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/BlobTables.java
index 13d6d93..92e6756 100644
--- a/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/BlobTables.java
+++ b/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/BlobTables.java
@@ -48,4 +48,11 @@ public interface BlobTables {
         String CHUNK_NUMBER = "chunkNumber";
         String DATA = "data";
     }
+
+    interface DumbBlobCache {
+        String TABLE_NAME = "blob_cache";
+        String ID = "id";
+        String DATA = "data";
+        String TTL_FOR_ROW = "ttl";
+    }
 }
diff --git a/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/CassandraBlobStore.java b/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/CassandraBlobStore.java
index cccee34..b59139a 100644
--- a/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/CassandraBlobStore.java
+++ b/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/CassandraBlobStore.java
@@ -44,7 +44,7 @@ import reactor.util.function.Tuples;
 
 public class CassandraBlobStore implements BlobStore {
 
-    public static final boolean LAZY_RESSOURCE_CLEANUP = false;
+    public static final boolean LAZY_RESOURCE_CLEANUP = false;
     public static final int FILE_THRESHOLD = 10000;
     private final HashBlobId.Factory blobIdFactory;
     private final BucketName defaultBucketName;
@@ -91,7 +91,7 @@ public class CassandraBlobStore implements BlobStore {
             () -> new FileBackedOutputStream(FILE_THRESHOLD),
             fileBackedOutputStream -> saveAndGenerateBlobId(bucketName, hashingInputStream, fileBackedOutputStream),
             Throwing.consumer(FileBackedOutputStream::reset).sneakyThrow(),
-            LAZY_RESSOURCE_CLEANUP);
+            LAZY_RESOURCE_CLEANUP);
     }
 
     private Mono<BlobId> saveAndGenerateBlobId(BucketName bucketName, HashingInputStream hashingInputStream, FileBackedOutputStream fileBackedOutputStream) {
diff --git a/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/cache/CassandraCacheConfiguration.java b/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/cache/CassandraCacheConfiguration.java
new file mode 100644
index 0000000..85be6ad
--- /dev/null
+++ b/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/cache/CassandraCacheConfiguration.java
@@ -0,0 +1,95 @@
+/****************************************************************
+ * 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.blob.cassandra.cache;
+
+import java.time.Duration;
+import java.util.Optional;
+
+import com.google.common.base.Preconditions;
+
+public class CassandraCacheConfiguration {
+
+    public static class Builder {
+        private static final Duration DEFAULT_READ_TIMEOUT = Duration.ofMillis(100);
+        private static final Duration MAX_READ_TIMEOUT = Duration.ofHours(1);
+        private static final Duration DEFAULT_TTL = Duration.ofDays(7);
+        private static final int DEFAULT_BYTE_THRESHOLD_SIZE = 8 * 1024;
+
+        private Optional<Duration> readTimeout = Optional.empty();
+        private Optional<Integer> sizeThresholdInBytes = Optional.empty();
+        private Optional<Duration> ttl = Optional.empty();
+
+        public Builder timeOut(Duration timeout) {
+            Preconditions.checkNotNull(timeout, "'Read timeout' must not to be null");
+            Preconditions.checkArgument(timeout.getSeconds() > 0, "'Read timeout' needs to be positive");
+            Preconditions.checkArgument(timeout.getSeconds() <= MAX_READ_TIMEOUT.getSeconds(),
+                "'Read timeout' needs to be less than %s sec", MAX_READ_TIMEOUT.getSeconds());
+
+            this.readTimeout = Optional.of(timeout);
+            return this;
+        }
+
+        public Builder sizeThresholdInBytes(int sizeThresholdInBytes) {
+            Preconditions.checkArgument(sizeThresholdInBytes >= 0, "'Threshold size' needs to be positive");
+
+            this.sizeThresholdInBytes = Optional.of(sizeThresholdInBytes);
+            return this;
+        }
+
+        public Builder ttl(Duration ttl) {
+            Preconditions.checkNotNull(ttl, "'TTL' must not to be null");
+            Preconditions.checkArgument(ttl.getSeconds() > 0, "'TTL' needs to be positive");
+            Preconditions.checkArgument(ttl.getSeconds() < Integer.MAX_VALUE,
+                "'TTL' must not greater than %s sec", Integer.MAX_VALUE);
+
+            this.ttl = Optional.of(ttl);
+            return this;
+        }
+
+        public CassandraCacheConfiguration build() {
+            return new CassandraCacheConfiguration(
+                readTimeout.orElse(DEFAULT_READ_TIMEOUT),
+                sizeThresholdInBytes.orElse(DEFAULT_BYTE_THRESHOLD_SIZE),
+                ttl.orElse(DEFAULT_TTL));
+        }
+    }
+
+    private final Duration readTimeOut;
+    private final int sizeThresholdInBytes;
+    private final Duration ttl;
+
+    private CassandraCacheConfiguration(Duration timeout, int sizeThresholdInBytes, Duration ttl) {
+        this.readTimeOut = timeout;
+        this.sizeThresholdInBytes = sizeThresholdInBytes;
+        this.ttl = ttl;
+    }
+
+    public Duration getReadTimeOut() {
+        return readTimeOut;
+    }
+
+    public Duration getTtl() {
+        return ttl;
+    }
+
+    public int getSizeThresholdInBytes() {
+        return sizeThresholdInBytes;
+    }
+}
diff --git a/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/BlobTables.java b/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/cache/CassandraDumbBlobCacheModule.java
similarity index 50%
copy from server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/BlobTables.java
copy to server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/cache/CassandraDumbBlobCacheModule.java
index 13d6d93..6d4734e 100644
--- a/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/BlobTables.java
+++ b/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/cache/CassandraDumbBlobCacheModule.java
@@ -17,35 +17,31 @@
  * under the License.                                           *
  ****************************************************************/
 
-package org.apache.james.blob.cassandra;
+package org.apache.james.blob.cassandra.cache;
 
-public interface BlobTables {
+import static com.datastax.driver.core.schemabuilder.TableOptions.CompactionOptions.TimeWindowCompactionStrategyOptions.CompactionWindowUnit.HOURS;
 
-    interface DefaultBucketBlobTable {
-        String TABLE_NAME = "blobs";
-        String ID = "id";
-        String NUMBER_OF_CHUNK = "position";
-    }
+import org.apache.james.backends.cassandra.components.CassandraModule;
+import org.apache.james.blob.cassandra.BlobTables;
 
-    interface DefaultBucketBlobParts {
-        String TABLE_NAME = "blobParts";
-        String ID = "id";
-        String CHUNK_NUMBER = "chunkNumber";
-        String DATA = "data";
-    }
+import com.datastax.driver.core.DataType;
+import com.datastax.driver.core.schemabuilder.SchemaBuilder;
 
-    interface BucketBlobTable {
-        String TABLE_NAME = "blobsInBucket";
-        String BUCKET = "bucket";
-        String ID = "id";
-        String NUMBER_OF_CHUNK = "position";
-    }
+public interface CassandraDumbBlobCacheModule {
 
-    interface BucketBlobParts {
-        String TABLE_NAME = "blobPartsInBucket";
-        String BUCKET = "bucket";
-        String ID = "id";
-        String CHUNK_NUMBER = "chunkNumber";
-        String DATA = "data";
-    }
+    double NO_READ_REPAIR = 0d;
+
+    CassandraModule MODULE = CassandraModule
+        .builder()
+        .table(BlobTables.DumbBlobCache.TABLE_NAME)
+        .options(options -> options
+            .compactionOptions(SchemaBuilder.timeWindowCompactionStrategy()
+                .compactionWindowSize(1)
+                .compactionWindowUnit(HOURS))
+            .readRepairChance(NO_READ_REPAIR))
+        .comment("Write through cache for small blobs stored in a slower blob store implementation.")
+        .statement(statement -> statement
+            .addPartitionKey(BlobTables.DumbBlobCache.ID, DataType.text())
+            .addColumn(BlobTables.DumbBlobCache.DATA, DataType.blob()))
+        .build();
 }
diff --git a/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/cache/CassandraDumbBlobStoreCache.java b/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/cache/CassandraDumbBlobStoreCache.java
new file mode 100644
index 0000000..74bd73b
--- /dev/null
+++ b/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/cache/CassandraDumbBlobStoreCache.java
@@ -0,0 +1,138 @@
+/****************************************************************
+ * 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.blob.cassandra.cache;
+
+import static com.datastax.driver.core.ConsistencyLevel.ALL;
+import static com.datastax.driver.core.ConsistencyLevel.ONE;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.bindMarker;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.delete;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.insertInto;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.ttl;
+import static org.apache.james.blob.cassandra.BlobTables.BucketBlobTable.ID;
+import static org.apache.james.blob.cassandra.BlobTables.DumbBlobCache.DATA;
+import static org.apache.james.blob.cassandra.BlobTables.DumbBlobCache.TABLE_NAME;
+import static org.apache.james.blob.cassandra.BlobTables.DumbBlobCache.TTL_FOR_ROW;
+
+import java.nio.ByteBuffer;
+
+import javax.inject.Inject;
+
+import org.apache.james.backends.cassandra.utils.CassandraAsyncExecutor;
+import org.apache.james.blob.api.BlobId;
+
+import com.datastax.driver.core.PreparedStatement;
+import com.datastax.driver.core.Row;
+import com.datastax.driver.core.Session;
+import com.google.common.annotations.VisibleForTesting;
+
+import reactor.core.publisher.Mono;
+
+public class CassandraDumbBlobStoreCache implements DumbBlobStoreCache {
+
+    private final CassandraAsyncExecutor cassandraAsyncExecutor;
+    private final PreparedStatement insertStatement;
+    private final PreparedStatement selectStatement;
+    private final PreparedStatement deleteStatement;
+
+    private final int readTimeOutFromDataBase;
+    private final int timeToLive;
+
+    @Inject
+    @VisibleForTesting
+    CassandraDumbBlobStoreCache(Session session, CassandraCacheConfiguration cacheConfiguration) {
+        this.cassandraAsyncExecutor = new CassandraAsyncExecutor(session);
+        this.insertStatement = prepareInsert(session);
+        this.selectStatement = prepareSelect(session);
+        this.deleteStatement = prepareDelete(session);
+
+        this.readTimeOutFromDataBase = Math.toIntExact(cacheConfiguration.getReadTimeOut().toMillis());
+        this.timeToLive = Math.toIntExact(cacheConfiguration.getTtl().getSeconds());
+    }
+
+    @Override
+    public Mono<Void> cache(BlobId blobId, byte[] bytes) {
+        return save(blobId, toByteBuffer(bytes));
+    }
+
+    @Override
+    public Mono<byte[]> read(BlobId blobId) {
+        return cassandraAsyncExecutor
+            .executeSingleRow(
+                selectStatement.bind()
+                    .setString(ID, blobId.asString())
+                    .setConsistencyLevel(ONE)
+                    .setReadTimeoutMillis(readTimeOutFromDataBase)
+            )
+            .map(this::toByteArray);
+    }
+
+    @Override
+    public Mono<Void> remove(BlobId blobId) {
+        return cassandraAsyncExecutor.executeVoid(
+            deleteStatement.bind()
+                .setString(ID, blobId.asString())
+                .setConsistencyLevel(ALL));
+    }
+
+    private Mono<Void> save(BlobId blobId, ByteBuffer data) {
+        return cassandraAsyncExecutor.executeVoid(
+            insertStatement.bind()
+                .setString(ID, blobId.asString())
+                .setBytes(DATA, data)
+                .setInt(TTL_FOR_ROW, timeToLive)
+                .setConsistencyLevel(ONE));
+    }
+
+    private ByteBuffer toByteBuffer(byte[] bytes) {
+        return ByteBuffer.wrap(bytes, 0, bytes.length);
+    }
+
+    private byte[] toByteArray(Row row) {
+        ByteBuffer byteBuffer = row.getBytes(DATA);
+        byte[] data = new byte[byteBuffer.remaining()];
+        byteBuffer.get(data);
+        return data;
+    }
+
+    private PreparedStatement prepareDelete(Session session) {
+        return session.prepare(
+            delete()
+                .from(TABLE_NAME)
+                .where(eq(ID, bindMarker(ID))));
+    }
+
+    private PreparedStatement prepareSelect(Session session) {
+        return session.prepare(
+            select()
+                .from(TABLE_NAME)
+                .where(eq(ID, bindMarker(ID))));
+    }
+
+    private PreparedStatement prepareInsert(Session session) {
+        return session.prepare(
+            insertInto(TABLE_NAME)
+                .value(ID, bindMarker(ID))
+                .value(DATA, bindMarker(DATA))
+                .using(ttl(bindMarker(TTL_FOR_ROW)))
+        );
+    }
+}
diff --git a/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/BlobTables.java b/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/cache/DumbBlobStoreCache.java
similarity index 59%
copy from server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/BlobTables.java
copy to server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/cache/DumbBlobStoreCache.java
index 13d6d93..de60a33 100644
--- a/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/BlobTables.java
+++ b/server/blob/blob-cassandra/src/main/java/org/apache/james/blob/cassandra/cache/DumbBlobStoreCache.java
@@ -16,36 +16,15 @@
  * specific language governing permissions and limitations      *
  * under the License.                                           *
  ****************************************************************/
+package org.apache.james.blob.cassandra.cache;
 
-package org.apache.james.blob.cassandra;
+import org.apache.james.blob.api.BlobId;
+import org.reactivestreams.Publisher;
 
-public interface BlobTables {
+public interface DumbBlobStoreCache {
+    Publisher<Void> cache(BlobId blobId, byte[] data);
 
-    interface DefaultBucketBlobTable {
-        String TABLE_NAME = "blobs";
-        String ID = "id";
-        String NUMBER_OF_CHUNK = "position";
-    }
+    Publisher<byte[]> read(BlobId blobId);
 
-    interface DefaultBucketBlobParts {
-        String TABLE_NAME = "blobParts";
-        String ID = "id";
-        String CHUNK_NUMBER = "chunkNumber";
-        String DATA = "data";
-    }
-
-    interface BucketBlobTable {
-        String TABLE_NAME = "blobsInBucket";
-        String BUCKET = "bucket";
-        String ID = "id";
-        String NUMBER_OF_CHUNK = "position";
-    }
-
-    interface BucketBlobParts {
-        String TABLE_NAME = "blobPartsInBucket";
-        String BUCKET = "bucket";
-        String ID = "id";
-        String CHUNK_NUMBER = "chunkNumber";
-        String DATA = "data";
-    }
+    Publisher<Void> remove(BlobId blobId);
 }
diff --git a/server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/cache/CassandraCacheConfigurationTest.java b/server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/cache/CassandraCacheConfigurationTest.java
new file mode 100644
index 0000000..2353298
--- /dev/null
+++ b/server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/cache/CassandraCacheConfigurationTest.java
@@ -0,0 +1,130 @@
+/****************************************************************
+ * 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.blob.cassandra.cache;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+
+import org.assertj.core.api.SoftAssertions;
+import org.junit.jupiter.api.Test;
+
+import com.google.common.base.Strings;
+
+public class CassandraCacheConfigurationTest {
+
+    byte[] EIGHT_KILOBYTES = Strings.repeat("01234567\n", 1000).getBytes(StandardCharsets.UTF_8);
+    private final Duration DEFAULT_TIME_OUT = Duration.ofSeconds(50);
+    private final int DEFAULT_THRESHOLD_SIZE_IN_BYTES = EIGHT_KILOBYTES.length;
+    private final Duration _1_SEC_TTL = Duration.ofSeconds(1);
+    private final Duration TOO_BIG_TTL = Duration.ofSeconds(Integer.MAX_VALUE + 1L);
+
+    private final Duration NEGATIVE_TIME_OUT = Duration.ofSeconds(-50);
+    private final Duration _2_HOURS_TIME_OUT = Duration.ofHours(2);
+    private final int NEGATIVE_THRESHOLD_SIZE_IN_BYTES = -1 * EIGHT_KILOBYTES.length;
+    private final Duration NEGATIVE_TTL = Duration.ofSeconds(-1);
+
+    @Test
+    void shouldReturnTheCorrectConfigured() {
+        CassandraCacheConfiguration cacheConfiguration = new CassandraCacheConfiguration.Builder()
+            .sizeThresholdInBytes(DEFAULT_THRESHOLD_SIZE_IN_BYTES)
+            .timeOut(DEFAULT_TIME_OUT)
+            .ttl(_1_SEC_TTL)
+            .build();
+
+        SoftAssertions.assertSoftly(soflty -> {
+            assertThat(cacheConfiguration.getReadTimeOut()).isEqualTo(DEFAULT_TIME_OUT);
+            assertThat(cacheConfiguration.getSizeThresholdInBytes()).isEqualTo(DEFAULT_THRESHOLD_SIZE_IN_BYTES);
+            assertThat(cacheConfiguration.getTtl()).isEqualTo(_1_SEC_TTL);
+        });
+    }
+
+    @Test
+    void shouldThrowWhenConfiguredNegativeTimeout() {
+        assertThatThrownBy(() -> new CassandraCacheConfiguration.Builder()
+            .sizeThresholdInBytes(DEFAULT_THRESHOLD_SIZE_IN_BYTES)
+            .timeOut(NEGATIVE_TIME_OUT)
+            .ttl(_1_SEC_TTL)
+            .build())
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    void shouldThrowWhenConfiguredTooLongTimeout() {
+        assertThatThrownBy(() -> new CassandraCacheConfiguration.Builder()
+            .sizeThresholdInBytes(DEFAULT_THRESHOLD_SIZE_IN_BYTES)
+            .timeOut(_2_HOURS_TIME_OUT)
+            .ttl(_1_SEC_TTL)
+            .build())
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    void shouldThrowWhenConfiguredNullTimeout() {
+        assertThatThrownBy(() -> new CassandraCacheConfiguration.Builder()
+            .sizeThresholdInBytes(DEFAULT_THRESHOLD_SIZE_IN_BYTES)
+            .timeOut(null)
+            .ttl(_1_SEC_TTL)
+            .build())
+            .isInstanceOf(NullPointerException.class);
+    }
+
+    @Test
+    void shouldThrowWhenConfiguredTooBigTTL() {
+        assertThatThrownBy(() -> new CassandraCacheConfiguration.Builder()
+            .sizeThresholdInBytes(DEFAULT_THRESHOLD_SIZE_IN_BYTES)
+            .timeOut(DEFAULT_TIME_OUT)
+            .ttl(TOO_BIG_TTL)
+            .build())
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    void shouldThrowWhenConfiguredNegativeTTL() {
+        assertThatThrownBy(() -> new CassandraCacheConfiguration.Builder()
+            .sizeThresholdInBytes(DEFAULT_THRESHOLD_SIZE_IN_BYTES)
+            .timeOut(DEFAULT_TIME_OUT)
+            .ttl(NEGATIVE_TTL)
+            .build())
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    void shouldThrowWhenConfiguredZeroTTL() {
+        assertThatThrownBy(() -> new CassandraCacheConfiguration.Builder()
+            .sizeThresholdInBytes(DEFAULT_THRESHOLD_SIZE_IN_BYTES)
+            .timeOut(DEFAULT_TIME_OUT)
+            .ttl(Duration.ofSeconds(0))
+            .build())
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    void shouldThrowWhenConfiguredNegativeThreshold() {
+        assertThatThrownBy(() -> new CassandraCacheConfiguration.Builder()
+            .sizeThresholdInBytes(NEGATIVE_THRESHOLD_SIZE_IN_BYTES)
+            .timeOut(DEFAULT_TIME_OUT)
+            .ttl(_1_SEC_TTL)
+            .build())
+            .isInstanceOf(IllegalArgumentException.class);
+    }
+}
diff --git a/server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/cache/CassandraDumbBlobStoreCacheTest.java b/server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/cache/CassandraDumbBlobStoreCacheTest.java
new file mode 100644
index 0000000..4658a01
--- /dev/null
+++ b/server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/cache/CassandraDumbBlobStoreCacheTest.java
@@ -0,0 +1,62 @@
+/****************************************************************
+ * 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.blob.cassandra.cache;
+
+import java.time.Duration;
+
+import org.apache.james.backends.cassandra.CassandraCluster;
+import org.apache.james.backends.cassandra.CassandraClusterExtension;
+import org.apache.james.blob.api.BlobId;
+import org.apache.james.blob.api.HashBlobId;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+public class CassandraDumbBlobStoreCacheTest implements DumbBlobStoreCacheContract {
+
+    @RegisterExtension
+    static CassandraClusterExtension cassandraCluster = new CassandraClusterExtension(CassandraDumbBlobCacheModule.MODULE);
+
+    private final Duration DEFAULT_READ_TIMEOUT = Duration.ofSeconds(50);
+    private final int DEFAULT_THRESHOLD_IN_BYTES = EIGHT_KILOBYTES.length;
+    private final Duration _1_SEC_TTL = Duration.ofSeconds(1);
+
+    private DumbBlobStoreCache testee;
+    private HashBlobId.Factory blobIdFactory;
+
+    @BeforeEach
+    void setUp(CassandraCluster cassandra) {
+        blobIdFactory = new HashBlobId.Factory();
+        CassandraCacheConfiguration cacheConfiguration = new CassandraCacheConfiguration.Builder()
+            .sizeThresholdInBytes(DEFAULT_THRESHOLD_IN_BYTES)
+            .timeOut(DEFAULT_READ_TIMEOUT)
+            .ttl(_1_SEC_TTL)
+            .build();
+        testee = new CassandraDumbBlobStoreCache(cassandra.getConf(), cacheConfiguration);
+    }
+
+    @Override
+    public DumbBlobStoreCache testee() {
+        return testee;
+    }
+
+    @Override
+    public BlobId.Factory blobIdFactory() {
+        return blobIdFactory;
+    }
+}
diff --git a/server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/cache/DumbBlobStoreCacheContract.java b/server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/cache/DumbBlobStoreCacheContract.java
new file mode 100644
index 0000000..a9cef55
--- /dev/null
+++ b/server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/cache/DumbBlobStoreCacheContract.java
@@ -0,0 +1,111 @@
+/****************************************************************
+ * 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.blob.cassandra.cache;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.awaitility.Awaitility.await;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+
+import org.apache.james.blob.api.BlobId;
+import org.awaitility.Duration;
+import org.junit.jupiter.api.Test;
+
+import com.google.common.base.Strings;
+
+import reactor.core.publisher.Mono;
+
+public interface DumbBlobStoreCacheContract {
+
+    byte[] EIGHT_KILOBYTES = Strings.repeat("01234567\n", 1024).getBytes(StandardCharsets.UTF_8);
+
+    DumbBlobStoreCache testee();
+
+    BlobId.Factory blobIdFactory();
+
+    @Test
+    default void shouldSaveWhenCacheSmallByteData() {
+        BlobId blobId = blobIdFactory().randomId();
+        assertThatCode(Mono.from(testee().cache(blobId, EIGHT_KILOBYTES))::block)
+            .doesNotThrowAnyException();
+
+        byte[] actual = Mono.from(testee().read(blobId)).block();
+        assertThat(actual).containsExactly(EIGHT_KILOBYTES);
+    }
+
+    @Test
+    default void shouldReturnExactlyDataWhenRead() {
+        BlobId blobId = blobIdFactory().randomId();
+        Mono.from(testee().cache(blobId, EIGHT_KILOBYTES)).block();
+
+        byte[] actual = Mono.from(testee().read(blobId)).block();
+        assertThat(actual).containsExactly(EIGHT_KILOBYTES);
+    }
+
+    @Test
+    default void shouldReturnEmptyWhenReadWithTimeOut() {
+        BlobId blobId = blobIdFactory().randomId();
+        Mono.from(testee().cache(blobId, EIGHT_KILOBYTES)).block();
+
+    }
+
+    @Test
+    default void shouldReturnNothingWhenDelete() {
+        BlobId blobId = blobIdFactory().randomId();
+        Mono.from(testee().cache(blobId, EIGHT_KILOBYTES)).block();
+        Mono.from(testee().remove(blobId)).block();
+
+        Optional<byte[]> actual = Mono.from(testee().read(blobId)).blockOptional();
+        assertThat(actual).isEmpty();
+    }
+
+    @Test
+    default void shouldDeleteExactlyAndReturnNothingWhenDelete() {
+        BlobId blobId = blobIdFactory().randomId();
+        BlobId blobId2 = blobIdFactory().randomId();
+        Mono.from(testee().cache(blobId, EIGHT_KILOBYTES)).block();
+        Mono.from(testee().cache(blobId2, EIGHT_KILOBYTES)).block();
+        Mono.from(testee().remove(blobId)).block();
+
+        byte[] readBlobId2 = Mono.from(testee().read(blobId2)).block();
+        assertThat(readBlobId2).containsExactly(EIGHT_KILOBYTES);
+    }
+
+    @Test
+    default void shouldReturnDataWhenCacheSmallDataInConfigurationTTL() {
+        BlobId blobId = blobIdFactory().randomId();
+        assertThatCode(Mono.from(testee().cache(blobId, EIGHT_KILOBYTES))::block)
+            .doesNotThrowAnyException();
+
+        await().atMost(Duration.ONE_SECOND).await().untilAsserted(()
+            -> assertThat(Mono.from(testee().read(blobId)).block()).containsExactly(EIGHT_KILOBYTES));
+    }
+
+    @Test
+    default void shouldNotReturnDataWhenCachedSmallDataOutOfConfigurationTTL() {
+        BlobId blobId = blobIdFactory().randomId();
+        assertThatCode(Mono.from(testee().cache(blobId, EIGHT_KILOBYTES))::block)
+            .doesNotThrowAnyException();
+
+        await().atMost(Duration.TWO_SECONDS).await().untilAsserted(()
+            -> assertThat(Mono.from(testee().read(blobId)).blockOptional()).isEmpty());
+    }
+}


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