diff --git a/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStore.java b/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStore.java index d5b9e9c657..a49f8bd0f8 100644 --- a/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStore.java +++ b/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStore.java @@ -3416,4 +3416,8 @@ public final class DocumentNodeStore int getUpdateLimit() { return updateLimit; } + + boolean isReadOnlyMode() { + return readOnlyMode; + } } diff --git a/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreMBean.java b/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreMBean.java index 0d026af923..c9a27e8da2 100644 --- a/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreMBean.java +++ b/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreMBean.java @@ -64,4 +64,14 @@ public interface DocumentNodeStoreMBean { CompositeData getBranchCommitHistory(); CompositeData getMergeBranchCommitHistory(); + + @Description("Trigger last revision recovery of nodes, below a given path and clusterId.\n" + + "Returns number of records updated after performing recovery") + int recover( + @Description("the path") + @Name("path") + String path, + @Description("cluster id") + @Name("clusterId") + int clusterId); } diff --git a/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreMBeanImpl.java b/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreMBeanImpl.java index 34a0309c3a..2a5ce1de20 100644 --- a/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreMBeanImpl.java +++ b/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreMBeanImpl.java @@ -17,6 +17,8 @@ package org.apache.jackrabbit.oak.plugins.document; import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.List; import java.util.TimeZone; import javax.management.NotCompliantMBeanException; @@ -27,8 +29,12 @@ import com.google.common.base.Predicate; import org.apache.jackrabbit.api.stats.RepositoryStatistics; import org.apache.jackrabbit.api.stats.TimeSeries; +import org.apache.jackrabbit.oak.commons.PathUtils; import org.apache.jackrabbit.oak.commons.jmx.AnnotatedStandardMBean; +import org.apache.jackrabbit.oak.plugins.document.util.Utils; import org.apache.jackrabbit.stats.TimeSeriesStatsUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import static com.google.common.collect.Iterables.filter; import static com.google.common.collect.Iterables.toArray; @@ -45,6 +51,7 @@ final class DocumentNodeStoreMBeanImpl extends AnnotatedStandardMBean implements private final DocumentNodeStore nodeStore; private final RepositoryStatistics repoStats; private final Iterable clusterNodes; + private final Logger log = LoggerFactory.getLogger(this.getClass()); DocumentNodeStoreMBeanImpl(DocumentNodeStore nodeStore, RepositoryStatistics repoStats, @@ -184,4 +191,52 @@ final class DocumentNodeStoreMBeanImpl extends AnnotatedStandardMBean implements private TimeSeries getTimeSeries(String name) { return repoStats.getTimeSeries(name, true); } + + @Override + public int recover(String path, int clusterId) { + boolean dryRun = nodeStore.isReadOnlyMode(); + int sum = 0; + if (path == null) { + throw new NullPointerException("Path not specified in jmx mbean"); + } + if (clusterId == 0) { + throw new NullPointerException("Recover clusterId not specified in jmx mbean"); + } + DocumentStore docStore = nodeStore.getDocumentStore(); + boolean isActive = false; + + for (ClusterNodeInfoDocument it : ClusterNodeInfoDocument.all(docStore)) { + if (it.getClusterId() == clusterId && it.isActive()) { + isActive = true; + } + } + + if (isActive) { + throw new IllegalStateException( + "Cannot run recover on clusterId " + clusterId + " as it's currently active"); + } + + String p = path; + for (;;) { + log.info("Running recovery on " + p); + NodeDocument nodeDocument = docStore.find(Collection.NODES, Utils.getIdFromPath(p)); + if(nodeDocument == null) { + throw new DocumentStoreException("Document node with given path = "+ p + " doesnot exist"); + } + List childDocs = getChildDocs(p); + sum += nodeStore.getLastRevRecoveryAgent().recover(childDocs, clusterId, dryRun); + if (PathUtils.denotesRoot(p)) { + break; + } + p = PathUtils.getParentPath(p); + } + return sum; + } + + private List getChildDocs(String path) { + Path pathRef = Path.fromString(path); + final String to = Utils.getKeyUpperLimit(pathRef); + final String from = Utils.getKeyLowerLimit(pathRef); + return nodeStore.getDocumentStore().query(Collection.NODES, from, to, 10000); + } } diff --git a/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreTest.java b/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreTest.java index 64e341799c..139a768125 100644 --- a/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreTest.java +++ b/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreTest.java @@ -686,6 +686,36 @@ public class DocumentNodeStoreTest { assertTrue(active[0].startsWith(cId2 + "=")); } + //OAK-8449 + @Test + public void lastRevisionRecovery() throws Exception { + DocumentStore docStore = new MemoryDocumentStore(); + DocumentNodeStore ns1 = builderProvider.newBuilder().setAsyncDelay(0) + .setClusterId(1).setDocumentStore(docStore) + .getNodeStore(); + int cId1 = ns1.getClusterId(); + + NodeBuilder builder = ns1.getRoot().builder(); + builder.child("foo").child("bar"); + merge(ns1, builder); + + builder = ns1.getRoot().builder(); + builder.child("foo").child("bar").setProperty("key", "value"); + merge(ns1, builder); + ns1.dispose(); + + UpdateOp op = new UpdateOp(Utils.getIdFromPath("/foo"), false); + op.removeMapEntry("_lastRev", new Revision(0, 0, cId1)); + + assertNotNull(docStore.findAndUpdate(Collection.NODES, op)); + + DocumentNodeStore ns2 = builderProvider.newBuilder().setAsyncDelay(0) + .setClusterId(2).setDocumentStore(docStore) + .getNodeStore(); + + assertEquals(1, ns2.getMBean().recover("/foo", cId1)); + } + // OAK-2288 @Test public void mergedBranchVisibility() throws Exception {