/*
* Copyright (C) 2015 SoftIndex LLC.
*
* 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.datakernel.stream.processor;
import io.datakernel.bytebuf.ByteBuf;
import io.datakernel.bytebuf.ByteBufPool;
import io.datakernel.eventloop.Eventloop;
import io.datakernel.jmx.EventStats;
import io.datakernel.jmx.ExceptionStats;
import io.datakernel.jmx.JmxAttribute;
import io.datakernel.jmx.JmxReducers.JmxReducerSum;
import io.datakernel.jmx.ValueStats;
import io.datakernel.serializer.BufferSerializer;
import io.datakernel.stream.AbstractStreamTransformer_1_1;
import io.datakernel.stream.StreamDataReceiver;
import io.datakernel.util.MemSize;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.Math.max;
/**
* Represent serializer which serializes data from some type to ByteBuffer.It is a {@link AbstractStreamTransformer_1_1}
* which receives specified type and streams ByteBufs.
*
* @param <T> original type of data
*/
@SuppressWarnings({"rawtypes", "unchecked"})
public final class StreamBinarySerializer<T> extends AbstractStreamTransformer_1_1<T, ByteBuf> {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private static final ArrayIndexOutOfBoundsException OUT_OF_BOUNDS_EXCEPTION = new ArrayIndexOutOfBoundsException();
public static final MemSize DEFAULT_BUFFER_SIZE = MemSize.kilobytes(16);
public static final MemSize MAX_SIZE_1 = MemSize.bytes(128); // (1 << (1 * 7))
public static final MemSize MAX_SIZE_2 = MemSize.kilobytes(16); // (1 << (2 * 7))
public static final MemSize MAX_SIZE_3 = MemSize.megabytes(2); // (1 << (3 * 7))
public static final MemSize MAX_SIZE = MAX_SIZE_3;
private final BufferSerializer<T> serializer;
private int defaultBufferSize = (int) DEFAULT_BUFFER_SIZE.get();
private int maxMessageSize = (int) (MAX_SIZE.get());
private int flushDelayMillis = 0;
private boolean skipSerializationErrors = false;
private InputConsumer inputConsumer;
private OutputProducer outputProducer;
public interface Inspector<T> extends AbstractStreamTransformer_1_1.Inspector {
void onUnderEstimate(T item, int estimatedMessageSize);
void onFullBuffer();
void onMessageOverflow(T item, int messageSize);
void onOutput(ByteBuf buf);
void onSerializationError(T item, Exception e);
}
public interface InspectorEx<T> extends Inspector<T> {
void onItem(T item, int messageSize);
}
public static class JmxInspector<T> extends AbstractStreamTransformer_1_1.JmxInspector implements Inspector<T> {
private static final double SMOOTHING_WINDOW = ValueStats.SMOOTHING_WINDOW_1_MINUTE;
private long underEstimations;
private long fullBuffers;
private long messageOverflows;
private final ValueStats outputBufs = ValueStats.create(SMOOTHING_WINDOW);
private final ExceptionStats serializationErrors = ExceptionStats.create();
@Override
public void onUnderEstimate(T item, int estimatedMessageSize) {
underEstimations++;
}
@Override
public void onFullBuffer() {
fullBuffers++;
}
@Override
public void onMessageOverflow(T item, int messageSize) {
messageOverflows++;
}
@Override
public void onOutput(ByteBuf buf) {
outputBufs.recordValue(buf.readRemaining());
}
@Override
public void onSerializationError(T item, Exception e) {
serializationErrors.recordException(e, item);
}
@JmxAttribute(reducer = JmxReducerSum.class)
public long getUnderEstimations() {
return underEstimations;
}
@JmxAttribute(reducer = JmxReducerSum.class)
public long getFullBuffers() {
return fullBuffers;
}
@JmxAttribute(reducer = JmxReducerSum.class)
public long getMessageOverflows() {
return messageOverflows;
}
@JmxAttribute
public ValueStats getOutputBufs() {
return outputBufs;
}
@JmxAttribute
public ExceptionStats getSerializationErrors() {
return serializationErrors;
}
}
public static class JmxInspectorEx<T> extends JmxInspector<T> implements InspectorEx<T> {
private static final double SMOOTHING_WINDOW = ValueStats.SMOOTHING_WINDOW_1_MINUTE;
private int lastSize;
private long count;
private final ValueStats underEstimations = ValueStats.create(SMOOTHING_WINDOW);
private final EventStats fullBuffers = EventStats.create(SMOOTHING_WINDOW);
private final ValueStats messageOverflows = ValueStats.create(SMOOTHING_WINDOW);
private final ValueStats outputBufs = ValueStats.create(SMOOTHING_WINDOW);
private final ExceptionStats serializationErrors = ExceptionStats.create();
@Override
public void onItem(T item, int messageSize) {
lastSize = messageSize;
count++;
}
@JmxAttribute
public int getLastSize() {
return lastSize;
}
@JmxAttribute
public long getCount() {
return count;
}
}
// region creators
private StreamBinarySerializer(Eventloop eventloop, BufferSerializer<T> serializer) {
super(eventloop);
this.serializer = serializer;
rebuild();
}
private void rebuild() {
if (outputProducer != null && outputProducer.outputBuf != null) outputProducer.outputBuf.recycle();
inputConsumer = new InputConsumer();
outputProducer = new OutputProducer(serializer, defaultBufferSize, maxMessageSize, flushDelayMillis, skipSerializationErrors);
}
@Override
protected AbstractInputConsumer getInputImpl() {
return inputConsumer;
}
@Override
protected AbstractOutputProducer getOutputImpl() {
return outputProducer;
}
/**
* Creates a new instance of this class
*
* @param eventloop event loop in which serializer will run
* @param serializer specified BufferSerializer for this type
*/
public static <T> StreamBinarySerializer<T> create(Eventloop eventloop, BufferSerializer<T> serializer) {
return new StreamBinarySerializer<>(eventloop, serializer);
}
public StreamBinarySerializer<T> withDefaultBufferSize(int bufferSize) {
this.defaultBufferSize = bufferSize;
rebuild();
return this;
}
public StreamBinarySerializer<T> withDefaultBufferSize(MemSize bufferSize) {
return withDefaultBufferSize((int) bufferSize.get());
}
public StreamBinarySerializer<T> withMaxMessageSize(int maxMessageSize) {
this.maxMessageSize = maxMessageSize;
rebuild();
return this;
}
public StreamBinarySerializer<T> withMaxMessageSize(MemSize maxMessageSize) {
return withMaxMessageSize((int) maxMessageSize.get());
}
public StreamBinarySerializer<T> withFlushDelay(int flushDelayMillis) {
this.flushDelayMillis = flushDelayMillis;
rebuild();
return this;
}
public StreamBinarySerializer<T> withSkipSerializationErrors() {
return withSkipSerializationErrors(true);
}
public StreamBinarySerializer<T> withSkipSerializationErrors(boolean skipSerializationErrors) {
this.skipSerializationErrors = skipSerializationErrors;
rebuild();
return this;
}
public StreamBinarySerializer<T> withInspector(Inspector<T> inspector) {
this.inspector = inspector;
rebuild();
return this;
}
// endregion
private final class InputConsumer extends AbstractInputConsumer {
@Override
protected void onUpstreamEndOfStream() {
outputProducer.flushAndClose();
}
@Override
public StreamDataReceiver<T> getDataReceiver() {
return outputProducer;
}
}
private final class OutputProducer extends AbstractOutputProducer implements StreamDataReceiver<T> {
private final BufferSerializer<T> serializer;
private final int defaultBufferSize;
private final int maxMessageSize;
private final int headerSize;
// TODO (dvolvach): queue of serialized buffers
private ByteBuf outputBuf;
private int estimatedMessageSize;
private final int flushDelayMillis;
private boolean flushPosted;
private final boolean skipSerializationErrors;
private final Inspector inspector = (Inspector) StreamBinarySerializer.this.inspector;
private final InspectorEx inspectorEx = StreamBinarySerializer.this.inspector instanceof InspectorEx ?
(InspectorEx) StreamBinarySerializer.this.inspector : null;
public OutputProducer(BufferSerializer<T> serializer, int defaultBufferSize, int maxMessageSize, int flushDelayMillis, boolean skipSerializationErrors) {
this.skipSerializationErrors = skipSerializationErrors;
this.serializer = checkNotNull(serializer);
this.maxMessageSize = maxMessageSize;
this.headerSize = varint32Size(maxMessageSize - 1);
this.estimatedMessageSize = 1;
this.defaultBufferSize = defaultBufferSize;
this.flushDelayMillis = flushDelayMillis;
}
@Override
protected void onDownstreamSuspended() {
inputConsumer.suspend();
}
@Override
protected void onDownstreamResumed() {
inputConsumer.resume();
resumeProduce();
}
private int varint32Size(int value) {
if ((value & 0xffffffff << 7) == 0) return 1;
if ((value & 0xffffffff << 14) == 0) return 2;
if ((value & 0xffffffff << 21) == 0) return 3;
if ((value & 0xffffffff << 28) == 0) return 4;
return 5;
}
private ByteBuf allocateBuffer() {
return ByteBufPool.allocate(max(defaultBufferSize, headerSize + estimatedMessageSize));
}
private void flushBuffer(StreamDataReceiver<ByteBuf> receiver) {
if (outputBuf != null) {
if (outputBuf.canRead()) {
if (inspector != null) inspector.onOutput(outputBuf);
if (outputProducer.getProducerStatus().isOpen()) {
receiver.onData(outputBuf);
}
} else {
outputBuf.recycle();
}
outputBuf = null;
}
}
private void writeSize(byte[] buf, int pos, int size) {
if (headerSize == 1) {
buf[pos] = (byte) size;
return;
}
buf[pos] = (byte) ((size & 0x7F) | 0x80);
size >>>= 7;
if (headerSize == 2) {
buf[pos + 1] = (byte) size;
return;
}
assert headerSize == 3;
buf[pos + 1] = (byte) ((size & 0x7F) | 0x80);
size >>>= 7;
buf[pos + 2] = (byte) size;
}
/**
* After receiving data it serializes it to buffer and adds it to the outputBuffer,
* and flushes bytes depending on the autoFlushDelay
*
* @param item receiving item
*/
@Override
public void onData(T item) {
if (outputBuf == null) {
outputBuf = allocateBuffer();
}
int positionBegin;
int positionItem;
for (; ; ) {
if (outputBuf.writeRemaining() < headerSize + estimatedMessageSize) {
onFullBuffer();
outputBuf = allocateBuffer();
}
positionBegin = outputBuf.writePosition();
positionItem = positionBegin + headerSize;
outputBuf.writePosition(positionItem);
try {
serializer.serialize(outputBuf, item);
} catch (ArrayIndexOutOfBoundsException e) {
onUnderEstimate(item, positionBegin, positionItem);
continue;
} catch (Exception e) {
onSerializationError(item, positionBegin, e);
return;
}
break;
}
int positionEnd = outputBuf.writePosition();
int messageSize = positionEnd - positionItem;
if (messageSize >= maxMessageSize) {
onMessageOverflow(item, positionBegin, messageSize);
return;
}
writeSize(outputBuf.array(), positionBegin, messageSize);
if (inspectorEx != null) inspectorEx.onItem(item, messageSize);
messageSize += messageSize >>> 2;
if (messageSize > estimatedMessageSize)
estimatedMessageSize = messageSize;
else
estimatedMessageSize -= estimatedMessageSize >>> 10;
if (!flushPosted) {
postFlush();
}
}
private void onFullBuffer() {
if (inspector != null) inspector.onFullBuffer();
flushBuffer(outputProducer.getDownstreamDataReceiver());
}
private void onSerializationError(T value, int positionBegin, Exception e) {
if (inspector != null) inspector.onSerializationError(value, e);
outputBuf.writePosition(positionBegin);
handleSerializationError(e);
}
private void onMessageOverflow(T value, int positionBegin, int messageSize) {
if (inspector != null) inspector.onMessageOverflow(value, messageSize);
outputBuf.writePosition(positionBegin);
handleSerializationError(OUT_OF_BOUNDS_EXCEPTION);
}
private void onUnderEstimate(T value, int positionBegin, int positionItem) {
if (inspector != null)
inspector.onUnderEstimate(value, estimatedMessageSize);
outputBuf.writePosition(positionBegin);
int messageSize = outputBuf.limit() - positionItem;
estimatedMessageSize = messageSize + 1 + (messageSize >>> 1);
}
private void handleSerializationError(Exception e) {
if (skipSerializationErrors) {
logger.warn("Skipping serialization error in {}", this, e);
} else {
closeWithError(e);
}
}
private void flushAndClose() {
flushBuffer(outputProducer.getDownstreamDataReceiver());
logger.trace("endOfStream {}, upstream: {}", this, inputConsumer.getUpstream());
outputProducer.sendEndOfStream();
}
/**
* Bytes will be sent immediately.
*/
private void flush() {
flushBuffer(outputProducer.getDownstreamDataReceiver());
flushPosted = false;
}
private void postFlush() {
flushPosted = true;
if (flushDelayMillis == 0) {
eventloop.postLater(new Runnable() {
@Override
public void run() {
if (outputProducer.getProducerStatus().isOpen()) {
flush();
}
}
});
} else {
eventloop.scheduleBackground(eventloop.currentTimeMillis() + flushDelayMillis, new Runnable() {
@Override
public void run() {
if (outputProducer.getProducerStatus().isOpen()) {
flush();
}
}
});
}
}
@Override
protected void doCleanup() {
if (outputBuf != null) {
outputBuf.recycle();
outputBuf = null;
}
}
}
public void flush() {
outputProducer.flush();
}
}