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