/** * This test checks for the bug described in SR11123. If an IN and its child-subtree is deleted, * an INDeleteInfo is written to the log. If there is a BINDelta in the log for a BIN-child of the * removed subtree (i.e. compressed), then recovery will apply it to the compressed IN. Since the * IN has no data in * it, that is not necessarily a problem. However, reinstantiating the * obsolete IN may cause a parent IN to split which is not allowed during IN recovery. * * <p>Here's the case: * * <p>| IN1 +---------------------------------+ | | IN2 IN6 / | / | \ BIN3 BIN4 BIN7 BIN8 BIN9 * * <p>IN2 and the subtree below are compressed away. During recovery replay, after the pass where * INs and INDeleteINfos are processed, the in-memory tree looks like this: * * <p>IN1 | IN6 / | \ BIN7 BIN8 BIN9 * * <p>However, let's assume that BINDeltas were written for BIN3, BIN4, BIN5 within the recovery * part of the log, before the subtree was compressed. We'll replay those BINDeltas in the * following pass, and in the faulty implementation, they cause the ghosts of BIN3, BIN4 to be * resurrected and applied to IN6. Let's assume that the max node size is 4 -- we won't be able to * connect BIN3, BIN4 because IN6 doesn't have the capacity, and we don't expect to have to do * splits. */ private void addData(Database db) throws DatabaseException { DatabaseEntry key = new DatabaseEntry(); DatabaseEntry data = new DatabaseEntry(); /* Populate a tree so there are 3 levels. */ for (int i = 0; i < 140; i += 10) { IntegerBinding.intToEntry(i, key); IntegerBinding.intToEntry(i, data); assertEquals(OperationStatus.SUCCESS, db.put(null, key, data)); } CheckpointConfig ckptConfig = new CheckpointConfig(); ckptConfig.setForce(true); env.checkpoint(ckptConfig); Tree tree = DbInternal.dbGetDatabaseImpl(db).getTree(); com.sleepycat.je.tree.Key.DUMP_TYPE = com.sleepycat.je.tree.Key.DumpType.BINARY; com.sleepycat.je.tree.Key.DUMP_INT_BINDING = true; if (DEBUG) { tree.dump(); } /* * Update a key on the BIN3 and a key on BIN4, to create reason for * a BINDelta. Force a BINDelta for BIN3 and BIN4 out to the log. */ IntegerBinding.intToEntry(0, key); IntegerBinding.intToEntry(100, data); assertEquals(OperationStatus.SUCCESS, db.put(null, key, data)); IntegerBinding.intToEntry(20, key); assertEquals(OperationStatus.SUCCESS, db.put(null, key, data)); EnvironmentImpl envImpl = DbInternal.envGetEnvironmentImpl(env); BIN bin = (BIN) tree.getFirstNode(); bin.log(envImpl.getLogManager(), true, false, false, false, null); bin = tree.getNextBin(bin, false /* traverseWithinDupTree */); bin.log(envImpl.getLogManager(), true, false, false, false, null); bin.releaseLatch(); /* * Delete all of left hand side of the tree, so that the subtree root * headed by IN2 is compressed. */ for (int i = 0; i < 50; i += 10) { IntegerBinding.intToEntry(i, key); assertEquals(OperationStatus.SUCCESS, db.delete(null, key)); } /* force a compression */ env.compress(); if (DEBUG) { tree.dump(); } }
/** Flush the target IN. */ private void flushIN( IN target, LogManager logManager, Map dirtyMap, boolean logProvisionally, boolean allowDeltas) throws DatabaseException { DatabaseImpl db = target.getDatabase(); Tree tree = db.getTree(); boolean targetWasRoot = false; if (target.isDbRoot()) { /* We're trying to flush the root. */ target.releaseLatch(); RootFlusher flusher = new RootFlusher(db, logManager, target); tree.withRootLatched(flusher); boolean flushed = flusher.getFlushed(); /* * We have to check if the root split between target.releaseLatch * and the execution of the root flusher. If it did split, this * target has to get handled like a regular node. */ targetWasRoot = flusher.stillRoot(); /* * Update the tree's owner, whether it's the env root or the * dbmapping tree. */ if (flushed) { DbTree dbTree = db.getDbEnvironment().getDbMapTree(); dbTree.modifyDbRoot(db); nFullINFlushThisRun++; nFullINFlush++; } if (!targetWasRoot) { /* * re-latch for another attempt, now that this is no longer * the root. */ target.latch(); } } if (!targetWasRoot) { SearchResult result = tree.getParentINForChildIN(target, true); /* * Found a parent, do the flush. If no parent found, the * compressor deleted this item before we got to processing it. */ if (result.exactParentFound) { try { ChildReference entry = result.parent.getEntry(result.index); IN renewedTarget = (IN) entry.fetchTarget(db, result.parent); renewedTarget.latch(); DbLsn newLsn = null; try { /* Still dirty? */ if (renewedTarget.getDirty()) { if (allowDeltas) { newLsn = renewedTarget.logAllowDeltas(logManager, logProvisionally); if (newLsn == null) { nDeltaINFlushThisRun++; nDeltaINFlush++; } } else { newLsn = renewedTarget.log(logManager, logProvisionally); } } } finally { renewedTarget.releaseLatch(); } /* Update parent if logging occurred */ if (newLsn != null) { nFullINFlushThisRun++; nFullINFlush++; if (renewedTarget instanceof BIN) { nFullBINFlush++; } result.parent.updateEntry(result.index, newLsn); addToDirtyMap(dirtyMap, result.parent); } } finally { result.parent.releaseLatch(); } } } }