package org.rakam.aws.kinesis;
import com.amazonaws.handlers.AsyncHandler;
import com.amazonaws.services.kinesis.AmazonKinesisAsyncClient;
import com.amazonaws.services.kinesis.model.PutRecordRequest;
import com.amazonaws.services.kinesis.model.PutRecordResult;
import com.amazonaws.services.kinesis.model.ResourceNotFoundException;
import com.amazonaws.services.kinesis.producer.KinesisProducer;
import com.amazonaws.services.kinesis.producer.KinesisProducerConfiguration;
import io.airlift.log.Logger;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.handler.codec.http.HttpResponseStatus;
import org.apache.avro.generic.FilteredRecordWriter;
import org.apache.avro.generic.GenericData;
import org.apache.avro.io.BinaryEncoder;
import org.apache.avro.io.DatumWriter;
import org.apache.avro.io.EncoderFactory;
import org.rakam.analysis.metadata.Metastore;
import org.rakam.aws.AWSConfig;
import org.rakam.aws.s3.S3BulkEventStore;
import org.rakam.collection.Event;
import org.rakam.collection.FieldDependencyBuilder.FieldDependency;
import org.rakam.plugin.EventStore;
import org.rakam.report.QueryExecution;
import org.rakam.report.QueryResult;
import org.rakam.util.RakamException;
import javax.inject.Inject;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import static io.netty.buffer.PooledByteBufAllocator.DEFAULT;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR;
import static io.netty.handler.codec.http.HttpResponseStatus.valueOf;
public class AWSKinesisEventStore
implements EventStore
{
private final static Logger LOGGER = Logger.get(AWSKinesisEventStore.class);
private final AmazonKinesisAsyncClient kinesis;
private final AWSConfig config;
private final S3BulkEventStore bulkClient;
private final KinesisProducer producer;
@Inject
public AWSKinesisEventStore(AWSConfig config,
Metastore metastore,
FieldDependency fieldDependency)
{
kinesis = new AmazonKinesisAsyncClient(config.getCredentials());
kinesis.setRegion(config.getAWSRegion());
if (config.getKinesisEndpoint() != null) {
kinesis.setEndpoint(config.getKinesisEndpoint());
}
this.config = config;
this.bulkClient = new S3BulkEventStore(metastore, config, fieldDependency);
KinesisProducerConfiguration producerConfiguration = new KinesisProducerConfiguration()
.setRegion(config.getRegion())
.setCredentialsProvider(config.getCredentials());
if (config.getKinesisEndpoint() != null) {
try {
URL url = new URL(config.getKinesisEndpoint());
producerConfiguration.setCustomEndpoint(url.getHost());
producerConfiguration.setPort(url.getPort());
producerConfiguration.setVerifyCertificate(false);
}
catch (MalformedURLException e) {
throw new IllegalStateException(String.format("Kinesis endpoint is invalid: %s", config.getKinesisEndpoint()));
}
}
producer = new KinesisProducer(producerConfiguration);
}
public CompletableFuture<int[]> storeBatchInline(List<Event> events)
{
ByteBuf[] byteBufs = new ByteBuf[events.size()];
try {
for (int i = 0; i < events.size(); i++) {
Event event = events.get(i);
ByteBuf buffer = getBuffer(event);
ByteBuffer data = buffer.nioBuffer();
try {
producer.addUserRecord(config.getEventStoreStreamName(),
getPartitionKey(event),
data);
}
catch (IllegalArgumentException e) {
if (data.remaining() > 1048576) {
throw new RakamException("Too many event properties, the total size of an event must be less than or equal to 1MB, got " + data.remaining(),
BAD_REQUEST);
}
}
byteBufs[i] = buffer;
}
// TODO: async callback?
producer.flush();
}
finally {
for (ByteBuf byteBuf : byteBufs) {
if (byteBuf != null) {
byteBuf.release();
}
}
}
return EventStore.COMPLETED_FUTURE_BATCH;
}
@Override
public void storeBulk(List<Event> events)
{
if (events.isEmpty()) {
return;
}
String project = events.get(0).project();
try {
bulkClient.upload(project, events, 3);
}
catch (OutOfMemoryError e) {
LOGGER.error(e, "OOM error while uploading bulk");
throw new RakamException("Too much data", HttpResponseStatus.BAD_REQUEST);
}
catch (Throwable e) {
LOGGER.error(e);
throw new RakamException("An error occurred while storing events", INTERNAL_SERVER_ERROR);
}
}
@Override
public CompletableFuture<int[]> storeBatchAsync(List<Event> events)
{
return storeBatchInline(events);
}
@Override
public CompletableFuture<Void> storeAsync(Event event)
{
CompletableFuture<Void> future = new CompletableFuture<>();
store(event, future, 3);
return future;
}
private String getPartitionKey(Event event)
{
Object user = event.getAttribute("_user");
return event.project() + "|" + (user == null ? event.collection() : user.toString());
}
public void store(Event event, CompletableFuture<Void> future, int tryCount)
{
ByteBuf buffer = getBuffer(event);
kinesis.putRecordAsync(config.getEventStoreStreamName(), buffer.nioBuffer(),
getPartitionKey(event), new AsyncHandler<PutRecordRequest, PutRecordResult>()
{
@Override
public void onError(Exception e)
{
if (e instanceof ResourceNotFoundException) {
try {
KinesisUtils.createAndWaitForStreamToBecomeAvailable(kinesis, config.getEventStoreStreamName(), 1);
}
catch (Exception e1) {
throw new RuntimeException("Couldn't send event to Amazon Kinesis", e);
}
}
LOGGER.error(e);
if (tryCount > 0) {
store(event, future, tryCount - 1);
}
else {
buffer.release();
future.completeExceptionally(new RakamException(INTERNAL_SERVER_ERROR));
}
}
@Override
public void onSuccess(PutRecordRequest request, PutRecordResult putRecordResult)
{
buffer.release();
future.complete(null);
}
});
}
private ByteBuf getBuffer(Event event)
{
DatumWriter writer = new FilteredRecordWriter(event.properties().getSchema(), GenericData.get());
ByteBuf buffer = DEFAULT.buffer(100);
buffer.writeByte(2);
BinaryEncoder encoder = EncoderFactory.get().directBinaryEncoder(
new ByteBufOutputStream(buffer), null);
try {
encoder.writeString(event.collection());
writer.write(event.properties(), encoder);
}
catch (Exception e) {
throw new RuntimeException("Couldn't serialize event", e);
}
return buffer;
}
}