/*
* Copyright (c) 2008-2017 the original author or authors.
*
* 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.cometd.oort;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.cometd.client.BayeuxClient;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.junit.Assert;
import org.junit.Test;
public class OortStringMapTest extends AbstractOortObjectTest {
public OortStringMapTest(String serverTransport) {
super(serverTransport);
}
@Test
public void testEntryPut() throws Exception {
String name = "test";
OortObject.Factory<ConcurrentMap<String, String>> factory = OortObjectFactories.forConcurrentMap();
OortStringMap<String> oortMap1 = new OortStringMap<>(oort1, name, factory);
OortStringMap<String> oortMap2 = new OortStringMap<>(oort2, name, factory);
startOortObjects(oortMap1, oortMap2);
final CountDownLatch setLatch = new CountDownLatch(2);
OortObject.Listener<ConcurrentMap<String, String>> objectListener = new OortObject.Listener.Adapter<ConcurrentMap<String, String>>() {
@Override
public void onUpdated(OortObject.Info<ConcurrentMap<String, String>> oldInfo, OortObject.Info<ConcurrentMap<String, String>> newInfo) {
setLatch.countDown();
}
};
oortMap1.addListener(objectListener);
oortMap2.addListener(objectListener);
final String key = "key";
final String value1 = "value1";
ConcurrentMap<String, String> map = factory.newObject(null);
map.put(key, value1);
oortMap1.setAndShare(map, null);
Assert.assertTrue(setLatch.await(5, TimeUnit.SECONDS));
final String value2 = "value2";
final CountDownLatch putLatch = new CountDownLatch(1);
oortMap2.addEntryListener(new OortMap.EntryListener.Adapter<String, String>() {
@Override
public void onPut(OortObject.Info<ConcurrentMap<String, String>> info, OortMap.Entry<String, String> entry) {
Assert.assertEquals(key, entry.getKey());
Assert.assertEquals(value1, entry.getOldValue());
Assert.assertEquals(value2, entry.getNewValue());
putLatch.countDown();
}
});
OortObject.Result.Deferred<String> result = new OortObject.Result.Deferred<>();
oortMap1.putAndShare(key, value2, result);
Assert.assertEquals(value1, result.get(5, TimeUnit.SECONDS));
Assert.assertTrue(putLatch.await(5, TimeUnit.SECONDS));
}
@Test
public void testEntryRemoved() throws Exception {
String name = "test";
OortObject.Factory<ConcurrentMap<String, String>> factory = OortObjectFactories.forConcurrentMap();
OortStringMap<String> oortMap1 = new OortStringMap<>(oort1, name, factory);
OortStringMap<String> oortMap2 = new OortStringMap<>(oort2, name, factory);
startOortObjects(oortMap1, oortMap2);
final CountDownLatch setLatch = new CountDownLatch(2);
OortObject.Listener<ConcurrentMap<String, String>> objectListener = new OortObject.Listener.Adapter<ConcurrentMap<String, String>>() {
@Override
public void onUpdated(OortObject.Info<ConcurrentMap<String, String>> oldInfo, OortObject.Info<ConcurrentMap<String, String>> newInfo) {
setLatch.countDown();
}
};
oortMap1.addListener(objectListener);
oortMap2.addListener(objectListener);
final String key = "key";
final String value1 = "value1";
ConcurrentMap<String, String> map = factory.newObject(null);
map.put(key, value1);
oortMap1.setAndShare(map, null);
Assert.assertTrue(setLatch.await(5, TimeUnit.SECONDS));
final CountDownLatch removeLatch = new CountDownLatch(1);
oortMap2.addEntryListener(new OortMap.EntryListener.Adapter<String, String>() {
@Override
public void onRemoved(OortObject.Info<ConcurrentMap<String, String>> info, OortMap.Entry<String, String> entry) {
Assert.assertEquals(key, entry.getKey());
Assert.assertEquals(value1, entry.getOldValue());
Assert.assertNull(entry.getNewValue());
removeLatch.countDown();
}
});
OortObject.Result.Deferred<String> result = new OortObject.Result.Deferred<>();
oortMap1.removeAndShare(key, result);
Assert.assertEquals(value1, result.get(5, TimeUnit.SECONDS));
Assert.assertTrue(removeLatch.await(5, TimeUnit.SECONDS));
}
@Test
public void testDeltaListener() throws Exception {
String name = "test";
OortObject.Factory<ConcurrentMap<String, String>> factory = OortObjectFactories.forConcurrentMap();
OortStringMap<String> oortMap1 = new OortStringMap<>(oort1, name, factory);
OortStringMap<String> oortMap2 = new OortStringMap<>(oort2, name, factory);
startOortObjects(oortMap1, oortMap2);
final CountDownLatch setLatch1 = new CountDownLatch(2);
OortObject.Listener<ConcurrentMap<String, String>> objectListener = new OortObject.Listener.Adapter<ConcurrentMap<String, String>>() {
@Override
public void onUpdated(OortObject.Info<ConcurrentMap<String, String>> oldInfo, OortObject.Info<ConcurrentMap<String, String>> newInfo) {
setLatch1.countDown();
}
};
oortMap1.addListener(objectListener);
oortMap2.addListener(objectListener);
ConcurrentMap<String, String> oldMap = factory.newObject(null);
String key1 = "key1";
String valueA1 = "valueA1";
oldMap.put(key1, valueA1);
String key2 = "key2";
String valueB = "valueB";
oldMap.put(key2, valueB);
oortMap1.setAndShare(oldMap, null);
Assert.assertTrue(setLatch1.await(5, TimeUnit.SECONDS));
ConcurrentMap<String, String> newMap = factory.newObject(null);
String valueA2 = "valueA2";
newMap.put(key1, valueA2);
String key3 = "key3";
String valueC = "valueC";
newMap.put(key3, valueC);
final List<OortMap.Entry<String, String>> puts = new ArrayList<>();
final List<OortMap.Entry<String, String>> removes = new ArrayList<>();
final AtomicReference<CountDownLatch> setLatch2 = new AtomicReference<>(new CountDownLatch(6));
oortMap1.addListener(new OortMap.DeltaListener<>(oortMap1));
oortMap2.addListener(new OortMap.DeltaListener<>(oortMap2));
OortMap.EntryListener<String, String> entryListener = new OortMap.EntryListener<String, String>() {
@Override
public void onPut(OortObject.Info<ConcurrentMap<String, String>> info, OortMap.Entry<String, String> entry) {
puts.add(entry);
setLatch2.get().countDown();
}
@Override
public void onRemoved(OortObject.Info<ConcurrentMap<String, String>> info, OortMap.Entry<String, String> entry) {
removes.add(entry);
setLatch2.get().countDown();
}
};
oortMap1.addEntryListener(entryListener);
oortMap2.addEntryListener(entryListener);
oortMap1.setAndShare(newMap, null);
Assert.assertTrue(setLatch2.get().await(5, TimeUnit.SECONDS));
Assert.assertEquals(4, puts.size());
Assert.assertEquals(2, removes.size());
puts.clear();
removes.clear();
setLatch2.set(new CountDownLatch(2));
// Stop Oort1 so that OortMap2 gets the notification
stopOort(oort1);
Assert.assertTrue(setLatch2.get().await(5, TimeUnit.SECONDS));
Assert.assertEquals(2, removes.size());
}
@Test
public void testGetFind() throws Exception {
String name = "test";
OortObject.Factory<ConcurrentMap<String, String>> factory = OortObjectFactories.forConcurrentMap();
OortStringMap<String> oortMap1 = new OortStringMap<>(oort1, name, factory);
OortStringMap<String> oortMap2 = new OortStringMap<>(oort2, name, factory);
startOortObjects(oortMap1, oortMap2);
final CountDownLatch putLatch = new CountDownLatch(4);
OortMap.EntryListener.Adapter<String, String> putListener = new OortMap.EntryListener.Adapter<String, String>() {
@Override
public void onPut(OortObject.Info<ConcurrentMap<String, String>> info, OortMap.Entry<String, String> entry) {
putLatch.countDown();
}
};
oortMap1.addEntryListener(putListener);
oortMap2.addEntryListener(putListener);
final String keyA = "keyA";
final String valueA = "valueA";
oortMap1.putAndShare(keyA, valueA, null);
final String keyB = "keyB";
final String valueB = "valueB";
oortMap2.putAndShare(keyB, valueB, null);
Assert.assertTrue(putLatch.await(5, TimeUnit.SECONDS));
Assert.assertEquals(valueA, oortMap1.get(keyA));
Assert.assertNull(oortMap1.get(keyB));
Assert.assertEquals(valueB, oortMap2.get(keyB));
Assert.assertNull(oortMap2.get(keyA));
Assert.assertEquals(valueA, oortMap1.find(keyA));
Assert.assertEquals(valueA, oortMap2.find(keyA));
Assert.assertEquals(valueB, oortMap1.find(keyB));
Assert.assertEquals(valueB, oortMap2.find(keyB));
OortObject.Info<ConcurrentMap<String, String>> info1A = oortMap1.findInfo(keyA);
Assert.assertNotNull(info1A);
Assert.assertTrue(info1A.isLocal());
Assert.assertEquals(oort1.getURL(), info1A.getOortURL());
OortObject.Info<ConcurrentMap<String, String>> info1B = oortMap1.findInfo(keyB);
Assert.assertNotNull(info1B);
Assert.assertFalse(info1B.isLocal());
Assert.assertEquals(oort2.getURL(), info1B.getOortURL());
oortMap2.removeEntryListener(putListener);
oortMap1.removeEntryListener(putListener);
}
@Test
public void testConcurrent() throws Exception {
String name = "concurrent";
OortObject.Factory<ConcurrentMap<String, String>> factory = OortObjectFactories.forConcurrentMap();
final OortStringMap<String> oortMap1 = new OortStringMap<>(oort1, name, factory);
OortStringMap<String> oortMap2 = new OortStringMap<>(oort2, name, factory);
startOortObjects(oortMap1, oortMap2);
int threads = 64;
final int iterations = 32;
final CyclicBarrier barrier = new CyclicBarrier(threads + 1);
final CountDownLatch latch1 = new CountDownLatch(threads);
final CountDownLatch latch2 = new CountDownLatch(threads * iterations);
oortMap2.addListener(new OortMap.DeltaListener<>(oortMap2));
oortMap2.addEntryListener(new OortMap.EntryListener.Adapter<String, String>() {
@Override
public void onPut(OortObject.Info<ConcurrentMap<String, String>> info, OortMap.Entry<String, String> entry) {
latch2.countDown();
}
});
for (int i = 0; i < threads; ++i) {
final int index = i;
new Thread(new Runnable() {
@Override
public void run() {
try {
barrier.await();
for (int j = 0; j < iterations; ++j) {
String key = String.valueOf(index * iterations + j);
oortMap1.putAndShare(key, key, null);
}
} catch (Throwable x) {
x.printStackTrace();
} finally {
latch1.countDown();
}
}
}).start();
}
// Wait for all threads to be ready.
barrier.await();
// Wait for all threads to finish.
Assert.assertTrue(latch1.await(15, TimeUnit.SECONDS));
Assert.assertTrue(latch2.await(15, TimeUnit.SECONDS));
ConcurrentMap<String, String> map1 = oortMap1.merge(OortObjectMergers.<String, String>concurrentMapUnion());
ConcurrentMap<String, String> map2 = oortMap2.merge(OortObjectMergers.<String, String>concurrentMapUnion());
Assert.assertEquals(map1, map2);
}
@Test
public void testEntryBeforeMap() throws Exception {
String name = "entry_before_map";
OortObject.Factory<ConcurrentMap<String, String>> factory = OortObjectFactories.forConcurrentMap();
OortStringMap<String> oortMap1 = new OortStringMap<>(oort1, name, factory);
OortStringMap<String> oortMap2 = new OortStringMap<String>(oort2, name, factory) {
private int peerMessages;
@Override
protected void onObject(Map<String, Object> data) {
super.onObject(data);
String oortURL = (String)data.get(Info.OORT_URL_FIELD);
if (!getOort().getURL().equals(oortURL)) {
++peerMessages;
// Simulate that the first message from the
// other peer for the whole map gets lost.
if (peerMessages == 1) {
removeInfo(oortURL);
}
}
}
};
startOortObjects(oortMap1, oortMap2);
Assert.assertNull(oortMap2.getInfo(oortMap1.getOort().getURL()));
final CountDownLatch objectLatch = new CountDownLatch(1);
oortMap2.addListener(new OortObject.Listener.Adapter<ConcurrentMap<String, String>>() {
@Override
public void onUpdated(OortObject.Info<ConcurrentMap<String, String>> oldInfo, OortObject.Info<ConcurrentMap<String, String>> newInfo) {
objectLatch.countDown();
}
});
// Put an entry in oortMap1, the update should arrive to oortMap2
// which does not have the Info object, so it should pull it.
oortMap1.putAndShare("key1", "value1", null);
Assert.assertTrue(objectLatch.await(5, TimeUnit.SECONDS));
ConcurrentMap<String, String> map1 = oortMap1.merge(OortObjectMergers.<String, String>concurrentMapUnion());
ConcurrentMap<String, String> map2 = oortMap2.merge(OortObjectMergers.<String, String>concurrentMapUnion());
Assert.assertEquals(map1, map2);
final AtomicReference<CountDownLatch> putLatch = new AtomicReference<>(new CountDownLatch(1));
oortMap2.addEntryListener(new OortMap.EntryListener.Adapter<String, String>() {
@Override
public void onPut(OortObject.Info<ConcurrentMap<String, String>> info, OortMap.Entry<String, String> entry) {
putLatch.get().countDown();
}
});
// Put another entry in oortMap1, the objects should sync.
oortMap1.putAndShare("key2", "value2", null);
Assert.assertTrue(putLatch.get().await(5, TimeUnit.SECONDS));
map1 = oortMap1.merge(OortObjectMergers.<String, String>concurrentMapUnion());
map2 = oortMap2.merge(OortObjectMergers.<String, String>concurrentMapUnion());
Assert.assertEquals(map1, map2);
// And again.
putLatch.set(new CountDownLatch(1));
oortMap1.putAndShare("key3", "value3", null);
Assert.assertTrue(putLatch.get().await(5, TimeUnit.SECONDS));
map1 = oortMap1.merge(OortObjectMergers.<String, String>concurrentMapUnion());
map2 = oortMap2.merge(OortObjectMergers.<String, String>concurrentMapUnion());
Assert.assertEquals(map1, map2);
}
@Test
public void testLostEntry() throws Exception {
String name = "lost_entry";
OortObject.Factory<ConcurrentMap<String, String>> factory = OortObjectFactories.forConcurrentMap();
final OortStringMap<String> oortMap1 = new OortStringMap<>(oort1, name, factory);
OortStringMap<String> oortMap2 = new OortStringMap<String>(oort2, name, factory) {
private int peerMessages;
@Override
protected void onObject(Map<String, Object> data) {
String oortURL = (String)data.get(Info.OORT_URL_FIELD);
if (!getOort().getURL().equals(oortURL)) {
if ("oort.map.entry".equals(data.get(Info.TYPE_FIELD))) {
++peerMessages;
// Simulate that the second entry update gets lost.
if (peerMessages == 2) {
return;
}
}
}
super.onObject(data);
}
};
startOortObjects(oortMap1, oortMap2);
final String key1 = "key1";
OortObject.Result.Deferred<String> result1 = new OortObject.Result.Deferred<>();
oortMap1.putAndShare(key1, "value1", result1);
result1.get(5, TimeUnit.SECONDS);
oortMap1.removeAndShare(key1, null);
// Wait for the update to be lost.
Thread.sleep(1000);
// Verify that the objects are out-of-sync.
Assert.assertNull(oortMap1.get(key1));
Assert.assertNotNull(oortMap2.find(key1));
// Update again, the maps should sync.
final String key2 = "key2";
final CountDownLatch latch = new CountDownLatch(2);
oortMap2.addListener(new OortMap.DeltaListener<>(oortMap2));
oortMap2.addEntryListener(new OortMap.EntryListener<String, String>() {
@Override
public void onPut(OortObject.Info<ConcurrentMap<String, String>> info, OortMap.Entry<String, String> entry) {
if (entry.getKey().equals(key2)) {
latch.countDown();
}
}
@Override
public void onRemoved(OortObject.Info<ConcurrentMap<String, String>> info, OortMap.Entry<String, String> entry) {
if (entry.getKey().equals(key1)) {
latch.countDown();
}
}
});
oortMap1.putAndShare(key2, "value2", null);
Assert.assertTrue(latch.await(5, TimeUnit.SECONDS));
// Make sure that the maps are in sync.
ConcurrentMap<String, String> map1 = oortMap1.merge(OortObjectMergers.<String, String>concurrentMapUnion());
ConcurrentMap<String, String> map2 = oortMap2.merge(OortObjectMergers.<String, String>concurrentMapUnion());
Assert.assertEquals(map1, map2);
}
@Test
public void testNodeSyncWithLargeMap() throws Exception {
// Reconfigure the Oorts.
stop();
Map<String, String> options = new HashMap<>();
options.put("ws.maxMessageSize", String.valueOf(64 * 1024 * 1024));
prepare(options);
String name = "large_sync";
OortObject.Factory<ConcurrentMap<String, String>> factory = OortObjectFactories.forConcurrentMap();
OortStringMap<String> oortMap1 = new OortStringMap<>(oort1, name, factory);
OortStringMap<String> oortMap2 = new OortStringMap<>(oort2, name, factory);
startOortObjects(oortMap1, oortMap2);
// Disconnect one node.
CountDownLatch leftLatch = new CountDownLatch(2);
CometLeftListener leftListener = new CometLeftListener(leftLatch);
oort1.addCometListener(leftListener);
oort2.addCometListener(leftListener);
OortComet comet12 = oort1.findComet(oort2.getURL());
OortComet comet21 = oort2.findComet(oort1.getURL());
comet21.disconnect();
Assert.assertTrue(leftLatch.await(5, TimeUnit.SECONDS));
Assert.assertTrue(comet12.waitFor(5000, BayeuxClient.State.DISCONNECTED));
Assert.assertTrue(comet21.waitFor(5000, BayeuxClient.State.DISCONNECTED));
// Update node1 with a large number of map entries.
final int size = 64 * 1024;
for (int i = 0; i < size; ++i) {
oortMap1.putAndShare(String.valueOf(i), i + "_abcdefghijklmnopqrstuvwxyz0123456789", null);
}
int size1 = oortMap1.merge(OortObjectMergers.<String, String>concurrentMapUnion()).size();
Assert.assertEquals(size, size1);
int size2 = oortMap2.merge(OortObjectMergers.<String, String>concurrentMapUnion()).size();
Assert.assertEquals(0, size2);
final CountDownLatch syncLatch = new CountDownLatch(1);
oortMap2.addListener(new OortObject.Listener.Adapter<ConcurrentMap<String, String>>() {
@Override
public void onUpdated(OortObject.Info<ConcurrentMap<String, String>> oldInfo, OortObject.Info<ConcurrentMap<String, String>> newInfo) {
if (newInfo.getOortURL().equals(oort1.getURL())) {
if (newInfo.getObject().size() == size) {
syncLatch.countDown();
}
}
}
});
// Reconnect the node.
CountDownLatch joinedLatch = new CountDownLatch(2);
CometJoinedListener joinedListener = new CometJoinedListener(joinedLatch);
oort1.addCometListener(joinedListener);
oort2.addCometListener(joinedListener);
OortComet oortComet12 = oort1.observeComet(oort2.getURL());
Assert.assertTrue(oortComet12.waitFor(5000, BayeuxClient.State.CONNECTED));
Assert.assertTrue(joinedLatch.await(5, TimeUnit.SECONDS));
OortComet oortComet21 = oort2.findComet(oort1.getURL());
Assert.assertNotNull(oortComet21);
Assert.assertTrue(oortComet21.waitFor(5000, BayeuxClient.State.CONNECTED));
// Wait for the maps to sync.
Assert.assertTrue(syncLatch.await(15, TimeUnit.SECONDS));
// Verify that the maps are in sync.
size1 = oortMap1.getInfo(oort1.getURL()).getObject().size();
size2 = oortMap2.getInfo(oort1.getURL()).getObject().size();
Assert.assertEquals(size1, size2);
}
@Test
public void testNodeHalfDisconnected() throws Exception {
stop();
long timeout = 5000;
long maxInterval = 3000;
Map<String, String> options = new HashMap<>();
options.put("timeout", String.valueOf(timeout));
options.put("maxInterval", String.valueOf(maxInterval));
prepare(options);
String name = "half_disconnection";
OortObject.Factory<ConcurrentMap<String, String>> factory = OortObjectFactories.forConcurrentMap();
final OortStringMap<String> oortMap1 = new OortStringMap<>(oort1, name, factory);
OortStringMap<String> oortMap2 = new OortStringMap<>(oort2, name, factory);
startOortObjects(oortMap1, oortMap2);
final CountDownLatch putLatch = new CountDownLatch(4);
OortMap.EntryListener<String, String> putListener = new OortMap.EntryListener.Adapter<String, String>() {
@Override
public void onPut(OortObject.Info<ConcurrentMap<String, String>> info, OortMap.Entry<String, String> entry) {
putLatch.countDown();
}
};
oortMap1.addEntryListener(putListener);
oortMap2.addEntryListener(putListener);
oortMap1.putAndShare("key1", "value1", null);
oortMap2.putAndShare("key2", "value2", null);
Assert.assertTrue(putLatch.await(5, TimeUnit.SECONDS));
// Stop only one of the connectors, so that the communication is half-disconnected.
final CountDownLatch leftLatch = new CountDownLatch(2);
oortMap1.getOort().addCometListener(new CometLeftListener(leftLatch));
oortMap1.addListener(new OortObject.Listener.Adapter<ConcurrentMap<String, String>>() {
@Override
public void onRemoved(OortObject.Info<ConcurrentMap<String, String>> info) {
leftLatch.countDown();
}
});
Server server1 = (Server)oortMap1.getOort().getBayeuxServer().getOption(Server.class.getName());
ServerConnector connector1 = (ServerConnector)server1.getConnectors()[0];
int port1 = connector1.getLocalPort();
connector1.stop();
Assert.assertTrue(leftLatch.await(2 * (timeout + maxInterval), TimeUnit.SECONDS));
// Give some time before reconnecting.
Thread.sleep(1000);
final CountDownLatch joinLatch = new CountDownLatch(2);
oortMap1.getOort().addCometListener(new CometJoinedListener(joinLatch));
oortMap1.addListener(new OortObject.Listener.Adapter<ConcurrentMap<String, String>>() {
@Override
public void onUpdated(OortObject.Info<ConcurrentMap<String, String>> oldInfo, OortObject.Info<ConcurrentMap<String, String>> newInfo) {
if (oldInfo == null) {
joinLatch.countDown();
}
}
});
connector1.setPort(port1);
connector1.start();
Assert.assertTrue(joinLatch.await(15, TimeUnit.SECONDS));
String value2 = oortMap1.find("key2");
Assert.assertNotNull(value2);
}
}