/* * * * This file is part of the Hesperides distribution. * * (https://github.com/voyages-sncf-technologies/hesperides) * * Copyright (c) 2016 VSCT. * * * * Hesperides is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as * * published by the Free Software Foundation, version 3. * * * * Hesperides is distributed in the hope that it will be useful, but * * WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * * General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see <http://www.gnu.org/licenses/>. * * */ package com.vsct.dt.hesperides.storage; import com.google.common.eventbus.EventBus; import com.vsct.dt.hesperides.exception.runtime.HesperidesException; import com.vsct.dt.hesperides.exception.runtime.StateLockedException; import io.dropwizard.lifecycle.Managed; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Set; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; /** * Created by william_montaz on 22/01/2015. */ public abstract class AbstractThreadAggregate implements Managed, StoragePrefixInterface { private static final Logger LOGGER = LoggerFactory.getLogger(AbstractThreadAggregate.class); /** * Used to store events, storing is actually made throug the try atomic method * storing is a single thread process, events queue and wait to be stored * More on comments on method tryatomic */ private final EventStore store; /** * Reference to the application bus to propagate events to listners */ private final EventBus eventBus; /** * Describes if the aggregate allows write operations. * Write operations are not allowed when there has been an unexpected error that could lead to inconsistency */ private AtomicBoolean writable = new AtomicBoolean(true); /** * UserProvider used to get user information */ private UserProvider userProvider; /** * Describe state of the aggregate. when it is replaying, the events are not stored */ protected AtomicBoolean isReplaying = new AtomicBoolean(false); /** * Convenient bus to implement replayability * The aggregate will only have to specify methods used to replay events * and use the @Subscribe annotation * It will automatically be registered on the replayBus for replay */ protected EventBus replayBus = new EventBus(); /** * Default user provider that can be changed later */ private static class DefaultUserProvider implements UserProvider { @Override public UserInfo getCurrentUserInfo() { return UserInfo.UNTRACKED; } } protected AbstractThreadAggregate(final EventBus eventBus, final EventStore eventStore) { this(eventBus, eventStore, new DefaultUserProvider()); } protected AbstractThreadAggregate(final EventBus eventBus, final EventStore eventStore, final UserProvider userProvider) { this.store = eventStore; this.eventBus = eventBus; this.userProvider = userProvider; } /** * Used to manipulate state and perform atomic write operations, being sure no else modifies the state concurently * Commands are executed in order, they produce events that are stored and then dispatched * A better implementation could allow concurrent modification depending on streamName */ protected <T> T tryAtomic(final String entityName, final HesperidesCommand<T> command) { UserInfo userInfo = userProvider.getCurrentUserInfo(); //Execute command and try to store the resulting event Future<T> future = this.executorService().submit(() -> { //If we go multi instance that's where we can synchronize state with event store //We are simple instance so dont do it final String key = getStreamPrefix() + "-" + entityName; try { if (writable.get() == false) { throw new StateLockedException("State write operations have been locked for state {}. This is due to an exception that occured when modifying state. APPLICATION RESTART IS NEEDED."); } T event = command.apply(); if (isReplaying.get() == false) { store.store(key, event, userInfo, command); eventBus.post(event); } else { command.complete(); } return event; } catch(HesperidesException e) { //avoid blocking state with HesperidesExceptions LOGGER.info("HesperidesException has been sent by command for entity '{}'. We dont block the state", entityName); throw e; } catch (RuntimeException e) { /* This is all that should have not happened, SO ITS BAAAAAAD */ //For a more advanced implementation we could restore the state back, //With this implementation we will just tell to restart the app in order //to get the state back LOGGER.error("A problem occured when storing the event for entity '{}'. It could now be inconsistent. APPLICATION RESTART IS NEEDED !!", entityName); LOGGER.error(e.getMessage()); writable.set(false); throw e; } } ); try { return future.get(); } catch (InterruptedException e) { e.printStackTrace(); throw new RuntimeException(e); } catch (ExecutionException e) { Throwable cause = e.getCause(); //Try to keep the real nature of the exception thrown if(cause instanceof HesperidesException){ throw (HesperidesException) cause; } else if(cause instanceof RuntimeException){ LOGGER.error("A problem occured when trying to execute command. This needs further investigation"); LOGGER.error(e.getMessage()); throw (RuntimeException) cause; } else { throw new RuntimeException(e); } } } public boolean isWritable() { return writable.get(); } /** * Aggregate managed by DropWizard * Start requires replaying the events * * @throws Exception */ @Override public void start() throws Exception { // Nothing } /** * Aggregate managed by DropWizard * Nothing especial needed for stop * * @throws Exception */ @Override public void stop() throws Exception { // Nothing } protected EventBus getEventBus() { return eventBus; } protected void replay(final String stream) throws StoreReadingException { isReplaying.set(true); replayBus.register(this); LOGGER.debug("Replay event for key '{}' from scratch.", stream); store.withEvents(stream, Long.MAX_VALUE, event -> replayBus.post(event)); isReplaying.set(false); //Release replayBus replayBus.unregister(this); } protected void replay(final String stream, final long start, final long stop) throws StoreReadingException { isReplaying.set(true); replayBus.register(this); LOGGER.debug("Replay event for key '{}' from {} to {}.", stream, start, stop); store.withEvents(stream, start, stop, event -> replayBus.post(event)); isReplaying.set(false); //Release replayBus replayBus.unregister(this); } protected void replay(final String stream, final long start, final long stop, final long stopTimestamp) throws StoreReadingException { isReplaying.set(true); replayBus.register(this); LOGGER.debug("Replay event for key '{}' until to timestamp '{}'.", stream, stopTimestamp); store.withEvents(stream, start, stop, stopTimestamp, event -> replayBus.post(event)); isReplaying.set(false); //Release replayBus replayBus.unregister(this); } protected void replay(final String stream, final long stopTimestamp) throws StoreReadingException { isReplaying.set(true); replayBus.register(this); LOGGER.debug("Replay event for key '{}' until to timestamp '{}'.", stream, stopTimestamp); store.withEvents(stream, stopTimestamp, event -> replayBus.post(event)); isReplaying.set(false); //Release replayBus replayBus.unregister(this); } /** * Allow to regenerate cache at startup. */ protected void regenerateCache(final String keySearch) { final Set<String> listStream = store.getStreamsLike(keySearch); for (String key : listStream) { LOGGER.info("Regenerate cache for stream {}.", key); clearCacheInDatabase(key); replay(key); } } protected void clearCacheInDatabase(final String key) { store.clearCache(key); } /** * Return exector service for tryAtomic function. * Allow to don't provide thread for Virtual***Aggregate. * * @return an executor service */ protected abstract ExecutorService executorService(); }