package voldemort.server.protocol.admin;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import voldemort.VoldemortException;
import voldemort.client.protocol.VoldemortFilter;
import voldemort.client.protocol.admin.filter.DefaultVoldemortFilter;
import voldemort.client.protocol.pb.ProtoUtils;
import voldemort.client.protocol.pb.VAdminProto;
import voldemort.client.protocol.pb.VAdminProto.UpdatePartitionEntriesRequest;
import voldemort.server.StoreRepository;
import voldemort.server.VoldemortConfig;
import voldemort.server.protocol.StreamRequestHandler;
import voldemort.store.ErrorCodeMapper;
import voldemort.store.StorageEngine;
import voldemort.store.metadata.MetadataStore;
import voldemort.store.stats.StreamingStats;
import voldemort.store.stats.StreamingStats.Operation;
import voldemort.utils.ByteArray;
import voldemort.utils.ByteUtils;
import voldemort.utils.EventThrottler;
import voldemort.utils.NetworkClassLoader;
import voldemort.utils.Time;
import voldemort.utils.Utils;
import voldemort.versioning.ObsoleteVersionException;
import voldemort.versioning.Versioned;
/**
* UpdatePartitionEntriesStreamRequestHandler implements the streaming logic for
* updating partition entries.
*
* This is the base class, which abstracts network IO to get an entry off the
* wire, and provides a hook
* {@link UpdatePartitionEntriesStreamRequestHandler#processEntry(ByteArray, Versioned)}
* to implement custom logic (if needed) to manage how the entry will be written
* to storage.
*
* The default implementation of processEntry(..) simply issues a storage engine
* put.
*
*/
public class UpdatePartitionEntriesStreamRequestHandler implements StreamRequestHandler {
protected VAdminProto.UpdatePartitionEntriesRequest request;
protected final VAdminProto.UpdatePartitionEntriesResponse.Builder responseBuilder = VAdminProto.UpdatePartitionEntriesResponse.newBuilder();
protected final ErrorCodeMapper errorCodeMapper;
protected final EventThrottler throttler;
protected final VoldemortFilter filter;
protected final StorageEngine<ByteArray, byte[], byte[]> storageEngine;
protected final MetadataStore metadataStore;
protected int counter;
protected final long startTime;
protected final StreamingStats streamStats;
protected final Logger logger = Logger.getLogger(getClass());
protected AtomicBoolean isBatchWriteOff;
public UpdatePartitionEntriesStreamRequestHandler(UpdatePartitionEntriesRequest request,
ErrorCodeMapper errorCodeMapper,
VoldemortConfig voldemortConfig,
StorageEngine<ByteArray, byte[], byte[]> storageEngine,
StoreRepository storeRepository,
NetworkClassLoader networkClassLoader,
MetadataStore metadataStore) {
super();
this.request = request;
this.errorCodeMapper = errorCodeMapper;
this.storageEngine = storageEngine;
this.metadataStore = metadataStore;
throttler = new EventThrottler(voldemortConfig.getStreamMaxReadBytesPerSec());
filter = (request.hasFilter()) ? AdminServiceRequestHandler.getFilterFromRequest(request.getFilter(),
voldemortConfig,
networkClassLoader)
: new DefaultVoldemortFilter();
startTime = System.currentTimeMillis();
if(voldemortConfig.isJmxEnabled()) {
this.streamStats = storeRepository.getStreamingStats(storageEngine.getName());
} else {
this.streamStats = null;
}
storageEngine.beginBatchModifications();
isBatchWriteOff = new AtomicBoolean(false);
}
@Override
public StreamRequestHandlerState handleRequest(DataInputStream inputStream,
DataOutputStream outputStream)
throws IOException {
if(!metadataStore.getPartitionStreamingEnabledUnlocked()) {
throw new VoldemortException("Partition streaming is disabled on node "
+ metadataStore.getNodeId() + " under "
+ metadataStore.getServerStateUnlocked() + " state.");
}
long startNs = System.nanoTime();
if(request == null) {
int size = 0;
try {
size = inputStream.readInt();
} catch(EOFException e) {
if(logger.isTraceEnabled())
logger.trace("Incomplete read for message size");
if(streamStats != null)
streamStats.reportNetworkTime(Operation.UPDATE_ENTRIES,
Utils.elapsedTimeNs(startNs, System.nanoTime()));
return StreamRequestHandlerState.INCOMPLETE_READ;
}
if(size == -1) {
long totalTime = (System.currentTimeMillis() - startTime) / 1000;
logger.info(getHandlerName() + " successfully updated " + counter
+ " entries for store '" + storageEngine.getName() + "' in "
+ totalTime + " s");
if(logger.isTraceEnabled())
logger.trace("Message size -1, completed partition update");
if(streamStats != null)
streamStats.reportNetworkTime(Operation.UPDATE_ENTRIES,
Utils.elapsedTimeNs(startNs, System.nanoTime()));
return StreamRequestHandlerState.COMPLETE;
}
if(logger.isTraceEnabled())
logger.trace("UpdatePartitionEntriesRequest message size: " + size);
byte[] input = new byte[size];
try {
ByteUtils.read(inputStream, input);
} catch(EOFException e) {
if(logger.isTraceEnabled())
logger.trace("Incomplete read for message");
return StreamRequestHandlerState.INCOMPLETE_READ;
} finally {
if(streamStats != null)
streamStats.reportNetworkTime(Operation.UPDATE_ENTRIES,
Utils.elapsedTimeNs(startNs, System.nanoTime()));
}
VAdminProto.UpdatePartitionEntriesRequest.Builder builder = VAdminProto.UpdatePartitionEntriesRequest.newBuilder();
builder.mergeFrom(input);
request = builder.build();
}
VAdminProto.PartitionEntry partitionEntry = request.getPartitionEntry();
ByteArray key = ProtoUtils.decodeBytes(partitionEntry.getKey());
Versioned<byte[]> value = ProtoUtils.decodeVersioned(partitionEntry.getVersioned());
if(filter.accept(key, value)) {
startNs = System.nanoTime();
try {
processEntry(key, value);
if(logger.isTraceEnabled())
logger.trace(getHandlerName() + " (Streaming put) successful");
} catch(ObsoleteVersionException e) {
// log and ignore
if(logger.isDebugEnabled())
logger.debug(getHandlerName()
+ " (Streaming put) threw ObsoleteVersionException, Ignoring.");
} finally {
if(streamStats != null) {
streamStats.reportStreamingPut(Operation.UPDATE_ENTRIES);
streamStats.reportStorageTime(Operation.UPDATE_ENTRIES,
Utils.elapsedTimeNs(startNs, System.nanoTime()));
}
}
throttler.maybeThrottle(key.length() + AdminServiceRequestHandler.valueSize(value));
}
// log progress
counter++;
if(0 == counter % STAT_RECORDS_INTERVAL) {
long totalTime = (System.currentTimeMillis() - startTime) / Time.MS_PER_SECOND;
logger.info(getHandlerName() + " updated " + counter + " entries for store '"
+ storageEngine.getName() + "' in " + totalTime + " s");
}
request = null;
return StreamRequestHandlerState.READING;
}
@Override
public StreamRequestDirection getDirection() {
return StreamRequestDirection.READING;
}
@Override
public void close(DataOutputStream outputStream) throws IOException {
ProtoUtils.writeMessage(outputStream, responseBuilder.build());
storageEngine.endBatchModifications();
isBatchWriteOff.compareAndSet(false, true);
}
@Override
public void handleError(DataOutputStream outputStream, VoldemortException e) throws IOException {
responseBuilder.setError(ProtoUtils.encodeError(errorCodeMapper, e));
if(logger.isEnabledFor(Level.ERROR))
logger.error(getHandlerName() + " handleUpdatePartitionEntries failed for request("
+ request + ")", e);
}
@Override
protected void finalize() {
// when the object is GCed, don't forget to end the batch-write mode, if
// not done already. After closer inspection of code, it seems like
// close() will always be called. But, just doing this for safety.
if(!isBatchWriteOff.get()) {
storageEngine.endBatchModifications();
}
}
@SuppressWarnings("unused")
protected void processEntry(ByteArray key, Versioned<byte[]> value) throws IOException {
storageEngine.put(key, value, null);
}
protected String getHandlerName() {
return "UpdateEntries";
}
}