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