/**
* Copyright 2016 LinkedIn Corp. All rights reserved.
*
* 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.
*/
package com.github.ambry.tools.perf.rest;
import com.github.ambry.router.AsyncWritableChannel;
import com.github.ambry.router.Callback;
import com.github.ambry.router.FutureResult;
import com.github.ambry.router.ReadableStreamChannel;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* An implementation of {@link ReadableStreamChannel} that uses a byte array repeatedly to construct a stream of the
* specified size.
*/
class PerfRSC implements ReadableStreamChannel {
private final ByteBuffer bytes;
private final long streamSize;
private final AtomicBoolean readIntoInvoked = new AtomicBoolean(false);
private final AtomicBoolean callbackInvoked = new AtomicBoolean(false);
private final AtomicBoolean isOpen = new AtomicBoolean(true);
private volatile long streamed = 0;
private volatile Exception channelWriteException = null;
private volatile AsyncWritableChannel writeChannel = null;
private volatile FutureResult<Long> futureResult = new FutureResult<Long>();
private volatile Callback<Long> readIntoCallback = null;
/**
* Create an instance of PerfRSC.
* @param bytes the bytes that form a chunk that will be repeatedly returned until {@code streamSize} is reached.
* @param streamSize the desired total size of the stream.
*/
public PerfRSC(byte[] bytes, long streamSize) {
if (streamSize < 0 || (streamSize > 0 && bytes.length < 1)) {
throw new IllegalArgumentException("Invalid argument(s)");
}
this.streamSize = streamSize;
if (bytes != null) {
this.bytes = ByteBuffer.wrap(bytes);
} else {
this.bytes = null;
}
}
@Override
public long getSize() {
return streamSize;
}
@Override
public Future<Long> readInto(AsyncWritableChannel asyncWritableChannel, Callback<Long> callback) {
if (readIntoInvoked.compareAndSet(false, true)) {
writeChannel = asyncWritableChannel;
readIntoCallback = callback;
writeToChannel();
} else {
throw new IllegalStateException("ReadInto should not be called more than once");
}
return futureResult;
}
@Override
public boolean isOpen() {
return isOpen.get();
}
@Override
public void close() {
isOpen.set(false);
}
/**
* Writes continuously to the provided {@link AsyncWritableChannel} until {@link #streamSize} is reached.
*/
private void writeToChannel() {
if (channelWriteException == null && streamed < streamSize) {
if (!isOpen()) {
invokeCallback(streamed, new ClosedChannelException());
} else {
int bytesLeft = streamSize - streamed > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) (streamSize - streamed);
bytes.rewind();
bytes.limit(Math.min(bytes.capacity(), bytesLeft));
writeChannel.write(bytes, new Callback<Long>() {
@Override
public void onCompletion(Long result, Exception callbackException) {
streamed += result;
channelWriteException = callbackException;
writeToChannel();
}
});
}
} else {
invokeCallback(streamed, channelWriteException);
}
}
/**
* Invokes the callback and updates the future once this is called. This function ensures that the callback is invoked
* just once.
* @param bytesRead the number of bytes read.
* @param exception the {@link Exception}, if any, to pass to the callback.
*/
private void invokeCallback(long bytesRead, Exception exception) {
if (callbackInvoked.compareAndSet(false, true)) {
futureResult.done(bytesRead, exception);
if (readIntoCallback != null) {
readIntoCallback.onCompletion(bytesRead, exception);
}
}
}
}