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..c800444c51 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,16 @@ public interface DocumentNodeStoreMBean { CompositeData getBranchCommitHistory(); CompositeData getMergeBranchCommitHistory(); + + @Description("Triggers last revision recovery of nodes, below a given path and clusterId.\n" + + "Returns number of records updated after performing recovery.\n" + + "Note: Recovery can only be performed on inactive clusterIds. If the clusterNode is in ReadOnly mode,\n" + + "it will return the no. of documents which needs update and won't perform 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..c2434508f0 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,18 +17,25 @@ 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; import javax.management.openmbean.CompositeData; import com.google.common.base.Function; +import com.google.common.base.Preconditions; 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 +52,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 +192,51 @@ 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; + + Preconditions.checkNotNull(path, "Path parameter is passed as NULL"); + Preconditions.checkArgument(PathUtils.isAbsolute(path), "Path not specified in jmx mbean"); + Preconditions.checkArgument(clusterId >= 0, "Illegal clusterId 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; + NodeDocument nodeDocument = docStore.find(Collection.NODES, Utils.getIdFromPath(p)); + if(nodeDocument == null) { + throw new DocumentStoreException("Document node with given path = "+ p + " doesnot exist"); + } + for (;;) { + log.info("Running recovery on child documents of path = " + p); + 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..8792c2c3d4 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,87 @@ 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(); + boolean exceptionThrown; + NodeBuilder builder = ns1.getRoot().builder(); + + //Validating null path + try { + exceptionThrown = false; + ns1.getMBean().recover(null, cId1); + } catch (NullPointerException e) { + exceptionThrown = true; + } + assertTrue(exceptionThrown); + + //Validating empty path + try { + exceptionThrown = false; + ns1.getMBean().recover("", cId1); + } catch (IllegalArgumentException e) { + exceptionThrown = true; + } + assertTrue(exceptionThrown); + + //Validating negative clusterId + try { + exceptionThrown = false; + ns1.getMBean().recover("/foo", -1); + } catch (IllegalArgumentException e) { + exceptionThrown = true; + } + assertTrue(exceptionThrown); + + //Validating recovery on active node + try { + exceptionThrown = false; + ns1.getMBean().recover("/foo", cId1); + } catch (IllegalStateException e) { + exceptionThrown = true; + } + assertTrue(exceptionThrown); + + 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)); + + //Validate no. of affected paths in readOnlyMode + DocumentNodeStore ns2 = builderProvider.newBuilder().setAsyncDelay(0) + .setClusterId(2).setDocumentStore(docStore).setReadOnlyMode() + .getNodeStore(); + assertEquals(1, ns2.getMBean().recover("/foo", cId1)); + + //Validate no. of recovered paths + DocumentNodeStore ns3 = builderProvider.newBuilder().setAsyncDelay(0) + .setClusterId(3).setDocumentStore(docStore) + .getNodeStore(); + assertEquals(1, ns3.getMBean().recover("/foo", cId1)); + + //Validating recovery on non existing path + try { + exceptionThrown = false; + ns2.getMBean().recover("/foo1", cId1); + } catch (DocumentStoreException e) { + exceptionThrown = true; + } + assertTrue(exceptionThrown); + } + // OAK-2288 @Test public void mergedBranchVisibility() throws Exception {