1 /*- 2 * See the file LICENSE for redistribution information. 3 * 4 * Copyright (c) 2002, 2014 Oracle and/or its affiliates. All rights reserved. 5 * 6 */ 7 8 package com.sleepycat.je.util; 9 10 import static org.junit.Assert.assertEquals; 11 import static org.junit.Assert.fail; 12 13 import java.io.BufferedReader; 14 import java.io.File; 15 import java.io.FileInputStream; 16 import java.io.FilenameFilter; 17 import java.io.IOException; 18 import java.io.InputStreamReader; 19 import java.io.RandomAccessFile; 20 import java.util.HashMap; 21 import java.util.HashSet; 22 import java.util.Iterator; 23 import java.util.Map; 24 import java.util.Set; 25 import java.util.StringTokenizer; 26 27 import org.junit.After; 28 import org.junit.Test; 29 30 import com.sleepycat.bind.tuple.IntegerBinding; 31 import com.sleepycat.je.Cursor; 32 import com.sleepycat.je.Database; 33 import com.sleepycat.je.DatabaseConfig; 34 import com.sleepycat.je.DatabaseEntry; 35 import com.sleepycat.je.DatabaseException; 36 import com.sleepycat.je.DatabaseNotFoundException; 37 import com.sleepycat.je.DbInternal; 38 import com.sleepycat.je.Environment; 39 import com.sleepycat.je.EnvironmentConfig; 40 import com.sleepycat.je.OperationStatus; 41 import com.sleepycat.je.Transaction; 42 import com.sleepycat.je.config.EnvironmentParams; 43 import com.sleepycat.je.log.FileManager; 44 import com.sleepycat.je.utilint.DbLsn; 45 import com.sleepycat.util.test.SharedTestUtils; 46 import com.sleepycat.util.test.TestBase; 47 import com.sleepycat.utilint.StringUtils; 48 49 public class DbScavengerTest extends TestBase { 50 51 private static final int TRANSACTIONAL = 1 << 0; 52 private static final int WRITE_MULTIPLE = 1 << 1; 53 private static final int PRINTABLE = 1 << 2; 54 private static final int ABORT_BEFORE = 1 << 3; 55 private static final int ABORT_AFTER = 1 << 4; 56 private static final int CORRUPT_LOG = 1 << 5; 57 private static final int DELETE_DATA = 1 << 6; 58 private static final int AGGRESSIVE = 1 << 7; 59 60 private static final int N_DBS = 3; 61 private static final int N_KEYS = 100; 62 private static final int N_DATA_BYTES = 100; 63 private static final int LOG_SIZE = 10000; 64 65 private final String envHomeName; 66 private final File envHome; 67 68 private Environment env; 69 70 private Database[] dbs = new Database[N_DBS]; 71 72 private final boolean duplicatesAllowed = true; 73 DbScavengerTest()74 public DbScavengerTest() { 75 envHome = SharedTestUtils.getTestDir(); 76 envHomeName = envHome.getAbsolutePath(); 77 } 78 79 @After tearDown()80 public void tearDown() { 81 if (env != null) { 82 try { 83 env.close(); 84 } catch (Exception e) { 85 System.out.println("TearDown: " + e); 86 } 87 env = null; 88 } 89 } 90 91 @Test testScavenger1()92 public void testScavenger1() { 93 doScavengerTest(PRINTABLE | TRANSACTIONAL | 94 ABORT_BEFORE | ABORT_AFTER); 95 } 96 97 @Test testScavenger2()98 public void testScavenger2() { 99 doScavengerTest(PRINTABLE | TRANSACTIONAL | ABORT_BEFORE); 100 } 101 102 @Test testScavenger3()103 public void testScavenger3() { 104 doScavengerTest(PRINTABLE | TRANSACTIONAL | ABORT_AFTER); 105 } 106 107 @Test testScavenger4()108 public void testScavenger4() { 109 doScavengerTest(PRINTABLE | TRANSACTIONAL); 110 } 111 112 @Test testScavenger5()113 public void testScavenger5() { 114 doScavengerTest(PRINTABLE | WRITE_MULTIPLE | TRANSACTIONAL); 115 } 116 117 @Test testScavenger6()118 public void testScavenger6() { 119 doScavengerTest(PRINTABLE); 120 } 121 122 @Test testScavenger7()123 public void testScavenger7() { 124 doScavengerTest(TRANSACTIONAL | ABORT_BEFORE | ABORT_AFTER); 125 } 126 127 @Test testScavenger8()128 public void testScavenger8() { 129 doScavengerTest(TRANSACTIONAL | ABORT_BEFORE); 130 } 131 132 @Test testScavenger9()133 public void testScavenger9() { 134 doScavengerTest(TRANSACTIONAL); 135 } 136 137 @Test testScavenger10()138 public void testScavenger10() { 139 doScavengerTest(TRANSACTIONAL | ABORT_AFTER); 140 } 141 142 @Test testScavenger11()143 public void testScavenger11() { 144 doScavengerTest(0); 145 } 146 147 @Test testScavenger12()148 public void testScavenger12() { 149 doScavengerTest(CORRUPT_LOG); 150 } 151 152 @Test testScavenger13()153 public void testScavenger13() { 154 doScavengerTest(DELETE_DATA); 155 } 156 157 @Test testScavenger14()158 public void testScavenger14() { 159 doScavengerTest(AGGRESSIVE); 160 } 161 162 @Test testScavengerAbortedDbLevelOperations()163 public void testScavengerAbortedDbLevelOperations() { 164 createEnv(true, true); 165 boolean doAbort = true; 166 byte[] dataBytes = new byte[N_DATA_BYTES]; 167 DatabaseEntry key = new DatabaseEntry(); 168 DatabaseEntry data = new DatabaseEntry(dataBytes); 169 IntegerBinding.intToEntry(1, key); 170 TestUtils.generateRandomAlphaBytes(dataBytes); 171 for (int i = 0; i < 2; i++) { 172 Transaction txn = env.beginTransaction(null, null); 173 for (int dbCnt = 0; dbCnt < N_DBS; dbCnt++) { 174 String databaseName = null; 175 if (doAbort) { 176 databaseName = "abortedDb" + dbCnt; 177 } else { 178 databaseName = "simpleDb" + dbCnt; 179 } 180 DatabaseConfig dbConfig = new DatabaseConfig(); 181 dbConfig.setAllowCreate(true); 182 dbConfig.setSortedDuplicates(duplicatesAllowed); 183 dbConfig.setTransactional(true); 184 if (dbs[dbCnt] != null) { 185 throw new RuntimeException("database already open"); 186 } 187 Database db = 188 env.openDatabase(txn, databaseName, dbConfig); 189 dbs[dbCnt] = db; 190 db.put(txn, key, data); 191 } 192 if (doAbort) { 193 txn.abort(); 194 dbs = new Database[N_DBS]; 195 } else { 196 txn.commit(); 197 } 198 doAbort = !doAbort; 199 } 200 201 closeEnv(); 202 createEnv(false, false); 203 openDbs(false, false, duplicatesAllowed, null); 204 dumpDbs(false, false); 205 206 /* Close the environment, delete it completely from the disk. */ 207 closeEnv(); 208 TestUtils.removeLogFiles("doScavengerTest", envHome, false); 209 210 /* Recreate and reload the environment from the scavenger files. */ 211 createEnv(true, true); 212 loadDbs(); 213 214 /* Verify that the data is the same as when it was created. */ 215 for (int dbCnt = 0; dbCnt < N_DBS; dbCnt++) { 216 String databaseName = "abortedDb" + dbCnt; 217 DatabaseConfig dbConfig = new DatabaseConfig(); 218 dbConfig.setAllowCreate(false); 219 try { 220 env.openDatabase(null, databaseName, dbConfig); 221 fail("expected DatabaseNotFoundException"); 222 } catch (DatabaseNotFoundException DNFE) { 223 /* Expected. */ 224 } 225 } 226 closeEnv(); 227 } 228 doScavengerTest(int config)229 private void doScavengerTest(int config) 230 throws DatabaseException { 231 232 boolean printable = (config & PRINTABLE) != 0; 233 boolean transactional = (config & TRANSACTIONAL) != 0; 234 boolean writeMultiple = (config & WRITE_MULTIPLE) != 0; 235 boolean abortBefore = (config & ABORT_BEFORE) != 0; 236 boolean abortAfter = (config & ABORT_AFTER) != 0; 237 boolean corruptLog = (config & CORRUPT_LOG) != 0; 238 boolean deleteData = (config & DELETE_DATA) != 0; 239 boolean aggressive = (config & AGGRESSIVE) != 0; 240 241 assert transactional || 242 (!abortBefore && !abortAfter); 243 244 Map[] dataMaps = new Map[N_DBS]; 245 Set<Long> lsnsToCorrupt = new HashSet<Long>(); 246 /* Create the environment and some data. */ 247 createEnvAndDbs(dataMaps, 248 writeMultiple, 249 transactional, 250 abortBefore, 251 abortAfter, 252 corruptLog, 253 lsnsToCorrupt, 254 deleteData); 255 closeEnv(); 256 createEnv(false, false); 257 if (corruptLog) { 258 corruptFiles(lsnsToCorrupt); 259 } 260 openDbs(false, false, duplicatesAllowed, null); 261 dumpDbs(printable, aggressive); 262 263 /* Close the environment, delete it completely from the disk. */ 264 closeEnv(); 265 TestUtils.removeLogFiles("doScavengerTest", envHome, false); 266 267 /* Recreate the environment and load it from the scavenger files. */ 268 createEnv(true, transactional); 269 loadDbs(); 270 271 /* Verify that the data is the same as when it was created. */ 272 openDbs(false, false, duplicatesAllowed, null); 273 verifyDbs(dataMaps); 274 closeEnv(); 275 } 276 closeEnv()277 private void closeEnv() 278 throws DatabaseException { 279 280 for (int i = 0; i < N_DBS; i++) { 281 if (dbs[i] != null) { 282 dbs[i].close(); 283 dbs[i] = null; 284 } 285 } 286 287 env.close(); 288 env = null; 289 } 290 createEnv(boolean create, boolean transactional)291 private void createEnv(boolean create, boolean transactional) 292 throws DatabaseException { 293 294 EnvironmentConfig envConfig = TestUtils.initEnvConfig(); 295 DbInternal.disableParameterValidation(envConfig); 296 envConfig.setConfigParam 297 (EnvironmentParams.ENV_RUN_CHECKPOINTER.getName(), "false"); 298 envConfig.setConfigParam(EnvironmentParams.ENV_RUN_CLEANER.getName(), 299 "false"); 300 envConfig.setConfigParam(EnvironmentParams.LOG_FILE_MAX.getName(), 301 "" + LOG_SIZE); 302 envConfig.setTransactional(transactional); 303 envConfig.setAllowCreate(create); 304 env = new Environment(envHome, envConfig); 305 } 306 createEnvAndDbs(Map[] dataMaps, boolean writeMultiple, boolean transactional, boolean abortBefore, boolean abortAfter, boolean corruptLog, Set<Long> lsnsToCorrupt, boolean deleteData)307 private void createEnvAndDbs(Map[] dataMaps, 308 boolean writeMultiple, 309 boolean transactional, 310 boolean abortBefore, 311 boolean abortAfter, 312 boolean corruptLog, 313 Set<Long> lsnsToCorrupt, 314 boolean deleteData) 315 throws DatabaseException { 316 317 createEnv(true, transactional); 318 Transaction txn = null; 319 if (transactional) { 320 txn = env.beginTransaction(null, null); 321 } 322 323 openDbs(true, transactional, duplicatesAllowed, txn); 324 325 if (transactional) { 326 txn.commit(); 327 } 328 329 long lastCorruptedFile = -1; 330 for (int dbCnt = 0; dbCnt < N_DBS; dbCnt++) { 331 Map<Integer, String> dataMap = new HashMap<Integer, String>(); 332 dataMaps[dbCnt] = dataMap; 333 Database db = dbs[dbCnt]; 334 335 for (int i = 0; i < N_KEYS; i++) { 336 byte[] dataBytes = new byte[N_DATA_BYTES]; 337 DatabaseEntry key = new DatabaseEntry(); 338 DatabaseEntry data = new DatabaseEntry(dataBytes); 339 IntegerBinding.intToEntry(i, key); 340 TestUtils.generateRandomAlphaBytes(dataBytes); 341 342 boolean corruptedThisEntry = false; 343 344 if (transactional) { 345 txn = env.beginTransaction(null, null); 346 } 347 348 if (transactional && 349 abortBefore) { 350 assertEquals(OperationStatus.SUCCESS, 351 db.put(txn, key, data)); 352 txn.abort(); 353 txn = env.beginTransaction(null, null); 354 } 355 356 assertEquals(OperationStatus.SUCCESS, 357 db.put(txn, key, data)); 358 if (corruptLog) { 359 long currentLsn = getLastLsn(); 360 long fileNumber = DbLsn.getFileNumber(currentLsn); 361 long fileOffset = DbLsn.getFileOffset(currentLsn); 362 if (fileOffset > (LOG_SIZE >> 1) && 363 /* We're writing in the second half of the file. */ 364 fileNumber > lastCorruptedFile) { 365 /* Corrupt this file. */ 366 lsnsToCorrupt.add(new Long(currentLsn)); 367 lastCorruptedFile = fileNumber; 368 corruptedThisEntry = true; 369 } 370 } 371 372 if (writeMultiple) { 373 assertEquals(OperationStatus.SUCCESS, 374 db.delete(txn, key)); 375 assertEquals(OperationStatus.SUCCESS, 376 db.put(txn, key, data)); 377 } 378 379 if (deleteData) { 380 assertEquals(OperationStatus.SUCCESS, 381 db.delete(txn, key)); 382 /* overload this for deleted data. */ 383 corruptedThisEntry = true; 384 } 385 386 if (!corruptedThisEntry) { 387 dataMap.put(new Integer(i), 388 StringUtils.fromUTF8(dataBytes)); 389 } 390 391 if (transactional) { 392 txn.commit(); 393 } 394 395 if (transactional && 396 abortAfter) { 397 txn = env.beginTransaction(null, null); 398 assertEquals(OperationStatus.SUCCESS, 399 db.put(txn, key, data)); 400 txn.abort(); 401 } 402 } 403 } 404 } 405 openDbs(boolean create, boolean transactional, boolean duplicatesAllowed, Transaction txn)406 private void openDbs(boolean create, 407 boolean transactional, 408 boolean duplicatesAllowed, 409 Transaction txn) 410 throws DatabaseException { 411 412 for (int dbCnt = 0; dbCnt < N_DBS; dbCnt++) { 413 String databaseName = "simpleDb" + dbCnt; 414 DatabaseConfig dbConfig = new DatabaseConfig(); 415 dbConfig.setAllowCreate(create); 416 dbConfig.setSortedDuplicates(duplicatesAllowed); 417 dbConfig.setTransactional(transactional); 418 if (dbs[dbCnt] != null) { 419 throw new RuntimeException("database already open"); 420 } 421 dbs[dbCnt] = env.openDatabase(txn, databaseName, dbConfig); 422 } 423 } 424 dumpDbs(boolean printable, boolean aggressive)425 private void dumpDbs(boolean printable, boolean aggressive) 426 throws DatabaseException { 427 428 try { 429 DbScavenger scavenger = 430 new DbScavenger(env, envHomeName, printable, aggressive, 431 false /* verbose */); 432 scavenger.dump(); 433 } catch (IOException IOE) { 434 throw new RuntimeException(IOE); 435 } 436 } 437 loadDbs()438 private void loadDbs() 439 throws DatabaseException { 440 441 try { 442 String dbNameBase = "simpleDb"; 443 for (int i = 0; i < N_DBS; i++) { 444 DbLoad loader = new DbLoad(); 445 File file = new File(envHomeName, dbNameBase + i + ".dump"); 446 FileInputStream is = new FileInputStream(file); 447 BufferedReader reader = 448 new BufferedReader(new InputStreamReader(is)); 449 loader.setEnv(env); 450 loader.setInputReader(reader); 451 loader.setNoOverwrite(false); 452 loader.setDbName(dbNameBase + i); 453 loader.load(); 454 is.close(); 455 } 456 } catch (IOException IOE) { 457 throw new RuntimeException(IOE); 458 } 459 } 460 verifyDbs(Map[] dataMaps)461 private void verifyDbs(Map[] dataMaps) 462 throws DatabaseException { 463 464 for (int i = 0; i < N_DBS; i++) { 465 Map dataMap = dataMaps[i]; 466 Cursor cursor = dbs[i].openCursor(null, null); 467 DatabaseEntry key = new DatabaseEntry(); 468 DatabaseEntry data = new DatabaseEntry(); 469 while (cursor.getNext(key, data, null) == 470 OperationStatus.SUCCESS) { 471 Integer keyInt = 472 new Integer(IntegerBinding.entryToInt(key)); 473 String databaseString = StringUtils.fromUTF8(data.getData()); 474 String originalString = (String) dataMap.get(keyInt); 475 if (originalString == null) { 476 fail("couldn't find " + keyInt); 477 } else if (databaseString.equals(originalString)) { 478 dataMap.remove(keyInt); 479 } else { 480 fail(" Mismatch: key=" + keyInt + 481 " Expected: " + originalString + 482 " Found: " + databaseString); 483 } 484 } 485 486 if (dataMap.size() > 0) { 487 fail("entries still remain for db " + i + ": " + 488 dataMap.keySet()); 489 } 490 491 cursor.close(); 492 } 493 } 494 495 private static DumpFileFilter dumpFileFilter = new DumpFileFilter(); 496 497 static class DumpFileFilter implements FilenameFilter { 498 499 /** 500 * Accept files of this format: 501 * *.dump 502 */ accept(File dir, String name)503 public boolean accept(File dir, String name) { 504 StringTokenizer tokenizer = new StringTokenizer(name, "."); 505 /* There should be two parts. */ 506 if (tokenizer.countTokens() == 2) { 507 tokenizer.nextToken(); 508 String fileSuffix = tokenizer.nextToken(); 509 510 /* Check the length and the suffix. */ 511 if (fileSuffix.equals("dump")) { 512 return true; 513 } 514 } 515 516 return false; 517 } 518 } 519 getLastLsn()520 private long getLastLsn() { 521 return DbInternal.getEnvironmentImpl(env). 522 getFileManager().getLastUsedLsn(); 523 } 524 corruptFiles(Set<Long> lsnsToCorrupt)525 private void corruptFiles(Set<Long> lsnsToCorrupt) 526 throws DatabaseException { 527 528 Iterator<Long> iter = lsnsToCorrupt.iterator(); 529 while (iter.hasNext()) { 530 long lsn = iter.next().longValue(); 531 corruptFile(DbLsn.getFileNumber(lsn), 532 DbLsn.getFileOffset(lsn)); 533 } 534 } 535 corruptFile(long fileNumber, long fileOffset)536 private void corruptFile(long fileNumber, long fileOffset) 537 throws DatabaseException { 538 539 String fileName = DbInternal.getEnvironmentImpl(env). 540 getFileManager().getFullFileName(fileNumber, 541 FileManager.JE_SUFFIX); 542 /* 543 System.out.println("corrupting 1 byte at " + 544 DbLsn.makeLsn(fileNumber, fileOffset)); 545 */ 546 try { 547 RandomAccessFile raf = new RandomAccessFile(fileName, "rw"); 548 raf.seek(fileOffset); 549 int current = raf.read(); 550 raf.seek(fileOffset); 551 raf.write(current + 1); 552 raf.close(); 553 } catch (IOException IOE) { 554 throw new RuntimeException(IOE); 555 } 556 } 557 } 558