/* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package com.github.geophile.erdo.map.diskmap; import com.github.geophile.erdo.TestFactory; import com.github.geophile.erdo.TestKey; import com.github.geophile.erdo.TestRecord; import com.github.geophile.erdo.map.testarraymap.TestArrayMap; import com.github.geophile.erdo.AbstractRecord; import com.github.geophile.erdo.Configuration; import com.github.geophile.erdo.RecordFactory; import com.github.geophile.erdo.apiimpl.TreePositionTracker; import com.github.geophile.erdo.apiimpl.DatabaseOnDisk; import com.github.geophile.erdo.map.LazyRecord; import com.github.geophile.erdo.map.MapCursor; import com.github.geophile.erdo.map.diskmap.tree.LevelOneMultiRecord; import com.github.geophile.erdo.map.mergescan.FastMergeCursor; import com.github.geophile.erdo.map.mergescan.MultiRecordKey; import com.github.geophile.erdo.transaction.TimestampSet; import com.github.geophile.erdo.util.FileUtil; import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import java.io.File; import java.io.IOException; import static junit.framework.Assert.*; public class DiskMapFastMergeTest { @BeforeClass public static void beforeClass() throws IOException { TestKey.testErdoId(ERDO_ID); FACTORY.recordFactory(ERDO_ID, RecordFactory.simpleRecordFactory(TestKey.class, TestRecord.class)); Configuration configuration = FACTORY.configuration(); configuration.diskPageSizeBytes(4096); configuration.diskSegmentSizeBytes(8192); configuration.keysInMemoryMapLimit(0); DB_DIRECTORY = new File(FileUtil.tempDirectory(), DB_NAME); DB_STRUCTURE = new DBStructure(DB_DIRECTORY.getCanonicalFile()); } @Before public void before() throws IOException, InterruptedException { FileUtil.deleteDirectory(DB_DIRECTORY); FileUtil.deleteDirectory(DB_STRUCTURE.dbDirectory()); db = DatabaseOnDisk.createDatabase(DB_DIRECTORY.getAbsoluteFile(), FACTORY); } @After public void after() throws IOException, InterruptedException { db.close(); FACTORY.reset(); } @Test public void testSingleLevel() throws IOException, InterruptedException { final int N = 10; TestArrayMap a = new TestArrayMap(FACTORY, new TimestampSet(111)); TestArrayMap b = new TestArrayMap(FACTORY, new TimestampSet(222)); int id = 0; // 0-9 A for (int i = 0; i < N; i++) { addRecord(a, id++, A_SMALL_FILLER); } // 10-19 B for (int i = 0; i < N; i++) { addRecord(b, id++, B_SMALL_FILLER); } // Load DiskMaps DiskMap diskMapA = newDiskMap(111); DiskMap diskMapB = newDiskMap(222); diskMapA.loadForConsolidation(a.cursor(null, false), a.keyScan(null, false)); diskMapB.loadForConsolidation(b.cursor(null, false), b.keyScan(null, false)); // Merge FastMergeCursor merge = new FastMergeCursor(); merge.addInput(diskMapA.consolidationScan()); merge.addInput(diskMapB.consolidationScan()); merge.start(); DiskMap mergedMap = newDiskMap(333); mergedMap.loadWithKeys(merge, 1000); // Check merged map MapCursor cursor = mergedMap.cursor(null, false); LazyRecord lazyRecord; int expected = 0; while ((lazyRecord = cursor.next()) != null) { TestRecord record = (TestRecord) lazyRecord.materializeRecord(); int key = record.key().key(); char v = record.stringValue().charAt(0); assertEquals(expected++, key); if (key < N) { assertEquals('a', v); } else if (key < 2 * N) { assertEquals('b', v); } else { fail(); } } assertEquals(N * 2, expected); } @Test public void testTwoLevelDisjoint() throws IOException, InterruptedException { // A has 0..(N-1). B has N..(2N-1). This should be a fast merge. Requires correct handling of end-of-file // keys in level 1. Incorrect behavior sees ranges 0..null and N..null in the merge, resulting in a // slow merge. Correct behavior sees 0..(N-1), N..(2N-1). (This was the motivation for file-terminating keys // in summary files.) final int N = 64; TestArrayMap a = new TestArrayMap(FACTORY, new TimestampSet(111)); TestArrayMap b = new TestArrayMap(FACTORY, new TimestampSet(222)); int id = 0; // 0-(N-1) A for (int i = 0; i < N; i++) { addRecord(a, id++, A_BIG_FILLER); } // N-(2N-1) B for (int i = N; i < 2 * N; i++) { addRecord(b, id++, B_BIG_FILLER); } // Load DiskMaps DiskMap diskMapA = newDiskMap(111); DiskMap diskMapB = newDiskMap(222); diskMapA.loadForConsolidation(a.cursor(null, false), a.keyScan(null, false)); diskMapB.loadForConsolidation(b.cursor(null, false), b.keyScan(null, false)); // Merge FastMergeCursor merge = new FastMergeCursor(); merge.addInput(diskMapA.consolidationScan()); merge.addInput(diskMapB.consolidationScan()); merge.start(); // First record AbstractRecord record = merge.next().materializeRecord(); assertTrue(record instanceof LevelOneMultiRecord); LevelOneMultiRecord multiRecord = (LevelOneMultiRecord) record; MultiRecordKey key = (MultiRecordKey) multiRecord.key(); assertEquals(0, ((TestKey) key.lo()).key()); assertEquals(N - 1, ((TestKey) key.hi()).key()); // Second record record = merge.next().materializeRecord(); assertTrue(record instanceof LevelOneMultiRecord); multiRecord = (LevelOneMultiRecord) record; key = (MultiRecordKey) multiRecord.key(); assertEquals(N, ((TestKey) key.lo()).key()); assertEquals(2 * N - 1, ((TestKey) key.hi()).key()); // There should be no more records assertTrue(merge.next() == null); TreePositionTracker.destroyRemainingTreePositions(null); } @Test public void testTwoLevel() throws IOException, InterruptedException { final int N = 1000; TestArrayMap a = new TestArrayMap(FACTORY, new TimestampSet(111)); TestArrayMap b = new TestArrayMap(FACTORY, new TimestampSet(222)); int id = 0; // 0-1999 AB: Interleave records between two maps for (int i = 0; i < N; i++) { addRecord(a, id++, A_BIG_FILLER); addRecord(b, id++, B_BIG_FILLER); } // 2000-2999 A: Put a long run in each map for (int i = 0; i < N; i++) { addRecord(a, id++, A_BIG_FILLER); } // 3000-3999 B for (int i = 0; i < N; i++) { addRecord(b, id++, B_BIG_FILLER); } // 4000-5999 AB: More interleaved records for (int i = 0; i < N; i++) { addRecord(a, id++, A_BIG_FILLER); addRecord(b, id++, B_BIG_FILLER); } // Load DiskMaps DiskMap diskMapA = newDiskMap(111); DiskMap diskMapB = newDiskMap(222); diskMapA.loadForConsolidation(a.cursor(null, false), a.keyScan(null, false)); diskMapB.loadForConsolidation(b.cursor(null, false), b.keyScan(null, false)); // Merge FastMergeCursor merge = new FastMergeCursor(); merge.addInput(diskMapA.consolidationScan()); merge.addInput(diskMapB.consolidationScan()); merge.start(); DiskMap mergedMap = newDiskMap(333); mergedMap.loadWithoutKeys(merge); // Check merged map MapCursor scan = mergedMap.cursor(null, false); LazyRecord lazyRecord; int expected = 0; while ((lazyRecord = scan.next()) != null) { TestRecord record = (TestRecord) lazyRecord.materializeRecord(); int key = record.key().key(); char v = record.stringValue().charAt(0); assertEquals(expected++, key); if (key < 2 * N) { assertEquals(key % 2 == 0 ? 'a' : 'b', v); } else if (key < 3 * N) { assertEquals('a', v); } else if (key < 4 * N) { assertEquals('b', v); } else if (key < 6 * N) { assertEquals(key % 2 == 0 ? 'a' : 'b', v); } else { fail(); } } assertEquals(N * 6, expected); } private DiskMap newDiskMap(long timestamp) throws IOException, InterruptedException { return DiskMap.create(db, new TimestampSet(timestamp), null); } private void addRecord(TestArrayMap map, int id, String value) throws IOException, InterruptedException { TestKey key = new TestKey(id); key.transactionTimestamp(map.timestamps().minTimestamp()); TestRecord record = new TestRecord(key); record.value(value); map.put(record, false); } private void dump(String label, DiskMap map) throws IOException, InterruptedException { MapCursor cursor = map.cursor(null, false); LazyRecord lazyRecord; int count = 0; while ((lazyRecord = cursor.next()) != null) { TestRecord record = (TestRecord) lazyRecord.materializeRecord(); System.out.println(String.format("%s %s: %s -> %s", label, count++, record.key(), record.stringValue().substring(0, 10))); } } private static final String A_BIG_FILLER = "aaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaa"; private static final String B_BIG_FILLER = "bbbbbbbbbbbbbbbbbbbb" + "bbbbbbbbbbbbbbbbbbbb" + "bbbbbbbbbbbbbbbbbbbb" + "bbbbbbbbbbbbbbbbbbbb" + "bbbbbbbbbbbbbbbbbbbb"; private static final String A_SMALL_FILLER = "a"; private static final String B_SMALL_FILLER = "b"; private static final int ERDO_ID = 1; private static final String DB_NAME = "erdo"; private static final TestFactory FACTORY = new TestFactory(); private static File DB_DIRECTORY; private static DBStructure DB_STRUCTURE; private DatabaseOnDisk db; }