/*
* Copyright © 2015 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.service;
import co.cask.cdap.api.flow.flowlet.StreamEvent;
import co.cask.cdap.api.stream.StreamEventData;
import co.cask.cdap.common.conf.CConfiguration;
import co.cask.cdap.common.io.Locations;
import co.cask.cdap.common.namespace.NamespacedLocationFactory;
import co.cask.cdap.data.runtime.LocationStreamFileWriterFactory;
import co.cask.cdap.data.stream.InMemoryStreamCoordinatorClient;
import co.cask.cdap.data.stream.NoopStreamAdmin;
import co.cask.cdap.data.stream.StreamCoordinatorClient;
import co.cask.cdap.data.stream.StreamDataFileReader;
import co.cask.cdap.data.stream.StreamDataFileWriter;
import co.cask.cdap.data.stream.StreamFileTestUtils;
import co.cask.cdap.data.stream.StreamFileType;
import co.cask.cdap.data.stream.StreamFileWriterFactory;
import co.cask.cdap.data.stream.StreamUtils;
import co.cask.cdap.data.stream.TimestampCloseable;
import co.cask.cdap.data2.transaction.stream.StreamAdmin;
import co.cask.cdap.data2.transaction.stream.StreamConfig;
import co.cask.cdap.proto.Id;
import com.google.common.base.Charsets;
import com.google.common.collect.AbstractIterator;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.apache.twill.filesystem.Location;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
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.nio.ByteBuffer;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Unit tests for the {@link ConcurrentStreamWriter}.
*/
public abstract class ConcurrentStreamWriterTestBase {
private static final Logger LOG = LoggerFactory.getLogger(ConcurrentStreamWriterTestBase.class);
private static final CConfiguration cConf = CConfiguration.create();
@ClassRule
public static final TemporaryFolder TMP_FOLDER = new TemporaryFolder();
private static final StreamCoordinatorClient COORDINATOR_CLIENT = new InMemoryStreamCoordinatorClient();
protected abstract NamespacedLocationFactory getNamespacedLocationFactory();
@BeforeClass
public static void startUp() {
COORDINATOR_CLIENT.startAndWait();
}
@AfterClass
public static void shutDown() {
COORDINATOR_CLIENT.stopAndWait();
}
@Test
public void testConcurrentWrite() throws Exception {
final String streamName = "testConcurrentWrite";
String namespace = "namespace";
Id.Stream streamId = Id.Stream.from(namespace, streamName);
StreamAdmin streamAdmin = new TestStreamAdmin(getNamespacedLocationFactory(), Long.MAX_VALUE, 1000);
int threads = Runtime.getRuntime().availableProcessors() * 4;
StreamFileWriterFactory fileWriterFactory = createStreamFileWriterFactory();
final ConcurrentStreamWriter streamWriter = createStreamWriter(streamId, streamAdmin, threads, fileWriterFactory);
// Starts n threads to write events through stream writer, each thread write 1000 events
final int msgPerThread = 1000;
final CountDownLatch startLatch = new CountDownLatch(1);
final CountDownLatch completion = new CountDownLatch(threads);
ExecutorService executor = Executors.newFixedThreadPool(threads);
// Half of the threads write events one by one, the other half writes in batch of size 10
for (int i = 0; i < threads / 2; i++) {
executor.execute(createWriterTask(streamId, streamWriter,
i, msgPerThread, 1, startLatch, completion));
}
for (int i = threads / 2; i < threads; i++) {
executor.execute(createWriterTask(streamId, streamWriter,
i, msgPerThread, 10, startLatch, completion));
}
startLatch.countDown();
Assert.assertTrue(completion.await(120, TimeUnit.SECONDS));
// Verify all events are written.
// There should be only one partition and one file inside
Location partitionLocation = streamAdmin.getConfig(streamId).getLocation().list().get(0);
Location streamLocation = StreamUtils.createStreamLocation(partitionLocation,
fileWriterFactory.getFileNamePrefix(),
0, StreamFileType.EVENT);
StreamDataFileReader reader = StreamDataFileReader.create(Locations.newInputSupplier(streamLocation));
List<StreamEvent> events = Lists.newArrayListWithCapacity(threads * msgPerThread);
// Should read all messages
Assert.assertEquals(threads * msgPerThread, reader.read(events, Integer.MAX_VALUE, 0, TimeUnit.SECONDS));
// Verify all messages as expected
Assert.assertTrue(verifyEvents(threads, msgPerThread, events));
reader.close();
streamWriter.close();
}
@Test
public void testConcurrentAppendFile() throws Exception {
final String streamName = "testConcurrentFile";
String namespace = "namespace";
Id.Stream streamId = Id.Stream.from(namespace, streamName);
StreamAdmin streamAdmin = new TestStreamAdmin(getNamespacedLocationFactory(), Long.MAX_VALUE, 1000);
int threads = Runtime.getRuntime().availableProcessors() * 4;
StreamFileWriterFactory fileWriterFactory = createStreamFileWriterFactory();
final ConcurrentStreamWriter streamWriter = createStreamWriter(streamId, streamAdmin, threads, fileWriterFactory);
int msgCount = 10000;
NamespacedLocationFactory locationFactory = getNamespacedLocationFactory();
// Half of the threads will be calling appendFile, then other half append event one by one
// Prepare the files first, each file has 10000 events.
final List<FileInfo> fileInfos = Lists.newArrayList();
for (int i = 0; i < threads / 2; i++) {
fileInfos.add(generateFile(locationFactory, i, msgCount));
}
// Append file and write events
final CountDownLatch startLatch = new CountDownLatch(1);
final CountDownLatch completion = new CountDownLatch(threads);
ExecutorService executor = Executors.newFixedThreadPool(threads);
for (int i = 0; i < threads / 2; i++) {
executor.execute(createAppendFileTask(streamId, streamWriter,
fileInfos.get(i), startLatch, completion));
}
for (int i = threads / 2; i < threads; i++) {
executor.execute(createWriterTask(streamId, streamWriter, i, msgCount, 50, startLatch, completion));
}
startLatch.countDown();
Assert.assertTrue(completion.await(4, TimeUnit.MINUTES));
// Verify all events are written.
// There should be only one partition
Location partitionLocation = streamAdmin.getConfig(streamId).getLocation().list().get(0);
List<Location> files = partitionLocation.list();
List<StreamEvent> events = Lists.newArrayListWithCapacity(threads * msgCount);
for (Location location : files) {
// Only create reader for the event file
if (StreamFileType.getType(location.getName()) != StreamFileType.EVENT) {
continue;
}
StreamDataFileReader reader = StreamDataFileReader.create(Locations.newInputSupplier(location));
reader.read(events, Integer.MAX_VALUE, 0, TimeUnit.SECONDS);
}
Assert.assertTrue(verifyEvents(threads, msgCount, events));
}
private boolean verifyEvents(int threads, int msgPerThread, List<StreamEvent> events) {
Set<String> messages = Sets.newHashSet();
for (StreamEvent event : events) {
Assert.assertTrue(messages.add(Charsets.UTF_8.decode(event.getBody()).toString()));
}
for (int i = 0; i < threads; i++) {
for (int j = 0; j < msgPerThread; j++) {
if (!messages.contains("Message " + j + " from " + i)) {
return false;
}
}
}
return true;
}
private ConcurrentStreamWriter createStreamWriter(Id.Stream streamId, StreamAdmin streamAdmin,
int threads, StreamFileWriterFactory writerFactory)
throws Exception {
StreamConfig streamConfig = streamAdmin.getConfig(streamId);
streamConfig.getLocation().mkdirs();
return new ConcurrentStreamWriter(COORDINATOR_CLIENT, streamAdmin,
writerFactory, threads, new TestMetricsCollectorFactory());
}
private Runnable createWriterTask(final Id.Stream streamId,
final ConcurrentStreamWriter streamWriter,
final int threadId, final int msgCount, final int batchSize,
final CountDownLatch startLatch, final CountDownLatch completion) {
return new Runnable() {
@Override
public void run() {
try {
startLatch.await();
if (batchSize == 1) {
// Write events one by one
for (int j = 0; j < msgCount; j++) {
ByteBuffer body = Charsets.UTF_8.encode("Message " + j + " from " + threadId);
streamWriter.enqueue(streamId, ImmutableMap.<String, String>of(), body);
}
} else {
// Writes event in batch of the given batch size
final AtomicInteger written = new AtomicInteger(0);
final MutableStreamEventData data = new MutableStreamEventData();
while (written.get() < msgCount) {
streamWriter.enqueue(streamId, new AbstractIterator<StreamEventData>() {
int count = 0;
@Override
protected StreamEventData computeNext() {
// Keep returning message until returned "batchSize" messages
if (written.get() >= msgCount || count == batchSize) {
return endOfData();
}
ByteBuffer body = Charsets.UTF_8.encode("Message " + written.get() + " from " + threadId);
count++;
written.incrementAndGet();
return data.setBody(body);
}
});
}
}
} catch (Exception e) {
LOG.error("Failed to write", e);
} finally {
completion.countDown();
}
}
};
}
private Runnable createAppendFileTask(final Id.Stream streamId,
final ConcurrentStreamWriter streamWriter, final FileInfo fileInfo,
final CountDownLatch startLatch, final CountDownLatch completion) {
return new Runnable() {
@Override
public void run() {
try {
startLatch.await();
streamWriter.appendFile(streamId, fileInfo.eventLocation, fileInfo.indexLocation,
fileInfo.events, fileInfo.closeable);
} catch (Exception e) {
LOG.error("Failed to append file", e);
} finally {
completion.countDown();
}
}
};
}
private StreamFileWriterFactory createStreamFileWriterFactory() {
CConfiguration cConf = CConfiguration.create();
return new LocationStreamFileWriterFactory(cConf);
}
private FileInfo generateFile(NamespacedLocationFactory locationFactory, int id, int events) throws IOException {
Id.Namespace dummyNs = Id.Namespace.from("dummy");
Location eventLocation = locationFactory.get(dummyNs).append(UUID.randomUUID().toString());
Location indexLocation = locationFactory.get(dummyNs).append(UUID.randomUUID().toString());
StreamDataFileWriter writer = new StreamDataFileWriter(Locations.newOutputSupplier(eventLocation),
Locations.newOutputSupplier(indexLocation),
1000L);
for (int i = 0; i < events; i++) {
writer.append(StreamFileTestUtils.createEvent(System.currentTimeMillis(), "Message " + i + " from " + id));
if (i % 50 == 0) {
writer.flush();
}
}
writer.flush();
return new FileInfo(eventLocation, indexLocation, writer, events);
}
private static final class FileInfo {
private final Location eventLocation;
private final Location indexLocation;
private final TimestampCloseable closeable;
private final long events;
private FileInfo(Location eventLocation, Location indexLocation, TimestampCloseable closeable, long events) {
this.eventLocation = eventLocation;
this.indexLocation = indexLocation;
this.closeable = closeable;
this.events = events;
}
}
private static final class TestStreamAdmin extends NoopStreamAdmin {
private final NamespacedLocationFactory namespacedLocationFactory;
private final long partitionDuration;
private final long indexInterval;
private TestStreamAdmin(NamespacedLocationFactory namespacedLocationFactory, long partitionDuration,
long indexInterval) {
this.namespacedLocationFactory = namespacedLocationFactory;
this.partitionDuration = partitionDuration;
this.indexInterval = indexInterval;
}
@Override
public boolean exists(Id.Stream streamId) throws Exception {
return true;
}
@Override
public StreamConfig getConfig(Id.Stream streamId) throws IOException {
Location streamLocation = StreamFileTestUtils.getStreamBaseLocation(namespacedLocationFactory, streamId);
return new StreamConfig(streamId, partitionDuration, indexInterval, Long.MAX_VALUE, streamLocation, null, 1000);
}
}
private static final class TestMetricsCollectorFactory implements StreamMetricsCollectorFactory {
@Override
public StreamMetricsCollector createMetricsCollector(Id.Stream streamId) {
return new StreamMetricsCollector() {
@Override
public void emitMetrics(long bytesWritten, long eventsWritten) {
// No-op
}
};
}
}
}