/*
* ModeShape (http://www.modeshape.org)
*
* Licensed 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.modeshape.jcr.bus;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.modeshape.jcr.cache.NodeKey;
import org.modeshape.jcr.cache.change.Change;
import org.modeshape.jcr.cache.change.ChangeSet;
import org.modeshape.jcr.cache.change.ChangeSetListener;
import org.modeshape.jcr.value.BinaryKey;
import org.modeshape.jcr.value.basic.ModeShapeDateTime;
/**
* Base class for different {@link org.modeshape.jcr.bus.ChangeBus} implementations.
*
* @author Horia Chiorean
*/
public abstract class AbstractChangeBusTest {
protected static final String WORKSPACE1 = "ws1";
protected static final String WORKSPACE2 = "ws2";
protected ChangeBus changeBus;
@Before
public void beforeEach() throws Exception {
changeBus = createRepositoryChangeBus();
changeBus.start();
}
protected abstract ChangeBus createRepositoryChangeBus() throws Exception;
@After
public void afterEach() {
changeBus.shutdown();
}
@Test
public void shouldNotAllowTheSameListenerTwice() throws Exception {
TestListener listener1 = new TestListener();
assertTrue(changeBus.register(listener1));
assertFalse(changeBus.register(listener1));
TestListener listener2 = new TestListener();
assertTrue(changeBus.register(listener2));
assertFalse(changeBus.register(listener2));
assertFalse(changeBus.register(null));
}
@Test
public void shouldAllowListenerRemoval() throws Exception {
TestListener listener1 = new TestListener();
assertTrue(changeBus.register(listener1));
assertTrue(changeBus.unregister(listener1));
TestListener listener2 = new TestListener();
assertFalse(changeBus.unregister(listener2));
}
@Test
public void shouldNotifyAllRegisteredListenersKeepingEventOrder() throws Exception {
TestListener listener1 = new TestListener(4);
changeBus.register(listener1);
TestListener listener2 = new TestListener(4);
changeBus.register(listener2);
changeBus.notify(new TestChangeSet(WORKSPACE1));
changeBus.notify(new TestChangeSet(WORKSPACE1));
changeBus.notify(new TestChangeSet(WORKSPACE2));
changeBus.notify(new TestChangeSet(WORKSPACE2));
assertChangesDispatched(listener1);
assertChangesDispatched(listener2);
}
@Test
public void shouldOnlyDispatchEventsAfterListenerRegistration() throws Exception {
changeBus.notify(new TestChangeSet(WORKSPACE1));
TestListener listener1 = new TestListener(4);
changeBus.register(listener1);
changeBus.notify(new TestChangeSet(WORKSPACE1));
changeBus.notify(new TestChangeSet(WORKSPACE1));
TestListener listener2 = new TestListener(2);
changeBus.register(listener2);
changeBus.notify(new TestChangeSet(WORKSPACE2));
changeBus.notify(new TestChangeSet(WORKSPACE2));
assertChangesDispatched(listener1);
assertChangesDispatched(listener2);
}
@Test
public void shouldDispatchEventsIfWorkspaceNameIsMissing() throws Exception {
TestListener listener = new TestListener(2);
changeBus.register(listener);
changeBus.notify(new TestChangeSet(null));
changeBus.notify(new TestChangeSet(null));
assertChangesDispatched(listener);
}
@Test
public void shouldNotDispatchEventsAfterListenerRemoval() throws Exception {
TestListener listener1 = new TestListener(3);
changeBus.register(listener1);
TestListener listener2 = new TestListener(2);
changeBus.register(listener2);
changeBus.notify(new TestChangeSet(WORKSPACE1));
changeBus.notify(new TestChangeSet(WORKSPACE2));
Thread.sleep(50);
changeBus.unregister(listener2);
Thread.sleep(50);
changeBus.notify(new TestChangeSet(WORKSPACE2));
assertChangesDispatched(listener1);
assertChangesDispatched(listener2);
}
@Test
public void shouldNotDispatchEventsIfShutdown() throws Exception {
TestListener listener = new TestListener(1);
changeBus.register(listener);
changeBus.notify(new TestChangeSet(WORKSPACE1));
Thread.sleep(50);
changeBus.shutdown();
changeBus.notify(new TestChangeSet(WORKSPACE2));
assertChangesDispatched(listener);
}
@Test
@Ignore( "This is a perf test" )
public void shouldNotifyLotsOfConsumersAsync() throws Exception {
int eventsPerBatch = 300000;
int listenersPerBatch = 30;
int batches = 4;
List<AbstractChangeBusTest.TestListener> listeners = new ArrayList<>();
long start = System.nanoTime();
for (int i = 0; i < batches; i++) {
listeners.addAll(submitBatch(eventsPerBatch, listenersPerBatch, (batches - i) * eventsPerBatch));
Thread.sleep(50);
}
for (AbstractChangeBusTest.TestListener listener : listeners) {
listener.assertExpectedEventsCount();
}
System.out.println("Elapsed: " + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start) + " millis");
}
private List<AbstractChangeBusTest.TestListener> submitBatch( int eventCount,
int listenerCount,
int expectedEventsCount ) throws Exception {
List<AbstractChangeBusTest.TestListener> listeners = new ArrayList<>();
for (int i = 0; i < listenerCount; i++) {
AbstractChangeBusTest.TestListener listener = new AbstractChangeBusTest.TestListener(expectedEventsCount, 500);
listeners.add(listener);
changeBus.register(listener);
}
for (int i = 0; i < eventCount; i++) {
changeBus.notify(new AbstractChangeBusTest.TestChangeSet("ws"));
}
return listeners;
}
protected void assertChangesDispatched( TestListener listener ) {
listener.assertExpectedEventsCount();
List<TestChangeSet> receivedChanges = listener.getObservedChangeSet();
Map<String, List<Long>> changeSetsPerWs = new HashMap<>();
for (TestChangeSet changeSet : receivedChanges) {
String wsName = changeSet.getWorkspaceName();
List<Long> receivedTimes = changeSetsPerWs.computeIfAbsent(wsName, k -> new ArrayList<>());
for (Long receivedTime : receivedTimes) {
assertTrue(receivedTime <= changeSet.time());
}
receivedTimes.add(changeSet.time());
}
}
protected static class TestChangeSet implements ChangeSet {
private static final long serialVersionUID = 1L;
private final String workspaceName;
private final long time;
private final String uuid;
protected TestChangeSet( String workspaceName ) {
this.uuid = UUID.randomUUID().toString();
this.workspaceName = workspaceName;
this.time = System.currentTimeMillis();
}
@Override
public Set<NodeKey> changedNodes() {
return Collections.emptySet();
}
@Override
public Set<BinaryKey> unusedBinaries() {
return Collections.emptySet();
}
@Override
public Set<BinaryKey> usedBinaries() {
return Collections.emptySet();
}
@Override
public boolean hasBinaryChanges() {
return false;
}
@Override
public int size() {
return 0;
}
@Override
public boolean isEmpty() {
return true;
}
@Override
public String getUserId() {
return null;
}
@Override
public Map<String, String> getUserData() {
return Collections.emptyMap();
}
@Override
public org.modeshape.jcr.api.value.DateTime getTimestamp() {
return new ModeShapeDateTime(time);
}
public long time() {
return time;
}
@Override
public String getProcessKey() {
return null;
}
@Override
public String getSessionId() {
return null;
}
@Override
public String getRepositoryKey() {
return null;
}
@Override
public String getWorkspaceName() {
return workspaceName;
}
@Override
public Iterator<Change> iterator() {
return Collections.<Change>emptySet().iterator();
}
@Override
public String getJournalId() {
return null;
}
@Override
public String getUUID() {
return uuid;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
TestChangeSet changes = (TestChangeSet) o;
return Objects.equals(workspaceName, changes.workspaceName) &&
Objects.equals(uuid, changes.uuid);
}
@Override
public int hashCode() {
return Objects.hash(workspaceName, uuid);
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("changes[");
sb.append("workspaceName='").append(workspaceName).append('\'');
sb.append(", time=").append(time);
sb.append(", uuid='").append(uuid).append('\'');
sb.append(']');
return sb.toString();
}
}
protected static class TestListener implements ChangeSetListener {
private final ConcurrentLinkedDeque<TestChangeSet> receivedChangeSet;
private final long timeoutMillis;
private final int expectedNumberOfEvents;
private final CountDownLatch latch;
protected TestListener() {
this(0);
}
protected TestListener( int expectedNumberOfEvents ) {
this(expectedNumberOfEvents, 350);
}
protected TestListener( int expectedNumberOfEvents,
long timeoutMillis ) {
this.latch = new CountDownLatch(expectedNumberOfEvents);
this.receivedChangeSet = new ConcurrentLinkedDeque<>();
this.timeoutMillis = timeoutMillis;
this.expectedNumberOfEvents = expectedNumberOfEvents;
}
@Override
public void notify( ChangeSet changeSet ) {
if (!(changeSet instanceof TestChangeSet)) {
throw new IllegalArgumentException("Invalid type of change set received");
}
receivedChangeSet.add((TestChangeSet)changeSet);
latch.countDown();
}
protected void assertExpectedEventsCount() {
try {
assertTrue("Not enough events received", latch.await(timeoutMillis, TimeUnit.MILLISECONDS));
assertEquals("Incorrect number of events received", expectedNumberOfEvents, receivedChangeSet.size());
} catch (InterruptedException e) {
Thread.interrupted();
fail("Interrupted while waiting to verify event count");
}
}
protected void assertExpectedEvents(ChangeSet...changes) {
if (expectedNumberOfEvents != changes.length) {
fail("The expected number of events must match what you expect. Fix the test");
}
if (changes.length == 0) {
try {
Thread.sleep(timeoutMillis);
} catch (InterruptedException e) {
Thread.interrupted();
}
assertTrue(assertNoEvents());
return;
}
assertExpectedEventsCount();
List<TestChangeSet> actualChanges = getObservedChangeSet();
assertEquals("Incorrect number of changes received", changes.length, actualChanges.size());
receivedChangeSet.forEach(change -> assertTrue(actualChanges.contains(change)));
}
protected List<TestChangeSet> getObservedChangeSet() {
return receivedChangeSet.stream().collect(Collectors.toList());
}
protected boolean assertNoEvents() {
return receivedChangeSet.isEmpty();
}
protected void clear() {
receivedChangeSet.clear();
}
}
}