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
The following commit(s) were added to refs/heads/master by this push: new cd94e5434e JAMES-3925 Define an API to store current values of JMAP uploads (#1699) cd94e5434e is described below commit cd94e5434e528fc076ebc3635ee4ab06cb2c0528 Author: hungphan227 <45198168+hungphan...@users.noreply.github.com> AuthorDate: Sat Aug 26 12:53:29 2023 +0700 JAMES-3925 Define an API to store current values of JMAP uploads (#1699) --- .../james/modules/data/CassandraJmapModule.java | 3 + .../james/modules/mailbox/MemoryQuotaModule.java | 3 + .../upload/CassandraUploadUsageRepository.java | 67 +++++++++++++++++++ .../upload/CassandraUploadUsageRepositoryTest.java | 48 ++++++++++++++ .../jmap/api/upload/UploadUsageRepository.java | 34 ++++++++++ .../upload/InMemoryUploadUsageRepository.java | 75 ++++++++++++++++++++++ .../api/upload/UploadUsageRepositoryContract.scala | 50 +++++++++++++++ .../upload/InMemoryUploadUsageRepositoryTest.java | 41 ++++++++++++ 8 files changed, 321 insertions(+) diff --git a/server/container/guice/cassandra/src/main/java/org/apache/james/modules/data/CassandraJmapModule.java b/server/container/guice/cassandra/src/main/java/org/apache/james/modules/data/CassandraJmapModule.java index e1b9584628..a6c0884c27 100644 --- a/server/container/guice/cassandra/src/main/java/org/apache/james/modules/data/CassandraJmapModule.java +++ b/server/container/guice/cassandra/src/main/java/org/apache/james/modules/data/CassandraJmapModule.java @@ -41,6 +41,7 @@ import org.apache.james.jmap.api.projections.MessageFastViewProjectionHealthChec import org.apache.james.jmap.api.pushsubscription.PushDeleteUserDataTaskStep; import org.apache.james.jmap.api.pushsubscription.PushSubscriptionRepository; import org.apache.james.jmap.api.upload.UploadRepository; +import org.apache.james.jmap.api.upload.UploadUsageRepository; import org.apache.james.jmap.cassandra.access.CassandraAccessModule; import org.apache.james.jmap.cassandra.access.CassandraAccessTokenRepository; import org.apache.james.jmap.cassandra.change.CassandraEmailChangeModule; @@ -57,6 +58,7 @@ import org.apache.james.jmap.cassandra.projections.CassandraMessageFastViewProje import org.apache.james.jmap.cassandra.pushsubscription.CassandraPushSubscriptionModule; import org.apache.james.jmap.cassandra.pushsubscription.CassandraPushSubscriptionRepository; import org.apache.james.jmap.cassandra.upload.CassandraUploadRepository; +import org.apache.james.jmap.cassandra.upload.CassandraUploadUsageRepository; import org.apache.james.jmap.cassandra.upload.UploadConfiguration; import org.apache.james.jmap.cassandra.upload.UploadDAO; import org.apache.james.jmap.cassandra.upload.UploadModule; @@ -80,6 +82,7 @@ public class CassandraJmapModule extends AbstractModule { bind(CassandraUploadRepository.class).in(Scopes.SINGLETON); bind(UploadDAO.class).in(Scopes.SINGLETON); bind(UploadRepository.class).to(CassandraUploadRepository.class); + bind(UploadUsageRepository.class).to(CassandraUploadUsageRepository.class); bind(UploadConfiguration.class).toInstance(UploadConfiguration.SINGLETON); bind(CassandraCustomIdentityDAO.class).in(Scopes.SINGLETON); diff --git a/server/container/guice/memory/src/main/java/org/apache/james/modules/mailbox/MemoryQuotaModule.java b/server/container/guice/memory/src/main/java/org/apache/james/modules/mailbox/MemoryQuotaModule.java index 1fd76c36f7..dc389d74d6 100644 --- a/server/container/guice/memory/src/main/java/org/apache/james/modules/mailbox/MemoryQuotaModule.java +++ b/server/container/guice/memory/src/main/java/org/apache/james/modules/mailbox/MemoryQuotaModule.java @@ -20,6 +20,8 @@ package org.apache.james.modules.mailbox; import org.apache.james.events.EventListener; +import org.apache.james.jmap.api.upload.UploadUsageRepository; +import org.apache.james.jmap.memory.upload.InMemoryUploadUsageRepository; import org.apache.james.mailbox.inmemory.quota.InMemoryCurrentQuotaManager; import org.apache.james.mailbox.inmemory.quota.InMemoryPerUserMaxQuotaManager; import org.apache.james.mailbox.quota.CurrentQuotaManager; @@ -52,6 +54,7 @@ public class MemoryQuotaModule extends AbstractModule { bind(MaxQuotaManager.class).to(InMemoryPerUserMaxQuotaManager.class); bind(QuotaManager.class).to(StoreQuotaManager.class); bind(CurrentQuotaManager.class).to(InMemoryCurrentQuotaManager.class); + bind(UploadUsageRepository.class).to(InMemoryUploadUsageRepository.class); bind(ListeningCurrentQuotaUpdater.class).in(Scopes.SINGLETON); bind(QuotaUpdater.class).to(ListeningCurrentQuotaUpdater.class); diff --git a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/upload/CassandraUploadUsageRepository.java b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/upload/CassandraUploadUsageRepository.java new file mode 100644 index 0000000000..83f52adca1 --- /dev/null +++ b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/upload/CassandraUploadUsageRepository.java @@ -0,0 +1,67 @@ +/**************************************************************** + * 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.jmap.cassandra.upload; + +import javax.inject.Inject; + +import org.apache.james.core.Username; +import org.apache.james.core.quota.QuotaComponent; +import org.apache.james.core.quota.QuotaSizeUsage; +import org.apache.james.core.quota.QuotaType; +import org.apache.james.jmap.api.upload.UploadUsageRepository; +import org.apache.james.mailbox.cassandra.quota.CassandraQuotaCurrentValueDao; + +import reactor.core.publisher.Mono; + +public class CassandraUploadUsageRepository implements UploadUsageRepository { + + private static final QuotaSizeUsage DEFAULT_QUOTA_SIZE_USAGE = QuotaSizeUsage.size(0); + + private CassandraQuotaCurrentValueDao cassandraQuotaCurrentValueDao; + + @Inject + public CassandraUploadUsageRepository(CassandraQuotaCurrentValueDao cassandraQuotaCurrentValueDao) { + this.cassandraQuotaCurrentValueDao = cassandraQuotaCurrentValueDao; + } + + @Override + public Mono<Void> increaseSpace(Username username, QuotaSizeUsage usage) { + return cassandraQuotaCurrentValueDao.increase(CassandraQuotaCurrentValueDao.QuotaKey.of(QuotaComponent.JMAP_UPLOADS, username.asString(), QuotaType.SIZE), + usage.asLong()); + } + + @Override + public Mono<Void> decreaseSpace(Username username, QuotaSizeUsage usage) { + return cassandraQuotaCurrentValueDao.decrease(CassandraQuotaCurrentValueDao.QuotaKey.of(QuotaComponent.JMAP_UPLOADS, username.asString(), QuotaType.SIZE), + usage.asLong()); + } + + @Override + public Mono<QuotaSizeUsage> getSpaceUsage(Username username) { + return cassandraQuotaCurrentValueDao.getQuotaCurrentValue(CassandraQuotaCurrentValueDao.QuotaKey.of(QuotaComponent.JMAP_UPLOADS, username.asString(), QuotaType.SIZE)) + .map(quotaCurrentValue -> QuotaSizeUsage.size(quotaCurrentValue.getCurrentValue())).defaultIfEmpty(DEFAULT_QUOTA_SIZE_USAGE); + } + + @Override + public Mono<Void> resetSpace(Username username, QuotaSizeUsage usage) { + return getSpaceUsage(username).flatMap(quotaSizeUsage -> Mono.from(decreaseSpace(username, quotaSizeUsage))) + .then(increaseSpace(username, usage)); + } +} diff --git a/server/data/data-jmap-cassandra/src/test/java/org/apache/james/jmap/cassandra/upload/CassandraUploadUsageRepositoryTest.java b/server/data/data-jmap-cassandra/src/test/java/org/apache/james/jmap/cassandra/upload/CassandraUploadUsageRepositoryTest.java new file mode 100644 index 0000000000..9eab33a494 --- /dev/null +++ b/server/data/data-jmap-cassandra/src/test/java/org/apache/james/jmap/cassandra/upload/CassandraUploadUsageRepositoryTest.java @@ -0,0 +1,48 @@ +/**************************************************************** + * 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.jmap.cassandra.upload; + +import org.apache.james.backends.cassandra.CassandraClusterExtension; +import org.apache.james.backends.cassandra.components.CassandraModule; +import org.apache.james.jmap.api.upload.UploadUsageRepository; +import org.apache.james.jmap.api.upload.UploadUsageRepositoryContract; +import org.apache.james.mailbox.cassandra.modules.CassandraQuotaModule; +import org.apache.james.mailbox.cassandra.quota.CassandraQuotaCurrentValueDao; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class CassandraUploadUsageRepositoryTest implements UploadUsageRepositoryContract { + + @RegisterExtension + static CassandraClusterExtension cassandraCluster = new CassandraClusterExtension(CassandraModule.aggregateModules(CassandraQuotaModule.MODULE)); + + private CassandraUploadUsageRepository cassandraUploadUsageRepository; + + @BeforeEach + private void setup() { + cassandraUploadUsageRepository = new CassandraUploadUsageRepository(new CassandraQuotaCurrentValueDao(cassandraCluster.getCassandraCluster().getConf())); + resetCounterToZero(); + } + + @Override + public UploadUsageRepository uploadUsageRepository() { + return cassandraUploadUsageRepository; + } +} diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/upload/UploadUsageRepository.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/upload/UploadUsageRepository.java new file mode 100644 index 0000000000..614196dc7a --- /dev/null +++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/upload/UploadUsageRepository.java @@ -0,0 +1,34 @@ +/**************************************************************** + * 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.jmap.api.upload; + +import org.apache.james.core.Username; +import org.apache.james.core.quota.QuotaSizeUsage; +import org.reactivestreams.Publisher; + +public interface UploadUsageRepository { + Publisher<Void> increaseSpace(Username username, QuotaSizeUsage usage); + + Publisher<Void> decreaseSpace(Username username, QuotaSizeUsage usage); + + Publisher<QuotaSizeUsage> getSpaceUsage(Username username); + + Publisher<Void> resetSpace(Username username, QuotaSizeUsage usage); +} diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/upload/InMemoryUploadUsageRepository.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/upload/InMemoryUploadUsageRepository.java new file mode 100644 index 0000000000..d0d682a839 --- /dev/null +++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/upload/InMemoryUploadUsageRepository.java @@ -0,0 +1,75 @@ +/**************************************************************** + * 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.jmap.memory.upload; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.james.core.Username; +import org.apache.james.core.quota.QuotaSizeUsage; +import org.apache.james.jmap.api.upload.UploadUsageRepository; + +import reactor.core.publisher.Mono; + +public class InMemoryUploadUsageRepository implements UploadUsageRepository { + + private static final QuotaSizeUsage DEFAULT_QUOTA_SIZE_USAGE = QuotaSizeUsage.size(0); + + private final Map<Username, AtomicReference<QuotaSizeUsage>> cache; + + public InMemoryUploadUsageRepository() { + cache = new ConcurrentHashMap<>(); + } + + @Override + public Mono<Void> increaseSpace(Username username, QuotaSizeUsage usage) { + return updateSpace(username, usage.asLong()); + } + + @Override + public Mono<Void> decreaseSpace(Username username, QuotaSizeUsage usage) { + return updateSpace(username, Math.negateExact(usage.asLong())); + } + + private Mono<Void> updateSpace(Username username, long amount) { + return Mono.fromRunnable(() -> { + AtomicReference<QuotaSizeUsage> quotaSizeUsageAtomicReference = cache.get(username); + if (Objects.isNull(quotaSizeUsageAtomicReference)) { + cache.put(username, new AtomicReference<>(QuotaSizeUsage.size(amount))); + } else { + quotaSizeUsageAtomicReference.updateAndGet(quotaSizeUsage -> quotaSizeUsage.add(amount)); + } + }); + } + + @Override + public Mono<QuotaSizeUsage> getSpaceUsage(Username username) { + return Mono.just(cache.getOrDefault(username, new AtomicReference<>(DEFAULT_QUOTA_SIZE_USAGE))) + .map(quotaSizeUsageAtomicReference -> quotaSizeUsageAtomicReference.get()); + } + + @Override + public Mono<Void> resetSpace(Username username, QuotaSizeUsage usage) { + return getSpaceUsage(username).flatMap(quotaSizeUsage -> Mono.from(decreaseSpace(username, quotaSizeUsage))) + .then(increaseSpace(username, usage)); + } +} diff --git a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/upload/UploadUsageRepositoryContract.scala b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/upload/UploadUsageRepositoryContract.scala new file mode 100644 index 0000000000..0ceff5890f --- /dev/null +++ b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/upload/UploadUsageRepositoryContract.scala @@ -0,0 +1,50 @@ +package org.apache.james.jmap.api.upload + +import org.apache.james.core.Username +import org.apache.james.core.quota.QuotaSizeUsage +import org.apache.james.jmap.api.upload.UploadUsageRepositoryContract.USER_NAME +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import reactor.core.publisher.Mono +import reactor.core.scala.publisher.SMono + +object UploadUsageRepositoryContract { + val USER_NAME = Username.of("ja...@abc.com") +} + +trait UploadUsageRepositoryContract { + def uploadUsageRepository: UploadUsageRepository + + def resetCounterToZero(): Unit = { + Mono.from(uploadUsageRepository.resetSpace(USER_NAME, QuotaSizeUsage.size(0))).block + } + + @Test + def increaseSpaceShouldIncreaseSuccessfully(): Unit = { + SMono.fromPublisher(uploadUsageRepository.increaseSpace(USER_NAME, QuotaSizeUsage.size(100))).block(); + val expected = SMono.fromPublisher(uploadUsageRepository.getSpaceUsage(USER_NAME)).block(); + assertThat(expected.asLong()).isEqualTo(100); + } + + @Test + def decreaseSpaceShouldDecreaseSuccessfully(): Unit = { + SMono.fromPublisher(uploadUsageRepository.increaseSpace(USER_NAME, QuotaSizeUsage.size(200))).block(); + SMono.fromPublisher(uploadUsageRepository.decreaseSpace(USER_NAME, QuotaSizeUsage.size(100))).block(); + val expected = SMono.fromPublisher(uploadUsageRepository.getSpaceUsage(USER_NAME)).block(); + assertThat(expected.asLong()).isEqualTo(100); + } + + @Test + def resetSpaceShouldResetSuccessfully(): Unit = { + SMono.fromPublisher(uploadUsageRepository.increaseSpace(USER_NAME, QuotaSizeUsage.size(200))).block(); + SMono.fromPublisher(uploadUsageRepository.resetSpace(USER_NAME, QuotaSizeUsage.size(100))).block(); + val expected = SMono.fromPublisher(uploadUsageRepository.getSpaceUsage(USER_NAME)).block(); + assertThat(expected.asLong()).isEqualTo(100); + } + + @Test + def getSpaceUsageShouldReturnZeroWhenRecordDoesNotExist(): Unit = { + assertThat(SMono.fromPublisher(uploadUsageRepository.getSpaceUsage(Username.of("aaa"))).block().asLong()).isEqualTo(0); + } + +} diff --git a/server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/upload/InMemoryUploadUsageRepositoryTest.java b/server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/upload/InMemoryUploadUsageRepositoryTest.java new file mode 100644 index 0000000000..db5a49d873 --- /dev/null +++ b/server/data/data-jmap/src/test/java/org/apache/james/jmap/memory/upload/InMemoryUploadUsageRepositoryTest.java @@ -0,0 +1,41 @@ +/**************************************************************** + * 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.jmap.memory.upload; + +import org.apache.james.jmap.api.upload.UploadUsageRepository; +import org.apache.james.jmap.api.upload.UploadUsageRepositoryContract; +import org.junit.jupiter.api.BeforeEach; + +public class InMemoryUploadUsageRepositoryTest implements UploadUsageRepositoryContract { + + private InMemoryUploadUsageRepository inMemoryUploadUsageRepository; + + @BeforeEach + private void setup() { + inMemoryUploadUsageRepository = new InMemoryUploadUsageRepository(); + resetCounterToZero(); + } + + @Override + public UploadUsageRepository uploadUsageRepository() { + return inMemoryUploadUsageRepository; + } + +} --------------------------------------------------------------------- To unsubscribe, e-mail: notifications-unsubscr...@james.apache.org For additional commands, e-mail: notifications-h...@james.apache.org