/** * Copyright (c) 2016, All Contributors (see CONTRIBUTORS file) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package com.eventsourcing.repository; import com.eventsourcing.*; import com.eventsourcing.cep.events.DescriptionChanged; import com.eventsourcing.events.CommandTerminatedExceptionally; import com.eventsourcing.events.EventCausalityEstablished; import com.eventsourcing.events.JavaExceptionOccurred; import com.eventsourcing.hlc.HybridTimestamp; import com.eventsourcing.hlc.PhysicalTimeProvider; import com.eventsourcing.index.IndexEngine; import com.google.common.util.concurrent.AbstractService; import com.googlecode.cqengine.ConcurrentIndexedCollection; import com.googlecode.cqengine.IndexedCollection; import lombok.Getter; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.stream.Stream; @Slf4j class CommandConsumerImpl extends AbstractService implements CommandConsumer { private Executor threadPool = Executors.newWorkStealingPool(Runtime.getRuntime().availableProcessors()); private final Repository repository; private final Journal journal; private final IndexEngine indexEngine; private final LockProvider lockProvider; @Getter private HybridTimestamp timestamp; @SneakyThrows public CommandConsumerImpl(Iterable<Class<? extends Command>> commandClasses, PhysicalTimeProvider timeProvider, Repository repository, Journal journal, IndexEngine indexEngine, LockProvider lockProvider) { this.repository = repository; this.journal = journal; this.indexEngine = indexEngine; this.lockProvider = lockProvider; Optional<HybridTimestamp> repositoryTimestamp = journal.getProperties().getRepositoryTimestamp(); if (repositoryTimestamp.isPresent()) { this.timestamp = new HybridTimestamp(timeProvider, repositoryTimestamp.get()); } else { this.timestamp = new HybridTimestamp(timeProvider); timestamp.update(); } } private void timestamp(Entity entity, HybridTimestamp timestamp) { if (entity.timestamp() == null) { timestamp.update(); entity.timestamp(timestamp.clone()); } else { timestamp.update(timestamp.clone()); } } private <S> EventStream<S> exceptionalTerminationStream(Exception e) { CommandTerminatedExceptionally commandTerminatedExceptionally = new CommandTerminatedExceptionally(); return EventStream.of(Stream.of( new CommandTerminatedExceptionally(), DescriptionChanged.builder() .description(e.getMessage()) .reference(commandTerminatedExceptionally.uuid()).build(), new JavaExceptionOccurred(e))); } private void onEvent(Event event, Map<Class<? extends Event>, IndexedCollection<EntityHandle<Event>>> txCollections, Map<EntitySubscriber, Set<UUID>> subscriptions, Collection<EntitySubscriber> subscribers ) { IndexedCollection<EntityHandle<Event>> coll = txCollections .computeIfAbsent(event.getClass(), klass -> new ConcurrentIndexedCollection<>()); coll.add(new ResolvedEntityHandle<>(event)); subscribers.stream() .filter(s -> s.matches(repository, event)) .forEach(s -> subscriptions.get(s).add(event.uuid())); } @Override public <T, S, C extends Command<S, T>> CompletableFuture<T> publish(C command, Collection<EntitySubscriber> subscribers) { Map<EntitySubscriber, Set<UUID>> subscriptions = new HashMap<>(); subscribers.forEach(s -> subscriptions.put(s, new HashSet<>())); Map<Class<? extends Event>, IndexedCollection<EntityHandle<Event>>> txCollections = new HashMap<>(); CompletableFuture<T> future = new CompletableFuture<>(); HybridTimestamp txTimestamp; synchronized (timestamp) { timestamp(command, timestamp); txTimestamp = timestamp.clone(); } final HybridTimestamp commandTimestamp = txTimestamp.clone(); threadPool.execute( new CommandHandler<>(commandTimestamp, command, txCollections, subscriptions, subscribers, future, txTimestamp)); return future; } @Override @SuppressWarnings("unchecked") protected void doStart() { notifyStarted(); } @Override protected void doStop() { notifyStopped(); } private class CommandHandler<S, T, C extends Command<S, T>> implements Runnable { private final HybridTimestamp commandTimestamp; private final C command; private final Map<Class<? extends Event>, IndexedCollection<EntityHandle<Event>>> txCollections; private final Map<EntitySubscriber, Set<UUID>> subscriptions; private final Collection<EntitySubscriber> subscribers; private final CompletableFuture<T> future; private final HybridTimestamp txTimestamp; public CommandHandler(HybridTimestamp commandTimestamp, C command, Map<Class<? extends Event>, IndexedCollection<EntityHandle<Event>>> txCollections, Map<EntitySubscriber, Set<UUID>> subscriptions, Collection<EntitySubscriber> subscribers, CompletableFuture<T> future, HybridTimestamp txTimestamp) { this.commandTimestamp = commandTimestamp; this.command = command; this.txCollections = txCollections; this.subscriptions = subscriptions; this.subscribers = subscribers; this.future = future; this.txTimestamp = txTimestamp; } @Override public void run() { HybridTimestamp ts = commandTimestamp.clone(); HybridTimestamp startingTxTimestamp = ts.clone(); TrackingLockProvider lockProvider = new TrackingLockProvider(CommandConsumerImpl.this.lockProvider); lockProvider.startAsync().awaitRunning(); EventStream<S> eventStream; Exception exception = null; try { eventStream = command.events(repository, lockProvider); } catch (Exception e) { eventStream = CommandConsumerImpl.this.exceptionalTerminationStream(e); exception = e; } boolean pending = true; main: while (pending) { Journal.Transaction tx = journal.beginTransaction(); Stream<? extends Event> stream = eventStream.getStream(); Iterator<? extends Event> iterator = stream.iterator(); try { while (iterator.hasNext()) { Event event = iterator.next(); CommandConsumerImpl.this.timestamp(event, ts); event = journal.journal(tx, event); EventCausalityEstablished causalityEstablished = EventCausalityEstablished.builder() .event(event.uuid()) .command( command.uuid()) .build(); CommandConsumerImpl.this.timestamp(causalityEstablished, ts); causalityEstablished = (EventCausalityEstablished) journal.journal(tx, causalityEstablished); CommandConsumerImpl.this.onEvent(event, txCollections, subscriptions, subscribers); CommandConsumerImpl.this .onEvent(causalityEstablished, txCollections, subscriptions, subscribers); } } catch (Exception e) { txCollections.clear(); tx.rollback(); eventStream = CommandConsumerImpl.this.exceptionalTerminationStream(e); ts = startingTxTimestamp; exception = e; continue main; } pending = false; Command<S, T> command_ = journal.journal(tx, command); tx.commit(); for (Map.Entry<Class<? extends Event>, IndexedCollection<EntityHandle<Event>>> pair : txCollections.entrySet()) { IndexedCollection<EntityHandle<Event>> value = pair.getValue(); indexEngine.getIndexedCollection((Class<Event>) pair.getKey()).addAll(value); } IndexedCollection<EntityHandle<Command<S, T>>> coll = indexEngine .getIndexedCollection((Class<Command<S, T>>) command_.getClass()); EntityHandle<Command<?, ?>> commandHandle = new JournalEntityHandle<>(journal, command_.uuid()); coll.add(new ResolvedEntityHandle<>(command_)); subscriptions.entrySet().stream() .forEach(entry -> entry.getKey() .accept(repository, entry.getValue() .stream() .map(uuid -> new JournalEntityHandle<>(journal, uuid)))); subscribers.stream() .filter(s -> s.matches(repository, command_)) .forEach(s -> s.accept(repository, Stream.of(commandHandle))); synchronized (timestamp) { timestamp.update(txTimestamp); journal.getProperties().setRepositoryTimestamp(timestamp); } if (exception == null) { T result = command.result(eventStream.getState(), repository, lockProvider); lockProvider.release(); future.complete(result); } else { lockProvider.release(); future.completeExceptionally(exception); } } } } }