/*
* Copyright 2014 GoDataDriven B.V.
*
* 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 io.divolte.server;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.ParametersAreNonnullByDefault;
import com.google.common.base.MoreObjects;
import org.apache.avro.generic.GenericRecord;
import org.apache.avro.io.DatumWriter;
import org.apache.avro.io.Encoder;
import org.apache.avro.io.EncoderFactory;
import org.apache.avro.specific.SpecificDatumWriter;
@ParametersAreNonnullByDefault
public final class AvroRecordBuffer {
private static final int INITIAL_BUFFER_SIZE = 100;
private static final AtomicInteger BUFFER_SIZE = new AtomicInteger(INITIAL_BUFFER_SIZE);
private final DivolteIdentifier partyId;
private final DivolteIdentifier sessionId;
private final ByteBuffer byteBuffer;
private AvroRecordBuffer(final DivolteIdentifier partyId, final DivolteIdentifier sessionId, final GenericRecord record) throws IOException {
this.partyId = Objects.requireNonNull(partyId);
this.sessionId = Objects.requireNonNull(sessionId);
/*
* We avoid ByteArrayOutputStream as it is fully synchronized and performs
* a lot of copying. Instead, we create a byte array and point a
* ByteBuffer to it and create a custom OutputStream implementation that
* writes directly to the ByteBuffer. If we under-allocate, we recreate
* the entire object using a larger byte array. All subsequent instances
* will also allocate the larger size array from that point onward.
*/
final ByteBuffer byteBuffer = ByteBuffer.allocate(BUFFER_SIZE.get());
final DatumWriter<GenericRecord> writer = new SpecificDatumWriter<>(record.getSchema());
final Encoder encoder = EncoderFactory.get().directBinaryEncoder(new ByteBufferOutputStream(byteBuffer), null);
writer.write(record, encoder);
// Prepare buffer for reading, and store it (read-only).
byteBuffer.flip();
this.byteBuffer = byteBuffer.asReadOnlyBuffer();
}
public DivolteIdentifier getPartyId() {
return partyId;
}
public DivolteIdentifier getSessionId() {
return sessionId;
}
public static AvroRecordBuffer fromRecord(final DivolteIdentifier partyId, final DivolteIdentifier sessionId, final GenericRecord record) {
for (;;) {
try {
return new AvroRecordBuffer(partyId, sessionId, record);
} catch (final BufferOverflowException boe) {
// Increase the buffer size by about 10%
// Because we only ever increase the buffer size, we discard the
// scenario where this thread fails to set the new size,
// as we can assume another thread increased it.
int currentSize = BUFFER_SIZE.get();
BUFFER_SIZE.compareAndSet(currentSize, (int) (currentSize * 1.1));
} catch (final IOException ioe) {
throw new RuntimeException("Serialization error.", ioe);
}
}
}
public ByteBuffer getByteBuffer() {
return byteBuffer.slice();
}
/**
* Convenience getter for determining the size without materializing a slice of the buffer.
* @return The internal buffer's size.
*/
public int size() {
return byteBuffer.limit();
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("partyId", getPartyId())
.add("sessionId", getSessionId())
.add("size", size())
.toString();
}
@ParametersAreNonnullByDefault
private static final class ByteBufferOutputStream extends OutputStream {
private final ByteBuffer underlying;
public ByteBufferOutputStream(final ByteBuffer underlying) {
this.underlying = Objects.requireNonNull(underlying);
}
@Override
public void write(final int b) throws IOException {
underlying.put((byte) b);
}
@Override
public void write(final byte[] b, final int off, final int len) throws IOException {
underlying.put(b, off, len);
}
}
}