/**
* Copyright 2013-2015 Seagate Technology LLC.
*
* This Source Code Form is subject to the terms of the Mozilla
* Public License, v. 2.0. If a copy of the MPL was not
* distributed with this file, You can obtain one at
* https://mozilla.org/MP:/2.0/.
*
* This program is distributed in the hope that it will be useful,
* but is provided AS-IS, WITHOUT ANY WARRANTY; including without
* the implied warranty of MERCHANTABILITY, NON-INFRINGEMENT or
* FITNESS FOR A PARTICULAR PURPOSE. See the Mozilla Public
* License for more details.
*
* See www.openkinetic.org for more project information
*/
package com.seagate.kinetic.simulator.internal;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.google.protobuf.ByteString;
import com.seagate.kinetic.common.lib.KineticMessage;
import com.seagate.kinetic.proto.Kinetic.Command;
import com.seagate.kinetic.proto.Kinetic.Command.Algorithm;
import com.seagate.kinetic.proto.Kinetic.Command.Batch;
import com.seagate.kinetic.proto.Kinetic.Command.KeyValue;
import com.seagate.kinetic.proto.Kinetic.Command.MessageType;
import com.seagate.kinetic.proto.Kinetic.Command.Security.ACL.Permission;
import com.seagate.kinetic.proto.Kinetic.Command.Status.StatusCode;
import com.seagate.kinetic.simulator.persist.BatchOperation;
import com.seagate.kinetic.simulator.persist.KVOp;
import com.seagate.kinetic.simulator.persist.KVValue;
import com.seagate.kinetic.simulator.persist.Store;
/**
* Batch operation handler.
*
* @author chiaming
*
*/
public class BatchOperationHandler {
private final static Logger logger = Logger.getLogger(BatchOperationHandler.class
.getName());
private long MAX_TIME_OUT = 30000;
private SimulatorEngine engine = null;
@SuppressWarnings("rawtypes")
private Store store = null;
private long cid = -1;
private int batchId = -1;
// boolean isClosed = false;
private BatchOperation<ByteString, KVValue> batch = null;
//
// private Map<ByteString, ByteString> map = new
// ConcurrentHashMap<ByteString, ByteString>();
// sequence list
private ArrayList<Long> sequenceList = new ArrayList<Long>();
// saved exception
private InvalidBatchException batchException = null;
public BatchOperationHandler(SimulatorEngine engine) {
// simulator engine
this.engine = engine;
// store
this.store = engine.getStore();
}
@SuppressWarnings("unchecked")
public synchronized void init(RequestContext context)
throws InvalidBatchException, InvalidRequestException {
if (this.batch != null) {
this.waitForBatchToFinish();
}
// this batch op handler belongs to this connection
this.cid = context.getRequestMessage().getCommand().getHeader()
.getConnectionID();
// batch Id
this.batchId = context.getRequestMessage().getCommand().getHeader()
.getBatchID();
// start batch
try {
// create new batch instance
batch = engine.getStore().createBatchOperation();
// clear seq list
sequenceList.clear();
// clear version map
// map.clear();
// clear exception
this.batchException = null;
} catch (KVStoreException e) {
throw new InvalidBatchException(e);
}
}
public synchronized void checkBatchMode(KineticMessage kmreq)
throws InvalidRequestException {
if (kmreq.getIsInvalidBatchMessage()) {
throw new InvalidRequestException(
"Invalid batch Id found in message: "
+ kmreq.getCommand().getHeader().getBatchID());
}
if (this.batch == null) {
return;
}
if (kmreq.getCommand().getHeader().getBatchID() == this.batchId
&& kmreq.getCommand().getHeader().getConnectionID() == this.cid) {
return;
}
this.waitForBatchToFinish();
}
private synchronized void waitForBatchToFinish() {
long totalWaitTime = 0;
long period = 3000;
long start = System.currentTimeMillis();
while (batch != null) {
try {
this.wait(period);
if (batch == null) {
return;
}
totalWaitTime = (System.currentTimeMillis() - start);
if (totalWaitTime >= MAX_TIME_OUT) {
throw new RuntimeException(
"Timeout waiting for batch mode to finish");
} else {
logger.warning("waiting for batch mode to finish., total wait time ="
+ totalWaitTime);
}
} catch (InterruptedException e) {
logger.log(Level.WARNING, e.getMessage(), e);
}
}
}
public synchronized void handleRequest(RequestContext context)
throws InvalidBatchException, NotAttemptedException,
KVStoreException, InvalidRequestException, KVSecurityException {
MessageType mtype = context.getMessageType();
/**
* messages will be queued or dequeued. no process until end batch is
* received.
*/
if (mtype == MessageType.START_BATCH
|| mtype == MessageType.ABORT_BATCH) {
return;
}
try {
/**
* start batch if message is tagged as first batch message
*/
if (context.getRequestMessage().getIsFirstBatchMessage()) {
this.init(context);
}
/**
* put op sequence to queue
*/
if (mtype == MessageType.PUT || mtype == MessageType.DELETE) {
this.addSequenceList(context);
}
// check if this is a valid batch message
checkBatch(context);
if (mtype == MessageType.END_BATCH) {
this.commitBatch(context);
} else if (mtype == MessageType.PUT) {
this.batchPut(context.getRequestMessage());
} else if (mtype == MessageType.DELETE) {
this.batchDelete(context.getRequestMessage());
} else {
throw new NotAttemptedException("invalid message type: "
+ mtype);
}
} catch (NotAttemptedException nae) {
logger.log(Level.WARNING, nae.getMessage(), nae);
// set status code and message
context.getCommandBuilder().getStatusBuilder()
.setCode(StatusCode.NOT_ATTEMPTED);
context.getCommandBuilder().getStatusBuilder()
.setStatusMessage(nae.getMessage());
this.saveFailedRequestContext(context);
close();
throw nae;
} catch (InvalidRequestException ire) {
logger.log(Level.WARNING, ire.getMessage(), ire);
// set status code and message
context.getCommandBuilder().getStatusBuilder()
.setCode(StatusCode.INVALID_REQUEST);
context.getCommandBuilder().getStatusBuilder()
.setStatusMessage(ire.getMessage());
this.saveFailedRequestContext(context);
close();
throw ire;
} catch (KVSecurityException kse) {
logger.log(Level.WARNING, kse.getMessage(), kse);
// set status code and message
context.getCommandBuilder().getStatusBuilder()
.setCode(StatusCode.NOT_AUTHORIZED);
context.getCommandBuilder().getStatusBuilder()
.setStatusMessage(kse.getMessage());
this.saveFailedRequestContext(context);
close();
throw kse;
} catch (KVStoreVersionMismatch vmismatch) {
logger.log(Level.WARNING, vmismatch.getMessage(), vmismatch);
// set status code and message
context.getCommandBuilder().getStatusBuilder()
.setCode(StatusCode.VERSION_MISMATCH);
context.getCommandBuilder().getStatusBuilder()
.setStatusMessage(vmismatch.getMessage());
this.saveFailedRequestContext(context);
close();
throw vmismatch;
} catch (KVStoreNotFound kvsne) {
logger.log(Level.WARNING, StatusCode.NOT_FOUND.toString(), kvsne);
// set status code
context.getCommandBuilder().getStatusBuilder()
.setCode(StatusCode.NOT_FOUND);
// set status message
context.getCommandBuilder().getStatusBuilder()
.setStatusMessage("key not found");
this.saveFailedRequestContext(context);
close();
throw kvsne;
} catch (KVStoreException kvse) {
logger.log(Level.WARNING, kvse.getMessage(), kvse);
context.getCommandBuilder().getStatusBuilder()
.setCode(StatusCode.INTERNAL_ERROR);
context.getCommandBuilder().getStatusBuilder()
.setStatusMessage(kvse.getMessage());
this.saveFailedRequestContext(context);
close();
throw kvse;
} catch (Exception e) {
logger.log(Level.WARNING, e.getMessage(), e);
context.getCommandBuilder().getStatusBuilder()
.setCode(StatusCode.INVALID_BATCH);
context.getCommandBuilder().getStatusBuilder()
.setStatusMessage(e.getMessage());
this.saveFailedRequestContext(context);
close();
}
}
private void saveFailedRequestContext(RequestContext context) {
if (batchException == null) {
this.batchException = new InvalidBatchException();
this.batchException.setFailedRequestContext(context);
}
}
private void addSequenceList(RequestContext context) {
this.sequenceList.add(context.getRequestMessage().getCommand().getHeader()
.getSequence());
}
private void checkBatch(RequestContext context)
throws InvalidBatchException, NotAttemptedException {
if (this.batch == null) {
String msg = "batch is not started or has ended";
if (context.getMessageType() == MessageType.END_BATCH) {
// batch had failed earlier
if (this.batchException != null) {
// get saved failed status code and message
StatusCode code = this.batchException
.getFailedRequestContext().getResponseMessage()
.getCommand().getStatus().getCode();
String smessage = this.batchException
.getFailedRequestContext().getResponseMessage()
.getCommand().getStatus().getStatusMessage();
// set failed status code
context.getCommandBuilder().getStatusBuilder()
.setCode(code);
// set status message
context.getCommandBuilder().getStatusBuilder()
.setStatusMessage(smessage);
// get failed request message sequence
long failedSeq = this.batchException
.getFailedRequestContext().getRequestMessage()
.getCommand().getHeader().getSequence();
// set failed request sequence
context.getCommandBuilder().getBodyBuilder()
.getBatchBuilder().setFailedSequence(failedSeq);
/**
* get batch construct for END_BATCH_RESPONSE message
*/
Batch.Builder bb = context.getCommandBuilder()
.getBodyBuilder().getBatchBuilder();
/**
* add sequence list to batch construct
*/
for (Long sequence : this.sequenceList) {
bb.addSequence(sequence.longValue());
}
// propagate the exception
throw new InvalidBatchException(smessage);
} else {
throw new InvalidBatchException(msg);
}
} else {
throw new NotAttemptedException(msg);
}
}
// check if request is from the same batch
if (context.getRequestMessage().getCommand().getHeader()
.getConnectionID() != this.cid
|| context.getRequestMessage().getCommand().getHeader()
.getBatchID() != this.batchId) {
throw new RuntimeException("DB is locked by: "
+ cid
+ "-"
+ batchId
+ ", request id: "
+ context.getRequestMessage().getCommand().getHeader()
.getConnectionID()
+ context.getRequestMessage().getCommand().getHeader()
.getBatchID());
}
}
private void batchDelete(KineticMessage km) throws KVStoreException,
InvalidRequestException, KVSecurityException {
KVOp.checkWrite(engine.getAclMap(), km, Permission.DELETE);
// proto request KV
KeyValue requestKeyValue = km.getCommand().getBody().getKeyValue();
ByteString key = requestKeyValue.getKey();
// check version if required
if (requestKeyValue.getForce() == false) {
checkVersion(km);
}
// batch delete entry
batch.delete(key);
}
private void batchPut(KineticMessage km) throws KVStoreException,
InvalidRequestException, KVSecurityException {
KVOp.checkWrite(engine.getAclMap(), km, Permission.WRITE);
ByteString key = km.getCommand().getBody().getKeyValue().getKey();
ByteString valueByteString = null;
if (km.getValue() != null) {
valueByteString = ByteString.copyFrom(km.getValue());
} else {
// set value to empty if null
valueByteString = ByteString.EMPTY;
}
// proto request KV
KeyValue requestKeyValue = km.getCommand().getBody().getKeyValue();
// check version if required
if (requestKeyValue.getForce() == false) {
batchPutCheckVersion(km);
}
// construct store KV
Algorithm al = null;
if (requestKeyValue.hasAlgorithm()) {
al = requestKeyValue.getAlgorithm();
}
KVValue data = new KVValue(requestKeyValue.getKey(),
requestKeyValue.getNewVersion(), requestKeyValue.getTag(), al,
valueByteString);
// batch put
batch.put(key, data);
}
private synchronized void commitBatch(RequestContext context) {
try {
/**
* db commit batch
*/
batch.commit();
/**
* add sequence sequenceList to end batch response message
*/
Command.Builder cb = context.getCommandBuilder();
/**
* add sequence list to batch construct
*/
for (Long sequence : sequenceList) {
cb.getBodyBuilder().getBatchBuilder()
.addSequence(sequence.longValue());
}
} finally {
this.close();
}
}
/**
* close the current batch operation.
*/
public synchronized void close() {
if (this.batch == null) {
return;
}
try {
// close db batch
batch.close();
} catch (Exception e) {
logger.log(Level.WARNING, e.getMessage(), e);
} finally {
batch = null;
this.notifyAll();
}
}
private static void compareVersion(ByteString storeDbVersion,
ByteString requestDbVersion) throws KVStoreVersionMismatch {
if (mySize(storeDbVersion) != mySize(requestDbVersion)) {
throw new KVStoreVersionMismatch("Version mismatch");
}
if (mySize(storeDbVersion) == 0) {
return;
}
if (!storeDbVersion.equals(requestDbVersion)) {
throw new KVStoreVersionMismatch("Version mismatch");
}
}
private static int mySize(ByteString s) {
if (s == null)
return 0;
return s.size();
}
private void batchPutCheckVersion(KineticMessage km)
throws KVStoreException {
KeyValue requestKeyValue = km.getCommand().getBody().getKeyValue();
ByteString requestDbVersion = requestKeyValue.getDbVersion();
ByteString key = requestKeyValue.getKey();
ByteString storeDbVersion = null;
try {
storeDbVersion = this.getDbVersion(key);
} catch (KVStoreException kvne) {
/**
* check if new entry
*/
if (kvne instanceof KVStoreNotFound) {
if (requestDbVersion == null
|| requestDbVersion == ByteString.EMPTY) {
/**
* new entry to put in store.
*/
return;
}
}
// db error, re-throw exception
throw kvne;
}
// compare request version with store version
compareVersion(storeDbVersion, requestDbVersion);
}
private void checkVersion(KineticMessage km) throws KVStoreException {
KeyValue requestKeyValue = km.getCommand().getBody().getKeyValue();
ByteString requestDbVersion = requestKeyValue.getDbVersion();
ByteString key = requestKeyValue.getKey();
ByteString storeDbVersion = this.getDbVersion(key);
// compare version with store
compareVersion(storeDbVersion, requestDbVersion);
}
@SuppressWarnings("unchecked")
private ByteString getDbVersion(ByteString key) throws KVStoreException {
KVValue storeKv = null;
ByteString storeDbVersion = null;
storeKv = (KVValue) store.get(key);
storeDbVersion = storeKv.getVersion();
return storeDbVersion;
}
public synchronized boolean isClosed() {
return (this.batch == null);
}
public long getConnectionId() {
return this.cid;
}
public int getBatchId() {
return this.batchId;
}
}