PageImpl (final int pageIndex, final IntFunction<PageImpl> uninitializedPageFactory) { //Don't try to be lock-free on updates, just avoid holding the _global_ lock during IO: // 1) put blankPage under the pagesLock, // 2) release the pagesLock, // 3) load page outside the pagesLock, under per-page lock final IntRef insertionIndexRef = new IntRef(); final PageImpl blankPage; pagesLock.lock(); try { final PageImpl alreadyInsertedPage = findPageOrInsertionIndex(pages, pageIndex, insertionIndexRef); if (alreadyInsertedPage != null) { //race: somebody inserted the page between lock-free speculative check and locked re-check return alreadyInsertedPage; } if (insertionIndexRef.get() < 0) { // (page == null && insertionIndex < 0) => no space remains in the table final int newTableSize = (int)Math.ceil((pagesCount / loadFactor) * GROWTH_FACTOR); rehashToSize(newTableSize); final PageImpl mustBeNull = findPageOrInsertionIndex(pages, pageIndex, insertionIndexRef); if (mustBeNull != null) { throw new AssertionError( "Bug: first lookup(pageIndex: " + pageIndex + ") founds nothing, same search after resize must find nothing too " + pages); } if (insertionIndexRef.get() < 0) { throw new AssertionError("Bug: table just resized {length: " + pages.length() + ", pages: " + pagesCount + "} => " + "there must be a free slot for pageIndex: " + pageIndex); } } blankPage = uninitializedPageFactory.apply(pageIndex); final int insertionIndex = insertionIndexRef.get(); pages.set(insertionIndex, blankPage); pagesCount++; if (pagesCount > pages.length() * loadFactor) { final int newTableSize = (int)Math.ceil((pagesCount / loadFactor) * GROWTH_FACTOR); rehashToSize(newTableSize); //RC: page table need not only enlargement, but also shrinking. PageTable grows big // temporarily, while we read the file intensively -- but after that period the // table should shrink. Contrary to the enlargement, shrinking is implemented by // maintenance thread, in the background. This is because a lot of entries in the // enlarged table would be tombstones (pages already reclaimed, but entries remain) // and maintaining the count of those tombstones is not convenient -- Page status // changes (...->tombstone) are generally detached from PageTable logic, and better // be kept that way. But the maintenance thread regularly scans all the pages anyway, // so it could easily count (tombstones vs alive) entries, and trigger shrinking if // there are <30-40% entries are alive. } } finally { pagesLock.unlock(); } return blankPage; //blankPage.pageLock().writeLock().lock(); //try { // if (blankPage.isTombstone()) { // throw new ClosedStorageException("Storage is already closed"); // } // if (!blankPage.isNotReadyYet()) { // //possible if short-circuit (NOT_READY_YET->TOMBSTONE) transition of storage close happens // throw new AssertionError("Page must be {NOT_READY_YET, TOMBSTONE}, but " + blankPage); // } // blankPage.prepareForUse(pageContentLoader); // if (blankPage.isNotReadyYet()) { // //RC: Page state could be any of {USABLE, ABOUT_TO_UNMAP, TOMBSTONE} here -- but not NOT_READY_YET. // // USABLE is the obvious case, but {ABOUT_TO_UNMAP, TOMBSTONE} could happen if new page // // allocation races with storage closing: in such a scenario the page could be entombed async-ly // // immediately after .prepareForUse() made it USABLE -- nothing prevents it, since page // // has usageCount=0 still. // throw new AssertionError("Page must be {USABLE, ABOUT_TO_UNMAP, TOMBSTONE}, but " + blankPage); // } // if (blankPage.isDirty()) { // throw new AssertionError("Page must NOT be dirty just after .pageLoader: " + blankPage); // } // return blankPage; //} //finally { // blankPage.pageLock().writeLock().unlock(); //} }