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;
9 
10 import static org.junit.Assert.assertEquals;
11 import static org.junit.Assert.assertSame;
12 import static org.junit.Assert.assertTrue;
13 import static org.junit.Assert.fail;
14 
15 import java.io.File;
16 import java.util.Arrays;
17 
18 import com.sleepycat.bind.tuple.IntegerBinding;
19 import com.sleepycat.je.DbInternal.Search;
20 import com.sleepycat.je.utilint.TestHook;
21 import junit.framework.Assert;
22 import org.junit.Test;
23 
24 import com.sleepycat.je.config.EnvironmentParams;
25 import com.sleepycat.je.dbi.DatabaseImpl;
26 import com.sleepycat.je.junit.JUnitThread;
27 import com.sleepycat.je.util.DualTestCase;
28 import com.sleepycat.je.util.TestUtils;
29 import com.sleepycat.util.test.SharedTestUtils;
30 import com.sleepycat.utilint.StringUtils;
31 
32 public class CursorTest extends DualTestCase {
33     private static final boolean DEBUG = false;
34     private static final int NUM_RECS = 257;
35 
36     /*
37      * Use a ridiculous value because we've seen extreme slowness on ocicat
38      * where dbperf is often running.
39      */
40     private static final long LOCK_TIMEOUT = 50000000L;
41 
42     private static final String DUPKEY = "DUPKEY";
43 
44     private Environment env;
45     private Database db;
46     private PhantomTestConfiguration config;
47 
48     private File envHome;
49 
50     private volatile int sequence;
51 
CursorTest()52     public CursorTest() {
53         envHome = SharedTestUtils.getTestDir();
54     }
55 
56     @Test
testGetConfig()57     public void testGetConfig()
58         throws DatabaseException {
59 
60         EnvironmentConfig envConfig = TestUtils.initEnvConfig();
61         envConfig.setTransactional(true);
62         envConfig.setAllowCreate(true);
63         envConfig.setTxnNoSync(Boolean.getBoolean(TestUtils.NO_SYNC));
64         env = create(envHome, envConfig);
65         Transaction txn = env.beginTransaction(null, null);
66         DatabaseConfig dbConfig = new DatabaseConfig();
67         dbConfig.setTransactional(true);
68         dbConfig.setSortedDuplicates(true);
69         dbConfig.setAllowCreate(true);
70         db = env.openDatabase(txn, "testDB", dbConfig);
71         txn.commit();
72         Cursor cursor = null;
73         Transaction txn1 =
74             env.beginTransaction(null, TransactionConfig.DEFAULT);
75         try {
76             cursor = db.openCursor(txn1, CursorConfig.DEFAULT);
77             CursorConfig config = cursor.getConfig();
78             if (config == CursorConfig.DEFAULT) {
79                 fail("didn't clone");
80             }
81         } catch (DatabaseException DBE) {
82             DBE.printStackTrace();
83             fail("caught DatabaseException " + DBE);
84         } finally {
85             if (cursor != null) {
86                 cursor.close();
87             }
88             txn1.abort();
89             db.close();
90             close(env);
91             env = null;
92         }
93     }
94 
95     /**
96      * Put some data in a database, take it out. Yank the file size down so we
97      * have many files.
98      */
99     @Test
testBasic()100     public void testBasic()
101         throws Throwable {
102 
103         try {
104             insertMultiDb(1);
105         } catch (Throwable t) {
106             t.printStackTrace();
107             throw t;
108         }
109     }
110 
111     @Test
testMulti()112     public void testMulti()
113         throws Throwable {
114 
115         try {
116             insertMultiDb(4);
117         } catch (Throwable t) {
118             t.printStackTrace();
119             throw t;
120         }
121     }
122 
123     /**
124      * Specifies a test configuration.  This is just a struct for holding
125      * parameters to be passed down to threads in inner classes.
126      */
127     class PhantomTestConfiguration {
128         String testName;
129         String thread1EntryToLock;
130         String thread1OpArg;
131         String thread2Start;
132         String expectedResult;
133         boolean doInsert;
134         boolean doGetNext;
135         boolean doCommit;
136 
PhantomTestConfiguration(String testName, String thread1EntryToLock, String thread1OpArg, String thread2Start, String expectedResult, boolean doInsert, boolean doGetNext, boolean doCommit)137         PhantomTestConfiguration(String testName,
138                                  String thread1EntryToLock,
139                                  String thread1OpArg,
140                                  String thread2Start,
141                                  String expectedResult,
142                                  boolean doInsert,
143                                  boolean doGetNext,
144                                  boolean doCommit) {
145             this.testName = testName;
146             this.thread1EntryToLock = thread1EntryToLock;
147             this.thread1OpArg = thread1OpArg;
148             this.thread2Start = thread2Start;
149             this.expectedResult = expectedResult;
150             this.doInsert = doInsert;
151             this.doGetNext = doGetNext;
152             this.doCommit = doCommit;
153         }
154     }
155 
156     /**
157      * This series of tests sets up a simple 2 BIN tree with a specific set of
158      * elements (see setupDatabaseAndEnv()).  It creates two threads.
159      *
160      * Thread 1 positions a cursor on an element on the edge of a BIN (either
161      * the last element on the left BIN or the first element on the right BIN).
162      * This locks that element.  It throws control to thread 2.
163      *
164      * Thread 2 positions a cursor on the adjacent element on the other BIN
165      * (either the first element on the right BIN or the last element on the
166      * left BIN, resp.)  It throws control to thread 1.  After it signals
167      * thread 1 to continue, thread 2 does either a getNext or getPrev.  This
168      * should block because thread 1 has the next/prev element locked.
169      *
170      * Thread 1 then waits a short time (250ms) so that thread 2 can execute
171      * the getNext/getPrev.  Thread 1 then inserts or deletes the "phantom
172      * element" right in between the cursors that were set up in the previous
173      * two steps, sleeps a second, and either commits or aborts.
174      *
175      * Thread 2 will then return from the getNext/getPrev.  The returned key
176      * from the getNext/getPrev is then verified.
177      *
178      * The Serializable isolation level is not used for either thread so as to
179      * allow phantoms; otherwise, this test would deadlock.
180      *
181      * These parameters are all configured through a PhantomTestConfiguration
182      * instance passed to phantomWorker which has the template for the steps
183      * described above.
184      */
185 
186     /**
187      * Phantom test inserting and committing a phantom while doing a getNext.
188      */
189     @Test
testPhantomInsertGetNextCommit()190     public void testPhantomInsertGetNextCommit()
191         throws Throwable {
192 
193         try {
194             phantomWorker
195                 (new PhantomTestConfiguration
196                  ("testPhantomInsertGetNextCommit",
197                   "F", "D", "C", "D",
198                   true, true, true));
199         } catch (Exception e) {
200             e.printStackTrace();
201             throw e;
202         }
203     }
204 
205     /**
206      * Phantom test inserting and aborting a phantom while doing a getNext.
207      */
208     @Test
testPhantomInsertGetNextAbort()209     public void testPhantomInsertGetNextAbort()
210         throws Throwable {
211 
212         phantomWorker
213             (new PhantomTestConfiguration
214              ("testPhantomInsertGetNextAbort",
215               "F", "D", "C", "F",
216               true, true, false));
217     }
218 
219     /**
220      * Phantom test inserting and committing a phantom while doing a getPrev.
221      */
222     @Test
testPhantomInsertGetPrevCommit()223     public void testPhantomInsertGetPrevCommit()
224         throws Throwable {
225 
226         phantomWorker
227             (new PhantomTestConfiguration
228              ("testPhantomInsertGetPrevCommit",
229               "C", "F", "G", "F",
230               true, false, true));
231     }
232 
233     /**
234      * Phantom test inserting and aborting a phantom while doing a getPrev.
235      */
236     @Test
testPhantomInsertGetPrevAbort()237     public void testPhantomInsertGetPrevAbort()
238         throws Throwable {
239 
240         phantomWorker
241             (new PhantomTestConfiguration
242              ("testPhantomInsertGetPrevAbort",
243               "C", "F", "G", "C",
244               true, false, false));
245     }
246 
247     /**
248      * Phantom test deleting and committing an edge element while doing a
249      * getNext.
250      */
251     @Test
testPhantomDeleteGetNextCommit()252     public void testPhantomDeleteGetNextCommit()
253         throws Throwable {
254 
255         phantomWorker
256             (new PhantomTestConfiguration
257              ("testPhantomDeleteGetNextCommit",
258               "F", "F", "C", "G",
259               false, true, true));
260     }
261 
262     /**
263      * Phantom test deleting and aborting an edge element while doing a
264      * getNext.
265      */
266     @Test
testPhantomDeleteGetNextAbort()267     public void testPhantomDeleteGetNextAbort()
268         throws Throwable {
269 
270         phantomWorker
271             (new PhantomTestConfiguration
272              ("testPhantomDeleteGetNextAbort",
273               "F", "F", "C", "F",
274               false, true, false));
275     }
276 
277     /**
278      * Phantom test deleting and committing an edge element while doing a
279      * getPrev.
280      */
281     @Test
testPhantomDeleteGetPrevCommit()282     public void testPhantomDeleteGetPrevCommit()
283         throws Throwable {
284 
285         phantomWorker
286             (new PhantomTestConfiguration
287              ("testPhantomDeleteGetPrevCommit",
288               "F", "F", "G", "C",
289               false, false, true));
290     }
291 
292     /**
293      * Phantom test deleting and aborting an edge element while doing a
294      * getPrev.
295      */
296     @Test
testPhantomDeleteGetPrevAbort()297     public void testPhantomDeleteGetPrevAbort()
298         throws Throwable {
299 
300         phantomWorker
301             (new PhantomTestConfiguration
302              ("testPhantomDeleteGetPrevAbort",
303               "F", "F", "G", "F",
304               false, false, false));
305     }
306 
307     /**
308      * Phantom Dup test inserting and committing a phantom while doing a
309      * getNext.
310      */
311     @Test
testPhantomDupInsertGetNextCommit()312     public void testPhantomDupInsertGetNextCommit()
313         throws Throwable {
314 
315         try {
316             phantomDupWorker
317                 (new PhantomTestConfiguration
318                  ("testPhantomDupInsertGetNextCommit",
319                   "F", "D", "C", "D",
320                   true, true, true));
321         } catch (Exception e) {
322             e.printStackTrace();
323             throw e;
324         }
325     }
326 
327     /**
328      * Phantom Dup test inserting and aborting a phantom while doing a getNext.
329      */
330     @Test
testPhantomDupInsertGetNextAbort()331     public void testPhantomDupInsertGetNextAbort()
332         throws Throwable {
333 
334         phantomDupWorker
335             (new PhantomTestConfiguration
336              ("testPhantomDupInsertGetNextAbort",
337               "F", "D", "C", "F",
338               true, true, false));
339     }
340 
341     /**
342      * Phantom Dup test inserting and committing a phantom while doing a
343      * getPrev.
344      */
345     @Test
testPhantomDupInsertGetPrevCommit()346     public void testPhantomDupInsertGetPrevCommit()
347         throws Throwable {
348 
349         phantomDupWorker
350             (new PhantomTestConfiguration
351              ("testPhantomDupInsertGetPrevCommit",
352               "C", "F", "G", "F",
353               true, false, true));
354     }
355 
356     /**
357      * Phantom Dup test inserting and aborting a phantom while doing a getPrev.
358      */
359     @Test
testPhantomDupInsertGetPrevAbort()360     public void testPhantomDupInsertGetPrevAbort()
361         throws Throwable {
362 
363         phantomDupWorker
364             (new PhantomTestConfiguration
365              ("testPhantomDupInsertGetPrevAbort",
366               "C", "F", "G", "C",
367               true, false, false));
368     }
369 
370     /**
371      * Phantom Dup test deleting and committing an edge element while doing a
372      * getNext.
373      */
374     @Test
testPhantomDupDeleteGetNextCommit()375     public void testPhantomDupDeleteGetNextCommit()
376         throws Throwable {
377 
378         phantomDupWorker
379             (new PhantomTestConfiguration
380              ("testPhantomDupDeleteGetNextCommit",
381               "F", "F", "C", "G",
382               false, true, true));
383     }
384 
385     /**
386      * Phantom Dup test deleting and aborting an edge element while doing a
387      * getNext.
388      */
389     @Test
testPhantomDupDeleteGetNextAbort()390     public void testPhantomDupDeleteGetNextAbort()
391         throws Throwable {
392 
393         phantomDupWorker
394             (new PhantomTestConfiguration
395              ("testPhantomDupDeleteGetNextAbort",
396               "F", "F", "C", "F",
397               false, true, false));
398     }
399 
400     /**
401      * Phantom Dup test deleting and committing an edge element while doing a
402      * getPrev.
403      */
404     @Test
testPhantomDupDeleteGetPrevCommit()405     public void testPhantomDupDeleteGetPrevCommit()
406         throws Throwable {
407 
408         phantomDupWorker
409             (new PhantomTestConfiguration
410              ("testPhantomDupDeleteGetPrevCommit",
411               "F", "F", "G", "C",
412               false, false, true));
413     }
414 
415     /**
416      * Phantom Dup test deleting and aborting an edge element while doing a
417      * getPrev.
418      */
419     @Test
testPhantomDupDeleteGetPrevAbort()420     public void testPhantomDupDeleteGetPrevAbort()
421         throws Throwable {
422 
423         phantomDupWorker
424             (new PhantomTestConfiguration
425              ("testPhantomDupDeleteGetPrevAbort",
426               "F", "F", "G", "F",
427               false, false, false));
428     }
429 
phantomWorker(PhantomTestConfiguration c)430     private void phantomWorker(PhantomTestConfiguration c)
431         throws Throwable {
432 
433         try {
434             this.config = c;
435             setupDatabaseAndEnv(false);
436 
437             if (config.doInsert &&
438                 !config.doGetNext) {
439 
440                 Transaction txnDel =
441                     env.beginTransaction(null, TransactionConfig.DEFAULT);
442 
443                 /*
444                  * Delete the first entry in the second bin so that we can
445                  * reinsert it in tester1 and have it be the first entry in
446                  * that bin.  If we left F and then tried to insert something
447                  * to the left of F, it would end up in the first bin.
448                  */
449                 assertEquals
450                     (OperationStatus.SUCCESS,
451                      db.delete(txnDel,
452                                new DatabaseEntry(StringUtils.toUTF8("F"))));
453                 txnDel.commit();
454             }
455 
456             JUnitThread tester1 =
457                 new JUnitThread(config.testName + "1") {
458                     public void testBody()
459                         throws Throwable {
460 
461                         Cursor cursor = null;
462                         try {
463                             Transaction txn1 =
464                                 env.beginTransaction(null, null);
465                             cursor = db.openCursor(txn1, CursorConfig.DEFAULT);
466                             OperationStatus status =
467                                 cursor.getSearchKey
468                                 (new DatabaseEntry(StringUtils.toUTF8
469                                     (config.thread1EntryToLock)),
470                                  new DatabaseEntry(),
471                                  LockMode.RMW);
472                             assertEquals(OperationStatus.SUCCESS, status);
473                             sequence++;  // 0 -> 1
474 
475                             /* Wait for tester2 to position cursor. */
476                             while (sequence < 2) {
477                                 Thread.yield();
478                             }
479 
480                             if (config.doInsert) {
481                                 status = db.put
482                                     (txn1,
483                                      new DatabaseEntry
484                                      (StringUtils.toUTF8(config.thread1OpArg)),
485                                      new DatabaseEntry(new byte[10]));
486                             } else {
487                                 status = db.delete
488                                     (txn1,
489                                      new DatabaseEntry
490                                      (StringUtils.toUTF8(config.thread1OpArg)));
491                             }
492                             assertEquals(OperationStatus.SUCCESS, status);
493                             sequence++;     // 2 -> 3
494 
495                             /*
496                              * Since we can't increment sequence when tester2
497                              * blocks on the getNext call, all we can do is
498                              * bump sequence right before the getNext, and then
499                              * wait a little in this thread for tester2 to
500                              * block.
501                              */
502                             try {
503                                 Thread.sleep(1000);
504                             } catch (InterruptedException IE) {
505                             }
506 
507                             cursor.close();
508                             cursor = null;
509                             if (config.doCommit) {
510                                 txn1.commit();
511                             } else {
512                                 txn1.abort();
513                             }
514                         } catch (DatabaseException DBE) {
515                             if (cursor != null) {
516                                 cursor.close();
517                             }
518                             DBE.printStackTrace();
519                             fail("caught DatabaseException " + DBE);
520                         }
521                     }
522                 };
523 
524             JUnitThread tester2 =
525                 new JUnitThread(config.testName + "2") {
526                     public void testBody()
527                         throws Throwable {
528 
529                         Cursor cursor = null;
530                         try {
531                             Transaction txn2 =
532                                 env.beginTransaction(null, null);
533                             txn2.setLockTimeout(LOCK_TIMEOUT);
534                             cursor = db.openCursor(txn2, CursorConfig.DEFAULT);
535 
536                             /* Wait for tester1 to position cursor. */
537                             while (sequence < 1) {
538                                 Thread.yield();
539                             }
540 
541                             OperationStatus status =
542                                 cursor.getSearchKey
543                                 (new DatabaseEntry
544                                  (StringUtils.toUTF8(config.thread2Start)),
545                                  new DatabaseEntry(),
546                                  LockMode.DEFAULT);
547                             assertEquals(OperationStatus.SUCCESS, status);
548 
549                             sequence++;           // 1 -> 2
550 
551                             /* Wait for tester1 to insert/delete. */
552                             while (sequence < 3) {
553                                 Thread.yield();
554                             }
555 
556                             DatabaseEntry nextKey = new DatabaseEntry();
557                             try {
558 
559                                 /*
560                                  * This will block until tester1 above commits.
561                                  */
562                                 if (config.doGetNext) {
563                                     status =
564                                         cursor.getNext(nextKey,
565                                                        new DatabaseEntry(),
566                                                        LockMode.DEFAULT);
567                                 } else {
568                                     status =
569                                         cursor.getPrev(nextKey,
570                                                        new DatabaseEntry(),
571                                                        LockMode.DEFAULT);
572                                 }
573                             } catch (DatabaseException DBE) {
574                                 System.out.println("t2 caught " + DBE);
575                             }
576                             assertEquals(3, sequence);
577                             assertEquals(config.expectedResult,
578                                          StringUtils.fromUTF8
579                                          (nextKey.getData()));
580                             cursor.close();
581                             cursor = null;
582                             txn2.commit();
583                         } catch (DatabaseException DBE) {
584                             if (cursor != null) {
585                                 cursor.close();
586                             }
587                             DBE.printStackTrace();
588                             fail("caught DatabaseException " + DBE);
589                         }
590                     }
591                 };
592 
593             tester1.start();
594             tester2.start();
595 
596             tester1.finishTest();
597             tester2.finishTest();
598         } finally {
599             db.close();
600             close(env);
601             env = null;
602         }
603     }
604 
phantomDupWorker(PhantomTestConfiguration c)605     private void phantomDupWorker(PhantomTestConfiguration c)
606         throws Throwable {
607 
608         Cursor cursor = null;
609         try {
610             this.config = c;
611             setupDatabaseAndEnv(true);
612 
613             if (config.doInsert &&
614                 !config.doGetNext) {
615 
616                 Transaction txnDel =
617                     env.beginTransaction(null, TransactionConfig.DEFAULT);
618                 cursor = db.openCursor(txnDel, CursorConfig.DEFAULT);
619 
620                 /*
621                  * Delete the first entry in the second bin so that we can
622                  * reinsert it in tester1 and have it be the first entry in
623                  * that bin.  If we left F and then tried to insert something
624                  * to the left of F, it would end up in the first bin.
625                  */
626                 assertEquals(OperationStatus.SUCCESS, cursor.getSearchBoth
627                              (new DatabaseEntry(StringUtils.toUTF8(DUPKEY)),
628                               new DatabaseEntry(StringUtils.toUTF8("F")),
629                               LockMode.DEFAULT));
630                 assertEquals(OperationStatus.SUCCESS, cursor.delete());
631                 cursor.close();
632                 cursor = null;
633                 txnDel.commit();
634             }
635 
636             JUnitThread tester1 =
637                 new JUnitThread(config.testName + "1") {
638                     public void testBody()
639                         throws Throwable {
640 
641                         Cursor cursor = null;
642                         Cursor c = null;
643                         try {
644                             Transaction txn1 =
645                                 env.beginTransaction(null, null);
646                             cursor = db.openCursor(txn1, CursorConfig.DEFAULT);
647                             OperationStatus status =
648                                 cursor.getSearchBoth
649                                 (new DatabaseEntry(StringUtils.toUTF8(DUPKEY)),
650                                  new DatabaseEntry(StringUtils.toUTF8
651                                      (config.thread1EntryToLock)),
652                                  LockMode.RMW);
653                             assertEquals(OperationStatus.SUCCESS, status);
654                             cursor.close();
655                             cursor = null;
656                             sequence++;  // 0 -> 1
657 
658                             /* Wait for tester2 to position cursor. */
659                             while (sequence < 2) {
660                                 Thread.yield();
661                             }
662 
663                             if (config.doInsert) {
664                                 status = db.put
665                                     (txn1,
666                                      new DatabaseEntry
667                                      (StringUtils.toUTF8(DUPKEY)),
668                                      new DatabaseEntry
669                                      (StringUtils.toUTF8
670                                         (config.thread1OpArg)));
671                             } else {
672                                 c = db.openCursor(txn1, CursorConfig.DEFAULT);
673                                 assertEquals(OperationStatus.SUCCESS,
674                                              c.getSearchBoth
675                                              (new DatabaseEntry
676                                               (StringUtils.toUTF8(DUPKEY)),
677                                               new DatabaseEntry
678                                               (StringUtils.toUTF8
679                                                (config.thread1OpArg)),
680                                               LockMode.DEFAULT));
681                                 assertEquals(OperationStatus.SUCCESS,
682                                              c.delete());
683                                 c.close();
684                                 c = null;
685                             }
686                             assertEquals(OperationStatus.SUCCESS, status);
687                             sequence++;     // 2 -> 3
688 
689                             /*
690                              * Since we can't increment sequence when tester2
691                              * blocks on the getNext call, all we can do is
692                              * bump sequence right before the getNext, and then
693                              * wait a little in this thread for tester2 to
694                              * block.
695                              */
696                             try {
697                                 Thread.sleep(1000);
698                             } catch (InterruptedException IE) {
699                             }
700 
701                             if (config.doCommit) {
702                                 txn1.commit();
703                             } else {
704                                 txn1.abort();
705                             }
706                         } catch (DatabaseException DBE) {
707                             if (cursor != null) {
708                                 cursor.close();
709                             }
710                             if (c != null) {
711                                 c.close();
712                             }
713                             DBE.printStackTrace();
714                             fail("caught DatabaseException " + DBE);
715                         }
716                     }
717                 };
718 
719             JUnitThread tester2 =
720                 new JUnitThread("testPhantomInsert2") {
721                     public void testBody()
722                         throws Throwable {
723 
724                         Cursor cursor = null;
725                         try {
726                             Transaction txn2 =
727                                 env.beginTransaction(null, null);
728                             txn2.setLockTimeout(LOCK_TIMEOUT);
729                             cursor = db.openCursor(txn2, CursorConfig.DEFAULT);
730 
731                             /* Wait for tester1 to position cursor. */
732                             while (sequence < 1) {
733                                 Thread.yield();
734                             }
735 
736                             OperationStatus status =
737                                 cursor.getSearchBoth
738                                 (new DatabaseEntry(StringUtils.toUTF8(DUPKEY)),
739                                  new DatabaseEntry
740                                  (StringUtils.toUTF8(config.thread2Start)),
741                                  LockMode.DEFAULT);
742                             assertEquals(OperationStatus.SUCCESS, status);
743 
744                             sequence++;           // 1 -> 2
745 
746                             /* Wait for tester1 to insert/delete. */
747                             while (sequence < 3) {
748                                 Thread.yield();
749                             }
750 
751                             DatabaseEntry nextKey = new DatabaseEntry();
752                             DatabaseEntry nextData = new DatabaseEntry();
753                             try {
754 
755                                 /*
756                                  * This will block until tester1 above commits.
757                                  */
758                                 if (config.doGetNext) {
759                                     status =
760                                         cursor.getNextDup(nextKey, nextData,
761                                                           LockMode.DEFAULT);
762                                 } else {
763                                     status =
764                                         cursor.getPrevDup(nextKey, nextData,
765                                                           LockMode.DEFAULT);
766                                 }
767                             } catch (DatabaseException DBE) {
768                                 System.out.println("t2 caught " + DBE);
769                             }
770                             assertEquals(3, sequence);
771                             byte[] data = nextData.getData();
772                             assertEquals(config.expectedResult,
773                                          StringUtils.fromUTF8(data));
774                             cursor.close();
775                             cursor = null;
776                             txn2.commit();
777                         } catch (DatabaseException DBE) {
778                             if (cursor != null) {
779                                 cursor.close();
780                             }
781                             DBE.printStackTrace();
782                             fail("caught DatabaseException " + DBE);
783                         }
784                     }
785                 };
786 
787             tester1.start();
788             tester2.start();
789 
790             tester1.finishTest();
791             tester2.finishTest();
792         } finally {
793             if (cursor != null) {
794                 cursor.close();
795             }
796             db.close();
797             close(env);
798             env = null;
799         }
800     }
801 
802     /**
803      * Sets up a small database with a tree containing 2 bins, one with A, B,
804      * and C, and the other with F, G, H, and I.
805      */
setupDatabaseAndEnv(boolean writeAsDuplicateData)806     private void setupDatabaseAndEnv(boolean writeAsDuplicateData)
807         throws DatabaseException {
808 
809         EnvironmentConfig envConfig = TestUtils.initEnvConfig();
810 
811         /* RepeatableRead isolation is required by this test. */
812         TestUtils.clearIsolationLevel(envConfig);
813 
814         DbInternal.disableParameterValidation(envConfig);
815         envConfig.setTransactional(true);
816         envConfig.setConfigParam(EnvironmentParams.NODE_MAX.getName(),
817                                  "6");
818         envConfig.setConfigParam(EnvironmentParams.NODE_MAX_DUPTREE.getName(),
819                                  "6");
820         envConfig.setConfigParam(EnvironmentParams.LOG_FILE_MAX.getName(),
821                                  "1024");
822         envConfig.setConfigParam(EnvironmentParams.ENV_CHECK_LEAKS.getName(),
823                                  "true");
824         envConfig.setAllowCreate(true);
825         envConfig.setTxnNoSync(Boolean.getBoolean(TestUtils.NO_SYNC));
826         env = create(envHome, envConfig);
827         Transaction txn = env.beginTransaction(null, null);
828         DatabaseConfig dbConfig = new DatabaseConfig();
829         dbConfig.setTransactional(true);
830         dbConfig.setSortedDuplicates(true);
831         dbConfig.setAllowCreate(true);
832         db = env.openDatabase(txn, "testDB", dbConfig);
833 
834         if (writeAsDuplicateData) {
835             writeDuplicateData(db, txn);
836         } else {
837             writeData(db, txn);
838         }
839 
840         txn.commit();
841     }
842 
843     String[] dataStrings = {
844         "A", "B", "C", "F", "G", "H", "I"
845     };
846 
writeData(Database db, Transaction txn)847     private void writeData(Database db, Transaction txn)
848         throws DatabaseException {
849 
850         for (int i = 0; i < dataStrings.length; i++) {
851             db.put(txn, new DatabaseEntry(StringUtils.toUTF8(dataStrings[i])),
852                    new DatabaseEntry(new byte[10]));
853         }
854     }
855 
writeDuplicateData(Database db, Transaction txn)856     private void writeDuplicateData(Database db, Transaction txn)
857         throws DatabaseException {
858 
859         for (int i = 0; i < dataStrings.length; i++) {
860             db.put(txn, new DatabaseEntry(StringUtils.toUTF8(DUPKEY)),
861                    new DatabaseEntry(StringUtils.toUTF8(dataStrings[i])));
862         }
863     }
864 
865     /**
866      * Insert data over many databases.
867      */
insertMultiDb(int numDbs)868     private void insertMultiDb(int numDbs)
869         throws DatabaseException {
870 
871         EnvironmentConfig envConfig = TestUtils.initEnvConfig();
872 
873         /* RepeatableRead isolation is required by this test. */
874         TestUtils.clearIsolationLevel(envConfig);
875 
876         DbInternal.disableParameterValidation(envConfig);
877         envConfig.setTransactional(true);
878         envConfig.setConfigParam
879             (EnvironmentParams.LOG_FILE_MAX.getName(), "1024");
880         envConfig.setConfigParam
881             (EnvironmentParams.ENV_CHECK_LEAKS.getName(), "true");
882         envConfig.setConfigParam
883             (EnvironmentParams.NODE_MAX.getName(), "6");
884         envConfig.setConfigParam
885             (EnvironmentParams.NODE_MAX_DUPTREE.getName(), "6");
886         envConfig.setTxnNoSync(Boolean.getBoolean(TestUtils.NO_SYNC));
887         envConfig.setAllowCreate(true);
888         Environment env = create(envHome, envConfig);
889 
890         Database[] myDb = new Database[numDbs];
891         Cursor[] cursor = new Cursor[numDbs];
892         Transaction txn =
893             env.beginTransaction(null, TransactionConfig.DEFAULT);
894 
895         /* In a non-replicated environment, the txn id should be positive. */
896         assertTrue(txn.getId() > 0);
897 
898         DatabaseConfig dbConfig = new DatabaseConfig();
899         dbConfig.setTransactional(true);
900         dbConfig.setAllowCreate(true);
901         dbConfig.setSortedDuplicates(true);
902         for (int i = 0; i < numDbs; i++) {
903             myDb[i] = env.openDatabase(txn, "testDB" + i, dbConfig);
904             cursor[i] = myDb[i].openCursor(txn, CursorConfig.DEFAULT);
905 
906             /*
907              * In a non-replicated environment, the db id should be
908              * positive.
909              */
910             DatabaseImpl dbImpl = DbInternal.getDatabaseImpl(myDb[i]);
911             assertTrue(dbImpl.getId().getId() > 0);
912         }
913 
914         /* Insert data in a round robin fashion to spread over log. */
915         DatabaseEntry key = new DatabaseEntry();
916         DatabaseEntry data = new DatabaseEntry();
917         for (int i = NUM_RECS; i > 0; i--) {
918             for (int c = 0; c < numDbs; c++) {
919                 key.setData(TestUtils.getTestArray(i + c));
920                 data.setData(TestUtils.getTestArray(i + c));
921                 if (DEBUG) {
922                     System.out.println("i = " + i +
923                                        TestUtils.dumpByteArray(key.getData()));
924                 }
925                 cursor[c].put(key, data);
926             }
927         }
928 
929         for (int i = 0; i < numDbs; i++) {
930             cursor[i].close();
931             myDb[i].close();
932         }
933         txn.commit();
934 
935         assertTrue(env.verify(null, System.err));
936         close(env);
937         env = null;
938 
939         envConfig.setAllowCreate(false);
940         env = create(envHome, envConfig);
941 
942         /*
943          * Before running the verifier, run the cleaner to make sure it has
944          * completed.  Otherwise, the cleaner will be running when we call
945          * verify, and open txns will be reported.
946          */
947         env.cleanLog();
948 
949         env.verify(null, System.err);
950 
951         /* Check each db in turn, using null transactions. */
952         dbConfig.setTransactional(false);
953         dbConfig.setAllowCreate(false);
954         for (int d = 0; d < numDbs; d++) {
955             Database checkDb = env.openDatabase(null, "testDB" + d,
956                                                 dbConfig);
957             Cursor myCursor = checkDb.openCursor(null, CursorConfig.DEFAULT);
958 
959             OperationStatus status =
960                 myCursor.getFirst(key, data, LockMode.DEFAULT);
961 
962             int i = 1;
963             while (status == OperationStatus.SUCCESS) {
964                 byte[] expectedKey = TestUtils.getTestArray(i + d);
965                 byte[] expectedData = TestUtils.getTestArray(i + d);
966 
967                 if (DEBUG) {
968                     System.out.println("Database " + d + " Key " + i +
969                                        " expected = " +
970                                        TestUtils.dumpByteArray(expectedKey) +
971                                        " seen = " +
972                                        TestUtils.dumpByteArray(key.getData()));
973                 }
974 
975                 assertTrue("Database " + d + " Key " + i + " expected = " +
976                            TestUtils.dumpByteArray(expectedKey) +
977                            " seen = " +
978                            TestUtils.dumpByteArray(key.getData()),
979                            Arrays.equals(expectedKey, key.getData()));
980                 assertTrue("Data " + i, Arrays.equals(expectedData,
981                                                       data.getData()));
982                 i++;
983 
984                 status = myCursor.getNext(key, data, LockMode.DEFAULT);
985             }
986             myCursor.close();
987             assertEquals("Number recs seen", NUM_RECS, i-1);
988             checkDb.close();
989         }
990         close(env);
991         env = null;
992     }
993 
994     /**
995      * This is a rudimentary test of DbInternal.search, just to make sure we're
996      * passing parameters down correctly the RangeCursor. RangeCursor is tested
997      * thoroughly elsewhere.
998      */
999     @Test
testDbInternalSearch()1000     public void testDbInternalSearch() {
1001 
1002         final EnvironmentConfig envConfig = TestUtils.initEnvConfig();
1003         envConfig.setAllowCreate(true);
1004         envConfig.setTransactional(true);
1005         final Environment env = create(envHome, envConfig);
1006 
1007         final DatabaseConfig dbConfig = new DatabaseConfig();
1008         dbConfig.setAllowCreate(true);
1009         dbConfig.setTransactional(true);
1010         final Database db = env.openDatabase(null, "testDB", dbConfig);
1011 
1012         insert(db, 1, 1);
1013         insert(db, 3, 3);
1014         insert(db, 5, 5);
1015 
1016         final Cursor cursor = db.openCursor(null, null);
1017 
1018         checkSearch(cursor, Search.GT,  0, 1);
1019         checkSearch(cursor, Search.GTE, 0, 1);
1020         checkSearch(cursor, Search.GT,  1, 3);
1021         checkSearch(cursor, Search.GTE, 1, 1);
1022         checkSearch(cursor, Search.GT,  2, 3);
1023         checkSearch(cursor, Search.GTE, 2, 3);
1024         checkSearch(cursor, Search.GT,  3, 5);
1025         checkSearch(cursor, Search.GTE, 3, 3);
1026         checkSearch(cursor, Search.GT,  4, 5);
1027         checkSearch(cursor, Search.GTE, 4, 5);
1028         checkSearch(cursor, Search.GT,  5, -1);
1029         checkSearch(cursor, Search.GTE, 5, 5);
1030         checkSearch(cursor, Search.GT,  6, -1);
1031         checkSearch(cursor, Search.GTE, 6, -1);
1032 
1033         checkSearch(cursor, Search.LT,  0, -1);
1034         checkSearch(cursor, Search.LTE, 0, -1);
1035         checkSearch(cursor, Search.LT,  1, -1);
1036         checkSearch(cursor, Search.LTE, 1, 1);
1037         checkSearch(cursor, Search.LT,  2, 1);
1038         checkSearch(cursor, Search.LTE, 2, 1);
1039         checkSearch(cursor, Search.LT,  3, 1);
1040         checkSearch(cursor, Search.LTE, 3, 3);
1041         checkSearch(cursor, Search.LT,  4, 3);
1042         checkSearch(cursor, Search.LTE, 4, 3);
1043         checkSearch(cursor, Search.LT,  5, 3);
1044         checkSearch(cursor, Search.LTE, 5, 5);
1045         checkSearch(cursor, Search.LT,  6, 5);
1046         checkSearch(cursor, Search.LTE, 6, 5);
1047 
1048         cursor.close();
1049         db.close();
1050         close(env);
1051     }
1052 
insert(Database db, int key, int data)1053     private void insert(Database db, int key, int data) {
1054         final DatabaseEntry keyEntry =
1055             new DatabaseEntry(new byte[] { (byte) key });
1056         final DatabaseEntry dataEntry =
1057             new DatabaseEntry(new byte[] { (byte) data });
1058 
1059         final OperationStatus status = db.put(null, keyEntry, dataEntry);
1060         assertSame(OperationStatus.SUCCESS, status);
1061     }
1062 
checkSearch(Cursor cursor, Search searchMode, int searchKey, int expectKey)1063     private void checkSearch(Cursor cursor,
1064                              Search searchMode,
1065                              int searchKey,
1066                              int expectKey) {
1067 
1068         final DatabaseEntry key = new DatabaseEntry(
1069             new byte[] { (byte) searchKey });
1070 
1071         final DatabaseEntry data = new DatabaseEntry();
1072 
1073         final OperationStatus status = DbInternal.search(
1074             cursor, key, null, data, searchMode, null);
1075 
1076         if (expectKey < 0) {
1077             assertSame(OperationStatus.NOTFOUND, status);
1078             return;
1079         }
1080 
1081         assertSame(OperationStatus.SUCCESS, status);
1082         assertEquals(expectKey, key.getData()[0]);
1083         assertEquals(expectKey, data.getData()[0]);
1084     }
1085 
1086     /**
1087      * This is a rudimentary test of DbInternal.searchBoth, just to make sure
1088      * we're passing parameters down correctly the RangeCursor. RangeCursor is
1089      * tested thoroughly elsewhere.
1090      */
1091     @Test
testDbInternalSearchBoth()1092     public void testDbInternalSearchBoth() {
1093 
1094         final EnvironmentConfig envConfig = TestUtils.initEnvConfig();
1095         envConfig.setAllowCreate(true);
1096         envConfig.setTransactional(true);
1097         final Environment env = create(envHome, envConfig);
1098 
1099         final DatabaseConfig dbConfig = new DatabaseConfig();
1100         dbConfig.setAllowCreate(true);
1101         dbConfig.setTransactional(true);
1102         final Database db = env.openDatabase(null, "testDB", dbConfig);
1103 
1104         final SecondaryConfig secConfig = new SecondaryConfig();
1105         secConfig.setAllowCreate(true);
1106         secConfig.setTransactional(true);
1107         secConfig.setSortedDuplicates(true);
1108         secConfig.setKeyCreator(new SecondaryKeyCreator() {
1109             @Override
1110             public boolean createSecondaryKey(SecondaryDatabase secondary,
1111                                               DatabaseEntry key,
1112                                               DatabaseEntry data,
1113                                               DatabaseEntry result) {
1114                 result.setData(data.getData());
1115                 return true;
1116             }
1117         });
1118         final SecondaryDatabase secDb = env.openSecondaryDatabase(
1119             null, "testDupsDB", db, secConfig);
1120 
1121         insert(db, 0, 1);
1122         insert(db, 1, 2);
1123         insert(db, 3, 2);
1124         insert(db, 5, 2);
1125 
1126         final SecondaryCursor cursor = secDb.openCursor(null, null);
1127 
1128         checkSearchBoth(cursor, Search.GT,  2, 0, 1);
1129         checkSearchBoth(cursor, Search.GTE, 2, 0, 1);
1130         checkSearchBoth(cursor, Search.GT,  2, 1, 3);
1131         checkSearchBoth(cursor, Search.GTE, 2, 1, 1);
1132         checkSearchBoth(cursor, Search.GT,  2, 2, 3);
1133         checkSearchBoth(cursor, Search.GTE, 2, 2, 3);
1134         checkSearchBoth(cursor, Search.GT,  2, 3, 5);
1135         checkSearchBoth(cursor, Search.GTE, 2, 3, 3);
1136         checkSearchBoth(cursor, Search.GT,  2, 4, 5);
1137         checkSearchBoth(cursor, Search.GTE, 2, 4, 5);
1138         checkSearchBoth(cursor, Search.GT,  2, 5, -1);
1139         checkSearchBoth(cursor, Search.GTE, 2, 5, 5);
1140         checkSearchBoth(cursor, Search.GT,  2, 6, -1);
1141         checkSearchBoth(cursor, Search.GTE, 2, 6, -1);
1142 
1143         checkSearchBoth(cursor, Search.LT,  2, 0, -1);
1144         checkSearchBoth(cursor, Search.LTE, 2, 0, -1);
1145         checkSearchBoth(cursor, Search.LT,  2, 1, -1);
1146         checkSearchBoth(cursor, Search.LTE, 2, 1, 1);
1147         checkSearchBoth(cursor, Search.LT,  2, 2, 1);
1148         checkSearchBoth(cursor, Search.LTE, 2, 2, 1);
1149         checkSearchBoth(cursor, Search.LT,  2, 3, 1);
1150         checkSearchBoth(cursor, Search.LTE, 2, 3, 3);
1151         checkSearchBoth(cursor, Search.LT,  2, 4, 3);
1152         checkSearchBoth(cursor, Search.LTE, 2, 4, 3);
1153         checkSearchBoth(cursor, Search.LT,  2, 5, 3);
1154         checkSearchBoth(cursor, Search.LTE, 2, 5, 5);
1155         checkSearchBoth(cursor, Search.LT,  2, 6, 5);
1156         checkSearchBoth(cursor, Search.LTE, 2, 6, 5);
1157 
1158         cursor.close();
1159         secDb.close();
1160         db.close();
1161         close(env);
1162     }
1163 
checkSearchBoth(Cursor cursor, Search searchMode, int searchKey, int searchPKey, int expectPKey)1164     private void checkSearchBoth(Cursor cursor,
1165                                  Search searchMode,
1166                                  int searchKey,
1167                                  int searchPKey,
1168                                  int expectPKey) {
1169 
1170         final DatabaseEntry key = new DatabaseEntry(
1171             new byte[] { (byte) searchKey });
1172 
1173         final DatabaseEntry pKey = new DatabaseEntry(
1174             new byte[] { (byte) searchPKey });
1175 
1176         final DatabaseEntry data = new DatabaseEntry();
1177 
1178         final OperationStatus status = DbInternal.searchBoth(
1179             cursor, key, pKey, data, searchMode, null);
1180 
1181         if (expectPKey < 0) {
1182             assertSame(OperationStatus.NOTFOUND, status);
1183             return;
1184         }
1185 
1186         assertSame(OperationStatus.SUCCESS, status);
1187         assertEquals(expectPKey, pKey.getData()[0]);
1188         assertEquals(searchKey, data.getData()[0]);
1189     }
1190 
1191     /**
1192      * Checks that Cursor.getSearchKeyRange (as well as internal range
1193      * searches) works even when insertions occur while doing a getNextBin in
1194      * the window where no latches are held. In particular there is a scenario
1195      * where it did not work, if a split during getNextBin arranges things in
1196      * a particular way.  This is a very specific scenario and requires many
1197      * insertions in the window, so it seems unlikely to occur in the wild.
1198      * This test creates that scenario.
1199      */
1200     @Test
testInsertionDuringGetNextBinDuringRangeSearch()1201     public void testInsertionDuringGetNextBinDuringRangeSearch() {
1202 
1203         /*
1204          * Disable daemons for predictability.  Disable BIN deltas so we can
1205          * compress away a deleted slot below (if a delta would be logged next,
1206          * slots won't be compressed).
1207          */
1208         final EnvironmentConfig envConfig = TestUtils.initEnvConfig();
1209         envConfig.setAllowCreate(true);
1210         envConfig.setTransactional(true);
1211         envConfig.setDurability(Durability.COMMIT_NO_SYNC);
1212         envConfig.setConfigParam(
1213             EnvironmentConfig.ENV_RUN_CHECKPOINTER, "false");
1214         envConfig.setConfigParam(
1215             EnvironmentConfig.ENV_RUN_CLEANER, "false");
1216         envConfig.setConfigParam(
1217             EnvironmentConfig.ENV_RUN_EVICTOR, "false");
1218         envConfig.setConfigParam(
1219             EnvironmentConfig.ENV_RUN_IN_COMPRESSOR, "false");
1220         envConfig.setConfigParam(
1221             EnvironmentConfig.TREE_BIN_DELTA, "0");
1222         env = create(envHome, envConfig);
1223 
1224         final DatabaseConfig dbConfig = new DatabaseConfig();
1225         dbConfig.setAllowCreate(true);
1226         dbConfig.setTransactional(true);
1227         final Database db = env.openDatabase(null, "testDB", dbConfig);
1228 
1229         final DatabaseEntry key = new DatabaseEntry();
1230         final DatabaseEntry value = new DatabaseEntry();
1231         OperationStatus status;
1232 
1233         /*
1234          * Create a tree that contains:
1235          *   A BIN with keys 0-63.
1236          *   Additional BINs with keys 1000-1063.
1237          *
1238          * Keys 1000-1063 are inserted in reverse order to make sure the split
1239          * occurs in the middle of the BIN (rather than a "special" split).
1240          *
1241          * Key 64 is deleted so that key 63 will be the last one in the BIN
1242          * when a split occurs later on, in the hook method below.
1243          */
1244         for (int i = 0; i < 64; i += 1) {
1245             insertRecord(db, i);
1246         }
1247         for (int i = 1063; i >= 1000; i -= 1) {
1248             insertRecord(db, i);
1249         }
1250         insertRecord(db, 64);
1251         deleteRecord(db, 64);
1252         env.compress();
1253 
1254         /*
1255          * Set a hook that will be called during the window when no INs are
1256          * latched when getNextBin is called when Cursor.searchInternal
1257          * processes a range search for key 500.  searchInternal first calls
1258          * CursorImpl.searchAndPosition which lands on key 63.  It then calls
1259          * CursorImpl.getNext, which does the getNextBin and calls the hook.
1260          */
1261         DbInternal.getDatabaseImpl(db).getTree().setGetParentINHook(
1262             new TestHook() {
1263                 @Override
1264                 public void doHook() {
1265                     /* Only process the first call to the hook. */
1266                     DbInternal.getDatabaseImpl(db).getTree().
1267                         setGetParentINHook(null);
1268                     /*
1269                      * Cause a split, leaving keys 0-63 in the first BIN and
1270                      * keys 64-130 in the second BIN.
1271                      */
1272                     for (int i = 64; i < 130; i += 1) {
1273                         insertRecord(db, i);
1274                     }
1275                 }
1276                 @Override public void hookSetup() { }
1277                 @Override public void doIOHook() { }
1278                 @Override public void doHook(Object obj) { }
1279                 @Override public Object getHookValue() { return null; }
1280             }
1281         );
1282 
1283         /*
1284          * Search for a key >= 500, which should find key 1000.  But due to the
1285          * bug, CursorImpl.getNext advances to key 64 in the second BIN, and
1286          * returns it.
1287          */
1288         IntegerBinding.intToEntry(500, key);
1289         final Cursor c = db.openCursor(null, null);
1290         status = c.getSearchKeyRange(key, value, null);
1291 
1292         c.close();
1293         db.close();
1294         close(env);
1295 
1296         Assert.assertSame(OperationStatus.SUCCESS, status);
1297         Assert.assertEquals(1000, IntegerBinding.entryToInt(key));
1298     }
1299 
insertRecord(Database db, int key)1300     private void insertRecord(Database db, int key) {
1301         final DatabaseEntry keyEntry = new DatabaseEntry();
1302         final DatabaseEntry dataEntry = new DatabaseEntry(new byte[1]);
1303         IntegerBinding.intToEntry(key, keyEntry);
1304         final OperationStatus status =
1305             db.putNoOverwrite(null, keyEntry, dataEntry);
1306         assertSame(OperationStatus.SUCCESS, status);
1307     }
1308 
deleteRecord(Database db, int key)1309     private void deleteRecord(Database db, int key) {
1310         final DatabaseEntry keyEntry = new DatabaseEntry();
1311         IntegerBinding.intToEntry(key, keyEntry);
1312         final OperationStatus status = db.delete(null, keyEntry);
1313         assertSame(OperationStatus.SUCCESS, status);
1314     }
1315 }
1316