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