/* * 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.AbstractKey; 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.SealedMap; import com.github.geophile.erdo.map.SealedMapBase; import com.github.geophile.erdo.map.diskmap.tree.Tree; import com.github.geophile.erdo.map.diskmap.tree.WriteableTree; import com.github.geophile.erdo.map.keyarray.KeyArray; import com.github.geophile.erdo.map.mergescan.AbstractMultiRecord; import com.github.geophile.erdo.transaction.TimestampSet; import com.github.geophile.erdo.util.FileUtil; import com.github.geophile.erdo.util.LongArray; import java.io.IOException; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; public class DiskMap extends SealedMapBase { // Consolidation.Element interface @Override public boolean durable() { assert !destroyed : this; return true; } // SealedMapBase interface @Override public MapCursor cursor(AbstractKey startKey, boolean singleKey) throws IOException, InterruptedException { assert !destroyed : this; return new DiskMapCursor(tree, tree.cursor(startKey), startKey, singleKey); } @Override public MapCursor keyScan(AbstractKey startKey, boolean singleKey) throws IOException, InterruptedException { assert !destroyed : this; if (LOG.isLoggable(Level.FINE)) { LOG.log(Level.FINE, "{0} keyScan: keys in memory: {1}", new Object[]{this, keys != null}); } return keys == null ? cursor(startKey, singleKey) : keys.cursor(startKey); } @Override public long recordCount() { return recordCount; } @Override public long estimatedSizeBytes() { assert !destroyed : this; assert tree != null; return tree.sizeBytes(); } @Override public void loadForConsolidation(MapCursor recordScan, MapCursor keyScan) throws UnsupportedOperationException, IOException, InterruptedException { assert !destroyed : this; LOG.log(Level.INFO, "Load {0}: starting", this); LazyRecord record; recordCount = 0; long count = 0; try { long inMemoryKeysLimit = factory.configuration().keysInMemoryMapLimit(); keys = new KeyArray(factory, (int) inMemoryKeysLimit); while ((record = recordScan.next()) != null) { // record could be a single or a multi-record. writeableTree.append can handle both. recordCount += writeableTree.append(record); if ((++count % INTERRUPT_CHECK_INTERVAL) == 0) { if (Thread.interrupted()) { throw new InterruptedException(); } } if (recordCount > inMemoryKeysLimit) { keys = null; } if (record instanceof AbstractMultiRecord) { // Can't get individual keys keys = null; } if (keys != null) { keys.append(record.key()); } if ((count % 100_000) == 0) { LOG.log(Level.INFO, "Load {0} records: {1}", new Object[]{this, count}); } record.destroyRecordReference(); } if (keys == null && // Couldn't build keys during record cursor keyScan != null && // We have a key cursor recordCount <= inMemoryKeysLimit) { // Size is OK keys = new KeyArray(factory, (int) recordCount); while ((record = keyScan.next()) != null) { keys.append(record.key()); if ((++count % INTERRUPT_CHECK_INTERVAL) == 0) { if (Thread.interrupted()) { throw new InterruptedException(); } } if ((count % 100_000) == 0) { LOG.log(Level.INFO, "Load {0} keys: {1}", new Object[]{this, count}); } } } if (keys != null) { keys.close(); } LOG.log(Level.INFO, "Load {0}: finished loading", this); closeTree(); LOG.log(Level.INFO, "Load {0}: closed", this); Manifest.write(dbStructure.manifestFile(tree.treeId()), this); } catch (InterruptedException e) { LOG.log(Level.WARNING, "Destroying {0} due to interruption", this); LOG.log(Level.WARNING, "Interruption", e); destroyPersistentState(); throw e; } } @Override public void destroyPersistentState() { if (!SAVE_OBSOLETE_TREES) { Tree treeToDestroy = tree == null ? writeableTree : tree; if (LOG.isLoggable(Level.INFO)) { LOG.log(Level.INFO, "{0} Destroying {1}", new Object[]{this, treeToDestroy}); } long treeId = treeToDestroy.treeId(); FileUtil.deleteFile(dbStructure.manifestFile(treeId)); treeToDestroy.destroy(); if (keys != null) { keys.destroy(); keys = null; destroyed = true; } } } @Override public boolean keysInMemory() { assert !destroyed : this; return keys != null; } @Override public MapCursor consolidationScan() throws IOException, InterruptedException { assert !destroyed : this; return new DiskMapCursor(tree, tree.consolidationScan(), null, false); } // DiskMap interface public LongArray obsoleteTreeIds() { assert !destroyed : this; return obsoleteTreeIds; } public Manifest manifest() { assert !destroyed : this; return manifest; } public static DiskMap recover(DatabaseOnDisk database, int treeId) throws IOException, InterruptedException { DiskMap diskMap = null; Manifest manifest = Manifest.read(database.dbStructure().manifestFile(treeId)); if (manifest != null && manifest.recordCount() > 0) { diskMap = new DiskMap(database, manifest); } return diskMap; } public static DiskMap create(DatabaseOnDisk database, TimestampSet timestamps, List<SealedMap> obsoleteTrees) throws IOException { return new DiskMap(database, timestamps, obsoleteTrees); } // For use by this package Tree tree() { assert !destroyed : this; assert tree != null : this; return tree; } // Used to create a new map DiskMap(DatabaseOnDisk database, TimestampSet timestamps, List<SealedMap> obsoleteTrees) throws IOException { super(database.factory()); this.dbStructure = database.dbStructure(); this.writeableTree = Tree.create(factory, dbStructure, mapId()); this.timestamps = timestamps; this.obsoleteTreeIds = new LongArray(); if (obsoleteTrees != null) { for (SealedMap obsoleteTree : obsoleteTrees) { this.obsoleteTreeIds.append(obsoleteTree.mapId()); } } } // Used to recover a map DiskMap(DatabaseOnDisk database, Manifest manifest) throws IOException, InterruptedException { super(database.factory(), manifest.treeId()); this.manifest = manifest; this.dbStructure = database.dbStructure(); this.timestamps = manifest.timestamps(); this.recordCount = manifest.recordCount(); this.obsoleteTreeIds = manifest.obsoleteTreeIds(); this.tree = Tree.recover(factory, dbStructure, manifest); assert recordCount <= Integer.MAX_VALUE : this; long inMemoryMapLimit = database.factory().configuration().keysInMemoryMapLimit(); if (recordCount <= inMemoryMapLimit) { this.keys = new KeyArray(factory, (int) recordCount); try { MapCursor cursor = cursor(null, false); LazyRecord record; while ((record = cursor.next()) != null) { this.keys.append(record.key()); record.destroyRecordReference(); } } finally { TreePositionTracker.destroyRemainingTreePositions(null); } this.keys.close(); } else { this.keys = null; } } // Used only by DiskMapFastMergeTest void loadWithKeys(MapCursor cursor, long recordCount) throws UnsupportedOperationException, IOException, InterruptedException { assert !destroyed : this; keys = new KeyArray(factory, (int) recordCount); LazyRecord record; long count = 0; try { while ((record = cursor.next()) != null) { writeableTree.append(record); keys.append(record.key()); if ((++count % INTERRUPT_CHECK_INTERVAL) == 0) { if (Thread.interrupted()) { throw new InterruptedException(); } } } keys.close(); this.recordCount = keys.size(); closeTree(); Manifest.write(dbStructure.manifestFile(tree.treeId()), this); } catch (InterruptedException e) { LOG.log(Level.WARNING, "Destroying {0} due to interruption", this); destroyPersistentState(); throw e; } } // Used only by DiskMapFastMergeTest void loadWithoutKeys(MapCursor cursor) throws UnsupportedOperationException, IOException, InterruptedException { assert !destroyed : this; recordCount = 0; LazyRecord record; long count = 0; try { while ((record = cursor.next()) != null) { recordCount += writeableTree.append(record); if ((++count % INTERRUPT_CHECK_INTERVAL) == 0) { if (Thread.interrupted()) { throw new InterruptedException(); } } } closeTree(); Manifest.write(dbStructure.manifestFile(tree.treeId()), this); } catch (InterruptedException e) { LOG.log(Level.WARNING, "Destroying {0} due to interruption", this); destroyPersistentState(); throw e; } } // For use by this class private void closeTree() throws IOException, InterruptedException { assert !destroyed : this; LOG.log(Level.INFO, "Closing tree {0}", this); tree = writeableTree.close(); writeableTree = null; } // Class state private static final Logger LOG = Logger.getLogger(DiskMap.class.getName()); private static final boolean SAVE_OBSOLETE_TREES = Boolean.getBoolean("saveObsoleteTrees"); private static int INTERRUPT_CHECK_INTERVAL = 100; // Object state private Manifest manifest; private Tree tree; private WriteableTree writeableTree; private final DBStructure dbStructure; private final LongArray obsoleteTreeIds; private long recordCount; private KeyArray keys; private boolean destroyed = false; }