/* * 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.tree; import com.github.geophile.erdo.AbstractKey; import com.github.geophile.erdo.AbstractRecord; import com.github.geophile.erdo.apiimpl.CursorImpl; import com.github.geophile.erdo.apiimpl.TreePositionTracker; import com.github.geophile.erdo.immutableitemcache.ImmutableItemManager; import com.github.geophile.erdo.map.Factory; import com.github.geophile.erdo.map.LazyRecord; import com.github.geophile.erdo.map.diskmap.DiskPage; import com.github.geophile.erdo.map.diskmap.DiskPageCache; import com.github.geophile.erdo.map.diskmap.PageId; import com.github.geophile.erdo.util.IdGenerator; import com.github.geophile.erdo.util.ThrowableUtil; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.util.logging.Level; import java.util.logging.Logger; // A TreePosition represents a position in a tree, at all levels of resolution between tree and record. public class TreePosition extends LazyRecord implements ImmutableItemManager<PageId, DiskPage> { // Object interface @Override public String toString() { StringBuilder buffer = new StringBuilder(); buffer.append('T'); if (tree == null) { buffer.append('?'); } else { buffer.append(tree.treeId()); if (level != null) { buffer.append("/L"); buffer.append(level.levelNumber()); if (atEnd) { buffer.append("/END"); } else if (segment != null) { buffer.append("/S"); buffer.append(segment.segmentNumber()); buffer.append('('); buffer.append(segment.segmentId()); buffer.append(')'); if (pageNumber != UNDEFINED) { buffer.append("/P"); buffer.append(pageNumber); if (recordNumber != UNDEFINED) { buffer.append("/R"); buffer.append(recordNumber); } } } } } buffer.append('['); buffer.append(id); buffer.append(']'); return buffer.toString(); } @Override public boolean equals(Object o) { boolean equals = false; if (o != null && o.getClass() == this.getClass()) { TreePosition that = (TreePosition) o; // If this and that are resolved to different units (e.g. page and record), then // comparisons for the smaller unit will lead to a result of false. equals = this.tree == that.tree && this.level == that.level && this.segment == that.segment && this.pageNumber == that.pageNumber && // At the end of a page, recordNumber could be a real record number, or LAST_RECORD_ON_PAGE (this.recordNumber == that.recordNumber || this.isLastRecordOnPage() && that.isLastRecordOnPage()); } return equals; } @Override public int hashCode() { throw new UnsupportedOperationException(); } // LazyRecord interface @Override public AbstractKey key() throws IOException, InterruptedException { assert inUse : this; checkResolvedToRecord(); // TODO: Don't call readKey unnecessarily, use key if it has the correct value. key = ensurePage().readKey(recordNumber, pageAccessBuffers); return key; } @Override public ByteBuffer keyBuffer() throws IOException, InterruptedException { assert inUse : this; checkResolvedToRecord(); ensurePage(); return pageAccessBuffers.keyBuffer(); } public AbstractRecord materializeRecord() throws IOException, InterruptedException { assert inUse : this; checkResolvedToRecord(); record = ensurePage().readRecord(recordNumber, pageAccessBuffers); return record; } @Override public ByteBuffer recordBuffer() throws IOException, InterruptedException { assert inUse : this; checkResolvedToRecord(); ensurePage(); return pageAccessBuffers.recordBuffer(); } @Override public long estimatedSizeBytes() throws IOException, InterruptedException { assert inUse : this; return recordBuffer().remaining() + keyBuffer().remaining(); } @Override public boolean prefersSerialized() { assert inUse : this; return true; } // Don't need to override destroyRecordReference, to call deactivateForPool. super.destroyRecordReference // returns the TreePosition to its pool, and AbstractPool.returnResource calls deactivateForPool. // ImmutableItemManager interface @Override public DiskPage getItemForCache(PageId id) throws IOException, InterruptedException { assert inUse : this; checkResolvedToPage(); File file = tree.dbStructure().segmentFile(segment.segmentId()); long offset = ((long) pageNumber) * tree.pageSizeBytes(); ByteBuffer pageBuffer = factory.pageMemoryManager().takePageBuffer(); factory.segmentFileManager().readPage(file, offset, pageBuffer); return new DiskPage(factory, id, tree.pageAddress(segment.segmentNumber(), pageNumber), level.levelNumber(), pageBuffer); } @Override public void cleanupItemEvictedFromCache(DiskPage item) { factory.pageMemoryManager().returnPageBuffer(item.pageBuffer()); } // TreePosition interface - access public Tree tree() { assert inUse : this; return tree; } public TreeLevel level() { assert inUse : this; return level; } public TreeSegment segment() { assert inUse : this; return segment; } public DiskPage page() throws IOException, InterruptedException { assert inUse : this; checkResolvedToPage(); return ensurePage(); } public int pageAddress() { assert inUse : this; checkResolvedToPage(); return tree.pageAddress(segment.segmentNumber(), pageNumber); } // TreePosition interface - setting absolute position public TreePosition level(int levelNumber) { assert inUse : this; level = tree.level(levelNumber); segment = null; pageNumber = UNDEFINED; page(null); recordNumber = UNDEFINED; clearCachedRecord(); return this; } public TreePosition goToFirstSegmentOfLevel() { assert inUse : this; checkResolvedToLevel(); segment = level.segment(0); return this; } public TreePosition goToLastSegmentOfLevel() { assert inUse : this; checkResolvedToLevel(); segment = level.segment(level.segments() - 1); return this; } public TreePosition goToFirstPageOfSegment() { assert inUse : this; checkResolvedToSegment(); pageNumber = 0; randomRead = true; // TODO: Assert segment has at least one page return this; } public TreePosition goToLastPageOfSegment() { assert inUse : this; checkResolvedToSegment(); pageNumber = segment.pages() - 1; randomRead = true; // TODO: Assert segment has at least one page return this; } public TreePosition goToFirstRecordOfPage() throws IOException, InterruptedException { assert inUse : this; checkResolvedToPage(); ensurePage(); // TODO: Assert page has at least one record recordNumber = 0; clearCachedRecord(); return this; } public TreePosition goToLastRecordOfPage() throws IOException, InterruptedException { assert inUse : this; checkResolvedToPage(); ensurePage(); // TODO: Assert page has at least one record recordNumber = page.nRecords() - 1; clearCachedRecord(); return this; } public TreePosition pageAddress(int pageAddress) { assert inUse : this; checkResolvedToLevel(); this.segment = level.segment(tree.segmentNumber(pageAddress)); this.pageNumber = tree.pageNumber(pageAddress); this.page(null); this.randomRead = true; this.recordNumber = UNDEFINED; clearCachedRecord(); return this; } public TreePosition recordNumber(int newRecordNumber) { assert inUse : this; checkResolvedToPage(); assert page != null : this; assert newRecordNumber >= 0 : newRecordNumber; assert newRecordNumber < page.nRecords() : newRecordNumber; recordNumber = newRecordNumber; clearCachedRecord(); return this; } // TreePosition interface - setting relative position public TreePosition goToEnd() { assert inUse : this; if (LOG.isLoggable(Level.INFO)) { LOG.log(Level.INFO, "{0}: atEnd = true", name()); } atEnd = true; segment = null; pageNumber = UNDEFINED; page(null); randomRead = true; recordNumber = UNDEFINED; clearCachedRecord(); return this; } public TreePosition goToNextSegment() { assert inUse : this; checkResolvedToSegment(); if (segment.segmentNumber() == level.segments() - 1) { atEnd = true; segment = null; } else { segment = level.segment(segment.segmentNumber() + 1); } pageNumber = UNDEFINED; page(null); randomRead = true; recordNumber = UNDEFINED; clearCachedRecord(); return this; } public TreePosition goToPreviousSegment() { assert inUse : this; checkResolvedToSegment(); if (segment.segmentNumber() == 0) { atEnd = true; segment = null; } else { segment = level.segment(segment.segmentNumber() - 1); } pageNumber = UNDEFINED; page(null); randomRead = true; recordNumber = UNDEFINED; clearCachedRecord(); return this; } public TreePosition goToNextPage() { assert inUse : this; checkResolvedToPage(); if (++pageNumber < segment.pages()) { page(null); randomRead = false; recordNumber = UNDEFINED; } else { goToNextSegment(); pageNumber = segment == null ? UNDEFINED : 0; } clearCachedRecord(); return this; } public TreePosition goToPreviousPage() { assert inUse : this; checkResolvedToPage(); if (--pageNumber >= 0) { page(null); randomRead = false; recordNumber = UNDEFINED; } else { goToPreviousSegment(); pageNumber = segment == null ? UNDEFINED : segment.pages() - 1; } clearCachedRecord(); return this; } public TreePosition goToNextRecord() throws IOException, InterruptedException { assert inUse : this; checkResolvedToRecord(); ensurePage(); if (++recordNumber == page.nRecords()) { goToNextPage(); recordNumber = 0; } clearCachedRecord(); return this; } public TreePosition goToPreviousRecord() throws IOException, InterruptedException { assert inUse : this; checkResolvedToRecord(); ensurePage(); if (recordNumber-- == 0) { goToPreviousPage(); recordNumber = LAST_RECORD_ON_PAGE; } clearCachedRecord(); return this; } public boolean atEnd() { assert inUse : this; return atEnd; } // TreePosition interface - lifecycle public TreePosition copy() { assert inUse : this; TreePosition copy = (TreePosition) pool.takeResource(); copyTo(copy); return copy; } public void initialize(Tree tree) { assert inUse : this; if (LOG.isLoggable(Level.INFO)) { LOG.log(Level.INFO, "{0}: initialize for {1}", new Object[]{name(), tree}); } this.tree = tree; factory = tree.factory(); diskPageCache = factory.diskPageCache(); atEnd = false; level = null; segment = null; pageNumber = UNDEFINED; page(null); pageAccessBuffers = null; randomRead = false; recordNumber = UNDEFINED; record = null; key = null; } // For use by this package void copyTo(TreePosition that) { assert inUse : this; assert that.inUse : that; if (LOG.isLoggable(Level.INFO)) { LOG.log(Level.INFO, "{0}: copy -> {1}", new Object[]{that.name(), that}); } that.tree = this.tree; that.factory = this.factory; that.diskPageCache = this.diskPageCache; that.atEnd = this.atEnd; that.level = this.level; that.segment = this.segment; that.pageNumber = this.pageNumber; that.page(this.page); // creates new access buffers that.randomRead = this.randomRead; that.recordNumber = this.recordNumber; that.record = this.record; that.key = this.key; } void activateForPool() { assert !inUse : this; if (LOG.isLoggable(Level.INFO)) { LOG.log(Level.INFO, "{0}: activateForPool", name()); } TreePositionTracker.registerTreePosition(CursorImpl.threadContext(), this); inUse = true; if (RECORD_TREE_POSITION_STACKS) { lastActivation = ThrowableUtil.toString(new Exception()); } } void deactivateForPool() { assert inUse : this; if (LOG.isLoggable(Level.INFO)) { LOG.log(Level.INFO, "{0}: deactivateForPool", name()); } TreePositionTracker.unregisterTreePosition(CursorImpl.threadContext(), this); tree = null; factory = null; diskPageCache = null; atEnd = false; level = null; segment = null; pageNumber = UNDEFINED; page(null); pageAccessBuffers = null; randomRead = false; recordNumber = UNDEFINED; record = null; key = null; inUse = false; if (RECORD_TREE_POSITION_STACKS) { lastDeactivation = ThrowableUtil.toString(new Exception()); } } TreePosition(TreePositionPool pool) { super(pool); } // For use by this class private DiskPage ensurePage() throws IOException, InterruptedException { if (page == null || page.pageAddress() != this.pageAddress()) { page(diskPageCache.page(segment, pageNumber, this)); if (recordNumber == LAST_RECORD_ON_PAGE) { recordNumber = page.nRecords() - 1; } if (recordNumber != UNDEFINED) { pageAccessBuffers = page.positionAccessBuffers(recordNumber, pageAccessBuffers); } } return page; } private void page(DiskPage newPage) { if (page != null) { page.removeReference(); } if (newPage == null) { page = null; pageAccessBuffers = null; } else { newPage.addReference(randomRead); page = newPage; pageAccessBuffers = page.accessBuffers(); } } private boolean isLastRecordOnPage() { return page == null && recordNumber == LAST_RECORD_ON_PAGE || page != null && recordNumber == page.nRecords() - 1; } private void checkResolvedToLevel() { assert !atEnd && level != null : this; } private void checkResolvedToSegment() { assert !atEnd && segment != null : this; } private void checkResolvedToPage() { assert !atEnd && pageNumber != UNDEFINED : this; } private void checkResolvedToRecord() { assert !atEnd && recordNumber != UNDEFINED : this; } private void clearCachedRecord() { record = null; key = null; } private String name() { return String.format("TP[%s]", id); } // Class state private static final int UNDEFINED = -1; private static final int LAST_RECORD_ON_PAGE = Integer.MAX_VALUE; private static final IdGenerator idGenerator = new IdGenerator(0); private static final Logger LOG = Logger.getLogger(TreePosition.class.getName()); private static final boolean RECORD_TREE_POSITION_STACKS = Boolean.getBoolean("recordTreePositionStacks"); // Object state // global stuff private Factory factory; private DiskPageCache diskPageCache; // termination private boolean atEnd = false; // tree private Tree tree; // level private TreeLevel level; // segment private TreeSegment segment; // page private int pageNumber; private DiskPage page; private DiskPage.AccessBuffers pageAccessBuffers; private boolean randomRead; // Indicates whether the way in which pageNumber was set will result in a random read. // record private int recordNumber; private AbstractRecord record; private AbstractKey key; // identification (to help interpret logs) private final long id = idGenerator.nextId(); // Pooling private boolean inUse = false; // Debugging private String lastActivation; private String lastDeactivation; }