This is an automated email from the ASF dual-hosted git repository. rcordier pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
commit dee6298f15cdec390e1cacba7f82f9e173e55ec7 Author: Benoit Tellier <btell...@linagora.com> AuthorDate: Thu Jul 23 11:23:06 2020 +0700 JAMES-3343 EventSourcing system to validate BlobStore StorageStrategy configuration --- .../guice/cassandra-rabbitmq-guice/pom.xml | 11 ++ ...obStoreConfigurationValidationStartUpCheck.java | 62 +++++++++++ .../modules/blobstore/BlobStoreModulesChooser.java | 35 +++++- .../validation/EventsourcingStorageStrategy.java | 53 +++++++++ .../validation/RegisterStorageStrategy.java | 35 ++++++ .../RegisterStorageStrategyCommandHandler.java | 53 +++++++++ .../validation/StorageStrategyAggregate.java | 106 ++++++++++++++++++ .../validation/StorageStrategyChanged.java | 51 +++++++++ .../validation/StorageStrategyChangedDTO.java | 108 ++++++++++++++++++ .../validation/StorageStrategyModule.java | 36 ++++++ ...orageStrategyValidationEventSourcingSystem.java | 60 ++++++++++ ...oreConfigurationValidationStartUpCheckTest.java | 124 +++++++++++++++++++++ .../validation/StorageStrategyChangedDTOTest.java | 79 +++++++++++++ ...eStrategyValidationEventSourcingSystemTest.java | 100 +++++++++++++++++ 14 files changed, 911 insertions(+), 2 deletions(-) diff --git a/server/container/guice/cassandra-rabbitmq-guice/pom.xml b/server/container/guice/cassandra-rabbitmq-guice/pom.xml index a4c6c5f..3fc2aa9 100644 --- a/server/container/guice/cassandra-rabbitmq-guice/pom.xml +++ b/server/container/guice/cassandra-rabbitmq-guice/pom.xml @@ -122,6 +122,17 @@ </dependency> <dependency> <groupId>${james.groupId}</groupId> + <artifactId>event-sourcing-event-store-cassandra</artifactId> + <type>test-jar</type> + <scope>test</scope> + </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>event-sourcing-event-store-memory</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>${james.groupId}</groupId> <artifactId>james-server-cassandra-guice</artifactId> </dependency> <dependency> diff --git a/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/BlobStoreConfigurationValidationStartUpCheck.java b/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/BlobStoreConfigurationValidationStartUpCheck.java new file mode 100644 index 0000000..4eace03 --- /dev/null +++ b/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/BlobStoreConfigurationValidationStartUpCheck.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.modules.blobstore; + +import javax.inject.Inject; + +import org.apache.james.lifecycle.api.StartUpCheck; +import org.apache.james.modules.blobstore.validation.EventsourcingStorageStrategy; + +import com.google.common.annotations.VisibleForTesting; + +public class BlobStoreConfigurationValidationStartUpCheck implements StartUpCheck { + private static final String BLOB_STORE_CONFIGURATION_VALIDATION = "blobStore-configuration-validation"; + private final BlobStoreConfiguration blobStoreConfiguration; + private final EventsourcingStorageStrategy eventsourcingStorageStrategy; + + @VisibleForTesting + @Inject + BlobStoreConfigurationValidationStartUpCheck(BlobStoreConfiguration blobStoreConfiguration, EventsourcingStorageStrategy eventsourcingStorageStrategy) { + this.blobStoreConfiguration = blobStoreConfiguration; + this.eventsourcingStorageStrategy = eventsourcingStorageStrategy; + } + + @Override + public CheckResult check() { + try { + eventsourcingStorageStrategy.registerStorageStrategy(blobStoreConfiguration.storageStrategy()); + return CheckResult.builder() + .checkName(BLOB_STORE_CONFIGURATION_VALIDATION) + .resultType(ResultType.GOOD) + .build(); + } catch (IllegalStateException e) { + return CheckResult.builder() + .checkName(BLOB_STORE_CONFIGURATION_VALIDATION) + .resultType(ResultType.BAD) + .description(e.getMessage()) + .build(); + } + } + + @Override + public String checkName() { + return BLOB_STORE_CONFIGURATION_VALIDATION; + } +} diff --git a/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/BlobStoreModulesChooser.java b/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/BlobStoreModulesChooser.java index 323f6c5..20bdf8f 100644 --- a/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/BlobStoreModulesChooser.java +++ b/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/BlobStoreModulesChooser.java @@ -26,6 +26,12 @@ import org.apache.james.blob.api.DumbBlobStore; import org.apache.james.blob.cassandra.CassandraDumbBlobStore; import org.apache.james.blob.cassandra.cache.CachedBlobStore; import org.apache.james.blob.objectstorage.ObjectStorageBlobStore; +import org.apache.james.eventsourcing.Event; +import org.apache.james.eventsourcing.eventstore.cassandra.dto.EventDTO; +import org.apache.james.eventsourcing.eventstore.cassandra.dto.EventDTOModule; +import org.apache.james.lifecycle.api.StartUpCheck; +import org.apache.james.modules.blobstore.validation.EventsourcingStorageStrategy; +import org.apache.james.modules.blobstore.validation.StorageStrategyModule; import org.apache.james.modules.mailbox.CassandraBlobStoreDependenciesModule; import org.apache.james.modules.objectstorage.ObjectStorageDependenciesModule; import org.apache.james.server.blob.deduplication.DeDuplicationBlobStore; @@ -36,6 +42,9 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.inject.AbstractModule; import com.google.inject.Module; +import com.google.inject.Scopes; +import com.google.inject.TypeLiteral; +import com.google.inject.multibindings.Multibinder; import com.google.inject.name.Names; public class BlobStoreModulesChooser { @@ -59,10 +68,32 @@ public class BlobStoreModulesChooser { } } + static class StoragePolicyConfigurationSanityEnforcementModule extends AbstractModule { + private BlobStoreConfiguration choosingConfiguration; + + StoragePolicyConfigurationSanityEnforcementModule(BlobStoreConfiguration choosingConfiguration) { + this.choosingConfiguration = choosingConfiguration; + } + + @Override + protected void configure() { + Multibinder<EventDTOModule<? extends Event, ? extends EventDTO>> eventDTOModuleBinder = Multibinder.newSetBinder(binder(), new TypeLiteral<EventDTOModule<? extends Event, ? extends EventDTO>>() {}); + eventDTOModuleBinder.addBinding().toInstance(StorageStrategyModule.STORAGE_STRATEGY); + + bind(BlobStoreConfiguration.class).toInstance(choosingConfiguration); + bind(EventsourcingStorageStrategy.class).in(Scopes.SINGLETON); + + Multibinder.newSetBinder(binder(), StartUpCheck.class) + .addBinding() + .to(BlobStoreConfigurationValidationStartUpCheck.class); + } + } + @VisibleForTesting public static List<Module> chooseModules(BlobStoreConfiguration choosingConfiguration) { - ImmutableList.Builder<Module> moduleBuilder = ImmutableList.<Module>builder().add( - chooseDumBlobStoreModule(choosingConfiguration.getImplementation())); + ImmutableList.Builder<Module> moduleBuilder = ImmutableList.<Module>builder() + .add(chooseDumBlobStoreModule(choosingConfiguration.getImplementation())) + .add( new StoragePolicyConfigurationSanityEnforcementModule(choosingConfiguration)); //TODO JAMES-3028 add the storage policy module for all implementation and unbind the ObjectStorageBlobStore if (choosingConfiguration.getImplementation() == BlobStoreConfiguration.BlobStoreImplName.CASSANDRA) { diff --git a/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/validation/EventsourcingStorageStrategy.java b/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/validation/EventsourcingStorageStrategy.java new file mode 100644 index 0000000..5e3cec2 --- /dev/null +++ b/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/validation/EventsourcingStorageStrategy.java @@ -0,0 +1,53 @@ +/**************************************************************** + * 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.modules.blobstore.validation; + +import javax.inject.Inject; + +import org.apache.james.eventsourcing.EventSourcingSystem; +import org.apache.james.eventsourcing.Subscriber; +import org.apache.james.eventsourcing.eventstore.EventStore; +import org.apache.james.server.blob.deduplication.StorageStrategy; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableSet; + +import reactor.core.publisher.Mono; + +public class EventsourcingStorageStrategy { + + private static final ImmutableSet<Subscriber> NO_SUBSCRIBER = ImmutableSet.of(); + + private final EventSourcingSystem eventSourcingSystem; + + @Inject + public EventsourcingStorageStrategy(EventStore eventStore) { + this.eventSourcingSystem = EventSourcingSystem.fromJava( + ImmutableSet.of(new RegisterStorageStrategyCommandHandler(eventStore)), + NO_SUBSCRIBER, + eventStore); + } + + public void registerStorageStrategy(StorageStrategy newStorageStrategy) { + Preconditions.checkNotNull(newStorageStrategy); + + Mono.from(eventSourcingSystem.dispatch(new RegisterStorageStrategy(newStorageStrategy))).block(); + } +} diff --git a/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/validation/RegisterStorageStrategy.java b/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/validation/RegisterStorageStrategy.java new file mode 100644 index 0000000..2b439b5 --- /dev/null +++ b/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/validation/RegisterStorageStrategy.java @@ -0,0 +1,35 @@ +/**************************************************************** + * 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.modules.blobstore.validation; + +import org.apache.james.eventsourcing.Command; +import org.apache.james.server.blob.deduplication.StorageStrategy; + +public class RegisterStorageStrategy implements Command { + private final StorageStrategy storageStrategy; + + public RegisterStorageStrategy(StorageStrategy storageStrategy) { + this.storageStrategy = storageStrategy; + } + + public StorageStrategy getStorageStrategy() { + return storageStrategy; + } +} diff --git a/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/validation/RegisterStorageStrategyCommandHandler.java b/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/validation/RegisterStorageStrategyCommandHandler.java new file mode 100644 index 0000000..ddd69ff --- /dev/null +++ b/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/validation/RegisterStorageStrategyCommandHandler.java @@ -0,0 +1,53 @@ +/**************************************************************** + * 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.modules.blobstore.validation; + +import java.util.List; + +import org.apache.james.eventsourcing.AggregateId; +import org.apache.james.eventsourcing.CommandHandler; +import org.apache.james.eventsourcing.Event; +import org.apache.james.eventsourcing.eventstore.EventStore; +import org.reactivestreams.Publisher; + +import reactor.core.publisher.Mono; + +public class RegisterStorageStrategyCommandHandler implements CommandHandler<RegisterStorageStrategy> { + static final String STORAGE_STRATEGY_CONFIGURATION_AGGREGATE_KEY = "BlobStoreStorageStrategyConfiguration"; + public static final AggregateId AGGREGATE_ID = () -> STORAGE_STRATEGY_CONFIGURATION_AGGREGATE_KEY; + + private final EventStore eventStore; + + public RegisterStorageStrategyCommandHandler(EventStore eventStore) { + this.eventStore = eventStore; + } + + @Override + public Class<RegisterStorageStrategy> handledClass() { + return RegisterStorageStrategy.class; + } + + @Override + public Publisher<List<? extends Event>> handle(RegisterStorageStrategy command) { + return Mono.from(eventStore.getEventsOfAggregate(AGGREGATE_ID)) + .map(history -> StorageStrategyAggregate.load(AGGREGATE_ID, history)) + .map(aggregate -> aggregate.registerStorageStrategy(command)); + } +} diff --git a/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/validation/StorageStrategyAggregate.java b/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/validation/StorageStrategyAggregate.java new file mode 100644 index 0000000..e949bd4 --- /dev/null +++ b/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/validation/StorageStrategyAggregate.java @@ -0,0 +1,106 @@ +/**************************************************************** + * 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.modules.blobstore.validation; + +import static org.apache.james.server.blob.deduplication.StorageStrategy.DEDUPLICATION; +import static org.apache.james.server.blob.deduplication.StorageStrategy.PASSTHROUGH; + +import java.util.List; +import java.util.Optional; + +import org.apache.james.eventsourcing.AggregateId; +import org.apache.james.eventsourcing.Event; +import org.apache.james.eventsourcing.eventstore.History; +import org.apache.james.server.blob.deduplication.StorageStrategy; + +import com.google.common.collect.ImmutableList; + +public class StorageStrategyAggregate { + static class State { + static State initial() { + return new State(Optional.empty()); + } + + static State forStorageStrategy(StorageStrategy storageStrategy) { + return new State(Optional.of(storageStrategy)); + } + + private final Optional<StorageStrategy> storageStrategy; + + State(Optional<StorageStrategy> storageStrategy) { + this.storageStrategy = storageStrategy; + } + + public Optional<StorageStrategy> getStorageStrategy() { + return storageStrategy; + } + + public boolean holds(StorageStrategy storageStrategy) { + return this.storageStrategy.filter(storageStrategy::equals).isPresent(); + } + + public boolean isAssignable(StorageStrategy storageStrategy) { + if (holds(DEDUPLICATION) && storageStrategy.equals(PASSTHROUGH)) { + return false; + } + return true; + } + } + + public static StorageStrategyAggregate load(AggregateId aggregateId, History history) { + return new StorageStrategyAggregate(aggregateId, history); + } + + private final AggregateId aggregateId; + private final History history; + private State state; + + public StorageStrategyAggregate(AggregateId aggregateId, History history) { + this.aggregateId = aggregateId; + this.history = history; + + this.state = State.initial(); + history.getEventsJava() + .forEach(this::apply); + } + + public List<Event> registerStorageStrategy(RegisterStorageStrategy command) { + if (state.holds(command.getStorageStrategy())) { + return ImmutableList.of(); + } + + if (!state.isAssignable(command.getStorageStrategy())) { + throw new IllegalStateException( + String.format("Cannot use %s as a BlobStoreStorageStrategy when current BlobStoreStorageStrategy is %s", + command.getStorageStrategy(), + state.getStorageStrategy())); + } + + return ImmutableList.of(new StorageStrategyChanged(history.getNextEventId(), aggregateId, command.getStorageStrategy())); + } + + private void apply(Event event) { + if (event instanceof StorageStrategyChanged) { + StorageStrategyChanged changed = (StorageStrategyChanged) event; + + this.state = State.forStorageStrategy(changed.getStorageStrategy()); + } + } +} diff --git a/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/validation/StorageStrategyChanged.java b/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/validation/StorageStrategyChanged.java new file mode 100644 index 0000000..b33236a --- /dev/null +++ b/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/validation/StorageStrategyChanged.java @@ -0,0 +1,51 @@ +/**************************************************************** + * 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.modules.blobstore.validation; + +import org.apache.james.eventsourcing.AggregateId; +import org.apache.james.eventsourcing.Event; +import org.apache.james.eventsourcing.EventId; +import org.apache.james.server.blob.deduplication.StorageStrategy; + +public class StorageStrategyChanged implements Event { + private final EventId eventId; + private final AggregateId aggregateId; + private final StorageStrategy storageStrategy; + + public StorageStrategyChanged(EventId eventId, AggregateId aggregateId, StorageStrategy storageStrategy) { + this.eventId = eventId; + this.aggregateId = aggregateId; + this.storageStrategy = storageStrategy; + } + + @Override + public EventId eventId() { + return eventId; + } + + @Override + public AggregateId getAggregateId() { + return aggregateId; + } + + public StorageStrategy getStorageStrategy() { + return storageStrategy; + } +} diff --git a/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/validation/StorageStrategyChangedDTO.java b/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/validation/StorageStrategyChangedDTO.java new file mode 100644 index 0000000..37a6208 --- /dev/null +++ b/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/validation/StorageStrategyChangedDTO.java @@ -0,0 +1,108 @@ +/**************************************************************** + * 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.modules.blobstore.validation; + +import java.util.Objects; + +import org.apache.james.eventsourcing.EventId; +import org.apache.james.eventsourcing.eventstore.cassandra.dto.EventDTO; +import org.apache.james.server.blob.deduplication.StorageStrategy; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Preconditions; + +class StorageStrategyChangedDTO implements EventDTO { + + static StorageStrategyChangedDTO from(StorageStrategyChanged storageStrategyChanged, String type) { + Preconditions.checkNotNull(storageStrategyChanged); + + StorageStrategy storageStrategy = storageStrategyChanged.getStorageStrategy(); + return new StorageStrategyChangedDTO( + storageStrategyChanged.eventId().serialize(), + storageStrategyChanged.getAggregateId().asAggregateKey(), + type, + storageStrategy.name()); + } + + static StorageStrategyChangedDTO from(StorageStrategyChanged storageStrategyChanged) { + return from(storageStrategyChanged, StorageStrategyModule.TYPE_NAME); + } + + private final int eventId; + private final String aggregateKey; + private final String type; + private final String storageStrategy; + + @JsonCreator + StorageStrategyChangedDTO( + @JsonProperty("eventId") int eventId, + @JsonProperty("aggregateKey") String aggregateKey, + @JsonProperty("type") String type, + @JsonProperty("storageStrategy") String storageStrategy) { + this.eventId = eventId; + this.aggregateKey = aggregateKey; + this.type = type; + this.storageStrategy = storageStrategy; + } + + @JsonIgnore + public StorageStrategyChanged toEvent() { + return new StorageStrategyChanged( + EventId.fromSerialized(eventId), + () -> aggregateKey, + StorageStrategy.valueOf(storageStrategy)); + } + + public int getEventId() { + return eventId; + } + + public String getAggregateKey() { + return aggregateKey; + } + + public String getType() { + return type; + } + + public String getStorageStrategy() { + return storageStrategy; + } + + @Override + public final boolean equals(Object o) { + if (o instanceof StorageStrategyChangedDTO) { + StorageStrategyChangedDTO that = (StorageStrategyChangedDTO) o; + + return Objects.equals(this.eventId, that.eventId) + && Objects.equals(this.aggregateKey, that.aggregateKey) + && Objects.equals(this.type, that.type) + && Objects.equals(this.storageStrategy, that.storageStrategy); + } + return false; + } + + @Override + public final int hashCode() { + return Objects.hash(eventId, aggregateKey, type, storageStrategy); + } +} diff --git a/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/validation/StorageStrategyModule.java b/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/validation/StorageStrategyModule.java new file mode 100644 index 0000000..6607810 --- /dev/null +++ b/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/validation/StorageStrategyModule.java @@ -0,0 +1,36 @@ +/**************************************************************** + * 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.modules.blobstore.validation; + +import org.apache.james.eventsourcing.eventstore.cassandra.dto.EventDTOModule; + +public interface StorageStrategyModule { + + String TYPE_NAME = "storage-strategy-changed"; + + EventDTOModule<StorageStrategyChanged, StorageStrategyChangedDTO> STORAGE_STRATEGY = + EventDTOModule + .forEvent(StorageStrategyChanged.class) + .convertToDTO(StorageStrategyChangedDTO.class) + .toDomainObjectConverter(StorageStrategyChangedDTO::toEvent) + .toDTOConverter(StorageStrategyChangedDTO::from) + .typeName(TYPE_NAME) + .withFactory(EventDTOModule::new); +} \ No newline at end of file diff --git a/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/validation/StorageStrategyValidationEventSourcingSystem.java b/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/validation/StorageStrategyValidationEventSourcingSystem.java new file mode 100644 index 0000000..7024599 --- /dev/null +++ b/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/validation/StorageStrategyValidationEventSourcingSystem.java @@ -0,0 +1,60 @@ +/**************************************************************** + * 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.modules.blobstore.validation; + +import javax.inject.Inject; + +import org.apache.james.eventsourcing.EventSourcingSystem; +import org.apache.james.eventsourcing.eventstore.EventStore; +import org.apache.james.lifecycle.api.StartUpCheck; +import org.apache.james.lifecycle.api.StartUpCheck.CheckResult; +import org.apache.james.modules.blobstore.BlobStoreConfiguration; + +import com.google.common.collect.ImmutableSet; + +import reactor.core.publisher.Mono; + +public class StorageStrategyValidationEventSourcingSystem { + public static final String CHECK = "blobstore-storage-strategy-configuration-check"; + private final EventSourcingSystem eventSourcingSystem; + + @Inject + public StorageStrategyValidationEventSourcingSystem(EventStore eventStore) { + this.eventSourcingSystem = EventSourcingSystem.fromJava( + ImmutableSet.of(new RegisterStorageStrategyCommandHandler(eventStore)), + ImmutableSet.of(), + eventStore); + } + + public CheckResult validate(BlobStoreConfiguration blobStoreConfiguration) { + return Mono.from(eventSourcingSystem.dispatch(new RegisterStorageStrategy(blobStoreConfiguration.storageStrategy()))) + .thenReturn(CheckResult.builder() + .checkName(CHECK) + .resultType(StartUpCheck.ResultType.GOOD) + .build()) + .onErrorResume(IllegalStateException.class, + e -> Mono.just(CheckResult.builder() + .checkName(CHECK) + .resultType(StartUpCheck.ResultType.BAD) + .description(e.getMessage()) + .build())) + .block(); + } +} diff --git a/server/container/guice/cassandra-rabbitmq-guice/src/test/java/org/apache/james/modules/blobstore/BlobStoreConfigurationValidationStartUpCheckTest.java b/server/container/guice/cassandra-rabbitmq-guice/src/test/java/org/apache/james/modules/blobstore/BlobStoreConfigurationValidationStartUpCheckTest.java new file mode 100644 index 0000000..81de1a4 --- /dev/null +++ b/server/container/guice/cassandra-rabbitmq-guice/src/test/java/org/apache/james/modules/blobstore/BlobStoreConfigurationValidationStartUpCheckTest.java @@ -0,0 +1,124 @@ +/**************************************************************** + * 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.modules.blobstore; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Set; +import java.util.stream.Stream; + +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.eventsourcing.Event; +import org.apache.james.eventsourcing.eventstore.EventStore; +import org.apache.james.eventsourcing.eventstore.cassandra.CassandraEventStore; +import org.apache.james.eventsourcing.eventstore.cassandra.CassandraEventStoreExtension; +import org.apache.james.eventsourcing.eventstore.cassandra.CassandraEventStoreModule; +import org.apache.james.eventsourcing.eventstore.cassandra.JsonEventSerializer; +import org.apache.james.eventsourcing.eventstore.cassandra.dto.EventDTO; +import org.apache.james.eventsourcing.eventstore.cassandra.dto.EventDTOModule; +import org.apache.james.lifecycle.api.StartUpCheck; +import org.apache.james.modules.blobstore.validation.EventsourcingStorageStrategy; +import org.apache.james.modules.blobstore.validation.StorageStrategyModule; +import org.apache.james.server.blob.deduplication.StorageStrategy; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import com.google.common.collect.ImmutableSet; + +class BlobStoreConfigurationValidationStartUpCheckTest { + @RegisterExtension + static final CassandraClusterExtension CASSANDRA_CLUSTER = new CassandraClusterExtension( + CassandraModule.aggregateModules( + CassandraSchemaVersionModule.MODULE, + CassandraEventStoreModule.MODULE())); + + private static final Set<EventDTOModule<? extends Event, ? extends EventDTO>> EVENT_DTO_MODULES = ImmutableSet.of(StorageStrategyModule.STORAGE_STRATEGY); + + + @RegisterExtension + CassandraEventStoreExtension eventStoreExtension = new CassandraEventStoreExtension(CASSANDRA_CLUSTER, + JsonEventSerializer.forModules(EVENT_DTO_MODULES).withoutNestedType()); + + private static BlobStoreConfiguration DEDUPLICATION_STRATEGY = BlobStoreConfiguration + .builder() + .cassandra() + .disableCache() + .deduplication(); + private static BlobStoreConfiguration PASSTHROUGH_STRATEGY = BlobStoreConfiguration + .builder() + .cassandra() + .disableCache() + .passthrough(); + + private EventsourcingStorageStrategy eventsourcingStorageStrategy; + + @BeforeEach + void setUp(EventStore eventStore) { + eventsourcingStorageStrategy = new EventsourcingStorageStrategy(eventStore); + } + + @ParameterizedTest + @MethodSource("storageStrategies") + void firstStartUpShouldReturnAGoodResult(BlobStoreConfiguration blobStoreConfiguration) { + BlobStoreConfigurationValidationStartUpCheck check = new BlobStoreConfigurationValidationStartUpCheck(blobStoreConfiguration, eventsourcingStorageStrategy); + assertThat(check.check().getResultType()).isEqualTo(StartUpCheck.ResultType.GOOD); + } + + @ParameterizedTest + @MethodSource("storageStrategies") + void startingUpTwiceWithTheStrategyShouldReturnGoodResults(BlobStoreConfiguration blobStoreConfiguration) { + BlobStoreConfigurationValidationStartUpCheck checkFirstStartUp = new BlobStoreConfigurationValidationStartUpCheck(blobStoreConfiguration, eventsourcingStorageStrategy); + assertThat(checkFirstStartUp.check().getResultType()).isEqualTo(StartUpCheck.ResultType.GOOD); + + BlobStoreConfigurationValidationStartUpCheck checkSecondStartUp = new BlobStoreConfigurationValidationStartUpCheck(blobStoreConfiguration, eventsourcingStorageStrategy); + assertThat(checkSecondStartUp.check().getResultType()).isEqualTo(StartUpCheck.ResultType.GOOD); + } + + @Test + void startingUpWithDeduplicationThenPassthroughTheStrategyShouldReturnABadResult() { + BlobStoreConfigurationValidationStartUpCheck checkFirstStartUp = new BlobStoreConfigurationValidationStartUpCheck(DEDUPLICATION_STRATEGY, eventsourcingStorageStrategy); + checkFirstStartUp.check(); + + BlobStoreConfigurationValidationStartUpCheck checkSecondStartUp = new BlobStoreConfigurationValidationStartUpCheck(PASSTHROUGH_STRATEGY, eventsourcingStorageStrategy); + assertThat(checkSecondStartUp.check().getResultType()).isEqualTo(StartUpCheck.ResultType.BAD); + } + + @Test + void startingUpWithPassthroughThenDeduplicationTheStrategyShouldReturnAGoodResult() { + BlobStoreConfigurationValidationStartUpCheck checkFirstStartUp = new BlobStoreConfigurationValidationStartUpCheck(PASSTHROUGH_STRATEGY, eventsourcingStorageStrategy); + checkFirstStartUp.check(); + + BlobStoreConfigurationValidationStartUpCheck checkSecondStartUp = new BlobStoreConfigurationValidationStartUpCheck(DEDUPLICATION_STRATEGY, eventsourcingStorageStrategy); + assertThat(checkSecondStartUp.check().getResultType()).isEqualTo(StartUpCheck.ResultType.GOOD); + } + + static Stream<Arguments> storageStrategies() { + return Stream.of( + Arguments.of(DEDUPLICATION_STRATEGY), + Arguments.of(PASSTHROUGH_STRATEGY) + ); + } +} \ No newline at end of file diff --git a/server/container/guice/cassandra-rabbitmq-guice/src/test/java/org/apache/james/modules/blobstore/validation/StorageStrategyChangedDTOTest.java b/server/container/guice/cassandra-rabbitmq-guice/src/test/java/org/apache/james/modules/blobstore/validation/StorageStrategyChangedDTOTest.java new file mode 100644 index 0000000..668bb7d --- /dev/null +++ b/server/container/guice/cassandra-rabbitmq-guice/src/test/java/org/apache/james/modules/blobstore/validation/StorageStrategyChangedDTOTest.java @@ -0,0 +1,79 @@ +/**************************************************************** + * 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.modules.blobstore.validation; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.apache.james.eventsourcing.EventId; +import org.apache.james.server.blob.deduplication.StorageStrategy; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.Test; + +import nl.jqno.equalsverifier.EqualsVerifier; + +class StorageStrategyChangedDTOTest { + + private static final int EVENT_ID_SERIALIZED = 10; + private static final EventId EVENT_ID = EventId.fromSerialized(EVENT_ID_SERIALIZED); + private static final String STORAGE_STRATEGY_AGGREGATE_KEY = "aggrKey"; + + @Test + void shouldMatchBeanContract() { + EqualsVerifier.forClass(StorageStrategyChangedDTO.class) + .verify(); + } + + @Test + void fromShouldThrowWhenStorageStrategyAddedIsNull() { + assertThatThrownBy(() -> StorageStrategyChangedDTO.from(null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + void fromShouldReturnCorrespondingDTO() { + StorageStrategyChanged configurationChanged = new StorageStrategyChanged( + EVENT_ID, + () -> STORAGE_STRATEGY_AGGREGATE_KEY, + StorageStrategy.DEDUPLICATION); + + StorageStrategyChangedDTO dto = StorageStrategyChangedDTO.from(configurationChanged); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(dto.getEventId()).isEqualTo(EVENT_ID_SERIALIZED); + softly.assertThat(dto.getType()).isEqualTo(StorageStrategyModule.TYPE_NAME); + softly.assertThat(dto.getStorageStrategy()).isEqualTo(StorageStrategy.DEDUPLICATION.name()); + }); + } + + @Test + void toEventShouldReturnCorrespondingStorageStrategyChangedEvent() { + StorageStrategyChangedDTO dto = new StorageStrategyChangedDTO( + EVENT_ID_SERIALIZED, + STORAGE_STRATEGY_AGGREGATE_KEY, + StorageStrategyModule.TYPE_NAME, + StorageStrategy.DEDUPLICATION.name()); + StorageStrategyChanged event = dto.toEvent(); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(event.eventId()).isEqualTo(EVENT_ID); + softly.assertThat(event.getAggregateId().asAggregateKey()).isEqualTo(STORAGE_STRATEGY_AGGREGATE_KEY); + softly.assertThat(event.getStorageStrategy()).isEqualTo(StorageStrategy.DEDUPLICATION); + }); + } +} diff --git a/server/container/guice/cassandra-rabbitmq-guice/src/test/java/org/apache/james/modules/blobstore/validation/StorageStrategyValidationEventSourcingSystemTest.java b/server/container/guice/cassandra-rabbitmq-guice/src/test/java/org/apache/james/modules/blobstore/validation/StorageStrategyValidationEventSourcingSystemTest.java new file mode 100644 index 0000000..04a4753 --- /dev/null +++ b/server/container/guice/cassandra-rabbitmq-guice/src/test/java/org/apache/james/modules/blobstore/validation/StorageStrategyValidationEventSourcingSystemTest.java @@ -0,0 +1,100 @@ +/**************************************************************** + * 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.modules.blobstore.validation; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.apache.james.eventsourcing.eventstore.History; +import org.apache.james.eventsourcing.eventstore.memory.InMemoryEventStore; +import org.apache.james.lifecycle.api.StartUpCheck; +import org.apache.james.modules.blobstore.BlobStoreConfiguration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import reactor.core.publisher.Mono; + +public class StorageStrategyValidationEventSourcingSystemTest { + private StorageStrategyValidationEventSourcingSystem testee; + private InMemoryEventStore eventStore; + + @BeforeEach + void setUp() { + eventStore = new InMemoryEventStore(); + testee = new StorageStrategyValidationEventSourcingSystem(eventStore); + } + + @Test + void startingForTheFirstTimeShouldSucceedWhenPassThrough() { + StartUpCheck.CheckResult checkResult = testee.validate(BlobStoreConfiguration.builder().implementation(BlobStoreConfiguration.BlobStoreImplName.OBJECTSTORAGE) + .disableCache() + .passthrough()); + + assertThat(checkResult.getResultType()).isEqualTo(StartUpCheck.ResultType.GOOD); + } + + @Test + void startingForTheFirstTimeShouldSucceedWhenDeduplication() { + StartUpCheck.CheckResult checkResult = testee.validate(BlobStoreConfiguration.builder().implementation(BlobStoreConfiguration.BlobStoreImplName.OBJECTSTORAGE) + .disableCache() + .deduplication()); + + assertThat(checkResult.getResultType()).isEqualTo(StartUpCheck.ResultType.GOOD); + } + + @Test + void startingShouldSucceedWhenTurningOnDeduplication() { + testee.validate(BlobStoreConfiguration.builder().implementation(BlobStoreConfiguration.BlobStoreImplName.OBJECTSTORAGE) + .disableCache() + .passthrough()); + + StartUpCheck.CheckResult checkResult = testee.validate(BlobStoreConfiguration.builder().implementation(BlobStoreConfiguration.BlobStoreImplName.OBJECTSTORAGE) + .disableCache() + .deduplication()); + + assertThat(checkResult.getResultType()).isEqualTo(StartUpCheck.ResultType.GOOD); + } + + @Test + void startingShouldFailWhenTurningOffDeduplication() { + testee.validate(BlobStoreConfiguration.builder().implementation(BlobStoreConfiguration.BlobStoreImplName.OBJECTSTORAGE) + .disableCache() + .deduplication()); + + StartUpCheck.CheckResult checkResult = testee.validate(BlobStoreConfiguration.builder().implementation(BlobStoreConfiguration.BlobStoreImplName.OBJECTSTORAGE) + .disableCache() + .passthrough()); + + assertThat(checkResult.getResultType()).isEqualTo(StartUpCheck.ResultType.BAD); + } + + @Test + void validatingSeveralTimeTheSameStrategyShouldNotAddEventsToTheHistory() { + testee.validate(BlobStoreConfiguration.builder().implementation(BlobStoreConfiguration.BlobStoreImplName.OBJECTSTORAGE) + .disableCache() + .deduplication()); + testee.validate(BlobStoreConfiguration.builder().implementation(BlobStoreConfiguration.BlobStoreImplName.OBJECTSTORAGE) + .disableCache() + .deduplication()); + + History history = Mono.from(eventStore.getEventsOfAggregate(RegisterStorageStrategyCommandHandler.AGGREGATE_ID)).block(); + + assertThat(history.getEventsJava()).hasSize(1); + } +} \ No newline at end of file --------------------------------------------------------------------- To unsubscribe, e-mail: server-dev-unsubscr...@james.apache.org For additional commands, e-mail: server-dev-h...@james.apache.org