package com.neocoretechs.bigsack.btree; import java.io.IOException; import java.io.Serializable; import com.neocoretechs.bigsack.io.Optr; import com.neocoretechs.bigsack.io.pooled.BlockAccessIndex; import com.neocoretechs.bigsack.io.pooled.GlobalDBIO; import com.neocoretechs.bigsack.io.pooled.ObjectDBIO; /* * Copyright (c) 2003,2014 NeoCoreTechs * All rights reserved. * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, this list of * conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation and/or * other materials provided with the distribution. * Neither the name of NeoCoreTechs nor the names of its contributors may be * used to endorse or promote products derived from this software without specific prior written permission. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED * OF THE POSSIBILITY OF SUCH DAMAGE. * */ /** * A key page in the bTree. Performs operations on its set of keys and * persistent object locations/data. Persists itself to the buffer pool as * serialized instance when necessary. * MAXKEYS is an attempt to keep keys from spanning page boundaries at the expense of some storage * a key overflow will cause a page split, at times unavoidable. * Important to note that the data is stored as arrays serialized out in this class. Related to that * is the concept of element 0 of those arrays being 'this', hence the special treatment in CRUD. * Unlike a binary search tree, each node of a B-tree may have a variable number of keys and children. * The keys are stored in non-decreasing order. Each node either is a leaf node or * it has some associated children that are the root nodes of subtrees. * The left child node of a node's element contains all nodes (elements) with keys less than or equal to the node element's key * but greater than the preceding node element's key. * If a node becomes full, a split operation is performed during the insert operation. * The split operation transforms a full node with 2*T-1 elements into two nodes with T-1 elements each * and moves the median key of the two nodes into its parent node. * The elements left of the median (middle) element of the split node remain in the original node. * The new node becomes the child node immediately to the right of the median element that was moved to the parent node. * * Example (T = 4): * 1. R = | 1 | 2 | 3 | 4 | 5 | 6 | 7 | * * 2. Add key 8 * * 3. R = | 4 | * / \ * | 1 | 2 | 3 | | 5 | 6 | 7 | 8 | * * @author Groff Copyright (C) NeoCoreTechs 2014,2015 */ public final class BTreeKeyPage implements Serializable { static final boolean DEBUG = false; static final long serialVersionUID = -2441425588886011772L; // number of keys per page; number of instances of the non transient fields of 'this' per DB block // Can be overridden by properties element. the number of maximum children is MAXKEYS+1 per node public static int MAXKEYS = 5; // Non transient number of keys on this page. Adjusted as necessary when inserting/deleting. int numKeys = 0; // Transient. The 'id' is really the location this page was retrieved from deep store. transient long pageId = -1L; @SuppressWarnings("rawtypes") // The array of keys, non transient. public Comparable[] keyArray; // The array of pages corresponding to the pageIds for the child nodes. Transient since we lazily retrieve pages via pageIds transient BTreeKeyPage[] pageArray; // The array of page ids from which the page array is filled. This data is persisted as virtual page pointers. Since // we align indexes on page boundaries we dont need an offset as we do with value data associated with the indexes for maps. long[] pageIdArray; // These are the data items for values associated with keys, should this be a map vs set. // These are lazily populated from the dataIdArray where an id exsists at that index. transient Object[] dataArray; // This array is present for maps where values are associated with keys. In sets it is absent or empty. // It contains the page and offset of the data item associated with a key. We pack value data on pages, hence // we need an additional 2 byte offset value to indicate that. Optr[] dataIdArray; // This transient array maintains boolean values indicating whether the data item at that index has been updated // and needs written back to deep store. transient boolean[] dataUpdatedArray; // Global is this leaf node flag. public boolean mIsLeafNode = true; // We treat as leaf since the logic is geared to proving it not // Global page updated flag. private transient boolean updated = false; // has the node been updated for purposes of write /** * No - arg cons to initialize pageArray to MAXKEYS + 1, this is called on deserialization */ public BTreeKeyPage() { if( DEBUG ) { System.out.println("BTreeKeyPage DEFAULT ctor"); } initTransients(); } public void initTransients() { pageArray = new BTreeKeyPage[MAXKEYS + 1]; dataArray = new Object[MAXKEYS]; dataUpdatedArray = new boolean[MAXKEYS]; } /** * Construct the page from scratch in a non deserialization context. * We provide the intended virtual page number, all fields are initialized and * we can either retrieve or store it from there. * @param ppos The virtual page id */ public BTreeKeyPage(long ppos) { if( DEBUG ) { System.out.println("BTreeKeyPage ctor loc:"+GlobalDBIO.valueOf(ppos)); } initTransients(); // Pre-allocate the arrays that hold persistent data pageIdArray = new long[MAXKEYS + 1]; dataIdArray = new Optr[MAXKEYS]; for (int i = 0; i <= MAXKEYS; i++) { pageIdArray[i] = -1L; if( i != MAXKEYS ) { dataIdArray[i] = Optr.emptyPointer; } } keyArray = new Comparable[MAXKEYS]; pageId = ppos; } /** * Given a Comparable object, search for that object on this page. * The key was found on this page and loc is index of * located key if atKey is true. * If atKey was false then the key * was not found on this page and index-1 is index of where the * key *should* be. The result is always to the right of the target key, therefore * to nav left one accesses index-1 key. * @param targetKey The target key to retrieve * @return TreeSearchResult the insertion point from 0 to MAXKEYS and flag of whether key was found */ @SuppressWarnings({ "rawtypes", "unchecked" }) synchronized TreeSearchResult search(Comparable targetKey) { assert(keyArray.length > 0) : "BTreeKeyPage.search key array length zero"; int middleIndex = 1; int leftIndex = 0; int rightIndex = numKeys - 1; // no keys, call for insert at 0 if( rightIndex == -1) return new TreeSearchResult(0, false); while (leftIndex <= rightIndex) { middleIndex = leftIndex + ((rightIndex - leftIndex) / 2); int cmpRes = keyArray[middleIndex].compareTo(targetKey); if (cmpRes < 0 ) { leftIndex = middleIndex + 1; } else if (cmpRes > 0 ) { rightIndex = middleIndex - 1; } else { return new TreeSearchResult(middleIndex, true); } } if( DEBUG ) System.out.println("BtreeKeyPage.search falling thru "+middleIndex+" "+leftIndex+" "+rightIndex+" "+this+" target:"+targetKey); return new TreeSearchResult(middleIndex, false); } /** * Delete the key/data item on this page. * Everything on the page is slid left. * numKeys is decremented * @param index the index of the item on this page to delete */ synchronized void delete(int index) { // If its the rightmost key ignore move if (index < numKeys - 1) // Move all up for (int i = index;i < (numKeys == MAXKEYS ? MAXKEYS - 1 : numKeys); i++) { keyArray[i] = keyArray[i + 1]; pageArray[i + 1] = pageArray[i + 2]; pageIdArray[i + 1] = pageIdArray[i + 2]; dataArray[i] = dataArray[i + 1]; dataIdArray[i] = dataIdArray[i + 1]; dataUpdatedArray[i] = dataUpdatedArray[i + 1]; } // Decrement key count and nullify rightmost item on the node --numKeys; keyArray[numKeys] = null; pageArray[numKeys + 1] = null; pageIdArray[numKeys + 1] = -1L; dataArray[numKeys] = null; dataIdArray[numKeys] = Optr.emptyPointer; dataUpdatedArray[numKeys] = false; // we took care of it setUpdated(true); } synchronized void deleteData(ObjectDBIO sdbio, int index) throws IOException { if (dataArray[index] != null && !dataIdArray[index].isEmptyPointer()) { if( DEBUG ) { System.out.print("Deleting :"+dataIdArray[index]+"\r\n"); System.out.println("Data: "+dataArray[index]+"\r\n"); } //if( Props.DEBUG ) System.out.println(" size "+ilen); sdbio.delete_object(dataIdArray[index], GlobalDBIO.getObjectAsBytes(dataArray[index]).length ); dataIdArray[index] = Optr.emptyPointer; dataUpdatedArray[index] = true; setUpdated(true); } //else { //throw new IOException("Attempt to delete null data index "+index+" for "+this); //} } /** * Retrieve a page based on an index to this page containing a page. * In effect, this is our lazy initialization of the 'pageArray' and we strictly * work in pageArray in this method. If the pageIdArray contains a valid non -1 entry, then * we retrieve and deserialize that virtual block to an entry in the pageArray at the index passed in the params * location. If we retrieve an instance we also fill in the transient fields from our current data * @param sdbio The session database io instance * @param index The index to the page array on this page that contains the virtual record to deserialize. * @return The deserialized page instance * @exception IOException If retrieval fails */ public synchronized BTreeKeyPage getPage(ObjectDBIO sdbio, int index) throws IOException { if(DEBUG) { System.out.println("BTreeKeyPage.getPage Entering BTreeKeyPage to retrieve target index "+index); for(int i = 0; i < pageIdArray.length; i++) { System.out.println("BTreeKeyPage.getPage initial index "+i+"="+GlobalDBIO.valueOf(pageIdArray[i])+" page:"+pageArray[i]); } } if (pageArray[index] == null && pageIdArray[index] != -1L) { // eligible to retrieve page if( DEBUG ) { System.out.println("BTreeKeyPage.getPage about to retrieve index:"+index+" loc:"+GlobalDBIO.valueOf(pageIdArray[index])); } pageArray[index] = (BTreeKeyPage) (sdbio.deserializeObject(pageIdArray[index])); // set up all the transient fields pageArray[index].initTransients(); pageArray[index].pageId = pageIdArray[index]; if( DEBUG ) { System.out.println("BTreeKeyPage.getPage retrieved index:"+index+" loc:"+GlobalDBIO.valueOf(pageIdArray[index])+" page:"+pageArray[index]); for(int i = 0; i < pageIdArray.length; i++)System.out.println(i+"="+GlobalDBIO.valueOf(pageIdArray[i])); } } return pageArray[index]; } /** * Primarily for getting root at boot where we don't have an index on a page that contains a * location. Otherwise, we use the overloaded getPage with index. This method * can be used to locate a block which has an index record on its boundary and deserialize that. * It starts somewhat from baseline as it initializes all the internal BTreeKeyPage structures. * No effort is made to guarantee the record being accessed is a viable BtreeKeyPage, that is assumed. * A getPage is performed after page setup. * @param sdbio The session database io instance * @param pos The block containing page * @return The deserialized page instance * @exception IOException If retrieval fails */ static BTreeKeyPage getPageFromPool(ObjectDBIO sdbio, long pos) throws IOException { assert(pos != -1L) : "Page index invalid in getPage "+sdbio.getDBName(); sdbio.findOrAddBlock(pos); BTreeKeyPage btk = (BTreeKeyPage) (sdbio.deserializeObject(pos)); // initialize transients btk.initTransients(); btk.pageId = pos; if( DEBUG ) System.out.println("BTreeKeyPage.getPageFromPool "+pos);//+" "+btk); //for(int i = 0; i <= MAXKEYS; i++) { // btk.pageArray[i] = btk.getPage(sdbio,i); //} return btk; } /** * Get a new page from the pool from a random tablespace. used for general inserts. * Call stealblk, create BTreeKeyPage with the page Id of stolen block, set the pageArray * to MAXKEYS+1, the dataArray to MAXKEYS, and the dataUpdatedArray to MAXKEYS * set updated to true, and return the newly formed * @param sdbio * @return * @throws IOException */ static BTreeKeyPage getPageFromPool(ObjectDBIO sdbio) throws IOException { BlockAccessIndex lbai = sdbio.stealblk(); long pageId = lbai.getBlockNum(); // extract tablespace since we steal blocks from any //int tablespace = GlobalDBIO.getTablespace(pageId); // initialize transients BTreeKeyPage btk = new BTreeKeyPage(pageId); btk.setUpdated(true); //for(int i = 0; i <= MAXKEYS; i++) { // btk.pageArray[i] = btk.getPage(sdbio,i); //} return btk; } /** * Serialize this page to deep store on a page boundary. * For data, we reset the new node position. For pages, we don't use * it because they are always on page boundaries (not packed). The data is written * the the blockbuffer, the push to deep store takes place at commit time or * when the buffer fills and it becomes necessary to open a spot. * @param sdbio The ObjectDBIO instance * @exception IOException If write fails */ public synchronized void putPage(ObjectDBIO sdbio) throws IOException { if (!isUpdated()) { if( DEBUG ) System.out.println("BTreeKeyPage.putPage page not updated:"+this); return; } byte[] pb = GlobalDBIO.getObjectAsBytes(this); if( DEBUG ) System.out.println("BTreeKeyPage putPage id:"+GlobalDBIO.valueOf(pageId)+" Got "+pb.length+" bytes"); if (pageId == -1L) { BlockAccessIndex lbai = sdbio.stealblk(); pageId = lbai.getBlockNum(); // extract tablespace since we steal blocks from any int tablespace = GlobalDBIO.getTablespace(pageId); if( DEBUG ) System.out.println("BTreeKeyPage putPage Stole block "+GlobalDBIO.valueOf(pageId)); sdbio.add_object(tablespace, lbai, pb, pb.length); } else { sdbio.add_object(Optr.valueOf(pageId), pb, pb.length); } // Persist the data items associated with the keys for (int i = 0; i < numKeys; i++) { // put the data item if (dataUpdatedArray[i]) { // if it gets nulled, should probably delete if (dataArray[i] != null) { // pack the page into this tablespace and within blocks at the last known good position dataIdArray[i] = sdbio.getIOManager().getNewNodePosition(GlobalDBIO.getTablespace(pageIdArray[i])); pb = GlobalDBIO.getObjectAsBytes(dataArray[i]); //System.out.println("ADDING DATA TO INSTANCE:"+dataArray[i]); sdbio.add_object(dataIdArray[i], pb, pb.length); } dataUpdatedArray[i] = false; } } if( DEBUG ) System.out.println("BTreeKeyPage putPage Added object @"+GlobalDBIO.valueOf(pageId)+" bytes:"+pb.length+" page:"+this); setUpdated(false); } /** * Recursively put the pages to deep store. * @param sdbio The BlockDBIO instance * @exception IOException if write fails */ public synchronized void putPages(ObjectDBIO sdbio) throws IOException { for (int i = 0; i <= numKeys; i++) { if (pageArray[i] != null) { pageArray[i].putPages(sdbio); pageIdArray[i] = pageArray[i].pageId; } } putPage(sdbio); } /** * Using fromPage, populate pageArray[index] = fromPage. * If fromPage is NOT null, pageIdArray[index] = fromPage.pageId * if fromPage IS null, pageIdArray[index] = -1L * set the updated flag to true * @param fromPage * @param index */ synchronized void putPageToArray(BTreeKeyPage fromPage, int index) { pageArray[index] = fromPage; if (fromPage != null) pageIdArray[index] = fromPage.pageId; else pageIdArray[index] = -1L; setUpdated(true); } @SuppressWarnings("rawtypes") /** * Using key, put keyArray[index] = key * set updated true * @param key * @param index */ synchronized void putKeyToArray(Comparable key, int index) { keyArray[index] = key; setUpdated(true); } /** * Lazy initialization for off-index-page 'value' objects attached to our BTree keys. * Essentially guarantees that if a virtual pointer Id is present in dataIdArray at index, * you get a valid instance back. * If dataArray[index] is null and dataIdArray[index] is NOT an empty pointer, * set the dataArray[index] to the deserialized object retrieved from deep store via dataIdArray[index]. * Set dataUpdatedArray[index] to false since we just retrieved an instance. * return the dataArray[index]. * @param sdbio The IO manager * @param index The index to populate if its possible to do so * @return dataArray[index] filled with deep store object * @throws IOException */ public synchronized Object getDataFromArray(ObjectDBIO sdbio, int index) throws IOException { if (dataArray[index] == null && dataIdArray[index] != null && !dataIdArray[index].isEmptyPointer() ) { dataArray[index] = sdbio.deserializeObject(dataIdArray[index]); dataUpdatedArray[index] = false; } return dataArray[index]; } /** * Set the dataArray[index] to 'data'. Set the dataidArray[index] to empty pointer, * set the dataUpdatedArray[index] to true, set data updated true; * @param data * @param index */ synchronized void putDataToArray(Object data, int index) { dataArray[index] = data; dataIdArray[index] = Optr.emptyPointer; dataUpdatedArray[index] = true; setUpdated(true); } /** * Set the pageArray[index] and pageIdArray[index] to default null values. * Set updated to true. * @param index The array index to annul */ synchronized void nullPageArray(int index) { pageArray[index] = null; pageIdArray[index] = -1L; setUpdated(true); } public synchronized String toString() { StringBuffer sb = new StringBuffer(); //sb.append("Page "); //sb.append(hashCode()); sb.append("<<<<<<<<<<BTreeKeyPage Id:"); sb.append(GlobalDBIO.valueOf(pageId)); sb.append(" Numkeys:"); sb.append(String.valueOf(numKeys)); sb.append(" Leaf:"); sb.append(mIsLeafNode); sb.append(" Updated:"); sb.append(String.valueOf(updated)+"\r\n"); sb.append("Key Array:\r\n"); if( keyArray == null ) { sb.append("Key ARRAY IS NULL\r\n"); } else { for (int i = 0; i < keyArray.length; i++) { sb.append(i+" ="); sb.append(keyArray[i]+"\r\n"); } } sb.append("BTree Page Array:\r\n"); if( pageArray == null ) { sb.append("PAGE ARRAY IS NULL\r\n"); } else { int j = 0; for (int i = 0 ; i < pageArray.length; i++) { // sb.append(i+"="); // sb.append(pageArray[i]+"\r\n"); if(pageArray[i] != null) ++j; } sb.append("Page Array Non null for "+j+" members\r\n"); } sb.append("BTree Page IDs:\r\n"); if( pageIdArray == null ) { sb.append(" PAGE ID ARRAY IS NULL\r\n "); } else { sb.append(" "); for (int i = 0; i < pageIdArray.length; i++) { sb.append(i+" id="); //sb.append(pageArray[i] == null ? null : String.valueOf(pageArray[i].hashCode())); sb.append(GlobalDBIO.valueOf(pageIdArray[i])); sb.append("\r\n"); } } sb.append("Data Array:\r\n"); if(dataArray==null) { sb.append(" DATA ARRAY NULL\r\n"); } else { for(int i = 0; i < dataArray.length; i++) { sb.append(i+"="); sb.append(dataArray[i]+"\r\n"); sb.append("updated="); sb.append(dataUpdatedArray[i]); sb.append("\r\n"); } } sb.append("Data IDs:\r\n"); if(dataIdArray==null) { sb.append(" DATA ID ARRAY NULL\r\n"); } else { for(int i = 0; i < dataIdArray.length; i++) { sb.append(i+" id="); sb.append(dataIdArray[i]); sb.append("\r\n"); } } sb.append(GlobalDBIO.valueOf(pageId)); sb.append(" >>>>>>>>>>>>>>End "); sb.append("\r\n"); return sb.toString(); } public boolean isUpdated() { return updated; } public void setUpdated(boolean updated) { this.updated = updated; } /** * Determine if this key is in the list of array elements for this page * @param key * @return */ boolean contains(int key) { return search(key).atKey; } /** * Find the place in the key array where the target key is less than the key value in the array element. * @param key The target key * @return The index where target is < array index value, if end, return numKeys */ int subtreeRootNodeIndex(Comparable key) { for (int i = 0; i < numKeys; i++) { if (key.compareTo(keyArray[i]) < 0) { return i; } } return numKeys; } }