Transaction message retry when no shard leader present
[controller.git] / opendaylight / md-sal / sal-distributed-datastore / src / main / java / org / opendaylight / controller / cluster / datastore / Shard.java
index 5025369907e46c7e9b226639f6066220590f333d..5b8a50e18896c0a3e10db64c10b1d6fdffdc41d8 100644 (file)
@@ -12,14 +12,13 @@ import akka.actor.ActorRef;
 import akka.actor.ActorSelection;
 import akka.actor.Cancellable;
 import akka.actor.Props;
-import akka.japi.Creator;
 import akka.persistence.RecoveryFailure;
 import akka.serialization.Serialization;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
 import java.io.IOException;
-import java.util.HashMap;
+import java.util.Collections;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import javax.annotation.Nonnull;
@@ -40,7 +39,10 @@ import org.opendaylight.controller.cluster.datastore.messages.CommitTransaction;
 import org.opendaylight.controller.cluster.datastore.messages.CommitTransactionReply;
 import org.opendaylight.controller.cluster.datastore.messages.CreateTransaction;
 import org.opendaylight.controller.cluster.datastore.messages.CreateTransactionReply;
+import org.opendaylight.controller.cluster.datastore.messages.DatastoreSnapshot;
+import org.opendaylight.controller.cluster.datastore.messages.DatastoreSnapshot.ShardSnapshot;
 import org.opendaylight.controller.cluster.datastore.messages.ForwardedReadyTransaction;
+import org.opendaylight.controller.cluster.datastore.messages.GetShardDataTree;
 import org.opendaylight.controller.cluster.datastore.messages.PeerAddressResolved;
 import org.opendaylight.controller.cluster.datastore.messages.ReadyLocalTransaction;
 import org.opendaylight.controller.cluster.datastore.messages.RegisterChangeListener;
@@ -61,12 +63,14 @@ import org.opendaylight.controller.cluster.raft.RaftActorSnapshotCohort;
 import org.opendaylight.controller.cluster.raft.RaftState;
 import org.opendaylight.controller.cluster.raft.base.messages.FollowerInitialSyncUpStatus;
 import org.opendaylight.controller.cluster.raft.messages.AppendEntriesReply;
+import org.opendaylight.controller.cluster.raft.messages.ServerRemoved;
 import org.opendaylight.controller.cluster.raft.protobuff.client.messages.CompositeModificationByteStringPayload;
 import org.opendaylight.controller.cluster.raft.protobuff.client.messages.CompositeModificationPayload;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTree;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidate;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataValidationFailedException;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.ModificationType;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.TreeType;
 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
 import scala.concurrent.duration.Duration;
 import scala.concurrent.duration.FiniteDuration;
@@ -114,19 +118,24 @@ public class Shard extends RaftActor {
     private final DataTreeChangeListenerSupport treeChangeSupport = new DataTreeChangeListenerSupport(this);
     private final DataChangeListenerSupport changeSupport = new DataChangeListenerSupport(this);
 
-    protected Shard(final ShardIdentifier name, final Map<String, String> peerAddresses,
-            final DatastoreContext datastoreContext, final SchemaContext schemaContext) {
-        super(name.toString(), new HashMap<>(peerAddresses), Optional.of(datastoreContext.getShardRaftConfig()),
-                DataStoreVersions.CURRENT_VERSION);
 
-        this.name = name.toString();
-        this.datastoreContext = datastoreContext;
+    private ShardSnapshot restoreFromSnapshot;
+
+    private final ShardTransactionMessageRetrySupport messageRetrySupport;
+
+    protected Shard(AbstractBuilder<?, ?> builder) {
+        super(builder.getId().toString(), builder.getPeerAddresses(),
+                Optional.of(builder.getDatastoreContext().getShardRaftConfig()), DataStoreVersions.CURRENT_VERSION);
+
+        this.name = builder.getId().toString();
+        this.datastoreContext = builder.getDatastoreContext();
+        this.restoreFromSnapshot = builder.getRestoreFromSnapshot();
 
         setPersistence(datastoreContext.isPersistent());
 
         LOG.info("Shard created : {}, persistent : {}", name, datastoreContext.isPersistent());
 
-        store = new ShardDataTree(schemaContext);
+        store = new ShardDataTree(builder.getSchemaContext(), builder.getTreeType());
 
         shardMBean = ShardMBeanFactory.getShardStatsMBean(name.toString(),
                 datastoreContext.getDataStoreMXBeanType());
@@ -138,7 +147,7 @@ public class Shard extends RaftActor {
 
         commitCoordinator = new ShardCommitCoordinator(store,
                 datastoreContext.getShardCommitQueueExpiryTimeoutInMillis(),
-                datastoreContext.getShardTransactionCommitQueueCapacity(), self(), LOG, this.name);
+                datastoreContext.getShardTransactionCommitQueueCapacity(), LOG, this.name);
 
         setTransactionCommitTimeout();
 
@@ -154,7 +163,7 @@ public class Shard extends RaftActor {
 
         snapshotCohort = new ShardSnapshotCohort(transactionActorFactory, store, LOG, this.name);
 
-
+        messageRetrySupport = new ShardTransactionMessageRetrySupport(this);
     }
 
     private void setTransactionCommitTimeout() {
@@ -162,11 +171,6 @@ public class Shard extends RaftActor {
                 datastoreContext.getShardTransactionCommitTimeoutInSeconds(), TimeUnit.SECONDS) / 2;
     }
 
-    public static Props props(final ShardIdentifier name, final Map<String, String> peerAddresses,
-            final DatastoreContext datastoreContext, final SchemaContext schemaContext) {
-        return Props.create(new ShardCreator(name, peerAddresses, datastoreContext, schemaContext));
-    }
-
     private Optional<ActorRef> createRoleChangeNotifier(String shardId) {
         ActorRef shardRoleChangeNotifier = this.getContext().actorOf(
             RoleChangeNotifier.getProps(shardId), shardId + "-notifier");
@@ -179,6 +183,8 @@ public class Shard extends RaftActor {
 
         super.postStop();
 
+        messageRetrySupport.close();
+
         if(txCommitTimeoutCheckSchedule != null) {
             txCommitTimeoutCheckSchedule.cancel();
         }
@@ -224,8 +230,7 @@ public class Shard extends RaftActor {
             } else if (BatchedModifications.class.isInstance(message)) {
                 handleBatchedModifications((BatchedModifications)message);
             } else if (message instanceof ForwardedReadyTransaction) {
-                commitCoordinator.handleForwardedReadyTransaction((ForwardedReadyTransaction) message,
-                        getSender(), this);
+                handleForwardedReadyTransaction((ForwardedReadyTransaction) message);
             } else if (message instanceof ReadyLocalTransaction) {
                 handleReadyLocalTransaction((ReadyLocalTransaction)message);
             } else if (CanCommitTransaction.SERIALIZABLE_CLASS.isInstance(message)) {
@@ -257,6 +262,12 @@ public class Shard extends RaftActor {
                 context().parent().tell(message, self());
             } else if(GET_SHARD_MBEAN_MESSAGE.equals(message)){
                 sender().tell(getShardMBean(), self());
+            } else if(message instanceof GetShardDataTree) {
+                sender().tell(store.getDataTree(), self());
+            } else if(message instanceof ServerRemoved){
+                context().parent().forward(message, context());
+            } else if(ShardTransactionMessageRetrySupport.TIMER_MESSAGE_CLASS.isInstance(message)) {
+                messageRetrySupport.onTimerMessage(message);
             } else {
                 super.onReceiveCommand(message);
             }
@@ -319,7 +330,7 @@ public class Shard extends RaftActor {
         return ModificationType.UNMODIFIED.equals(candidate.getRootNode().getModificationType());
     }
 
-    void continueCommit(final CohortEntry cohortEntry) throws Exception {
+    void continueCommit(final CohortEntry cohortEntry) {
         final DataTreeCandidate candidate = cohortEntry.getCandidate();
 
         // If we do not have any followers and we are not using persistence
@@ -329,7 +340,7 @@ public class Shard extends RaftActor {
             applyModificationToState(cohortEntry.getReplySender(), cohortEntry.getTransactionID(), candidate);
         } else {
             Shard.this.persistData(cohortEntry.getReplySender(), cohortEntry.getTransactionID(),
-                DataTreeCandidatePayload.create(candidate));
+                    DataTreeCandidatePayload.create(candidate));
         }
     }
 
@@ -402,12 +413,6 @@ public class Shard extends RaftActor {
         commitCoordinator.handleCanCommit(canCommit.getTransactionID(), getSender(), this);
     }
 
-    private void noLeaderError(String errMessage, Object message) {
-        // TODO: rather than throwing an immediate exception, we could schedule a timer to try again to make
-        // it more resilient in case we're in the process of electing a new leader.
-        getSender().tell(new akka.actor.Status.Failure(new NoShardLeaderException(errMessage, persistenceId())), getSelf());
-    }
-
     protected void handleBatchedModificationsLocal(BatchedModifications batched, ActorRef sender) {
         try {
             commitCoordinator.handleBatchedModifications(batched, sender, this);
@@ -444,7 +449,8 @@ public class Shard extends RaftActor {
                 LOG.debug("{}: Forwarding BatchedModifications to leader {}", persistenceId(), leader);
                 leader.forward(batched, getContext());
             } else {
-                noLeaderError("Could not commit transaction " + batched.getTransactionID(), batched);
+                messageRetrySupport.addMessageToRetry(batched, getSender(),
+                        "Could not commit transaction " + batched.getTransactionID());
             }
         }
     }
@@ -483,7 +489,29 @@ public class Shard extends RaftActor {
                 message.setRemoteVersion(getCurrentBehavior().getLeaderPayloadVersion());
                 leader.forward(message, getContext());
             } else {
-                noLeaderError("Could not commit transaction " + message.getTransactionID(), message);
+                messageRetrySupport.addMessageToRetry(message, getSender(),
+                        "Could not commit transaction " + message.getTransactionID());
+            }
+        }
+    }
+
+    private void handleForwardedReadyTransaction(ForwardedReadyTransaction forwardedReady) {
+        if (isLeader()) {
+            failIfIsolatedLeader(getSender());
+
+            commitCoordinator.handleForwardedReadyTransaction(forwardedReady, getSender(), this);
+        } else {
+            ActorSelection leader = getLeader();
+            if (leader != null) {
+                LOG.debug("{}: Forwarding ForwardedReadyTransaction to leader {}", persistenceId(), leader);
+
+                ReadyLocalTransaction readyLocal = new ReadyLocalTransaction(forwardedReady.getTransactionID(),
+                        forwardedReady.getTransaction().getSnapshot(), forwardedReady.isDoImmediateCommit());
+                readyLocal.setRemoteVersion(getCurrentBehavior().getLeaderPayloadVersion());
+                leader.forward(readyLocal, getContext());
+            } else {
+                messageRetrySupport.addMessageToRetry(forwardedReady, getSender(),
+                        "Could not commit transaction " + forwardedReady.getTransactionID());
             }
         }
     }
@@ -589,11 +617,14 @@ public class Shard extends RaftActor {
     @Override
     @Nonnull
     protected RaftActorRecoveryCohort getRaftActorRecoveryCohort() {
-        return new ShardRecoveryCoordinator(store, store.getSchemaContext(), persistenceId(), LOG);
+        return new ShardRecoveryCoordinator(store, store.getSchemaContext(),
+                restoreFromSnapshot != null ? restoreFromSnapshot.getSnapshot() : null, persistenceId(), LOG);
     }
 
     @Override
     protected void onRecoveryComplete() {
+        restoreFromSnapshot = null;
+
         //notify shard manager
         getContext().parent().tell(new ActorInitialized(), getSelf());
 
@@ -680,6 +711,10 @@ public class Shard extends RaftActor {
     @Override
     protected void onLeaderChanged(String oldLeader, String newLeader) {
         shardMBean.incrementLeadershipChangeCount();
+
+        if(hasLeader()) {
+            messageRetrySupport.retryMessages();
+        }
     }
 
     @Override
@@ -696,44 +731,120 @@ public class Shard extends RaftActor {
         return datastoreContext;
     }
 
-    protected abstract static class AbstractShardCreator implements Creator<Shard> {
-        private static final long serialVersionUID = 1L;
+    @VisibleForTesting
+    public ShardDataTree getDataStore() {
+        return store;
+    }
 
-        protected final ShardIdentifier name;
-        protected final Map<String, String> peerAddresses;
-        protected final DatastoreContext datastoreContext;
-        protected final SchemaContext schemaContext;
+    @VisibleForTesting
+    ShardStats getShardMBean() {
+        return shardMBean;
+    }
 
-        protected AbstractShardCreator(final ShardIdentifier name, final Map<String, String> peerAddresses,
-                final DatastoreContext datastoreContext, final SchemaContext schemaContext) {
-            this.name = Preconditions.checkNotNull(name, "name should not be null");
-            this.peerAddresses = Preconditions.checkNotNull(peerAddresses, "peerAddresses should not be null");
-            this.datastoreContext = Preconditions.checkNotNull(datastoreContext, "dataStoreContext should not be null");
-            this.schemaContext = Preconditions.checkNotNull(schemaContext, "schemaContext should not be null");
-        }
+    public static Builder builder() {
+        return new Builder();
     }
 
-    private static class ShardCreator extends AbstractShardCreator {
-        private static final long serialVersionUID = 1L;
+    public static abstract class AbstractBuilder<T extends AbstractBuilder<T, S>, S extends Shard> {
+        private final Class<S> shardClass;
+        private ShardIdentifier id;
+        private Map<String, String> peerAddresses = Collections.emptyMap();
+        private DatastoreContext datastoreContext;
+        private SchemaContext schemaContext;
+        private DatastoreSnapshot.ShardSnapshot restoreFromSnapshot;
+        private volatile boolean sealed;
 
-        ShardCreator(final ShardIdentifier name, final Map<String, String> peerAddresses,
-                final DatastoreContext datastoreContext, final SchemaContext schemaContext) {
-            super(name, peerAddresses, datastoreContext, schemaContext);
+        protected AbstractBuilder(Class<S> shardClass) {
+            this.shardClass = shardClass;
         }
 
-        @Override
-        public Shard create() throws Exception {
-            return new Shard(name, peerAddresses, datastoreContext, schemaContext);
+        protected void checkSealed() {
+            Preconditions.checkState(!sealed, "Builder isalready sealed - further modifications are not allowed");
         }
-    }
 
-    @VisibleForTesting
-    public ShardDataTree getDataStore() {
-        return store;
+        @SuppressWarnings("unchecked")
+        private T self() {
+            return (T) this;
+        }
+
+        public T id(ShardIdentifier id) {
+            checkSealed();
+            this.id = id;
+            return self();
+        }
+
+        public T peerAddresses(Map<String, String> peerAddresses) {
+            checkSealed();
+            this.peerAddresses = peerAddresses;
+            return self();
+        }
+
+        public T datastoreContext(DatastoreContext datastoreContext) {
+            checkSealed();
+            this.datastoreContext = datastoreContext;
+            return self();
+        }
+
+        public T schemaContext(SchemaContext schemaContext) {
+            checkSealed();
+            this.schemaContext = schemaContext;
+            return self();
+        }
+
+        public T restoreFromSnapshot(DatastoreSnapshot.ShardSnapshot restoreFromSnapshot) {
+            checkSealed();
+            this.restoreFromSnapshot = restoreFromSnapshot;
+            return self();
+        }
+
+        public ShardIdentifier getId() {
+            return id;
+        }
+
+        public Map<String, String> getPeerAddresses() {
+            return peerAddresses;
+        }
+
+        public DatastoreContext getDatastoreContext() {
+            return datastoreContext;
+        }
+
+        public SchemaContext getSchemaContext() {
+            return schemaContext;
+        }
+
+        public DatastoreSnapshot.ShardSnapshot getRestoreFromSnapshot() {
+            return restoreFromSnapshot;
+        }
+
+        public TreeType getTreeType() {
+            switch (datastoreContext.getLogicalStoreType()) {
+            case CONFIGURATION:
+                return TreeType.CONFIGURATION;
+            case OPERATIONAL:
+                return TreeType.OPERATIONAL;
+            }
+
+            throw new IllegalStateException("Unhandled logical store type " + datastoreContext.getLogicalStoreType());
+        }
+
+        protected void verify() {
+            Preconditions.checkNotNull(id, "id should not be null");
+            Preconditions.checkNotNull(peerAddresses, "peerAddresses should not be null");
+            Preconditions.checkNotNull(datastoreContext, "dataStoreContext should not be null");
+            Preconditions.checkNotNull(schemaContext, "schemaContext should not be null");
+        }
+
+        public Props props() {
+            sealed = true;
+            verify();
+            return Props.create(shardClass, this);
+        }
     }
 
-    @VisibleForTesting
-    ShardStats getShardMBean() {
-        return shardMBean;
+    public static class Builder extends AbstractBuilder<Builder, Shard> {
+        private Builder() {
+            super(Shard.class);
+        }
     }
 }