blob: f8cc03118cd0840df5c6412680309b39223f7e67 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.jackrabbit.oak.plugins.document;
import static org.apache.jackrabbit.oak.plugins.document.Collection.NODES;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.jackrabbit.oak.plugins.document.DocumentMK.Builder;
import org.apache.jackrabbit.oak.plugins.document.memory.MemoryDocumentStore;
import org.apache.jackrabbit.oak.plugins.document.util.LeaseCheckDocumentStoreWrapper;
import org.apache.jackrabbit.oak.plugins.document.util.Utils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import junitx.util.PrivateAccessor;
public class Sweep2TestHelper {
static void testPre18UpgradeSimulations(DocumentNodeStore ns, DocumentMKBuilderProvider builderProvider) {
// make sure the DocumentNodeStore is disposed before testing if this hasn't been done yet
ns.dispose();
MemoryDocumentStore memStore = getMemoryDocumentStore(ns);
Set<Integer> activeIds = new HashSet<Integer>();
activeIds.add(ns.getClusterId());
ClusterViewDocument.readOrUpdate(ns, activeIds, null, null);
testPre18UpgradeSimulations(memStore, builderProvider);
}
static void testPre18UpgradeSimulations(MemoryDocumentStore memStore, DocumentMKBuilderProvider builderProvider) {
doTestPre18Upgrade(memStore.copy(), builderProvider.newBuilder(), false);
doTestPre18Upgrade(memStore.copy(), builderProvider.newBuilder(), true);
}
static MemoryDocumentStore getMemoryDocumentStore(DocumentNodeStore ns) {
DocumentStore ds = ns.getDocumentStore();
if (ds instanceof LeaseCheckDocumentStoreWrapper) {
LeaseCheckDocumentStoreWrapper w = (LeaseCheckDocumentStoreWrapper)ds;
try {
ds = (DocumentStore) PrivateAccessor.getField(w, "delegate");
} catch (NoSuchFieldException e) {
fail("Could unwrap DocumentStore : " + w);
}
}
if (!(ds instanceof MemoryDocumentStore)) {
throw new IllegalStateException("only supporting MemoryDocumentStore for this test for now - DocumentStore cloning not yet generally supported");
}
MemoryDocumentStore memStore = (MemoryDocumentStore) ds;
return memStore;
}
/**
* Does a simulation of the following steps:
* <ol>
* <li>create content in an oak version pre 1.8</li>
* <li>if simulatePreFixUpgrade then simulate an upgrade to a version post 1.8 but without a fix for the branch commit problem</li>
* <li>upgrade to a fixed version</li>
* </ol>
* @param ns
* @param newBuilder
* @param simulatePreFixUpgrade if true, simulate step 2 as well, if false, leave out that step
*/
private static void doTestPre18Upgrade(MemoryDocumentStore memStore, DocumentMK.Builder newBuilder, boolean simulatePreFixUpgrade) {
// step 2 : keep a copy in memStoreOriginal for debugging purpose
MemoryDocumentStore memStoreOriginal = memStore.copy();
// step 3: simulate DocumentNodeStore<init> calling branches.init(store, this) calling doc.purgeUncommittedRevisions(context)
// by doing another restart/shutdown cycle
MemoryDocumentStore memStoreOriginalAdjusted = memStoreOriginal.copy();
DocumentNodeStore ns = newBuilder
.setClusterId(1)
.setDocumentStore(memStoreOriginalAdjusted).build();
ns.dispose();
// step 4 : simulate any existing content changes done so far having happened pre 1.8
// by deleting "_bc" and "_sweepRev"
// ("_sweepRev" also didn't exist pre 1.8 and otherwise prevents
// backgroundSweep()/forceBackgroundSweep()
revertToPre18State(memStore);
// step 5 : keep a copy of the store pre18 upgrade for debugging purpose
@SuppressWarnings("unused")
MemoryDocumentStore memStorePre18Upgrade = memStore.copy();
// optional step 6 : simulate an upgrade to post 1.8 without a fix yet
if (simulatePreFixUpgrade) {
NodeDocumentSweeper.SWEEP_ONE_PREDICATE = Utils.PROPERTY_OR_DELETED;
ns = newBuilder
.setClusterId(1)
.setDocumentStore(memStore).build();
ns.dispose();
// put back the fix asap
NodeDocumentSweeper.SWEEP_ONE_PREDICATE = Utils.PROPERTY_OR_DELETED_OR_COMMITROOT_OR_REVISIONS;
// remove the sweep2Status - this is what manages whether the sweep2 was applied or not
removeSweep2Status(memStore, false);
}
// step 7 : startup new instance (1.8+) - thanks to the above simulatePre18Behaviour
// this will now run backgroundSweep()/forceBackgroundSweep()
ns = newBuilder
.setAsyncDelay(1)
.setClusterId(1)
.setDocumentStore(memStore).build();
assertTrue("sweep2 was too slow", waitForSweep2Done(ns, 10000));
for (NodeDocument originalNode : memStoreOriginalAdjusted.query(NODES, NodeDocument.MIN_ID_VALUE, NodeDocument.MAX_ID_VALUE, Integer.MAX_VALUE)) {
NodeDocument actualNode = memStore.find(NODES, originalNode.getId());
assertBranchCommitsEqual(ns, ns.getSweepRevisions(), originalNode, actualNode);
}
assertEquals(0, scanForMissingBranchCommits(ns).size());
}
private static boolean waitForSweep2Done(DocumentNodeStore ns, long maxWaitMillis) {
final long timeout = System.currentTimeMillis() + maxWaitMillis;
while(System.currentTimeMillis() < timeout) {
Sweep2StatusDocument sweep2StatusDoc = Sweep2StatusDocument.readFrom(ns.getDocumentStore());
if (sweep2StatusDoc != null && sweep2StatusDoc.isSwept()) {
return true;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
fail("interruptedException: " + e);
}
}
return false;
}
private static void assertBranchCommitsEqual(RevisionContext rc, RevisionVector sweepRevs, NodeDocument expectedNode, NodeDocument actualNode) {
Set<Revision> expectedBcs = new HashSet<>(expectedNode.getValueMap("_bc").keySet());
Set<Revision> actualBcs = new HashSet<>(actualNode.getValueMap("_bc").keySet());
if (!expectedBcs.equals(actualBcs) && expectedBcs.containsAll(actualBcs)) {
// there might have to be more bc than we actually have.
// check if they have been swept
Iterator<Revision> it = expectedBcs.iterator();
while(it.hasNext()) {
Revision expectedBc = it.next();
if (!sweepRevs.isRevisionNewer(expectedBc)) {
it.remove();
}
}
it = actualBcs.iterator();
while(it.hasNext()) {
Revision actualBc = it.next();
if (!sweepRevs.isRevisionNewer(actualBc)) {
it.remove();
}
}
if (expectedBcs.equals(actualBcs)) {
// perfect
return;
}
}
assertEquals("\"_bc\" mismatch on " + expectedNode.getId(), expectedNode.getValueMap("_bc").keySet(), actualNode.getValueMap("_bc").keySet());
}
/**
* Remove data from the store bringing it to a pre Oak 1.8 state
* @param store
*/
static void revertToPre18State(DocumentStore store) {
// A : remove all "_bc"
for (NodeDocument nodeDocument :
store.query(NODES, NodeDocument.MIN_ID_VALUE, NodeDocument.MAX_ID_VALUE, Integer.MAX_VALUE)) {
UpdateOp removeBc = new UpdateOp(nodeDocument.getId(), false);
removeBc.remove("_bc");
assertNotNull(store.findAndUpdate(Collection.NODES, removeBc));
}
// B : remove "_sweepRev"
UpdateOp removeSweepRev = new UpdateOp(Utils.getIdFromPath("/"), false);
removeSweepRev.remove("_sweepRev");
assertNotNull(store.findAndUpdate(Collection.NODES, removeSweepRev));
// C : remove the "sweep2Status" from the settings
removeSweep2Status(store, false);
}
static void removeSweep2Status(DocumentStore store) {
// most of the tests rely on sweep2 being done for all clusterIds.
// hence setting emptySweepRev to true by default here to achieve that.
removeSweep2Status(store, true);
}
/**
* Removes the "sweep2Status" from the settings collection as well
* as emptying the "_sweepRev" from the "0:/" root if desired.
* <p/>
* The reason for emptying the "_sweepRev", and not straight removing it,
* is that it has an influence on whether Sweep2Helper.isSweep2Necessary
* considers a sweep2 necessary or not: if it's not there, then the
* repository never saw a sweep1 - if it is there, then it did.
* Now for the latter, if it saw a sweep1, the sweep2 is only done
* for clusterIds in that list, not for all. But some of the tests
* would like to have sweep2 done for other clusterIds as well.
* So this is to account for that fact.
* @param store the store on which to do the modifications
* @param emptySweepRev whether or not "_sweepRev" should be emptied.
*/
static void removeSweep2Status(DocumentStore store, boolean emptySweepRev) {
store.remove(Collection.SETTINGS, "sweep2Status");
if (emptySweepRev) {
String rootId = Utils.getIdFromPath("/");
NodeDocument rootDoc = store.find(Collection.NODES, rootId);
SortedMap<Revision, String> sweepRevs = rootDoc.getLocalMap(NodeDocument.SWEEP_REV);
if (!sweepRevs.isEmpty()) {
UpdateOp emptySweepRevOp = new UpdateOp(rootId, false);
for (Revision rev : sweepRevs.keySet()) {
emptySweepRevOp.removeMapEntry("_sweepRev", rev);
}
assertNotNull(store.findAndUpdate(Collection.NODES, emptySweepRevOp));
}
}
}
/**
* This simulates the provided store having done the changes
* "pre 1.8" - ie it "ages" the document in a way that it looks
* like they were applied "pre 1.8".
* <p/>
* The way this is done is to remove all the "_bc" entries
* (as they didn't exist before 1.8) - *and then* doing a normal sweep (ie "sweep1")
*/
static DocumentNodeStore applyPre18Aging(DocumentStore memStore, DocumentMKBuilderProvider builderProvider, int newClusterId) {
return applyPre18Aging(memStore, builderProvider, new int[] {newClusterId}).get(0);
}
/**
* This simulates the provided store having done the changes
* "pre 1.8" - ie it "ages" the document in a way that it looks
* like they were applied "pre 1.8".
* <p/>
* The way this is done is to remove all the "_bc" entries
* (as they didn't exist before 1.8) - *and then* doing a normal sweep (ie "sweep1")
*/
static List<DocumentNodeStore> applyPre18Aging(DocumentStore memStore, DocumentMKBuilderProvider builderProvider, int... newClusterId) {
revertToPre18State(memStore);
// mark as swept2 to avoid a proper sweep2, only makes sense for testing
Sweep2StatusDocument.forceReleaseSweep2LockAndMarkSwept(memStore, 1);
NodeDocumentSweeper.SWEEP_ONE_PREDICATE = Utils.PROPERTY_OR_DELETED;
Builder builder = builderProvider.newBuilder();
List<DocumentNodeStore> nss = new LinkedList<>();
for (int aClusterId : newClusterId) {
DocumentNodeStore ns = builder
.setClusterId(aClusterId)
.setDocumentStore(memStore).build();
nss.add(ns);
}
NodeDocumentSweeper.SWEEP_ONE_PREDICATE = Utils.PROPERTY_OR_DELETED_OR_COMMITROOT_OR_REVISIONS;
return nss;
}
static List<Path> scanForMissingBranchCommits(DocumentNodeStore ns) {
List<NodeDocument> nodes = ns.getDocumentStore().query(Collection.NODES, NodeDocument.MIN_ID_VALUE, NodeDocument.MAX_ID_VALUE, Integer.MAX_VALUE);
List<Path> paths = new LinkedList<>();
for (NodeDocument nodeDocument : nodes) {
if (nodeDocument.isSplitDocument()) {
// as per MissingLastRevSeeker.getCandidates only documents
// with a MODIFIED_IN_SECS (_modified) are considered for sweeping.
// split documents do not contain that, so they are not part of sweeping.
// hence we should filter those out for the test here
continue;
}
if (containsMissingBranchCommit(ns, nodeDocument)) {
paths.add(nodeDocument.getPath());
}
}
return paths;
}
static boolean containsMissingBranchCommit(DocumentNodeStore ns, NodeDocument nodeDocument) {
final RevisionVector emptySweepRevision = new RevisionVector();
CommitValueResolver cvr = new CachingCommitValueResolver(
0 /* to make sure each commit value is calculated explicitly, separately */,
() -> emptySweepRevision);
MissingBcSweeper2 sweeper = new MissingBcSweeper2(ns, cvr, null, new AtomicBoolean(false));
final List<Map<Path, UpdateOp>> updatesList = new LinkedList<>();
sweeper.sweep2(Arrays.asList(nodeDocument), new NodeDocumentSweepListener() {
@Override
public void sweepUpdate(Map<Path, UpdateOp> updates) throws DocumentStoreException {
updatesList.add(updates);
}
});
return !updatesList.isEmpty();
}
}