protected void applyUndelete(MessageInfo messageInfo, RemoteReplicaInfo remoteReplicaInfo) throws StoreException {
    DataNodeId remoteNode = remoteReplicaInfo.getReplicaId().getDataNodeId();
    try {
      messageInfo = new MessageInfo.Builder(messageInfo).isUndeleted(true).isDeleted(false).build();
      remoteReplicaInfo.getLocalStore().undelete(messageInfo);
      logger.trace("Remote node: {} Thread name: {} Remote replica: {} Key undelete id: {}", remoteNode, threadName,
          remoteReplicaInfo.getReplicaId(), messageInfo.getStoreKey());
    } catch (StoreException e) {
      // The blob may be undeleted, which is alright
      if (e.getErrorCode() == StoreErrorCodes.LifeVersionConflict || e.getErrorCode() == StoreErrorCodes.IDUndeleted) {
        logger.trace("Remote node: {} Thread name: {} Remote replica: {} Key {}: {}", remoteNode, threadName,
            remoteReplicaInfo.getReplicaId(), messageInfo.getStoreKey(), e.getErrorCode().name());
      } else {
        throw e;
      }
    }
    // A Repair event for an undelete signifies that an undelete message was received from the remote and it is fired
    // as long as the undelete is guaranteed to have taken effect locally.
    if (notification != null) {
      notification.onBlobReplicaUndeleted(dataNodeId.getHostname(), dataNodeId.getPort(),
          messageInfo.getStoreKey().getID(), BlobReplicaSourceType.REPAIRED);
    }
  }
Applies an undelete to the blob described by {@code messageInfo}.
@param messageInfo the {@link MessageInfo} that will be transformed into an undelete
@param remoteReplicaInfo The remote replica that is being replicated from
@throws StoreException
ambry-replication/src/main/java/com/github/ambry/replication/ReplicaThread.java