+
+ /**
+ * Reset the data change listener to its initial status.
+ * By resetting the listener we will be able to recover all the data lost before
+ */
+ @VisibleForTesting
+ protected synchronized void resetListener() {
+ requireNonNull(this.listenerRegistration, "Listener on topology " + this + " hasn't been initialized.");
+ LOG.debug("Resetting data change listener for topology builder {}", getInstanceIdentifier());
+ // unregister current listener to prevent incoming data tree change first
+ unregisterDataChangeListener();
+ // create new transaction chain to reset the chain status
+ resetTransactionChain();
+ // reset the operational topology data so that we can have clean status
+ destroyOperationalTopology();
+ initOperationalTopology();
+ // re-register the data change listener to reset the operational topology
+ // we are expecting to receive all the pre-exist route change on the next onDataTreeChanged() call
+ registerDataChangeListener();
+ }
+
+ /**
+ * Reset the transaction chain only so that the PingPong transaction chain will become usable again.
+ * However, there will be data loss if we do not apply the previous failed transaction again
+ */
+ @VisibleForTesting
+ protected synchronized void resetTransactionChain() {
+ LOG.debug("Resetting transaction chain for topology builder {}", getInstanceIdentifier());
+ destroyTransactionChain();
+ initTransactionChain();
+ }
+
+ /**
+ * There are a few reasons we want to schedule a listener restart in a delayed manner:
+ * 1. we should avoid restarting the listener as when the topology is big, there might be huge overhead
+ * rebuilding the whole linkstate topology again and again
+ * 2. the #onTransactionChainFailed() normally get invoked after a delay. During that time gap, more
+ * data changes might still be pushed to #onDataTreeChanged(). And because #onTransactionChainFailed()
+ * is not invoked yet, listener restart/transaction chain restart is not done. Thus the new changes
+ * will still cause error and another #onTransactionChainFailed() might be invoked later. The listener
+ * will be restarted again in that case, which is unexpected. Restarting of transaction chain only introduce
+ * little overhead and it's okay to be restarted within a small time window.
+ * Note: when the listener is restarted, we can disregard all the incoming data changes before the restart is
+ * done, as after the listener unregister/reregister, the first #onDataTreeChanged() call will contain the a
+ * complete set of existing changes
+ *
+ * @return if the listener get restarted, return true; otherwise false
+ */
+ @VisibleForTesting
+ protected synchronized boolean restartTransactionChainOnDemand() {
+ if (this.listenerScheduledRestartTime > 0) {
+ // when the #this.listenerScheduledRestartTime timer timed out we can reset the listener,
+ // otherwise we should only reset the transaction chain
+ if (System.currentTimeMillis() > this.listenerScheduledRestartTime) {
+ // reset the the restart timer
+ this.listenerScheduledRestartTime = 0;
+ this.listenerScheduledRestartEnforceCounter = 0;
+ resetListener();
+ return true;
+ }
+
+ resetTransactionChain();
+ }
+ return false;
+ }
+
+ @VisibleForTesting
+ protected synchronized void scheduleListenerRestart() {
+ if (0 == this.listenerScheduledRestartTime) {
+ this.listenerScheduledRestartTime = System.currentTimeMillis() + this.listenerResetLimitInMillsec;
+ } else if (System.currentTimeMillis() > this.listenerScheduledRestartTime
+ && ++this.listenerScheduledRestartEnforceCounter < this.listenerResetEnforceCounter) {
+ // if the transaction failure happens again, we will delay the listener restart up to
+ // #LISTENER_RESET_LIMIT_IN_MILLSEC times
+ this.listenerScheduledRestartTime += this.listenerResetLimitInMillsec;
+ }
+ LOG.debug("A listener restart was scheduled at {} (current system time is {})",
+ this.listenerScheduledRestartTime, System.currentTimeMillis());
+ }
+
+ @Override
+ public final synchronized void onTransactionChainFailed(final TransactionChain transactionChain,
+ final Transaction transaction, final Throwable cause) {
+ LOG.error("Topology builder for {} failed in transaction {}.", getInstanceIdentifier(),
+ transaction != null ? transaction.getIdentifier() : null, cause);
+ scheduleListenerRestart();
+ restartTransactionChainOnDemand();
+ }
+
+ @Override
+ public final void onTransactionChainSuccessful(final TransactionChain transactionChain) {
+ LOG.info("Topology builder for {} shut down", getInstanceIdentifier());
+ }