/*
* Copyright © 2014-2016 Cask Data, Inc.
*
* 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 co.cask.cdap.notifications;
import co.cask.cdap.api.TxRunnable;
import co.cask.cdap.api.common.Bytes;
import co.cask.cdap.api.data.DatasetContext;
import co.cask.cdap.api.dataset.DatasetDefinition;
import co.cask.cdap.api.dataset.DatasetProperties;
import co.cask.cdap.api.dataset.lib.KeyValueTable;
import co.cask.cdap.common.guice.DiscoveryRuntimeModule;
import co.cask.cdap.common.guice.LocationRuntimeModule;
import co.cask.cdap.common.utils.Tasks;
import co.cask.cdap.data.runtime.DataFabricModules;
import co.cask.cdap.data.runtime.DataSetServiceModules;
import co.cask.cdap.data.runtime.DataSetsModules;
import co.cask.cdap.data2.datafabric.dataset.service.DatasetService;
import co.cask.cdap.data2.datafabric.dataset.service.executor.DatasetOpExecutor;
import co.cask.cdap.data2.dataset2.DatasetFramework;
import co.cask.cdap.explore.guice.ExploreClientModule;
import co.cask.cdap.metrics.guice.MetricsClientRuntimeModule;
import co.cask.cdap.notifications.feeds.NotificationFeedManager;
import co.cask.cdap.notifications.feeds.NotificationFeedNotFoundException;
import co.cask.cdap.notifications.service.NotificationContext;
import co.cask.cdap.notifications.service.NotificationHandler;
import co.cask.cdap.notifications.service.NotificationService;
import co.cask.cdap.notifications.service.TxRetryPolicy;
import co.cask.cdap.proto.Id;
import co.cask.cdap.proto.NamespaceMeta;
import co.cask.cdap.store.NamespaceStore;
import co.cask.cdap.store.guice.NamespaceStoreModule;
import co.cask.tephra.TransactionContext;
import co.cask.tephra.TransactionManager;
import co.cask.tephra.TransactionSystemClient;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMultiset;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Multiset;
import com.google.common.collect.Sets;
import com.google.inject.Injector;
import com.google.inject.Module;
import org.apache.twill.common.Cancellable;
import org.junit.Assert;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public abstract class NotificationTest {
private static final Logger LOG = LoggerFactory.getLogger(NotificationTest.class);
protected static NotificationFeedManager feedManager;
private static DatasetFramework dsFramework;
private static TransactionSystemClient txClient;
private static TransactionManager txManager;
private static DatasetOpExecutor dsOpService;
private static DatasetService datasetService;
private static NamespaceStore namespaceStore;
private static NotificationService notificationService;
private static final Id.Namespace namespace = Id.Namespace.from("namespace");
protected static final Id.NotificationFeed FEED1 = new Id.NotificationFeed.Builder()
.setNamespaceId(namespace.getId()).setCategory("stream").setName("foo").setDescription("").build();
protected static final Id.NotificationFeed FEED2 = new Id.NotificationFeed.Builder()
.setNamespaceId(namespace.getId()).setCategory("stream").setName("bar").setDescription("").build();
protected static NotificationService getNotificationService() {
return notificationService;
}
protected static List<Module> getCommonModules() {
return ImmutableList.of(
new DiscoveryRuntimeModule().getInMemoryModules(),
new DataSetsModules().getStandaloneModules(),
new DataSetServiceModules().getInMemoryModules(),
new LocationRuntimeModule().getInMemoryModules(),
new MetricsClientRuntimeModule().getInMemoryModules(),
new ExploreClientModule(),
new DataFabricModules().getInMemoryModules(),
new NamespaceStoreModule().getInMemoryModules()
);
}
public static void startServices(Injector injector) throws Exception {
notificationService = injector.getInstance(NotificationService.class);
notificationService.startAndWait();
feedManager = injector.getInstance(NotificationFeedManager.class);
dsFramework = injector.getInstance(DatasetFramework.class);
txManager = injector.getInstance(TransactionManager.class);
txManager.startAndWait();
dsOpService = injector.getInstance(DatasetOpExecutor.class);
dsOpService.startAndWait();
datasetService = injector.getInstance(DatasetService.class);
datasetService.startAndWait();
txClient = injector.getInstance(TransactionSystemClient.class);
namespaceStore = injector.getInstance(NamespaceStore.class);
}
public static void stopServices() throws Exception {
notificationService.stopAndWait();
datasetService.stopAndWait();
dsOpService.stopAndWait();
txManager.stopAndWait();
}
@Test (expected = NotificationFeedNotFoundException.class)
public void feedNotCreatedTest() throws Exception {
notificationService.subscribe(FEED1, new NotificationHandler<String>() {
@Override
public Type getNotificationType() {
return String.class;
}
@Override
public void received(String notification, NotificationContext notificationContext) {
// No-op
}
});
}
@Test
public void testCreateGetAndListFeeds() throws Exception {
// no feeds at the beginning
Assert.assertEquals(0, feedManager.listFeeds(namespace).size());
// create feed 1
feedManager.createFeed(FEED1);
// check get and list feed
Assert.assertEquals(FEED1, feedManager.getFeed(FEED1));
Assert.assertEquals(ImmutableList.of(FEED1), feedManager.listFeeds(namespace));
// create feed 2
feedManager.createFeed(FEED2);
// check get and list feed
Assert.assertEquals(FEED2, feedManager.getFeed(FEED2));
Assert.assertTrue(checkFeedList(ImmutableSet.of(FEED1, FEED2), feedManager.listFeeds(namespace)));
// clear the feeds
feedManager.deleteFeed(FEED1);
feedManager.deleteFeed(FEED2);
namespaceStore.delete(namespace);
Assert.assertEquals(0, feedManager.listFeeds(namespace).size());
}
private boolean checkFeedList(ImmutableSet<Id.NotificationFeed> expected, List<Id.NotificationFeed> actual) {
Assert.assertEquals(expected.size(), actual.size());
for (Id.NotificationFeed feed : actual) {
if (!expected.contains(feed)) {
return false;
}
}
return true;
}
@Test
public void useTransactionTest() throws Exception {
// Performing admin operations to create dataset instance
// keyValueTable is a system dataset module
namespaceStore.create(new NamespaceMeta.Builder().setName(namespace).build());
Id.DatasetInstance myTableInstance = Id.DatasetInstance.from(namespace, "myTable");
dsFramework.addInstance("keyValueTable", myTableInstance, DatasetProperties.EMPTY);
final CountDownLatch receivedLatch = new CountDownLatch(1);
Assert.assertTrue(feedManager.createFeed(FEED1));
try {
Cancellable cancellable = notificationService.subscribe(FEED1, new NotificationHandler<String>() {
private int received = 0;
@Override
public Type getNotificationType() {
return String.class;
}
@Override
public void received(final String notification, NotificationContext notificationContext) {
notificationContext.execute(new TxRunnable() {
@Override
public void run(DatasetContext context) throws Exception {
KeyValueTable table = context.getDataset("myTable");
table.write("foo", String.format("%s-%d", notification, received++));
receivedLatch.countDown();
}
}, TxRetryPolicy.maxRetries(5));
}
});
// Short delay for the subscriber to setup the subscription.
TimeUnit.MILLISECONDS.sleep(500);
try {
notificationService.publish(FEED1, "foobar");
// Waiting for the subscriber to receive that notification
Assert.assertTrue(receivedLatch.await(5, TimeUnit.SECONDS));
// Read the KeyValueTable for the value updated from the subscriber.
// Need to poll it couple times since after the received method returned,
// the tx may not yet committed when we try to read it here.
final KeyValueTable table = dsFramework.getDataset(myTableInstance, DatasetDefinition.NO_ARGUMENTS, null);
Assert.assertNotNull(table);
final TransactionContext txContext = new TransactionContext(txClient, table);
Tasks.waitFor(true, new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
txContext.start();
try {
return "foobar-0".equals(Bytes.toString(table.read("foo")));
} finally {
txContext.finish();
}
}
}, 5, TimeUnit.SECONDS);
} finally {
cancellable.cancel();
}
} finally {
dsFramework.deleteInstance(myTableInstance);
feedManager.deleteFeed(FEED1);
namespaceStore.delete(namespace);
}
}
@Test
public void onePublisherOneSubscriberTest() throws Exception {
Set<Id.NotificationFeed> feeds = ImmutableSet.of(FEED1);
testPubSub(feeds, 1, 20, feeds, 1, String.class, new Function<SimpleNotification, String>() {
@Override
public String apply(SimpleNotification input) {
return input.getPayload();
}
});
}
@Test
public void onePublisherMultipleSubscribersTest() throws Exception {
Set<Id.NotificationFeed> feeds = ImmutableSet.of(FEED1);
testPubSub(feeds, 1, 20, feeds, 10, String.class, new Function<SimpleNotification, String>() {
@Override
public String apply(SimpleNotification input) {
return input.getPayload();
}
});
}
@Test
public void multiplePublishersOneSubscriberTest() throws Exception {
/*
This configuration should not happen, as, by design, we want only one publisher to publisher the changes attached
to a resource. But since the low level APIs allow it, this should still be tested.
*/
Set<Id.NotificationFeed> feeds = ImmutableSet.of(FEED1);
testPubSub(feeds, 5, 15, feeds, 1, String.class, new Function<SimpleNotification, String>() {
@Override
public String apply(SimpleNotification input) {
return input.getPayload();
}
});
}
@Test
public void multipleFeedsOneSubscriber() throws Exception {
Set<Id.NotificationFeed> feeds = ImmutableSet.of(FEED1, FEED2);
testPubSub(feeds, 1, 15, feeds, 1, SimpleNotification.class, Functions.<SimpleNotification>identity());
}
@Test
public void twoFeedsPublishOneFeedSubscribeTest() throws Exception {
// Test two publishers on two different feeds, but only one subscriber subscribing to one of the feeds
testPubSub(ImmutableSet.of(FEED1, FEED2), 1, 15,
ImmutableSet.of(FEED1), 1, SimpleNotification.class, Functions.<SimpleNotification>identity());
}
/**
* Testing publishers/subscribers interaction.
*
* @param pubFeeds set of feeds to publish to
* @param publishersPerFeed number of publishers doing concurrent publishing for each feed
* @param messagesPerPublisher number of messages being published by each publisher
* @param subFeeds set of feeds to subscribe to
* @param subscribersPerFeed number of subscribers for each feed
* @param payloadType Class reprenseting the data type of the payload of the notification
* @param payloadFunction a function that transform {@link SimpleNotification} type to the payload type
* @param <T> type of the payload
*/
private <T> void testPubSub(Set<Id.NotificationFeed> pubFeeds, int publishersPerFeed, final int messagesPerPublisher,
Set<Id.NotificationFeed> subFeeds, int subscribersPerFeed,
final Class<T> payloadType,
final Function<SimpleNotification, T> payloadFunction) throws Exception {
for (Id.NotificationFeed feedId : Sets.union(pubFeeds, subFeeds)) {
Assert.assertTrue(feedManager.createFeed(feedId));
}
try {
int totalMessages = subFeeds.size() * publishersPerFeed * messagesPerPublisher * subscribersPerFeed;
final CountDownLatch latch = new CountDownLatch(totalMessages);
final Queue<T> receivedMessages = new ConcurrentLinkedQueue<>();
List<Cancellable> cancellables = Lists.newArrayList();
try {
for (Id.NotificationFeed feedId : subFeeds) {
for (int i = 0; i < subscribersPerFeed; i++) {
Cancellable cancellable = notificationService.subscribe(feedId, new NotificationHandler<T>() {
@Override
public Type getNotificationType() {
return payloadType;
}
@Override
public void received(T notification, NotificationContext notificationContext) {
LOG.debug("Received notification payload: {}", notification);
receivedMessages.offer(notification);
latch.countDown();
}
});
cancellables.add(cancellable);
}
}
// Give the subscriber some time to prepare for published messages before starting the publisher
TimeUnit.MILLISECONDS.sleep(500);
// Starts publishers
final Map<Id.NotificationFeed, Queue<T>> publishedMessages = new ConcurrentHashMap<>();
ExecutorService executor = Executors.newFixedThreadPool(pubFeeds.size() * publishersPerFeed);
try {
for (final Id.NotificationFeed feedId : pubFeeds) {
final Queue<T> publishedQueue = new ConcurrentLinkedQueue<>();
publishedMessages.put(feedId, publishedQueue);
// Let all publishers start together
final CyclicBarrier publisherBarrier = new CyclicBarrier(publishersPerFeed);
for (int i = 0; i < publishersPerFeed; i++) {
final int publisherId = i;
executor.submit(new Callable<Void>() {
@Override
public Void call() throws Exception {
publisherBarrier.await();
for (int i = 0; i < messagesPerPublisher; i++) {
T notification = payloadFunction.apply(new SimpleNotification(publisherId,
String.format("%s-%d", feedId, i)));
notificationService.publish(feedId, notification);
publishedQueue.add(notification);
TimeUnit.MILLISECONDS.sleep(10);
}
return null;
}
});
}
}
// Wait for subscriptions getting all messages
Assert.assertTrue(latch.await(5000, TimeUnit.SECONDS));
} finally {
executor.shutdown();
}
// Verify the result.
Multiset<T> received = HashMultiset.create(receivedMessages);
Assert.assertEquals(totalMessages, received.size());
// For each unique message published that has subscription,
// there should be (publisher per feed * subscriber per feed) of them
for (Id.NotificationFeed feedId : subFeeds) {
for (T notification : ImmutableMultiset.copyOf(publishedMessages.get(feedId)).elementSet()) {
Assert.assertEquals(publishersPerFeed * subscribersPerFeed, received.count(notification));
}
}
} finally {
for (Cancellable cancellable : cancellables) {
cancellable.cancel();
}
}
} finally {
for (Id.NotificationFeed feedId : Sets.union(pubFeeds, subFeeds)) {
feedManager.deleteFeed(feedId);
}
}
}
private static final class SimpleNotification {
private final int publisherId;
private final String payload;
private SimpleNotification(int publisherId, String payload) {
this.publisherId = publisherId;
this.payload = payload;
}
public String getPayload() {
return payload;
}
@Override
public String toString() {
return String.format("id: %d, payload: %s", publisherId, payload);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
SimpleNotification that = (SimpleNotification) o;
return Objects.equals(publisherId, that.publisherId) &&
Objects.equals(payload, that.payload);
}
@Override
public int hashCode() {
return Objects.hash(publisherId, payload);
}
}
}