/* * Copyright 2014 Higher Frequency Trading * * http://www.higherfrequencytrading.com * * 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 net.openhft.chronicle.engine.map; import net.openhft.chronicle.core.Jvm; import net.openhft.chronicle.core.io.Closeable; import net.openhft.chronicle.engine.ThreadMonitoringTest; import net.openhft.chronicle.engine.api.map.MapEvent; import net.openhft.chronicle.engine.api.map.MapView; import net.openhft.chronicle.engine.api.pubsub.InvalidSubscriberException; import net.openhft.chronicle.engine.api.pubsub.Subscriber; import net.openhft.chronicle.engine.api.pubsub.TopicPublisher; import net.openhft.chronicle.engine.api.pubsub.TopicSubscriber; import net.openhft.chronicle.engine.api.tree.Asset; import net.openhft.chronicle.engine.api.tree.AssetTree; import net.openhft.chronicle.engine.server.ServerEndpoint; import net.openhft.chronicle.engine.tree.VanillaAssetTree; import net.openhft.chronicle.network.TCPRegistry; import net.openhft.chronicle.network.connection.TcpChannelHub; import net.openhft.chronicle.wire.WireType; import net.openhft.chronicle.wire.YamlLogging; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; import java.io.IOException; import java.util.Arrays; import java.util.Collection; import java.util.Map; import java.util.concurrent.*; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.SECONDS; import static net.openhft.chronicle.core.io.Closeable.closeQuietly; import static net.openhft.chronicle.engine.Utils.methodName; import static net.openhft.chronicle.engine.Utils.yamlLoggger; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; /** * test using the listener both remotely or locally via the engine * * @author Rob Austin. */ @RunWith(value = Parameterized.class) public class SubscriptionEventTest extends ThreadMonitoringTest { private static final String NAME = "test"; private static MapView<String, String> map; private final Boolean isRemote; private final WireType wireType; @NotNull @Rule public TestName name = new TestName(); private AssetTree assetTree = new VanillaAssetTree().forTesting(); private VanillaAssetTree serverAssetTree; private ServerEndpoint serverEndpoint; public SubscriptionEventTest(boolean isRemote, WireType wireType) { this.isRemote = isRemote; this.wireType = wireType; } @Parameters public static Collection<Object[]> data() { return Arrays.asList( new Object[]{false, null} // check connection is fine after a reconnect CE-187 , new Object[]{true, WireType.TEXT} , new Object[]{true, WireType.BINARY} ); } @Before public void before() throws IOException { serverAssetTree = new VanillaAssetTree().forTesting(); if (isRemote) { methodName(name.getMethodName()); @NotNull final String hostPort = "SubscriptionEventTest." + name.getMethodName() + ".host.port"; TCPRegistry.reset(); TCPRegistry.createServerSocketChannelFor(hostPort); serverEndpoint = new ServerEndpoint(hostPort, serverAssetTree); assetTree = new VanillaAssetTree().forRemoteAccess(hostPort, wireType); } else { assetTree = serverAssetTree; } map = assetTree.acquireMap(NAME, String.class, String.class); } public void preAfter() { Closeable.closeQuietly(assetTree); Closeable.closeQuietly(serverEndpoint); Closeable.closeQuietly(serverAssetTree); closeQuietly(map); TcpChannelHub.closeAllHubs(); TCPRegistry.reset(); } @Test public void testSubscribeToChangesToTheMap() throws IOException, InterruptedException { @NotNull final BlockingQueue<MapEvent> eventsQueue = new LinkedBlockingQueue<>(); yamlLoggger(() -> { try { YamlLogging.writeMessage("Sets up a subscription to listen to map events. And " + "subsequently puts and entry into the map, notice that the InsertedEvent is " + "received from the server"); Subscriber<MapEvent> add = eventsQueue::add; assetTree.registerSubscriber(NAME, MapEvent.class, add); YamlLogging.writeMessage("puts an entry into the map so that an event will be " + "triggered"); map.put("Hello", "World"); assertEquals(1, map.size()); MapEvent object = eventsQueue.poll(2, SECONDS); Assert.assertTrue("Object was " + object, object instanceof InsertedEvent); assetTree.unregisterSubscriber(NAME, add); } catch (Exception e) { throw Jvm.rethrow(e); } }); } /** * REMOTE ONLY TEST * * @throws IOException * @throws InterruptedException */ @Test public void testPushingEntriesToTheServerDirectly() throws IOException, InterruptedException { if (!isRemote) return; @NotNull final BlockingQueue<MapEvent> eventsQueue = new LinkedBlockingQueue<>(); yamlLoggger(() -> { try { Subscriber<MapEvent> add = eventsQueue::add; assetTree.registerSubscriber(NAME, MapEvent.class, add); @NotNull final Map serverMap = serverAssetTree.acquireMap(NAME, String.class, String.class); YamlLogging.writeMessage("puts an entry into the map so that an event will be " + "triggered"); serverMap.put("sever-key", "server-value"); map.put("hello", "world"); Object object = eventsQueue.take(); eventsQueue.take(); Assert.assertTrue(object instanceof InsertedEvent); // server map Assert.assertEquals(2, serverMap.size()); // client map Assert.assertEquals(2, map.size()); assetTree.unregisterSubscriber(NAME, add); } catch (Exception e) { throw Jvm.rethrow(e); } }); } @Test public void testTopicSubscribe() throws InvalidSubscriberException { YamlLogging.setAll(false); class TopicDetails<T, M> { private final M message; private final T topic; public TopicDetails(final T topic, final M message) { this.topic = topic; this.message = message; } @NotNull @Override public String toString() { return "TopicDetails{" + "message=" + message + ", topic=" + topic + '}'; } } @NotNull final BlockingQueue<TopicDetails> eventsQueue = new LinkedBlockingQueue<>(); yamlLoggger(() -> { try { // todo fix the text YamlLogging.writeMessage("Sets up a subscription to listen to map events. And " + "subsequently puts and entry into the map, notice that the InsertedEvent is " + "received from the server"); @NotNull TopicPublisher<String, String> topicPublisher = assetTree.acquireTopicPublisher(NAME, String.class, String.class); @NotNull TopicSubscriber<String, String> subscriber = (topic, message) -> eventsQueue.add(new TopicDetails<>(topic, message)); assetTree.registerTopicSubscriber(NAME, String.class, String.class, subscriber); YamlLogging.writeMessage("puts an entry into the map so that an event will be " + "triggered"); topicPublisher.publish("Hello", "World"); TopicDetails take = eventsQueue.take(); System.out.println(take); // todo fix the text for the unsubscribe. assetTree.unregisterTopicSubscriber(NAME, subscriber); } catch (Exception e) { throw Jvm.rethrow(e); } }); } @Test public void testUnsubscribeToMapEvents() throws IOException, InterruptedException { // not supported for remote if (isRemote) return; @NotNull final BlockingQueue<MapEvent> eventsQueue = new LinkedBlockingQueue<>(); yamlLoggger(() -> { try { YamlLogging.writeMessage("Sets up a subscription to listen to key events. "); Subscriber<MapEvent> subscriber = eventsQueue::add; assetTree.registerSubscriber(NAME, MapEvent.class, subscriber); YamlLogging.writeMessage("unsubscribes to changes to the map"); assetTree.unregisterSubscriber(NAME, subscriber); YamlLogging.writeMessage("puts the entry into the map, but since we have " + "unsubscribed no event should be send form the server to the client"); map.put("Hello", "World"); Object object = eventsQueue.poll(500, MILLISECONDS); Assert.assertNull(object); } catch (Exception e) { throw Jvm.rethrow(e); } }); } @Test public void testSubscribeToKeyEvents() throws IOException, InterruptedException, InvalidSubscriberException { @NotNull final BlockingQueue<String> eventsQueue = new LinkedBlockingQueue<>(); yamlLoggger(() -> { try { YamlLogging.writeMessage("Sets up a subscription to listen to key events. And " + "subsequently puts and entry into the map, notice that the InsertedEvent is " + "received from the server"); Subscriber<String> subscriber = eventsQueue::add; assetTree.registerSubscriber(NAME, String.class, subscriber); YamlLogging.writeMessage("puts an entry into the map so that an event will be " + "triggered"); map.put("Hello", "World"); Object object = eventsQueue.poll(500, MILLISECONDS); Assert.assertEquals("Hello", object); // todo fix the text for the unsubscribe. assetTree.unregisterSubscriber(NAME, subscriber); } catch (InterruptedException e) { throw Jvm.rethrow(e); } }); } @Test public void testSubscribeToValueBasedOnKeys() throws IOException, InterruptedException, InvalidSubscriberException { yamlLoggger(() -> { try { YamlLogging.writeMessage("Sets up a subscription to listen to key events. And " + "subsequently puts and entry into the map, notice that the InsertedEvent is " + "received from the server"); @NotNull ConcurrentMap<String, String> map = assetTree.acquireMap(NAME, String.class, String.class); map.put("Key-1", "Value-1"); map.put("Key-2", "Value-2"); assertEquals(2, map.size()); @NotNull ArrayBlockingQueue<String> q = new ArrayBlockingQueue<>(1); assetTree.registerSubscriber(NAME + "/Key-1?bootstrap=true", String.class, q::add); @Nullable Asset asset = assetTree.getAsset(NAME + "/Key-1"); assertTrue(asset.isSubAsset()); Object take = q.poll(5, TimeUnit.SECONDS); Assert.assertEquals(take, "Value-1"); map.put("Key-1", "Value-2"); Object take2 = q.poll(5, TimeUnit.SECONDS); Assert.assertEquals(take2, "Value-2"); assertEquals(2, map.size()); } catch (InterruptedException e) { throw Jvm.rethrow(e); } }); } //@Ignore("see https://higherfrequencytrading.atlassian.net/browse/CE-110") @Test public void testUnSubscribeToKeyEvents() throws IOException, InterruptedException { @NotNull final BlockingQueue<String> eventsQueue = new LinkedBlockingQueue<>(); yamlLoggger(() -> { try { YamlLogging.writeMessage("Sets up a subscription to listen to key events. Then " + "unsubscribes and puts and entry into the map, no subsequent event should" + " be received from the server"); Subscriber<String> add = eventsQueue::add; assetTree.registerSubscriber(NAME, String.class, add); Jvm.pause(500); // need to unsubscribe the same object which was subscribed to. assetTree.unregisterSubscriber(NAME, add); eventsQueue.clear(); YamlLogging.writeMessage("puts an entry into the map so that an event will be " + "triggered"); @NotNull String expected = "World"; map.getAndPut("Hello", expected); Object object = eventsQueue.poll(1000, MILLISECONDS); Assert.assertNull(object); } catch (InterruptedException e) { throw Jvm.rethrow(e); } }); } @Test public void testSubscribeToKeyEventsAndRemoveKey() throws IOException, InterruptedException { @NotNull final BlockingQueue<String> eventsQueue = new ArrayBlockingQueue<>(1024); yamlLoggger(() -> { try { YamlLogging.writeMessage("Sets up a subscription to listen to key events. And " + "subsequently puts and entry into the map followed by a remove, notice " + "that the " + "'reply: Hello' is received twice, one for the put and one for the " + "remove."); assetTree.registerSubscriber(NAME, String.class, eventsQueue::add); YamlLogging.writeMessage("puts an entry into the map so that an event will be " + "triggered"); @NotNull String expected = "World"; map.put("Hello", expected); map.remove("Hello"); String putEvent = eventsQueue.poll(5, SECONDS); String removeEvent = eventsQueue.poll(5, SECONDS); Assert.assertTrue(putEvent instanceof String); Assert.assertTrue(removeEvent instanceof String); } catch (InterruptedException e) { throw Jvm.rethrow(e); } }); } @Test public void testSubscribeToMapEventsAndRemoveKey() throws IOException, InterruptedException { @NotNull final BlockingQueue<MapEvent> eventsQueue = new LinkedBlockingQueue<>(); yamlLoggger(() -> { try { YamlLogging.writeMessage("Sets up a subscription to listen to key events. And " + "subsequently puts and entry into the map followed by a remove, notice " + "that the " + "'reply: Hello' is received twice, one for the put and one for the " + "remove."); assetTree.registerSubscriber(NAME, MapEvent.class, eventsQueue::add); YamlLogging.writeMessage("puts an entry into the map so that an event will be " + "triggered"); @NotNull String expected = "World"; map.put("Hello", expected); Object putEvent = eventsQueue.poll(1, SECONDS); Assert.assertTrue(putEvent instanceof InsertedEvent); Jvm.pause(1); map.remove("Hello"); Object removeEvent = eventsQueue.poll(5, SECONDS); Assert.assertTrue("event=" + removeEvent.getClass(), removeEvent instanceof RemovedEvent); } catch (InterruptedException e) { throw Jvm.rethrow(e); } }); } }