public Page pageByIndex(final int pageIndex,
                          final boolean forModification) throws IOException {
    if (readOnly && forModification) {
      throw new IOException("Read-only storage can't be modified");
    }

    if (pageIndex < 0) {
      throw new AssertionError("Page " + pageIndex + " must be >=0");
    }

    final long startedAtNs = pageCacheStatistics.startTimestampNs();

    for (int attempt = 0; ; attempt++) {
      if (isClosed()) {
        throw new ClosedStorageException("Storage is already closed: " + file);
      }
      final PageImpl page = pages.lookupOrCreate(
        pageIndex,
        this::createUninitializedPage
      );

      try {
        //busy-spin on: (check page is USABLE, and increment useCount)
        while (!page.tryAcquireForUse(this)) {
          if (page.isNotReadyYet()) { //page just created: try to load it
            if (page.tryPrepareForUse(this::loadPageData)) {
              continue;
            }
            //MAYBE RC: Page loading could take quite a while. What could we do to avoid busy-spinning
            //          on the page loading by another thread?
            //          One idea is to have something like (optional, lazy) CompletableFuture in Page,
            //          which is returned from .tryPrepareForUse() instead of 'false', and could be
            //          waited on (or used to suspend coroutine).
            //          Another approach: FPCache has a shared Condition instance, which it notifies at
            //          the end of each turn
          }
          Thread.yield();//MAYBE RC: Thread.onSpinWait(); (java9+)
        }
        pageCacheStatistics.pageRequested(page.pageSize(), startedAtNs);
        return page;
      }
      catch (IOException e) {
        if (attempt > MAX_ATTEMPTS_TO_ACQUIRE_PAGE) {
          throw new AssertionError(page + " can't be acquired in " + attempt + " attempts: either we're very unlucky, or it is a bug");
        }
        LOG.trace("Page " + page + " likely released -> request it again (" + e.getMessage() + ")");

        //Worst case scenario: page is in the middle of unmapping (>=ABOUT_TO_UNMAP). We can't
        // interrupt this process: page unmapping must be finalized, and then on subsequent
        // request page will be mapped back, anew.

        //But at least we could try to assist page reclamation:
        if (page.isAboutToUnmap()) {
          // This branch is not only an optimization: it prevents a pathological scenario (IDEA-323586)
          // there housekeeper thread skips its turns because there are already enough pages prepared to
          // reclaim, and because of that the particular page remains in ABOUT_TO_UNMAP forever, stalling
          // everybody who requesting it.
          final boolean succeed = page.tryMoveTowardsPreTombstone(/*entombYoung: */false);
          if (succeed) {
            pageCache.unmapPageAndReclaimBuffer(page);
          }
        }
        else {
          if (attempt < 10) {
            Thread.yield();
          }
          else {
            //instead of blind yield -- better to wait for housekeeper to make a turn
            pageCache.waitForHousekeepingTurn(1 /*ms*/);
          }
        }
      }
    }
  }
public Page pageByIndex(final int pageIndex,
                          final boolean forModification)