import com.google.common.base.Optional;
import com.google.common.util.concurrent.CheckedFuture;
-import com.google.common.util.concurrent.ListenableFuture;
import org.opendaylight.controller.md.sal.common.api.data.AsyncDataBroker.DataChangeScope;
import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
-import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import static org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType.CONFIGURATION;
private NormalizedNode<?, ?> readDataViaTransaction(final DOMDataReadTransaction transaction,
LogicalDatastoreType datastore, YangInstanceIdentifier path) {
LOG.trace("Read " + datastore.name() + " via Restconf: {}", path);
- final ListenableFuture<Optional<NormalizedNode<?, ?>>> listenableFuture = transaction.read(datastore, path);
- if (listenableFuture != null) {
- Optional<NormalizedNode<?, ?>> optional;
- try {
- LOG.debug("Reading result data from transaction.");
- optional = listenableFuture.get();
- } catch (InterruptedException | ExecutionException e) {
- throw new RestconfDocumentedException("Problem to get data from transaction.", e.getCause());
+ final CheckedFuture<Optional<NormalizedNode<?, ?>>, ReadFailedException> listenableFuture =
+ transaction.read(datastore, path);
- }
- if (optional != null) {
- if (optional.isPresent()) {
- return optional.get();
- }
- }
+ try {
+ Optional<NormalizedNode<?, ?>> optional = listenableFuture.checkedGet();
+ return optional.isPresent() ? optional.get() : null;
+ } catch(ReadFailedException e) {
+ throw new RestconfDocumentedException(e.getMessage(), e, e.getErrorList());
}
- return null;
}
private CheckedFuture<Void, TransactionCommitFailedException> postDataViaTransaction(
final DOMDataReadWriteTransaction rWTransaction, final LogicalDatastoreType datastore,
final YangInstanceIdentifier path, final NormalizedNode<?, ?> payload, DataNormalizationOperation<?> root) {
- ListenableFuture<Optional<NormalizedNode<?, ?>>> futureDatastoreData = rWTransaction.read(datastore, path);
+ CheckedFuture<Optional<NormalizedNode<?, ?>>, ReadFailedException> futureDatastoreData =
+ rWTransaction.read(datastore, path);
try {
- final Optional<NormalizedNode<?, ?>> optionalDatastoreData = futureDatastoreData.get();
+ final Optional<NormalizedNode<?, ?>> optionalDatastoreData = futureDatastoreData.checkedGet();
if (optionalDatastoreData.isPresent() && payload.equals(optionalDatastoreData.get())) {
- String errMsg = "Post Configuration via Restconf was not executed because data already exists";
- LOG.trace(errMsg + ":{}", path);
+ LOG.trace("Post Configuration via Restconf was not executed because data already exists :{}", path);
throw new RestconfDocumentedException("Data already exists for path: " + path, ErrorType.PROTOCOL,
ErrorTag.DATA_EXISTS);
}
- } catch (InterruptedException | ExecutionException e) {
- LOG.trace("It wasn't possible to get data loaded from datastore at path " + path);
+ } catch(ReadFailedException e) {
+ LOG.warn("Error reading from datastore with path: " + path, e);
}
ensureParentsByMerge(datastore, path, rWTransaction, root);
try {
currentOp = currentOp.getChild(currentArg);
} catch (DataNormalizationException e) {
- throw new IllegalArgumentException(
- String.format("Invalid child encountered in path %s", normalizedPath), e);
+ throw new RestconfDocumentedException(
+ String.format("Error normalizing data for path %s", normalizedPath), e);
}
currentArguments.add(currentArg);
YangInstanceIdentifier currentPath = YangInstanceIdentifier.create(currentArguments);
- final Boolean exists;
-
try {
- CheckedFuture<Boolean, ReadFailedException> future =
- rwTx.exists(store, currentPath);
- exists = future.checkedGet();
+ boolean exists = rwTx.exists(store, currentPath).checkedGet();
+ if (!exists && iterator.hasNext()) {
+ rwTx.merge(store, currentPath, currentOp.createDefault(currentArg));
+ }
} catch (ReadFailedException e) {
LOG.error("Failed to read pre-existing data from store {} path {}", store, currentPath, e);
- throw new IllegalStateException("Failed to read pre-existing data", e);
- }
-
-
- if (!exists && iterator.hasNext()) {
- rwTx.merge(store, currentPath, currentOp.createDefault(currentArg));
+ throw new RestconfDocumentedException("Failed to read pre-existing data", e);
}
}
}
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+import java.util.Collection;
import java.util.List;
+
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response.Status;
+
import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorTag;
import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorType;
+import org.opendaylight.yangtools.yang.common.RpcError;
/**
* Unchecked exception to communicate error information, as defined in the ietf restcong draft, to be sent to the
}
/**
- * Constructs an instance with an error message and exception cause. The stack trace of the exception is included in
- * the error info.
+ * Constructs an instance with an error message and exception cause.
+ * The stack trace of the exception is included in the error info.
*
* @param message
* A string which provides a plain text string describing the error.
/**
* Constructs an instance with the given errors.
*/
- public RestconfDocumentedException(List<RestconfError> errors) {
- this.errors = ImmutableList.copyOf(errors);
- Preconditions.checkArgument(!this.errors.isEmpty(), "RestconfError list can't be empty");
+ public RestconfDocumentedException(String message, Throwable cause, List<RestconfError> errors) {
+ super(message, cause);
+ if(!errors.isEmpty()) {
+ this.errors = ImmutableList.copyOf(errors);
+ } else {
+ this.errors = ImmutableList.of(new RestconfError(RestconfError.ErrorType.APPLICATION,
+ RestconfError.ErrorTag.OPERATION_FAILED, message));
+ }
+
status = null;
}
+ /**
+ * Constructs an instance with the given RpcErrors.
+ */
+ public RestconfDocumentedException(String message, Throwable cause, Collection<RpcError> rpcErrors) {
+ this(message, cause, convertToRestconfErrors(rpcErrors));
+ }
+
/**
* Constructs an instance with an HTTP status and no error information.
*
status = null;
}
+ private static List<RestconfError> convertToRestconfErrors(Collection<RpcError> rpcErrors) {
+ List<RestconfError> errorList = Lists.newArrayList();
+ if(rpcErrors != null) {
+ for (RpcError rpcError : rpcErrors) {
+ errorList.add(new RestconfError(rpcError));
+ }
+ }
+
+ return errorList;
+ }
+
+
public List<RestconfError> getErrors() {
return errors;
}
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import org.apache.commons.lang3.StringUtils;
import org.opendaylight.controller.md.sal.common.api.data.AsyncDataBroker.DataChangeScope;
import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
+import org.opendaylight.controller.md.sal.common.api.data.OptimisticLockFailedException;
+import org.opendaylight.controller.md.sal.common.api.data.TransactionCommitFailedException;
import org.opendaylight.controller.md.sal.common.impl.util.compat.DataNormalizer;
import org.opendaylight.controller.md.sal.dom.api.DOMMountPoint;
import org.opendaylight.controller.sal.rest.api.Draft02;
import org.opendaylight.yangtools.concepts.Codec;
import org.opendaylight.yangtools.yang.common.QName;
import org.opendaylight.yangtools.yang.common.QNameModule;
-import org.opendaylight.yangtools.yang.common.RpcError;
import org.opendaylight.yangtools.yang.common.RpcResult;
import org.opendaylight.yangtools.yang.data.api.CompositeNode;
import org.opendaylight.yangtools.yang.data.api.MutableCompositeNode;
private void checkRpcSuccessAndThrowException(final RpcResult<CompositeNode> rpcResult) {
if (rpcResult.isSuccessful() == false) {
- Collection<RpcError> rpcErrors = rpcResult.getErrors();
- if (rpcErrors == null || rpcErrors.isEmpty()) {
- throw new RestconfDocumentedException(
- "The operation was not successful and there were no RPC errors returned", ErrorType.RPC,
- ErrorTag.OPERATION_FAILED);
- }
-
- List<RestconfError> errorList = Lists.newArrayList();
- for (RpcError rpcError : rpcErrors) {
- errorList.add(new RestconfError(rpcError));
- }
-
- throw new RestconfDocumentedException(errorList);
+ throw new RestconfDocumentedException("The operation was not successful", null,
+ rpcResult.getErrors());
}
}
iiWithData.getSchemaNode());
YangInstanceIdentifier normalizedII;
+ if (mountPoint != null) {
+ normalizedII = new DataNormalizer(mountPoint.getSchemaContext()).toNormalized(
+ iiWithData.getInstanceIdentifier());
+ } else {
+ normalizedII = controllerContext.toNormalized(iiWithData.getInstanceIdentifier());
+ }
- try {
- if (mountPoint != null) {
- normalizedII = new DataNormalizer(mountPoint.getSchemaContext()).toNormalized(iiWithData
- .getInstanceIdentifier());
- broker.commitConfigurationDataPut(mountPoint, normalizedII, datastoreNormalizedNode).get();
- } else {
- normalizedII = controllerContext.toNormalized(iiWithData.getInstanceIdentifier());
- broker.commitConfigurationDataPut(normalizedII, datastoreNormalizedNode).get();
+ /*
+ * There is a small window where another write transaction could be updating the same data
+ * simultaneously and we get an OptimisticLockFailedException. This error is likely
+ * transient and The WriteTransaction#submit API docs state that a retry will likely
+ * succeed. So we'll try again if that scenario occurs. If it fails a third time then it
+ * probably will never succeed so we'll fail in that case.
+ *
+ * By retrying we're attempting to hide the internal implementation of the data store and
+ * how it handles concurrent updates from the restconf client. The client has instructed us
+ * to put the data and we should make every effort to do so without pushing optimistic lock
+ * failures back to the client and forcing them to handle it via retry (and having to
+ * document the behavior).
+ */
+ int tries = 2;
+ while(true) {
+ try {
+ if (mountPoint != null) {
+ broker.commitConfigurationDataPut(mountPoint, normalizedII,
+ datastoreNormalizedNode).checkedGet();
+ } else {
+ broker.commitConfigurationDataPut(normalizedII,
+ datastoreNormalizedNode).checkedGet();
+ }
+
+ break;
+ } catch (TransactionCommitFailedException e) {
+ if(e instanceof OptimisticLockFailedException) {
+ if(--tries <= 0) {
+ LOG.debug("Got OptimisticLockFailedException on last try - failing");
+ throw new RestconfDocumentedException(e.getMessage(), e, e.getErrorList());
+ }
+
+ LOG.debug("Got OptimisticLockFailedException - trying again");
+ } else {
+ throw new RestconfDocumentedException(e.getMessage(), e, e.getErrorList());
+ }
}
- } catch (Exception e) {
- throw new RestconfDocumentedException("Error updating data", e);
}
return Response.status(Status.OK).build();
normalizedII = controllerContext.toNormalized(iiWithData.getInstanceIdentifier());
broker.commitConfigurationDataPost(normalizedII, datastoreNormalizedData);
}
+ } catch(RestconfDocumentedException e) {
+ throw e;
} catch (Exception e) {
throw new RestconfDocumentedException("Error creating data", e);
}
normalizedII = controllerContext.toNormalized(iiWithData.getInstanceIdentifier());
broker.commitConfigurationDataPost(normalizedII, datastoreNormalizedData);
}
+ } catch(RestconfDocumentedException e) {
+ throw e;
} catch (Exception e) {
throw new RestconfDocumentedException("Error creating data", e);
}
private CompositeNode normalizeNode(final Node<?> node, final DataSchemaNode schema, final DOMMountPoint mountPoint) {
if (schema == null) {
- QName nodeType = node == null ? null : node.getNodeType();
- String localName = nodeType == null ? null : nodeType.getLocalName();
+ String localName = node == null ? null :
+ node instanceof NodeWrapper ? ((NodeWrapper<?>)node).getLocalName() :
+ node.getNodeType().getLocalName();
throw new RestconfDocumentedException("Data schema node was not found for " + localName,
ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
restconfImpl.invokeRpc("toaster:cancel-toast", "", uriInfo);
fail("Expected an exception to be thrown.");
} catch (RestconfDocumentedException e) {
- verifyRestconfDocumentedException(e, 0, ErrorType.RPC, ErrorTag.OPERATION_FAILED,
+ verifyRestconfDocumentedException(e, 0, ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED,
Optional.<String> absent(), Optional.<String> absent());
}
}
import com.google.common.base.Optional;
import com.google.common.util.concurrent.CheckedFuture;
+
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
+
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
+
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.BeforeClass;
import org.junit.Test;
+import org.opendaylight.controller.md.sal.common.api.data.OptimisticLockFailedException;
import org.opendaylight.controller.md.sal.common.api.data.TransactionCommitFailedException;
import org.opendaylight.controller.md.sal.dom.api.DOMMountPoint;
import org.opendaylight.controller.md.sal.dom.api.DOMMountPointService;
assertEquals(200, put(uri, MediaType.APPLICATION_XML, xmlData3));
}
+ @Test
+ public void putWithOptimisticLockFailedException() throws UnsupportedEncodingException {
+
+ String uri = "/config/ietf-interfaces:interfaces/interface/eth0";
+
+ doThrow(OptimisticLockFailedException.class).
+ when(brokerFacade).commitConfigurationDataPut(
+ any(YangInstanceIdentifier.class), any(NormalizedNode.class));
+
+ assertEquals(500, put(uri, MediaType.APPLICATION_XML, xmlData));
+
+ doThrow(OptimisticLockFailedException.class).doReturn(mock(CheckedFuture.class)).
+ when(brokerFacade).commitConfigurationDataPut(
+ any(YangInstanceIdentifier.class), any(NormalizedNode.class));
+
+ assertEquals(200, put(uri, MediaType.APPLICATION_XML, xmlData));
+ }
+
+ @Test
+ public void putWithTransactionCommitFailedException() throws UnsupportedEncodingException {
+
+ String uri = "/config/ietf-interfaces:interfaces/interface/eth0";
+
+ doThrow(TransactionCommitFailedException.class).
+ when(brokerFacade).commitConfigurationDataPut(
+ any(YangInstanceIdentifier.class), any(NormalizedNode.class));
+
+ assertEquals(500, put(uri, MediaType.APPLICATION_XML, xmlData));
+ }
+
private int put(String uri, String mediaType, String data) throws UnsupportedEncodingException {
return target(uri).request(mediaType).put(Entity.entity(data, mediaType)).getStatus();
}
List<RestconfError> errorList = Arrays.asList(new RestconfError(ErrorType.APPLICATION, ErrorTag.LOCK_DENIED,
"mock error1"), new RestconfError(ErrorType.RPC, ErrorTag.ROLLBACK_FAILED, "mock error2"));
- stageMockEx(new RestconfDocumentedException(errorList));
+ stageMockEx(new RestconfDocumentedException("mock", null, errorList));
Response resp = target("/operational/foo").request(MediaType.APPLICATION_JSON).get();
List<RestconfError> errorList = Arrays.asList(new RestconfError(ErrorType.APPLICATION, ErrorTag.LOCK_DENIED,
"mock error1"), new RestconfError(ErrorType.RPC, ErrorTag.ROLLBACK_FAILED, "mock error2"));
- stageMockEx(new RestconfDocumentedException(errorList));
+ stageMockEx(new RestconfDocumentedException("mock", null, errorList));
Response resp = target("/operational/foo").request(MediaType.APPLICATION_XML).get();