1 /*- 2 * See the file LICENSE for redistribution information. 3 * 4 * Copyright (c) 2004, 2010 Oracle and/or its affiliates. All rights reserved. 5 * 6 */ 7 package com.sleepycat.je.recovery; 8 9 import static org.junit.Assert.assertEquals; 10 import static org.junit.Assert.fail; 11 12 import java.util.HashSet; 13 14 import org.junit.Test; 15 16 import com.sleepycat.bind.tuple.IntegerBinding; 17 import com.sleepycat.je.CheckpointConfig; 18 import com.sleepycat.je.Database; 19 import com.sleepycat.je.DatabaseConfig; 20 import com.sleepycat.je.DatabaseEntry; 21 import com.sleepycat.je.DatabaseException; 22 import com.sleepycat.je.DbInternal; 23 import com.sleepycat.je.Environment; 24 import com.sleepycat.je.EnvironmentConfig; 25 import com.sleepycat.je.OperationStatus; 26 import com.sleepycat.je.StatsConfig; 27 import com.sleepycat.je.config.EnvironmentParams; 28 import com.sleepycat.je.dbi.EnvironmentImpl; 29 import com.sleepycat.je.log.LogEntryType; 30 import com.sleepycat.je.log.ReplicationContext; 31 import com.sleepycat.je.log.Trace; 32 import com.sleepycat.je.log.entry.SingleItemEntry; 33 import com.sleepycat.je.recovery.stepwise.TestData; 34 import com.sleepycat.je.util.TestUtils; 35 import com.sleepycat.je.utilint.TestHook; 36 37 /** 38 * Test situations where a new root is created 39 */ 40 public class CheckNewRootTest extends CheckBase { 41 42 private static final boolean DEBUG = false; 43 private static final String DB_NAME = "simpleDB"; 44 45 private final boolean useDups = false; 46 private static CheckpointConfig FORCE_CONFIG = new CheckpointConfig(); 47 static { 48 FORCE_CONFIG.setForce(true); 49 } 50 51 /** 52 * Create a tree, make sure the root changes and is logged 53 * before any checkpointing. The bug found in [#13897] was this: 54 * 55 * 100 BIN a 56 * 110 RootIN b 57 * 120 MapLN points to root IN at 110 58 * 130 RootIN b written as part of compression 59 * 140 ckpt start 60 * 150 ckpt end 61 * 62 * Since the compression was writing a root IN w/out updating the mapLN, 63 * the obsolete root at 110 was recovered instead of newer rootIN at 130. 64 */ 65 @Test testWrittenByCompression()66 public void testWrittenByCompression() 67 throws Throwable { 68 69 EnvironmentConfig envConfig = setupEnvConfig(); 70 DatabaseConfig dbConfig = setupDbConfig(); 71 72 /* Run the full test case w/out truncating the log. */ 73 testOneCase(DB_NAME, envConfig, dbConfig, 74 new TestGenerator(true /* generate log description. */){ 75 @Override 76 void generateData(Database db) 77 throws DatabaseException { 78 setupWrittenByCompression(db); 79 } 80 }, 81 envConfig, dbConfig); 82 83 /* 84 * Now run the test in a stepwise loop, truncate after each log entry. 85 * Our baseline expected set is empty -- no records expected. 86 */ 87 HashSet<TestData> currentExpected = new HashSet<TestData>(); 88 stepwiseLoop(DB_NAME, envConfig, dbConfig, currentExpected, 0); 89 } 90 91 /** 92 * Create a populated tree, delete all records, then begin to insert again. 93 */ setupWrittenByCompression(Database db)94 private void setupWrittenByCompression(Database db) 95 throws DatabaseException { 96 setStepwiseStart(); 97 98 DatabaseEntry key = new DatabaseEntry(); 99 DatabaseEntry data = new DatabaseEntry(); 100 101 /* Populate a tree so it grows to 2 levels, with 2 BINs. */ 102 for (int i = 0; i < 10; i ++) { 103 IntegerBinding.intToEntry(i, key); 104 IntegerBinding.intToEntry(i, data); 105 assertEquals(OperationStatus.SUCCESS, db.put(null, key, data)); 106 } 107 108 Trace.trace(DbInternal.getEnvironmentImpl(env), "After inserts"); 109 env.checkpoint(FORCE_CONFIG); 110 if (DEBUG) { 111 System.out.println(db.getStats(new StatsConfig())); 112 } 113 114 /* Now delete all of 1 BIN. */ 115 for (int i = 0; i < 5; i ++) { 116 IntegerBinding.intToEntry(i, key); 117 assertEquals(OperationStatus.SUCCESS, db.delete(null, key)); 118 } 119 120 /* Compress, removing a BIN. */ 121 env.compress(); 122 if (DEBUG) { 123 System.out.println("After compress"); 124 System.out.println(db.getStats(new StatsConfig())); 125 } 126 127 /* Checkpoint again. */ 128 env.checkpoint(FORCE_CONFIG); 129 } 130 131 /** 132 * Create a tree, make sure the root changes and is logged 133 * before any checkpointing. The bug found in [#13897] was this: 134 * 135 * 110 RootIN b 136 * 120 MapLN points to root IN at 110 137 * 130 BINb split 138 * 140 RootIN b written as part of split 139 * 150 ckpt start 140 * 160 ckpt end 141 * 142 * Since the compression was writing a root IN w/out updating the mapLN, 143 * the obsolete root at 110 was recovered instead of newer rootIN at 130. 144 */ 145 @Test testWrittenBySplit()146 public void testWrittenBySplit() 147 throws Throwable { 148 149 EnvironmentConfig envConfig = setupEnvConfig(); 150 DatabaseConfig dbConfig = setupDbConfig(); 151 152 /* Run the full test case w/out truncating the log. */ 153 testOneCase(DB_NAME, envConfig, dbConfig, 154 new TestGenerator(true /* generate log description. */){ 155 @Override 156 void generateData(Database db) 157 throws DatabaseException { 158 setupWrittenBySplits(db); 159 } 160 }, 161 envConfig, dbConfig); 162 163 /* 164 * Now run the test in a stepwise loop, truncate after each log entry. 165 * Our baseline expected set is empty -- no records expected. 166 */ 167 HashSet<TestData> currentExpected = new HashSet<TestData>(); 168 stepwiseLoop(DB_NAME, envConfig, dbConfig, currentExpected, 0); 169 } 170 171 /** 172 */ setupWrittenBySplits(Database db)173 private void setupWrittenBySplits(Database db) 174 throws DatabaseException { 175 setStepwiseStart(); 176 177 DatabaseEntry key = new DatabaseEntry(); 178 DatabaseEntry data = new DatabaseEntry(); 179 180 /* Create a tree and checkpoint. */ 181 IntegerBinding.intToEntry(0, key); 182 IntegerBinding.intToEntry(0, data); 183 assertEquals(OperationStatus.SUCCESS, db.put(null, key, data)); 184 env.checkpoint(FORCE_CONFIG); 185 Trace.trace(DbInternal.getEnvironmentImpl(env), "After creation"); 186 187 /* Populate a tree so it splits. */ 188 for (int i = 1; i < 6; i ++) { 189 IntegerBinding.intToEntry(i, key); 190 IntegerBinding.intToEntry(i, data); 191 assertEquals(OperationStatus.SUCCESS, db.put(null, key, data)); 192 } 193 194 Trace.trace(DbInternal.getEnvironmentImpl(env), "After inserts"); 195 env.checkpoint(FORCE_CONFIG); 196 } 197 198 /* 199 * Scenario from [#13897]: tree is created. Log looks like this 200 * provisional BIN 201 * root IN 202 * checkpoint start 203 * LN is logged but not yet attached to BIN 204 * checkpoint end 205 * BIN is dirtied, but is not part of checkpoint, because dirtying wasn't 206 * seen 207 * In this case, getParentForBIN hangs, because there is no root. 208 * This test is for debugging only, because it's not really possible to 209 * run a real checkpoint in the small window when the bin is not dirty. 210 * Attempts to run a checkpoint programmatically result in failing the 211 * assert that no latches are held when the inlist latch is taken. 212 * Instead, we do this pseudo checkpoint, to make the hang reproduce. But 213 * this test will still fail even with the fixed code because the fix 214 * now causes the rootIN to get re-logged, and the pseudo checkpoint 215 * doesn't do that logging. 216 */ xxtestCreateNewTree()217 public void xxtestCreateNewTree() // This test for debugging only 218 throws Throwable { 219 220 EnvironmentConfig envConfig = setupEnvConfig(); 221 DatabaseConfig dbConfig = setupDbConfig(); 222 223 /* Run the full test case w/out truncating the log. */ 224 testOneCase(DB_NAME, envConfig, dbConfig, 225 new TestGenerator(true /* generate log description. */){ 226 @Override 227 void generateData(Database db) 228 throws DatabaseException { 229 setupCreateNewTree(db); 230 } 231 }, 232 envConfig, dbConfig); 233 234 /* 235 * Now run the test in a stepwise loop, truncate after each log entry. 236 * Our baseline expected set is empty -- no records expected. 237 */ 238 HashSet<TestData> currentExpected = new HashSet<TestData>(); 239 stepwiseLoop(DB_NAME, envConfig, dbConfig, currentExpected, 0); 240 } 241 242 /** 243 * Create a populated tree, delete all records, then begin to insert again. 244 */ setupCreateNewTree(Database db)245 private void setupCreateNewTree(Database db) 246 throws DatabaseException { 247 248 DatabaseEntry key = new DatabaseEntry(); 249 DatabaseEntry data = new DatabaseEntry(); 250 251 TestHook ckptHook = new CheckpointHook(env); 252 DbInternal.getDatabaseImpl(db).getTree().setCkptHook(ckptHook); 253 254 env.checkpoint(FORCE_CONFIG); 255 256 /* 257 * Create in the log 258 * provisional BIN, IN, ckpt start, LN 259 */ 260 IntegerBinding.intToEntry(1, key); 261 IntegerBinding.intToEntry(1, data); 262 assertEquals(OperationStatus.SUCCESS, db.put(null, key, data)); 263 } 264 265 /* 266 * Force a checkpoint into the log. Use another thread, lest the asserts 267 * about held latches take effect. 268 */ 269 private static class CheckpointHook implements TestHook { 270 private final Environment env; 271 CheckpointHook(Environment env)272 CheckpointHook(Environment env) { 273 this.env = env; 274 } 275 doHook()276 public void doHook() { 277 try { 278 EnvironmentImpl envImpl = 279 DbInternal.getEnvironmentImpl(env); 280 SingleItemEntry<CheckpointStart> startEntry = 281 SingleItemEntry.create(LogEntryType.LOG_CKPT_START, 282 new CheckpointStart(100, "test")); 283 long checkpointStart = envImpl.getLogManager().log 284 (startEntry, 285 ReplicationContext.NO_REPLICATE); 286 CheckpointEnd ckptEnd = new CheckpointEnd 287 ("test", 288 checkpointStart, 289 envImpl.getRootLsn(), 290 envImpl.getTxnManager().getFirstActiveLsn(), 291 envImpl.getNodeSequence().getLastLocalNodeId(), 292 envImpl.getNodeSequence().getLastReplicatedNodeId(), 293 envImpl.getDbTree().getLastLocalDbId(), 294 envImpl.getDbTree().getLastReplicatedDbId(), 295 envImpl.getTxnManager().getLastLocalTxnId(), 296 envImpl.getTxnManager().getLastReplicatedTxnId(), 297 100, 298 true /*cleanedFilesToDelete*/, 299 envImpl.getCleaner().getLogSummary()); 300 SingleItemEntry<CheckpointEnd> endEntry = 301 SingleItemEntry.create(LogEntryType.LOG_CKPT_END, ckptEnd); 302 envImpl.getLogManager().logForceFlush 303 (endEntry, 304 true, // fsyncRequired 305 ReplicationContext.NO_REPLICATE); 306 } catch (DatabaseException e) { 307 fail(e.getMessage()); 308 } 309 } 310 getHookValue()311 public Object getHookValue() { 312 throw new UnsupportedOperationException(); 313 } 314 doIOHook()315 public void doIOHook() { 316 throw new UnsupportedOperationException(); 317 } 318 hookSetup()319 public void hookSetup() { 320 throw new UnsupportedOperationException(); 321 } doHook(Object obj)322 public void doHook(Object obj) { 323 throw new UnsupportedOperationException(); 324 } 325 } 326 327 /** 328 * Make sure eviction doesn't evict roots. If it did, we'd need to 329 * log the mapLN to be sure that recovery is correct. 330 */ 331 @Test testChangeAndEvictRoot()332 public void testChangeAndEvictRoot() 333 throws Throwable { 334 335 EnvironmentConfig envConfig = setupEnvConfig(); 336 DatabaseConfig dbConfig = setupDbConfig(); 337 338 /* Run the full test case w/out truncating the log. */ 339 testOneCase(DB_NAME, envConfig, dbConfig, 340 new TestGenerator(true /* generate log description. */){ 341 @Override 342 void generateData(Database db) 343 throws DatabaseException { 344 setupEvictedRoot(db); 345 } 346 }, 347 envConfig, dbConfig); 348 349 /* 350 * Now run the test in a stepwise loop, truncate after each log entry. 351 * Our baseline expected set is empty -- no records expected. 352 */ 353 HashSet<TestData> currentExpected = new HashSet<TestData>(); 354 stepwiseLoop(DB_NAME, envConfig, dbConfig, currentExpected, 0); 355 } 356 357 /** 358 * Create a populated tree, delete all records, then begin to insert again. 359 */ setupEvictedRoot(Database db)360 private void setupEvictedRoot(Database db) 361 throws DatabaseException { 362 setStepwiseStart(); 363 364 DatabaseEntry key = new DatabaseEntry(); 365 DatabaseEntry data = new DatabaseEntry(); 366 367 /* Populate a tree so it grows to 2 levels, with 2 BINs. */ 368 for (int i = 0; i < 10; i ++) { 369 IntegerBinding.intToEntry(i, key); 370 IntegerBinding.intToEntry(i, data); 371 assertEquals(OperationStatus.SUCCESS, db.put(null, key, data)); 372 } 373 374 Trace.trace(DbInternal.getEnvironmentImpl(env), "After inserts"); 375 env.checkpoint(FORCE_CONFIG); 376 377 /* 378 * Add another record so that the eviction below will log 379 * a different versions of the IN nodes. 380 */ 381 IntegerBinding.intToEntry(10, key); 382 IntegerBinding.intToEntry(10, data); 383 assertEquals(OperationStatus.SUCCESS, db.put(null, key, data)); 384 385 /* Evict */ 386 TestHook<Boolean> evictHook = new TestHook<Boolean>() { 387 public void doIOHook() { 388 throw new UnsupportedOperationException(); 389 } 390 public void doHook() { 391 throw new UnsupportedOperationException(); 392 } 393 public Boolean getHookValue() { 394 return Boolean.TRUE; 395 } 396 public void hookSetup() { 397 throw new UnsupportedOperationException(); 398 } 399 public void doHook(Boolean obj) { 400 throw new UnsupportedOperationException(); 401 } 402 }; 403 DbInternal.getEnvironmentImpl(env).getEvictor(). 404 setRunnableHook(evictHook); 405 env.evictMemory(); 406 407 /* Checkpoint again. */ 408 env.checkpoint(FORCE_CONFIG); 409 } 410 setupEnvConfig()411 private EnvironmentConfig setupEnvConfig() { 412 EnvironmentConfig envConfig = TestUtils.initEnvConfig(); 413 turnOffEnvDaemons(envConfig); 414 envConfig.setConfigParam(EnvironmentParams.NODE_MAX.getName(), 415 "4"); 416 envConfig.setAllowCreate(true); 417 return envConfig; 418 } 419 setupDbConfig()420 private DatabaseConfig setupDbConfig() { 421 DatabaseConfig dbConfig = new DatabaseConfig(); 422 dbConfig.setSortedDuplicates(useDups); 423 dbConfig.setAllowCreate(true); 424 return dbConfig; 425 } 426 } 427