All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.arcadedb.engine.PageManager Maven / Gradle / Ivy

There is a newer version: 24.11.1
Show newest version
/*
 * Copyright © 2021-present Arcade Data Ltd ([email protected])
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * SPDX-FileCopyrightText: 2021-present Arcade Data Ltd ([email protected])
 * SPDX-License-Identifier: Apache-2.0
 */
package com.arcadedb.engine;

import com.arcadedb.ContextConfiguration;
import com.arcadedb.GlobalConfiguration;
import com.arcadedb.exception.ConcurrentModificationException;
import com.arcadedb.exception.ConfigurationException;
import com.arcadedb.exception.DatabaseMetadataException;
import com.arcadedb.log.LogManager;
import com.arcadedb.utility.CallableNoReturn;
import com.arcadedb.utility.CodeUtils;
import com.arcadedb.utility.FileUtils;
import com.arcadedb.utility.LockContext;

import java.io.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.logging.*;

/**
 * Manages pages from disk to RAM. Each page can have different size.
 */
public class PageManager extends LockContext {
  private final    FileManager                       fileManager;
  private final    ConcurrentMap readCache;
  private final    TransactionManager                txManager;
  // MANAGE CONCURRENT ACCESS TO THE PAGES. THE VALUE IS TRUE FOR WRITE OPERATION AND FALSE FOR READ
  private final    ConcurrentMap    pendingFlushPages                     = new ConcurrentHashMap<>();
  private final    long                              maxRAM;
  private final    AtomicLong                        totalReadCacheRAM                     = new AtomicLong();
  private final    AtomicLong                        totalWriteCacheRAM                    = new AtomicLong();
  private final    AtomicLong                        totalPagesRead                        = new AtomicLong();
  private final    AtomicLong                        totalPagesReadSize                    = new AtomicLong();
  private final    AtomicLong                        totalPagesWritten                     = new AtomicLong();
  private final    AtomicLong                        totalPagesWrittenSize                 = new AtomicLong();
  private final    AtomicLong                        cacheHits                             = new AtomicLong();
  private final    AtomicLong                        cacheMiss                             = new AtomicLong();
  private final    AtomicLong                        totalConcurrentModificationExceptions = new AtomicLong();
  private final    AtomicLong                        evictionRuns                          = new AtomicLong();
  private final    AtomicLong                        pagesEvicted                          = new AtomicLong();
  private volatile long                              lastCheckForRAM                       = 0;
  private final    PageManagerFlushThread            flushThread;
  private final    int                               freePageRAM;

  public interface ConcurrentPageAccessCallback {
    void access() throws IOException;
  }

  public static class PPageManagerStats {
    public long maxRAM;
    public long readCacheRAM;
    public long writeCacheRAM;
    public long pagesRead;
    public long pagesReadSize;
    public long pagesWritten;
    public long pagesWrittenSize;
    public int  pageFlushQueueLength;
    public long cacheHits;
    public long cacheMiss;
    public long concurrentModificationExceptions;
    public long evictionRuns;
    public long pagesEvicted;
    public int  readCachePages;
  }

  public PageManager(final FileManager fileManager, final TransactionManager txManager, final ContextConfiguration configuration,
      final String databaseName) {
    this.fileManager = fileManager;
    this.txManager = txManager;

    this.freePageRAM = configuration.getValueAsInteger(GlobalConfiguration.FREE_PAGE_RAM);
    this.readCache = new ConcurrentHashMap<>(configuration.getValueAsInteger(GlobalConfiguration.INITIAL_PAGE_CACHE_SIZE));

    maxRAM = configuration.getValueAsLong(GlobalConfiguration.MAX_PAGE_RAM) * 1024 * 1024;
    if (maxRAM < 0)
      throw new ConfigurationException(GlobalConfiguration.MAX_PAGE_RAM.getKey() + " configuration is invalid (" + maxRAM + " MB)");

    flushThread = new PageManagerFlushThread(this, configuration, databaseName);
    flushThread.start();
  }

  public void close() {
    if (flushThread != null) {
      try {
        flushThread.closeAndJoin();
      } catch (final InterruptedException e) {
        Thread.currentThread().interrupt();
      }
    }

    readCache.clear();
    totalReadCacheRAM.set(0);
    totalWriteCacheRAM.set(0);
  }

  public void suspendFlushAndExecute(final CallableNoReturn callback) throws IOException, InterruptedException {
    flushThread.flushPagesFromQueueToDisk(0L);
    flushThread.setSuspended(true);
    try {
      CodeUtils.executeIgnoringExceptions(callback, "Error during suspend flush", true);
    } finally {
      flushThread.setSuspended(false);
    }
  }

  public boolean isPageFlushingSuspended() {
    return flushThread.isSuspended();
  }

  /**
   * Test only API.
   */
  public void kill() {
    if (flushThread != null) {
      try {
        flushThread.interrupt();
        flushThread.closeAndJoin();
      } catch (final InterruptedException e) {
        Thread.currentThread().interrupt();
      }
    }

    readCache.clear();
    totalReadCacheRAM.set(0);
    totalWriteCacheRAM.set(0);
  }

  public void clear() {
    readCache.clear();
    totalReadCacheRAM.set(0);
  }

  public void deleteFile(final int fileId) {
    for (final Iterator it = readCache.values().iterator(); it.hasNext(); ) {
      final CachedPage p = it.next();
      if (p.getPageId().getFileId() == fileId) {
        totalReadCacheRAM.addAndGet(-1L * p.getPhysicalSize());
        it.remove();
      }
    }
  }

  private int getMostRecentVersionOfPage(final PageId pageId, final int pageSize) throws IOException {
    CachedPage page = readCache.get(pageId);
    if (page == null)
      page = loadPage(pageId, pageSize, false, false);

    if (page != null)
      return page.getVersion();

    // NOT EXISTS, RETURN 0
    return 0;
  }

  public ImmutablePage getImmutablePage(final PageId pageId, final int pageSize, final boolean isNew,
      final boolean createIfNotExists) throws IOException {
    final CachedPage page = getCachedPage(pageId, pageSize, isNew, createIfNotExists);
    if (page != null)
      // RETURN ALWAYS A VIEW OF THE PAGE. THIS PREVENTS CONCURRENCY ON THE BUFFER POSITION
      return page.useAsImmutable();
    return null;
  }

  public MutablePage getMutablePage(final PageId pageId, final int pageSize, final boolean isNew, final boolean createIfNotExists)
      throws IOException {
    final CachedPage page = getCachedPage(pageId, pageSize, isNew, createIfNotExists);
    if (page != null)
      // RETURN ALWAYS A VIEW OF THE PAGE. THIS PREVENT CONCURRENCY ON THE BUFFER POSITION
      return page.useAsMutable();
    return null;
  }

  public void checkPageVersion(final MutablePage page, final boolean isNew) throws IOException {
    final PageId pageId = page.getPageId();

    if (!fileManager.existsFile(pageId.getFileId()))
      throw new ConcurrentModificationException(
          "Concurrent modification on page " + pageId + ". The file with id " + pageId.getFileId()
              + " does not exist anymore. Please retry the operation (threadId=" + Thread.currentThread().getId() + ")");

    final int mostRecentPageVersion = getMostRecentVersionOfPage(pageId, page.getPhysicalSize());

    if (mostRecentPageVersion != page.getVersion()) {
      totalConcurrentModificationExceptions.incrementAndGet();

      throw new ConcurrentModificationException(
          "Concurrent modification on page " + pageId + " in file '" + fileManager.getFile(pageId.getFileId()).getFileName()
              + "' (current v." + page.getVersion() + " <> database v." + mostRecentPageVersion
              + "). Please retry the operation (threadId=" + Thread.currentThread().getId() + ")");
    }
  }

  public void updatePages(final Map newPages, final Map modifiedPages,
      final boolean asyncFlush) throws IOException, InterruptedException {
    lock();
    try {
      final List pagesToWrite = new ArrayList<>((newPages != null ? newPages.size() : 0) + modifiedPages.size());

      if (newPages != null)
        for (final MutablePage p : newPages.values())
          pagesToWrite.add(updatePageVersion(p, true));

      for (final MutablePage p : modifiedPages.values())
        pagesToWrite.add(updatePageVersion(p, false));

      writePages(pagesToWrite, asyncFlush);

    } finally {
      unlock();
    }
  }

  public MutablePage updatePageVersion(final MutablePage page, final boolean isNew) throws IOException, InterruptedException {
    final PageId pageId = page.getPageId();

    final int mostRecentPageVersion = getMostRecentVersionOfPage(pageId, page.getPhysicalSize());
    if (mostRecentPageVersion != page.getVersion()) {
      totalConcurrentModificationExceptions.incrementAndGet();
      throw new ConcurrentModificationException(
          "Concurrent modification on page " + pageId + " in file '" + fileManager.getFile(pageId.getFileId()).getFileName()
              + "' (current v." + page.getVersion() + " <> database v." + mostRecentPageVersion
              + "). Please retry the operation (threadId=" + Thread.currentThread().getId() + ")");
    }

    page.incrementVersion();
    page.updateMetadata();

    LogManager.instance()
        .log(this, Level.FINE, "Updated page %s (size=%d records=%d threadId=%d)", null, page, page.getPhysicalSize(),
            page.readShort(0), Thread.currentThread().getId());

    return page;
  }

  public void overwritePage(final MutablePage page) throws IOException {
    readCache.remove(page.pageId);

    flushPage(page);

    LogManager.instance().log(this, Level.FINE, "Overwritten page %s (size=%d threadId=%d)", null, page, page.getPhysicalSize(),
        Thread.currentThread().getId());
  }

  public PPageManagerStats getStats() {
    final PPageManagerStats stats = new PPageManagerStats();
    stats.maxRAM = maxRAM;
    stats.readCacheRAM = totalReadCacheRAM.get();
    stats.writeCacheRAM = totalWriteCacheRAM.get();
    stats.readCachePages = readCache.size();
    stats.pagesRead = totalPagesRead.get();
    stats.pagesReadSize = totalPagesReadSize.get();
    stats.pagesWritten = totalPagesWritten.get();
    stats.pagesWrittenSize = totalPagesWrittenSize.get();
    stats.pageFlushQueueLength = flushThread.queue.size();
    stats.cacheHits = cacheHits.get();
    stats.cacheMiss = cacheMiss.get();
    stats.concurrentModificationExceptions = totalConcurrentModificationExceptions.get();
    stats.evictionRuns = evictionRuns.get();
    stats.pagesEvicted = pagesEvicted.get();
    return stats;
  }

  public void removePageFromCache(final PageId pageId) {
    final CachedPage page = readCache.remove(pageId);
    if (page != null)
      totalReadCacheRAM.addAndGet(-1L * page.getPhysicalSize());
  }

  public void writePages(final List updatedPages, final boolean asyncFlush) throws IOException, InterruptedException {
    if (asyncFlush) {
      for (final MutablePage page : updatedPages)
        // SAVE A COPY OF THE PAGE IN CACHE BECAUSE IT WILL BE FLUSHED ASYNCHRONOUSLY
        putPageInReadCache(new CachedPage(page, true));

      // ASYNCHRONOUS FLUSH: ONLY IF NOT ALREADY IN THE QUEUE, ENQUEUE THE PAGE TO BE FLUSHED BY A SEPARATE THREAD
      flushThread.scheduleFlushOfPages(updatedPages);
    } else {
      // SYNCHRONOUS FLUSH
      for (final MutablePage page : updatedPages) {
        flushPage(page);
        // ADD THE PAGE IN TO READ CACHE. FROM THIS POINT THE PAGE IS NEVER MODIFIED, SO IT CAN BE CACHED
        putPageInReadCache(new CachedPage(page, false));
      }
    }
  }

  protected void flushPage(final MutablePage page) throws IOException {
    if (fileManager.existsFile(page.pageId.getFileId())) {
      final PaginatedComponentFile file = (PaginatedComponentFile) fileManager.getFile(page.pageId.getFileId());
      if (!file.isOpen())
        throw new DatabaseMetadataException("Cannot flush pages on disk because file '" + file.getFileName() + "' is closed");

      LogManager.instance()
          .log(this, Level.FINE, "Flushing page %s to disk (threadId=%d)...", null, page, Thread.currentThread().getId());

      // ACQUIRE A LOCK ON THE I/O OPERATION TO AVOID PARTIAL READS/WRITES
      concurrentPageAccess(page.pageId, true, () -> {
        final int written = file.write(page);
        totalPagesWrittenSize.addAndGet(written);
      });

      totalPagesWritten.incrementAndGet();

      txManager.notifyPageFlushed(page);

    } else
      LogManager.instance()
          .log(this, Level.FINE, "Cannot flush page %s because the file has been dropped (threadId=%d)...", null, page,
              Thread.currentThread().getId());
  }

  private CachedPage loadPage(final PageId pageId, final int size, final boolean createIfNotExists, final boolean cache)
      throws IOException {
    // ASSURE THE PAGE IS NOT IN THE FLUSHING QUEUE
    CachedPage page = flushThread.getCachedPageFromMutablePageInQueue(pageId);
    if (page == null) {
      final PaginatedComponentFile file = (PaginatedComponentFile) fileManager.getFile(pageId.getFileId());

      final boolean isNewPage = pageId.getPageNumber() >= file.getTotalPages();
      if (!createIfNotExists && isNewPage)
        // AVOID CREATING AN EMPTY PAGE JUST TO CHECK THE VERSION
        return null;

      checkForPageDisposal();

      page = new CachedPage(this, pageId, size);

      if (!isNewPage) {
        // ACQUIRE A LOCK ON THE I/O OPERATION TO AVOID PARTIAL READS/WRITES
        final CachedPage finalPage = page;
        concurrentPageAccess(pageId, false, () -> file.read(finalPage));
      }

      page.loadMetadata();

      LogManager.instance().log(this, Level.FINE, "Loaded page %s (threadId=%d)", null, page, Thread.currentThread().getId());
    }

    totalPagesRead.incrementAndGet();
    totalPagesReadSize.addAndGet(page.getPhysicalSize());

    if (cache)
      putPageInReadCache(page);

    return page;
  }

  private void concurrentPageAccess(final PageId pageId, final Boolean writeAccess, final ConcurrentPageAccessCallback callback)
      throws IOException {
    // ACQUIRE A LOCK ON THE I/O OPERATION TO AVOID PARTIAL READS/WRITES
    while (!Thread.currentThread().isInterrupted()) {
      if (pendingFlushPages.putIfAbsent(pageId, writeAccess) == null)
        try {
          callback.access();
          break;
        } finally {
          pendingFlushPages.remove(pageId);
        }

      // WAIT AND RETRY
      Thread.yield();
    }
  }

  private void checkForPageDisposal() {
    final long now = System.currentTimeMillis();
    if (now - lastCheckForRAM < 100)
      return;

    final long totalRAM = totalReadCacheRAM.get();
    if (totalRAM < maxRAM)
      return;

    final long ramToFree = totalRAM * freePageRAM / 100;

    evictOldestPages(ramToFree, totalRAM);
  }

  private synchronized void evictOldestPages(final long ramToFree, final long totalRAM) {
    evictionRuns.incrementAndGet();

    LogManager.instance()
        .log(this, Level.FINE, "Reached max RAM for page cache. Freeing pages from cache (target=%d current=%d max=%d threadId=%d)",
            null, ramToFree, totalRAM, maxRAM, Thread.currentThread().getId());

    // GET THE  OLDEST PAGES
    // ORDER PAGES BY LAST ACCESS + SIZE
    final TreeSet pagesOrderedByAge = new TreeSet<>((o1, o2) -> {
      final int lastAccessed = Long.compare(o1.getLastAccessed(), o2.getLastAccessed());
      if (lastAccessed != 0)
        return lastAccessed;

      // SAME TIMESTAMP, CHECK THE PAGE SIZE: LARGER PAGE SHOULD BE REMOVED FIRST THAN OTHERS
      final int pageSize = -Long.compare(o1.getPhysicalSize(), o2.getPhysicalSize());
      if (pageSize != 0)
        return pageSize;

      return o1.getPageId().compareTo(o2.getPageId());
    });

    pagesOrderedByAge.addAll(readCache.values());

    // REMOVE OLDEST PAGES FROM RAM
    long freedRAM = 0;
    for (final CachedPage page : pagesOrderedByAge) {
      final CachedPage removedPage = readCache.remove(page.getPageId());
      if (removedPage != null) {
        freedRAM += page.getPhysicalSize();
        totalReadCacheRAM.addAndGet(-1L * page.getPhysicalSize());
        pagesEvicted.incrementAndGet();

        if (freedRAM > ramToFree)
          break;
      }
    }

    final long newTotalRAM = totalReadCacheRAM.get();

    LogManager.instance()
        .log(this, Level.FINE, "Freed %s RAM (current=%s max=%s threadId=%d)", null, FileUtils.getSizeAsString(freedRAM),
            FileUtils.getSizeAsString(newTotalRAM), FileUtils.getSizeAsString(maxRAM), Thread.currentThread().getId());

    if (newTotalRAM > maxRAM)
      LogManager.instance().log(this, Level.WARNING, "Cannot free pages in RAM (current=%s > max=%s threadId=%d)", null,
          FileUtils.getSizeAsString(newTotalRAM), FileUtils.getSizeAsString(maxRAM), Thread.currentThread().getId());

    lastCheckForRAM = System.currentTimeMillis();
  }

  private void putPageInReadCache(final CachedPage page) {
    if (readCache.put(page.getPageId(), page) == null)
      totalReadCacheRAM.addAndGet(page.getPhysicalSize());

    checkForPageDisposal();
  }

  private CachedPage getCachedPage(final PageId pageId, final int pageSize, final boolean isNew, final boolean createIfNotExists)
      throws IOException {
    checkForPageDisposal();

    CachedPage page = readCache.get(pageId);
    if (page == null) {
      page = loadPage(pageId, pageSize, createIfNotExists, true);
      if (page == null) {
        if (isNew)
          return null;
      } else
        return page;

      cacheMiss.incrementAndGet();

    } else {
      cacheHits.incrementAndGet();
      page.updateLastAccesses();
    }

    if (page == null)
      throw new IllegalArgumentException(
          "Page id '" + pageId + "' does not exist (threadId=" + Thread.currentThread().getId() + ")");

    return page;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy