com.eventsourcing.repository.RepositoryTest.java Source code

Java tutorial

Introduction

Here is the source code for com.eventsourcing.repository.RepositoryTest.java

Source

/**
 * 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 boguspackage.BogusCommand;
import boguspackage.BogusEvent;
import com.eventsourcing.*;
import com.eventsourcing.events.CommandTerminatedExceptionally;
import com.eventsourcing.events.EventCausalityEstablished;
import com.eventsourcing.events.JavaExceptionOccurred;
import com.eventsourcing.hlc.HybridTimestamp;
import com.eventsourcing.hlc.NTPServerTimeProvider;
import com.eventsourcing.hlc.PhysicalTimeProvider;
import com.eventsourcing.index.*;
import com.eventsourcing.inmem.MemoryIndexEngine;
import com.eventsourcing.layout.LayoutConstructor;
import com.eventsourcing.migrations.events.EntityLayoutIntroduced;
import com.eventsourcing.repository.commands.IntroduceEntityLayouts;
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.AbstractService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.Service;
import com.googlecode.cqengine.IndexedCollection;
import com.googlecode.cqengine.resultset.ResultSet;
import lombok.*;
import lombok.experimental.Accessors;
import org.apache.commons.net.ntp.TimeStamp;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;

import static com.eventsourcing.queries.QueryFactory.*;
import static com.eventsourcing.index.IndexEngine.IndexFeature.*;
import static org.testng.Assert.*;

public abstract class RepositoryTest {

    private Repository repository;
    private Journal journal;
    private IndexEngine indexEngine;
    private LocalLockProvider lockProvider;
    private NTPServerTimeProvider timeProvider;
    private TimeStamp startTime;

    @BeforeClass
    public void setUpEnv() throws Exception {
        startTime = new TimeStamp(new Date());
        repository = new StandardRepository();
        repository.addCommandSetProvider(
                new PackageCommandSetProvider(new Package[] { RepositoryTest.class.getPackage() }));
        repository.addEventSetProvider(
                new PackageEventSetProvider(new Package[] { RepositoryTest.class.getPackage() }));
        journal = createJournal();
        repository.setJournal(journal);
        timeProvider = new NTPServerTimeProvider(new String[] { "localhost" });
        repository.setPhysicalTimeProvider(timeProvider);
        indexEngine = createIndexEngine();
        repository.setIndexEngine(indexEngine);
        lockProvider = new LocalLockProvider();
        repository.setLockProvider(lockProvider);
        repository.startAsync().awaitRunning();

        long size = journal.size(EntityLayoutIntroduced.class);
        assertTrue(size > 0);
        // make sure layout introductions don't duplicate
        repository.publish(
                new IntroduceEntityLayouts(Iterables.concat(repository.getCommands(), repository.getEvents())))
                .join();
        assertEquals(journal.size(EntityLayoutIntroduced.class), size);
    }

    protected IndexEngine createIndexEngine() {
        return new MemoryIndexEngine();
    }

    protected abstract Journal createJournal();

    @AfterClass
    public void tearDownEnv() throws Exception {
        repository.stopAsync().awaitTerminated();
    }

    @BeforeMethod
    public void setUp() throws Exception {
        journal.clear();
        indexEngine.getIndexedCollection(EntityLayoutIntroduced.class).clear();
        repository.publish(
                new IntroduceEntityLayouts(Iterables.concat(repository.getCommands(), repository.getEvents())))
                .join();
    }

    @Accessors(fluent = true)
    @ToString
    @Indices({ TestEvent.class, TestEventExtraIndices.class })
    public static class TestEvent extends StandardEvent {
        @Getter
        private final String string;

        @Index({ EQ, SC })
        public final static SimpleIndex<TestEvent, String> ATTR = SimpleIndex.as(TestEvent::string);

        @Index({ EQ, SC })
        @Deprecated
        public final static SimpleIndex<TestEvent, String> ATTR_DEP = SimpleIndex.as(TestEvent::string);

        @Index
        public final static MultiValueIndex<TestEvent, String> ATTRS = MultiValueIndex.as(TestEvent::strings);

        public List<String> strings() {
            return Arrays.asList(string);
        }

        @Builder
        public TestEvent(HybridTimestamp timestamp, String string) {
            super(timestamp);
            this.string = string;
        }
    }

    public static class TestEventExtraIndices {
        public static SimpleIndex<TestEvent, List<String>> ATTRL = TestEvent::strings;
    }

    @ToString
    public static class RepositoryTestCommand extends StandardCommand<Void, String> {

        @Getter
        private final String value;

        @Builder
        public RepositoryTestCommand(HybridTimestamp timestamp, String value) {
            super(timestamp);
            this.value = value == null ? "test" : value;
        }

        @Override
        public EventStream<Void> events() {
            return EventStream.of(TestEvent.builder().string(value).build());
        }

        @Override
        public String result() {
            return "hello, world";
        }

        @Index({ EQ, SC })
        public final static SimpleIndex<RepositoryTestCommand, String> ATTR = SimpleIndex
                .as(RepositoryTestCommand::getValue);

    }

    @Test
    @SneakyThrows
    public void initialTimestamp() {
        HybridTimestamp t = repository.getTimestamp();
        long ts = t.timestamp();
        TimeStamp soon = new TimeStamp(new Date(new Date().toInstant().plus(1, ChronoUnit.SECONDS).toEpochMilli()));
        TimeStamp t1 = new TimeStamp(ts);
        assertTrue(HybridTimestamp.compare(t1, startTime) > 0);
        assertTrue(HybridTimestamp.compare(t1, soon) < 0);
    }

    @Test
    public void discovery() {
        assertTrue(repository.getCommands().contains(RepositoryTestCommand.class));
    }

    @Test
    @SneakyThrows
    public void layoutIntroduction() {
        try (ResultSet<EntityHandle<EntityLayoutIntroduced>> resultSet = repository
                .query(EntityLayoutIntroduced.class, all(EntityLayoutIntroduced.class))) {
            assertTrue(resultSet.isNotEmpty());
        }
    }

    @Test
    @SneakyThrows
    public void basicPublish() {
        assertEquals("hello, world", repository.publish(RepositoryTestCommand.builder().build()).get());
    }

    @Test
    @SneakyThrows
    public void subscribe() {
        final AtomicBoolean gotEvent = new AtomicBoolean();
        final AtomicBoolean gotCommand = new AtomicBoolean();
        repository.addEntitySubscriber(new ClassEntitySubscriber<TestEvent>(TestEvent.class) {
            @Override
            public void onEntity(EntityHandle<TestEvent> entity) {
                gotEvent.set(journal.get(entity.uuid()).isPresent());
            }
        });
        repository
                .addEntitySubscriber(new ClassEntitySubscriber<RepositoryTestCommand>(RepositoryTestCommand.class) {
                    @Override
                    public void onEntity(EntityHandle<RepositoryTestCommand> entity) {
                        gotCommand.set(journal.get(entity.uuid()).isPresent());
                    }
                });
        repository.publish(RepositoryTestCommand.builder().build()).get();
        assertTrue(gotEvent.get());
        assertTrue(gotCommand.get());
    }

    @Test
    @SneakyThrows
    public void timestamping() {
        IndexedCollection<EntityHandle<TestEvent>> coll = indexEngine.getIndexedCollection(TestEvent.class);
        coll.clear();
        IndexedCollection<EntityHandle<RepositoryTestCommand>> coll1 = indexEngine
                .getIndexedCollection(RepositoryTestCommand.class);
        coll1.clear();

        repository.publish(RepositoryTestCommand.builder().build()).get();
        TestEvent test = coll.retrieve(equal(TestEvent.ATTR, "test")).uniqueResult().get();
        assertNotNull(test.timestamp());

        RepositoryTestCommand test1 = coll1.retrieve(equal(RepositoryTestCommand.ATTR, "test")).uniqueResult()
                .get();
        assertNotNull(test1.timestamp());

        assertTrue(test.timestamp().compareTo(test1.timestamp()) > 0);
    }

    @Test
    @SneakyThrows
    public void commandTimestamping() {
        HybridTimestamp timestamp = new HybridTimestamp(timeProvider);
        timestamp.update();
        RepositoryTestCommand command1 = RepositoryTestCommand.builder().value("forced").timestamp(timestamp)
                .build();
        RepositoryTestCommand command2 = RepositoryTestCommand.builder().build();
        IndexedCollection<EntityHandle<RepositoryTestCommand>> coll1 = indexEngine
                .getIndexedCollection(RepositoryTestCommand.class);
        coll1.clear();

        repository.publish(command2).get();
        repository.publish(command1).get();

        RepositoryTestCommand test1 = coll1.retrieve(equal(RepositoryTestCommand.ATTR, "forced")).uniqueResult()
                .get();
        RepositoryTestCommand test2 = coll1.retrieve(equal(RepositoryTestCommand.ATTR, "test")).uniqueResult()
                .get();

        assertTrue(test1.timestamp().compareTo(test2.timestamp()) < 0);
        assertTrue(repository.getTimestamp().compareTo(test1.timestamp()) > 0);
        assertTrue(repository.getTimestamp().compareTo(test2.timestamp()) > 0);
    }

    @ToString
    public static class TimestampingEventCommand extends StandardCommand<Void, String> {

        private final HybridTimestamp eventTimestamp;

        @LayoutConstructor
        public TimestampingEventCommand(HybridTimestamp timestamp) {
            super(timestamp);
            eventTimestamp = null;
        }

        @Builder
        public TimestampingEventCommand(HybridTimestamp timestamp, HybridTimestamp eventTimestamp) {
            super(timestamp);
            this.eventTimestamp = eventTimestamp;
        }

        @Override
        public EventStream<Void> events() {
            return EventStream
                    .of(new Event[] { TestEvent.builder().string("test").timestamp(eventTimestamp).build(),
                            TestEvent.builder().string("followup").build() });
        }

        @Override
        public String result() {
            return "hello, world";
        }
    }

    @Test
    @SneakyThrows
    public void eventTimestamping() {
        IndexedCollection<EntityHandle<TestEvent>> coll = indexEngine.getIndexedCollection(TestEvent.class);
        coll.clear();

        HybridTimestamp timestamp = new HybridTimestamp(timeProvider);
        timestamp.update();
        TimestampingEventCommand command = TimestampingEventCommand.builder().eventTimestamp(timestamp).build();

        repository.publish(command).get();

        TestEvent test = coll.retrieve(equal(TestEvent.ATTR, "test")).uniqueResult().get();

        assertTrue(test.timestamp().compareTo(command.timestamp()) < 0);
        assertTrue(repository.getTimestamp().compareTo(test.timestamp()) > 0);

        TestEvent followup = coll.retrieve(equal(TestEvent.ATTR, "followup")).uniqueResult().get();
        assertTrue(test.timestamp().compareTo(followup.timestamp()) < 0);

        assertTrue(repository.getTimestamp().compareTo(followup.timestamp()) > 0);
    }

    @Test
    @SneakyThrows
    public void indexing() {
        IndexedCollection<EntityHandle<TestEvent>> coll = indexEngine.getIndexedCollection(TestEvent.class);
        coll.clear();
        IndexedCollection<EntityHandle<RepositoryTestCommand>> coll1 = indexEngine
                .getIndexedCollection(RepositoryTestCommand.class);
        coll1.clear();

        repository.publish(RepositoryTestCommand.builder().build()).get();
        assertTrue(coll.retrieve(equal(TestEvent.ATTR, "test")).isNotEmpty());
        assertTrue(coll.retrieve(equal(TestEvent.ATTR_DEP, "test")).isNotEmpty());
        assertTrue(coll.retrieve(contains(TestEvent.ATTR, "es")).isNotEmpty());
        assertEquals(coll.retrieve(equal(TestEvent.ATTR, "test")).uniqueResult().get().string(), "test");
        assertEquals(coll.retrieve(equal(TestEvent.ATTRS, "test")).uniqueResult().get().string(), "test");
        assertEquals(coll.retrieve(equal(TestEventExtraIndices.ATTRL, Arrays.asList("test"))).uniqueResult().get()
                .string(), "test");
        assertTrue(coll.retrieve(equal(TestEventExtraIndices.ATTRL, Arrays.asList("test1"))).isEmpty());

        assertTrue(coll1.retrieve(equal(RepositoryTestCommand.ATTR, "test")).isNotEmpty());
        assertTrue(coll1.retrieve(contains(RepositoryTestCommand.ATTR, "es")).isNotEmpty());

    }

    @Accessors(fluent = true)
    @ToString
    public static class TestEventWithQueryOptions extends StandardEvent {
        @Getter
        private final String string;

        @Index({ EQ, SC })
        public static SimpleIndex<TestEventWithQueryOptions, String> ATTR = SimpleIndex
                .withQueryOptions((object, queryOptions) -> {
                    if (queryOptions.get(TestEventWithQueryOptions.class) != null) {
                        return "QueryOptions";
                    } else {
                        return object.string();
                    }
                });

        @Builder
        public TestEventWithQueryOptions(String string) {
            this.string = string;
        }
    }

    @ToString
    public static class TestEventWithQueryOptionsCommand extends StandardCommand<Void, String> {

        @Getter
        private final String value;

        @Builder
        public TestEventWithQueryOptionsCommand(HybridTimestamp timestamp, String value) {
            super(timestamp);
            this.value = value == null ? "test" : value;
        }

        @Override
        public EventStream<Void> events() {
            return EventStream.of(TestEventWithQueryOptions.builder().string(value).build());
        }

        @Override
        public String result() {
            return "hello, world";
        }
    }

    @Test
    @SneakyThrows
    public void queryOptionsInIndices() {
        IndexedCollection<EntityHandle<TestEventWithQueryOptions>> coll = indexEngine
                .getIndexedCollection(TestEventWithQueryOptions.class);
        coll.clear();

        repository.publish(TestEventWithQueryOptionsCommand.builder().build()).get();
        assertTrue(coll.retrieve(equal(TestEventWithQueryOptions.ATTR, "test")).isNotEmpty());

    }

    @Test
    @SneakyThrows
    public void publishingNewCommand() {
        assertFalse(repository.getCommands().contains(BogusCommand.class));
        repository.addCommandSetProvider(
                new PackageCommandSetProvider(new Package[] { BogusCommand.class.getPackage() }));
        repository
                .addEventSetProvider(new PackageEventSetProvider(new Package[] { BogusEvent.class.getPackage() }));
        assertTrue(repository.getCommands().contains(BogusCommand.class));
        assertTrue(repository.getEvents().contains(BogusEvent.class));
        Assert.assertEquals("bogus", repository.publish(BogusCommand.builder().build()).get());
        // testing its indexing
        IndexedCollection<EntityHandle<BogusEvent>> coll = indexEngine.getIndexedCollection(BogusEvent.class);
        assertTrue(coll.retrieve(equal(BogusEvent.ATTR, "bogus")).isNotEmpty());
        assertTrue(coll.retrieve(contains(BogusEvent.ATTR, "us")).isNotEmpty());
        Assert.assertEquals(coll.retrieve(equal(BogusEvent.ATTR, "bogus")).uniqueResult().get().string(), "bogus");
    }

    public static class LockCommand extends StandardCommand<Void, Void> {
        @Builder
        public LockCommand(HybridTimestamp timestamp) {
            super(timestamp);
        }

        @Override
        public EventStream<Void> events(Repository repository, LockProvider lockProvider) {
            lockProvider.lock("LOCK");
            return EventStream.empty();
        }
    }

    @Test(timeOut = 1000)
    @SneakyThrows
    public void lockTracking() {
        repository.publish(LockCommand.builder().build()).get();
        Lock lock = lockProvider.lock("LOCK");
        assertTrue(lock.isLocked());
        lock.unlock();
    }

    public static class ExceptionalLockCommand extends StandardCommand<Void, Void> {
        @Builder
        public ExceptionalLockCommand(HybridTimestamp timestamp) {
            super(timestamp);
        }

        @Override
        public EventStream<Void> events(Repository repository, LockProvider lockProvider) {
            lockProvider.lock("LOCK");
            throw new IllegalStateException();
        }
    }

    @Test(timeOut = 1000)
    @SneakyThrows
    public void exceptionalLockTracking() {
        repository.publish(ExceptionalLockCommand.builder().build()).exceptionally(throwable -> null).get();
        Lock lock = lockProvider.lock("LOCK");
        assertTrue(lock.isLocked());
        lock.unlock();
    }

    public static class ExceptionalCommand extends StandardCommand<Void, Object> {
        @Getter
        private boolean resultCalled = false;

        @Builder
        public ExceptionalCommand(HybridTimestamp timestamp) {
            super(timestamp);
        }

        @Override
        public EventStream<Void> events() {
            throw new IllegalStateException();
        }

        @Override
        public Object result() {
            resultCalled = true;
            return null;
        }
    }

    @Test
    @SneakyThrows
    public void exceptionalCommand() {
        ExceptionalCommand command = ExceptionalCommand.builder().build();
        Object o = repository.publish(command).exceptionally(throwable -> throwable).get();
        assertTrue(o instanceof IllegalStateException);
        Optional<Entity> commandLookup = journal.get(command.uuid());
        assertTrue(commandLookup.isPresent());
        assertTrue(command.hasTerminatedExceptionally(repository));
        // result() was not called
        assertFalse(command.isResultCalled());
        try (ResultSet<EntityHandle<CommandTerminatedExceptionally>> resultSet = repository.query(
                CommandTerminatedExceptionally.class,
                and(all(CommandTerminatedExceptionally.class),
                        existsIn(indexEngine.getIndexedCollection(EventCausalityEstablished.class),
                                CommandTerminatedExceptionally.ID, EventCausalityEstablished.EVENT)))) {
            assertEquals(resultSet.size(), 1);
        }
        assertEquals(command.exceptionalTerminationCause(repository).getClassName(),
                IllegalStateException.class.getName());
        try (ResultSet<EntityHandle<JavaExceptionOccurred>> resultSet = repository.query(
                JavaExceptionOccurred.class,
                and(all(JavaExceptionOccurred.class),
                        existsIn(indexEngine.getIndexedCollection(EventCausalityEstablished.class),
                                JavaExceptionOccurred.ID, EventCausalityEstablished.EVENT)))) {
            assertEquals(resultSet.size(), 1);
            EntityHandle<JavaExceptionOccurred> result = resultSet.uniqueResult();
            assertEquals(result.get().getClassName(), IllegalStateException.class.getName());
        }

    }

    @ToString
    public static class StreamExceptionCommand extends StandardCommand<Void, Void> {

        private final UUID eventUUID;

        @Getter
        private boolean resultCalled = false;

        @LayoutConstructor
        public StreamExceptionCommand(HybridTimestamp timestamp) {
            super(timestamp);
            eventUUID = null;
        }

        @Builder
        public StreamExceptionCommand(HybridTimestamp timestamp, UUID eventUUID) {
            super(timestamp);
            this.eventUUID = eventUUID == null ? UUID.randomUUID() : eventUUID;
        }

        @Override
        public EventStream<Void> events() {
            return EventStream.of(Stream.concat(
                    Stream.of(TestEvent.builder().string("test").build().uuid(eventUUID)), Stream.generate(() -> {
                        throw new IllegalStateException();
                    })));
        }

        @Override
        public Void result() {
            resultCalled = true;
            return null;
        }
    }

    @Test
    @SneakyThrows
    public void streamExceptionIndexing() {
        IndexedCollection<EntityHandle<TestEvent>> coll = indexEngine.getIndexedCollection(TestEvent.class);
        coll.clear();
        UUID eventUUID = UUID.randomUUID();
        StreamExceptionCommand command = StreamExceptionCommand.builder().eventUUID(eventUUID).build();
        CompletableFuture<Void> future = repository.publish(command);
        while (!future.isDone()) {
            Thread.sleep(10);
        } // to avoid throwing an exception
        assertTrue(future.isCompletedExceptionally());
        // result() was not called
        assertFalse(command.isResultCalled());
        try (ResultSet<EntityHandle<CommandTerminatedExceptionally>> resultSet = repository.query(
                CommandTerminatedExceptionally.class,
                and(all(CommandTerminatedExceptionally.class),
                        existsIn(indexEngine.getIndexedCollection(EventCausalityEstablished.class),
                                CommandTerminatedExceptionally.ID, EventCausalityEstablished.EVENT)))) {
            assertEquals(resultSet.size(), 1);
        }
        assertTrue(journal.get(command.uuid()).isPresent());
        assertFalse(journal.get(eventUUID).isPresent());
        assertTrue(repository.query(TestEvent.class, equal(TestEvent.ATTR, "test")).isEmpty());
    }

    public static class StatePassageCommand extends StandardCommand<String, String> {

        @Builder
        public StatePassageCommand(HybridTimestamp timestamp) {
            super(timestamp);
        }

        @Override
        public EventStream<String> events(Repository repository, LockProvider lockProvider) throws Exception {
            return EventStream.empty("hello");
        }

        @Override
        public String result(String state) {
            return state;
        }
    }

    @Test
    @SneakyThrows
    public void statePassage() {
        String s = repository.publish(StatePassageCommand.builder().build()).get();
        assertEquals(s, "hello");
    }

    public static class PassingLockProvider extends StandardCommand<Boolean, Boolean> {

        @Builder
        public PassingLockProvider(HybridTimestamp timestamp) {
            super(timestamp);
        }

        @Override
        public EventStream<Boolean> events(Repository repository, LockProvider lockProvider) throws Exception {
            return EventStream.empty(lockProvider != null);
        }

        @Override
        public Boolean result(Boolean passed) {
            return passed;
        }
    }

    @Test
    @SneakyThrows
    public void passingLock() {
        assertTrue(repository.publish(PassingLockProvider.builder().build()).get());
    }

    @Accessors(fluent = true)
    @ToString
    public static class TestOptionalEvent extends StandardEvent {
        @Getter
        private final Optional<String> optional;

        @Index({ EQ, UNIQUE })
        public final static SimpleIndex<TestOptionalEvent, UUID> ATTR = SimpleIndex.as(StandardEntity::uuid);

        @Builder
        public TestOptionalEvent(Optional<String> optional) {
            this.optional = optional;
        }
    }

    @Accessors(fluent = true)
    @ToString
    public static class TestOptionalCommand extends StandardCommand<Void, Void> {
        @Getter
        private final Optional<String> optional;

        @Builder
        public TestOptionalCommand(HybridTimestamp timestamp, Optional<String> optional) {
            super(timestamp);
            this.optional = optional;
        }

        @Override
        public EventStream<Void> events() {
            return EventStream.of(TestOptionalEvent.builder().optional(optional).build());
        }

        @Index({ EQ, UNIQUE })
        public final static SimpleIndex<TestOptionalCommand, UUID> ATTR = SimpleIndex.as(StandardEntity::uuid);

    }

    @Test
    @SneakyThrows
    public void commandGoesThroughLayoutSerialization() {
        TestOptionalCommand command = TestOptionalCommand.builder().build();
        repository.publish(command).get();

        TestOptionalCommand test = repository
                .query(TestOptionalCommand.class, equal(TestOptionalCommand.ATTR, command.uuid())).uniqueResult()
                .get();
        assertFalse(test.optional().isPresent());

    }

    @Test
    @SneakyThrows
    public void eventGoesThroughLayoutSerialization() {
        TestOptionalCommand command = TestOptionalCommand.builder().build();
        repository.publish(command).get();

        TestOptionalEvent testOptionalEvent = repository
                .query(TestOptionalEvent.class, all(TestOptionalEvent.class)).uniqueResult().get();
        assertFalse(testOptionalEvent.optional().isPresent());
    }

    @Test
    @SneakyThrows
    public void causalRelationship() {
        RepositoryTestCommand command = RepositoryTestCommand.builder().build();
        repository.publish(command).get();
        try (ResultSet<EntityHandle<EventCausalityEstablished>> resultSet = repository
                .query(EventCausalityEstablished.class, equal(EventCausalityEstablished.COMMAND, command.uuid()))) {
            assertEquals(resultSet.size(), 1);
        }
    }

    public static class LongRunningCommandEvents extends StandardCommand<Void, Void> {
        final CompletableFuture<Void> future = new CompletableFuture<>();

        @Override
        public EventStream<Void> events() throws Exception {
            future.get();
            return EventStream.empty();
        }
    }

    @Test
    @SneakyThrows
    public void longRunningCommandEventsShouldNotBlock() {
        LongRunningCommandEvents command = new LongRunningCommandEvents();
        repository.publish(command);
        repository.publish(RepositoryTestCommand.builder().value("1").build()).get(5_000, TimeUnit.MILLISECONDS);
        command.future.complete(null);
    }

    public static class LongRunningCommandEventStreamGeneration extends StandardCommand<Void, Void> {
        final CompletableFuture<Void> future = new CompletableFuture<>();

        @Override
        public EventStream<Void> events() throws Exception {
            return EventStream.of(Stream.of(1).map(i -> {
                try {
                    future.get();
                } catch (InterruptedException | ExecutionException e) {
                    e.printStackTrace();
                }
                return TestEvent.builder().build();
            }));
        }
    }

    @Test
    @SneakyThrows
    public void longRunningCommandEventStreamGenerationShouldNotBlock() {
        LongRunningCommandEventStreamGeneration command = new LongRunningCommandEventStreamGeneration();
        repository.publish(command);
        repository.publish(RepositoryTestCommand.builder().value("1").build()).get(5_000, TimeUnit.MILLISECONDS);
        command.future.complete(null);
    }

    @Test
    @SneakyThrows
    public void longRunningCommandEventStreamGenerationSameCommandShouldNotBlock() {
        LongRunningCommandEventStreamGeneration cmd1 = new LongRunningCommandEventStreamGeneration();
        repository.publish(cmd1);
        LongRunningCommandEventStreamGeneration cmd2 = new LongRunningCommandEventStreamGeneration();
        cmd2.future.complete(null);
        repository.publish(cmd2).get(5_000, TimeUnit.MILLISECONDS);
        cmd1.future.complete(null);
    }

    @Accessors(fluent = true)
    class StickyTimeProvider extends AbstractService implements PhysicalTimeProvider {

        private final PhysicalTimeProvider timeProvider;

        StickyTimeProvider(PhysicalTimeProvider timeProvider) {
            this.timeProvider = timeProvider;
        }

        @Setter
        private boolean sticky = false;
        private Long lastValue = null;

        @Override
        public long getPhysicalTime() {
            if (sticky && lastValue == null) {
                lastValue = timeProvider.getPhysicalTime();
            }
            if (sticky) {
                return lastValue;
            }
            return timeProvider.getPhysicalTime();
        }

        @Override
        protected void doStart() {
            timeProvider.startAsync().awaitRunning();
            notifyStarted();
        }

        @Override
        protected void doStop() {
            timeProvider.stopAsync().awaitTerminated();
            notifyStopped();
        }
    }

    @Test
    @SneakyThrows
    public void repositoryTimestamp() {
        StandardRepository newRepository = new StandardRepository();
        StickyTimeProvider newTimeProvider = new StickyTimeProvider(new NTPServerTimeProvider());
        newTimeProvider.startAsync().awaitRunning();
        newTimeProvider.sticky(true);

        newRepository.setPhysicalTimeProvider(newTimeProvider);
        HybridTimestamp hybridTimestamp = new HybridTimestamp(newTimeProvider);
        hybridTimestamp.update();
        Journal newJournal = createJournal();
        newJournal.setRepository(newRepository);
        newJournal.getProperties().setRepositoryTimestamp(hybridTimestamp);
        newRepository.setJournal(newJournal);
        IndexEngine newIndexEngine = createIndexEngine();
        newRepository.setIndexEngine(newIndexEngine);
        LocalLockProvider newLockProvider = new LocalLockProvider();
        newRepository.setLockProvider(newLockProvider);
        newRepository.startAsync().awaitRunning();

        // Because the time is not advancing, the logical counter is
        // this proves that the saved timestamp was used
        assertEquals(newRepository.getTimestamp().getLogicalTime(), hybridTimestamp.getLogicalTime());
        assertTrue(newRepository.getTimestamp().getLogicalCounter() > hybridTimestamp.getLogicalCounter());

        // This tests that the repository saves the timestamp back
        assertEquals(newJournal.getProperties().getRepositoryTimestamp().get(), newRepository.getTimestamp());

        newRepository.stopAsync().awaitTerminated();
        newTimeProvider.stopAsync().awaitTerminated();
    }
}