/*
* Copyright © 2014 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.data.stream;
import co.cask.cdap.common.conf.CConfiguration;
import co.cask.cdap.common.namespace.NamespacedLocationFactory;
import co.cask.cdap.data2.transaction.stream.StreamAdmin;
import co.cask.cdap.data2.transaction.stream.StreamConfig;
import co.cask.cdap.proto.Id;
import co.cask.cdap.proto.StreamProperties;
import com.google.common.base.Throwables;
import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;
/**
*
*/
public abstract class StreamCoordinatorTestBase {
@ClassRule
public static TemporaryFolder tmpFolder = new TemporaryFolder();
private static final Logger LOG = LoggerFactory.getLogger(StreamCoordinatorTestBase.class);
protected static CConfiguration cConf = CConfiguration.create();
protected abstract StreamCoordinatorClient getStreamCoordinator();
protected abstract StreamAdmin getStreamAdmin();
protected static void setupNamespaces(NamespacedLocationFactory namespacedLocationFactory) throws IOException {
// FileStreamAdmin expects namespace directory to exist.
// Simulate namespace create
namespacedLocationFactory.get(Id.Namespace.DEFAULT).mkdirs();
}
@Test
public void testGeneration() throws Exception {
final StreamAdmin streamAdmin = getStreamAdmin();
final String streamName = "testGen";
final Id.Stream streamId = Id.Stream.from(Id.Namespace.DEFAULT, streamName);
streamAdmin.create(streamId);
StreamCoordinatorClient coordinator = getStreamCoordinator();
final CountDownLatch genIdChanged = new CountDownLatch(1);
coordinator.addListener(streamId, new StreamPropertyListener() {
@Override
public void generationChanged(Id.Stream streamId, int generation) {
if (generation == 10) {
genIdChanged.countDown();
}
}
});
// Do concurrent calls to nextGeneration using two threads
final CyclicBarrier barrier = new CyclicBarrier(2);
for (int i = 0; i < 2; i++) {
Thread t = new Thread() {
@Override
public void run() {
try {
barrier.await();
for (int i = 0; i < 5; i++) {
streamAdmin.truncate(streamId);
}
} catch (Exception e) {
throw Throwables.propagate(e);
}
}
};
t.start();
}
Assert.assertTrue(genIdChanged.await(10, TimeUnit.SECONDS));
}
@Test
public void testConfig() throws Exception {
final StreamAdmin streamAdmin = getStreamAdmin();
final String streamName = "testConfig";
final Id.Stream streamId = Id.Stream.from(Id.Namespace.DEFAULT, streamName);
streamAdmin.create(streamId);
StreamCoordinatorClient coordinator = getStreamCoordinator();
final BlockingDeque<Integer> thresholds = new LinkedBlockingDeque<>();
final BlockingDeque<Long> ttls = new LinkedBlockingDeque<>();
coordinator.addListener(streamId, new StreamPropertyListener() {
@Override
public void thresholdChanged(Id.Stream streamId, int threshold) {
thresholds.add(threshold);
}
@Override
public void ttlChanged(Id.Stream streamId, long ttl) {
ttls.add(ttl);
}
});
// Have two threads, one update the threshold, one update the ttl
final CyclicBarrier barrier = new CyclicBarrier(2);
final CountDownLatch completeLatch = new CountDownLatch(2);
for (int i = 0; i < 2; i++) {
final int threadId = i;
Thread t = new Thread() {
@Override
public void run() {
try {
barrier.await();
for (int i = 0; i < 100; i++) {
Long ttl = (threadId == 0) ? (long) (i * 1000) : null;
Integer threshold = (threadId == 1) ? i : null;
streamAdmin.updateConfig(streamId, new StreamProperties(ttl, null, threshold));
}
completeLatch.countDown();
} catch (Exception e) {
throw Throwables.propagate(e);
}
}
};
t.start();
}
Assert.assertTrue(completeLatch.await(60, TimeUnit.SECONDS));
// Check the last threshold and ttl are correct. We don't check if the listener gets every update as it's
// possible that it doesn't see every updates, but only the latest value (that's what ZK watch guarantees).
Assert.assertTrue(validateLastElement(thresholds, 99));
Assert.assertTrue(validateLastElement(ttls, 99000L));
// Verify the config is right
StreamConfig config = streamAdmin.getConfig(streamId);
Assert.assertEquals(99, config.getNotificationThresholdMB());
Assert.assertEquals(99000L, config.getTTL());
}
@Test
public void testDeleteStream() throws Exception {
final Id.Stream streamId = Id.Stream.from(Id.Namespace.DEFAULT, "test");
StreamAdmin streamAdmin = getStreamAdmin();
streamAdmin.create(streamId);
Assert.assertTrue(streamAdmin.exists(streamId));
StreamCoordinatorClient streamCoordinator = getStreamCoordinator();
final CountDownLatch latch = new CountDownLatch(1);
streamCoordinator.addListener(streamId, new StreamPropertyListener() {
@Override
public void deleted(Id.Stream id) {
if (id.equals(streamId)) {
latch.countDown();
}
}
});
streamAdmin.drop(streamId);
Assert.assertTrue(latch.await(5, TimeUnit.SECONDS));
}
private <T> boolean validateLastElement(BlockingDeque<T> deque, T value) throws InterruptedException {
int count = 0;
T peekValue = deque.peekLast();
while (!value.equals(peekValue) && count++ < 20) {
TimeUnit.MILLISECONDS.sleep(100);
LOG.info("Expected {}, got {}", value, peekValue);
peekValue = deque.peekLast();
}
return count < 20;
}
}