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

swaydb.core.map.Maps.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2020 Simer JS Plaha ([email protected] - @simerplaha)
 *
 * This file is a part of SwayDB.
 *
 * SwayDB is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * SwayDB is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with SwayDB. If not, see .
 *
 * Additional permission under the GNU Affero GPL version 3 section 7:
 * If you modify this Program or any covered work, only by linking or combining
 * it with separate works, the licensors of this Program grant you additional
 * permission to convey the resulting work.
 */

package swaydb.core.map

import java.nio.file.Path

import com.typesafe.scalalogging.LazyLogging
import swaydb.Error.Map.ExceptionHandler
import swaydb.IO._
import swaydb.core.actor.ByteBufferSweeper.ByteBufferSweeperActor
import swaydb.core.actor.FileSweeper.FileSweeperActor
import swaydb.core.brake.BrakePedal
import swaydb.core.io.file.Effect._
import swaydb.core.io.file.{Effect, ForceSaveApplier}
import swaydb.core.map.serializer.{MapEntryReader, MapEntryWriter}
import swaydb.core.map.timer.Timer
import swaydb.core.util.DropIterator
import swaydb.core.util.queue.VolatileQueue
import swaydb.data.accelerate.{Accelerator, LevelZeroMeter}
import swaydb.data.config.{MMAP, RecoveryMode}
import swaydb.data.order.KeyOrder
import swaydb.{Error, IO}

import scala.annotation.tailrec
import scala.collection.mutable.ListBuffer

private[core] object Maps extends LazyLogging {

  val closeErrorMessage = "Cannot perform write on a closed instance."

  def memory[K, V, C <: MapCache[K, V]](fileSize: Long,
                                        acceleration: LevelZeroMeter => Accelerator)(implicit keyOrder: KeyOrder[K],
                                                                                     fileSweeper: FileSweeperActor,
                                                                                     bufferCleaner: ByteBufferSweeperActor,
                                                                                     writer: MapEntryWriter[MapEntry.Put[K, V]],
                                                                                     cacheBuilder: MapCacheBuilder[C],
                                                                                     timer: Timer,
                                                                                     forceSaveApplier: ForceSaveApplier): Maps[K, V, C] = {
    val map =
      Map.memory[K, V, C](
        fileSize = fileSize,
        flushOnOverflow = false
      )

    new Maps[K, V, C](
      queue = VolatileQueue[Map[K, V, C]](map),
      fileSize = fileSize,
      acceleration = acceleration,
      currentMap = map
    )
  }

  def persistent[K, V, C <: MapCache[K, V]](path: Path,
                                            mmap: MMAP.Map,
                                            fileSize: Long,
                                            acceleration: LevelZeroMeter => Accelerator,
                                            recovery: RecoveryMode)(implicit keyOrder: KeyOrder[K],
                                                                    fileSweeper: FileSweeperActor,
                                                                    bufferCleaner: ByteBufferSweeperActor,
                                                                    writer: MapEntryWriter[MapEntry.Put[K, V]],
                                                                    reader: MapEntryReader[MapEntry[K, V]],
                                                                    cacheBuilder: MapCacheBuilder[C],
                                                                    timer: Timer,
                                                                    forceSaveApplier: ForceSaveApplier): IO[swaydb.Error.Map, Maps[K, V, C]] = {
    logger.debug("{}: Maps persistent started. Initialising recovery.", path)
    //reverse to keep the newest maps at the top.
    recover[K, V, C](
      folder = path,
      mmap = mmap,
      fileSize = fileSize,
      recovery = recovery
    ).map(_.reverse) flatMap {
      recoveredMapsReversed =>
        logger.info(s"{}: Recovered {} ${if (recoveredMapsReversed.isEmpty || recoveredMapsReversed.size > 1) "logs" else "log"}.", path, recoveredMapsReversed.size)
        val nextMapId =
          recoveredMapsReversed.headOption match {
            case Some(lastMaps) =>
              lastMaps match {
                case PersistentMap(path, _, _, _, _, _) =>
                  path.incrementFolderId
                case _ =>
                  path.resolve(0.toFolderId)
              }
            case None =>
              path.resolve(0.toFolderId)
          }
        //delete maps that are empty.
        val (emptyMaps, otherMaps) = recoveredMapsReversed.partition(_.cache.isEmpty)
        if (emptyMaps.nonEmpty) logger.info(s"{}: Deleting empty {} maps {}.", path, emptyMaps.size, emptyMaps.flatMap(_.pathOption).map(_.toString).mkString(", "))
        emptyMaps foreachIO (map => IO(map.delete)) match {
          case Some(IO.Left(error)) =>
            logger.error(s"{}: Failed to delete empty maps {}", path, emptyMaps.flatMap(_.pathOption).map(_.toString).mkString(", "))
            IO.Left(error)

          case None =>
            logger.debug(s"{}: Creating next map with ID {} maps.", path, nextMapId)
            val queue = VolatileQueue[Map[K, V, C]](otherMaps)
            IO {
              Map.persistent[K, V, C](
                folder = nextMapId,
                mmap = mmap,
                flushOnOverflow = false,
                fileSize = fileSize,
                dropCorruptedTailEntries = recovery.drop
              )
            } map {
              nextMap =>
                logger.debug(s"{}: Next map created with ID {}.", path, nextMapId)
                new Maps[K, V, C](
                  queue = queue.addHead(nextMap.item),
                  fileSize = fileSize,
                  acceleration = acceleration,
                  currentMap = nextMap.item
                )
            }
        }
    }
  }

  private def recover[K, V, C <: MapCache[K, V]](folder: Path,
                                                 mmap: MMAP.Map,
                                                 fileSize: Long,
                                                 recovery: RecoveryMode)(implicit keyOrder: KeyOrder[K],
                                                                         fileSweeper: FileSweeperActor,
                                                                         bufferCleaner: ByteBufferSweeperActor,
                                                                         writer: MapEntryWriter[MapEntry.Put[K, V]],
                                                                         mapReader: MapEntryReader[MapEntry[K, V]],
                                                                         cacheBuilder: MapCacheBuilder[C],
                                                                         forceSaveApplier: ForceSaveApplier): IO[swaydb.Error.Map, ListBuffer[Map[K, V, C]]] = {
    /**
     * Performs corruption handling based on the the value set for [[RecoveryMode]].
     */
    def applyRecoveryMode(exception: Throwable,
                          mapPath: Path,
                          otherMapsPaths: List[Path],
                          recoveredMaps: ListBuffer[Map[K, V, C]]): IO[swaydb.Error.Map, ListBuffer[Map[K, V, C]]] =
      exception match {
        case exception: IllegalStateException =>
          recovery match {
            case RecoveryMode.ReportFailure =>
              //return failure immediately without effecting the current state of Level0
              IO.failed(exception)

            case RecoveryMode.DropCorruptedTailEntries =>
              //Ignore the corrupted file and jump to to the next Map.
              //The recovery itself should've read most of the non-corrupted head entries on best effort basis
              //and added to the recoveredMaps.
              doRecovery(otherMapsPaths, recoveredMaps)

            case RecoveryMode.DropCorruptedTailEntriesAndMaps =>
              //skip and delete all the files after the corruption file and return the successfully recovered maps
              //if the files were deleted successfully.
              logger.info(s"{}: Skipping files after corrupted file. Recovery mode: {}", mapPath, recovery.name)
              otherMapsPaths foreachIO { //delete Maps after the corruption.
                mapPath =>
                  IO(Effect.walkDelete(mapPath)) match {
                    case IO.Right(_) =>
                      logger.info(s"{}: Deleted file after corruption. Recovery mode: {}", mapPath, recovery.name)
                      IO.unit

                    case IO.Left(error) =>
                      logger.error(s"{}: IO.Left to delete file after corruption file. Recovery mode: {}", mapPath, recovery.name)
                      IO.Left(error)
                  }
              } match {
                case Some(IO.Left(error)) =>
                  IO.Left(error)

                case None =>
                  IO.Right(recoveredMaps)
              }
          }

        case exception: Throwable =>
          IO.failed(exception)
      }

    /**
     * Start recovery for all the input maps.
     */
    @tailrec
    def doRecovery(maps: List[Path],
                   recoveredMaps: ListBuffer[Map[K, V, C]]): IO[swaydb.Error.Map, ListBuffer[Map[K, V, C]]] =
      maps match {
        case Nil =>
          IO.Right(recoveredMaps)

        case mapPath :: otherMapsPaths =>
          logger.debug(s"{}: Recovering.", mapPath)

          IO {
            Map.persistent[K, V, C](
              folder = mapPath,
              mmap = mmap,
              flushOnOverflow = false,
              fileSize = fileSize,
              dropCorruptedTailEntries = recovery.drop
            )
          } match {
            case IO.Right(recoveredMap) =>
              //recovered immutable memory map's files should be closed after load as they are always read from in memory
              // and does not require the files to be opened.
              IO(recoveredMap.item.close()) match {
                case IO.Right(_) =>
                  recoveredMaps += recoveredMap.item //recoveredMap.item can also be partially recovered file based on RecoveryMode set.
                  //if the recoveredMap's recovery result is a failure (partially recovered file),
                  //apply corruption handling based on the value set for RecoveryMode.
                  recoveredMap.result match {
                    case IO.Right(_) => //Recovery was successful. Recover next map.
                      doRecovery(
                        maps = otherMapsPaths,
                        recoveredMaps = recoveredMaps
                      )

                    case IO.Left(error) =>
                      applyRecoveryMode(
                        exception = error.exception,
                        mapPath = mapPath,
                        otherMapsPaths = otherMapsPaths,
                        recoveredMaps = recoveredMaps
                      )
                  }

                case IO.Left(error) => //failed to close the file.
                  IO.Left(error)
              }

            case IO.Left(error) =>
              //if there was a full failure perform failure handling based on the value set for RecoveryMode.
              applyRecoveryMode(
                exception = error.exception,
                mapPath = mapPath,
                otherMapsPaths = otherMapsPaths,
                recoveredMaps = recoveredMaps
              )
          }
      }

    doRecovery(
      maps = folder.folders,
      recoveredMaps = ListBuffer.empty
    )
  }

  def nextMapUnsafe[K, V, C <: MapCache[K, V]](nextMapSize: Long,
                                               currentMap: Map[K, V, C])(implicit keyOrder: KeyOrder[K],
                                                                         fileSweeper: FileSweeperActor,
                                                                         bufferCleaner: ByteBufferSweeperActor,
                                                                         writer: MapEntryWriter[MapEntry.Put[K, V]],
                                                                         skipListMerger: MapCacheBuilder[C],
                                                                         forceSaveApplier: ForceSaveApplier): Map[K, V, C] =
    currentMap match {
      case currentMap: PersistentMap[K, V, C] =>
        currentMap.close()
        Map.persistent[K, V, C](
          folder = currentMap.path.incrementFolderId,
          mmap = currentMap.mmap,
          flushOnOverflow = false,
          fileSize = nextMapSize
        )

      case _ =>
        Map.memory[K, V, C](
          fileSize = nextMapSize,
          flushOnOverflow = false
        )
    }
}

private[core] class Maps[K, V, C <: MapCache[K, V]](private val queue: VolatileQueue[Map[K, V, C]],
                                                    fileSize: Long,
                                                    acceleration: LevelZeroMeter => Accelerator,
                                                    @volatile private var currentMap: Map[K, V, C])(implicit keyOrder: KeyOrder[K],
                                                                                                    fileSweeper: FileSweeperActor,
                                                                                                    val bufferCleaner: ByteBufferSweeperActor,
                                                                                                    writer: MapEntryWriter[MapEntry.Put[K, V]],
                                                                                                    cacheBuilder: MapCacheBuilder[C],
                                                                                                    val timer: Timer,
                                                                                                    forceSaveApplier: ForceSaveApplier) extends LazyLogging { self =>

  @volatile private var closed: Boolean = false

  //this listener is invoked when currentMap is full.
  @volatile private var onNextMapListener: () => Unit = () => ()
  // This is crucial for write performance use null instead of Option.
  private var brakePedal: BrakePedal = _

  @volatile private var totalMapsCount: Int = queue.size

  val meter =
    new LevelZeroMeter {
      override def defaultMapSize: Long = fileSize

      override def currentMapSize: Long = currentMap.fileSize

      override def mapsCount: Int = self.queue.size
    }

  private[core] def onNextMapCallback(event: () => Unit): Unit =
    onNextMapListener = event

  def write(mapEntry: Timer => MapEntry[K, V]): Unit =
    synchronized {
      if (brakePedal != null && brakePedal.applyBrakes()) brakePedal = null
      persist(mapEntry(timer))
    }

  private def initNextMap(mapSize: Long) = {
    val nextMap =
      Maps.nextMapUnsafe(
        nextMapSize = mapSize,
        currentMap = currentMap
      )

    queue addHead nextMap
    currentMap = nextMap
    totalMapsCount += 1
    onNextMapListener()
  }

  /**
   * @param entry entry to add
   * @return IO.Right(true) when new map gets added to maps. This return value is currently used
   *         in LevelZero to determine if there is a map that should be converted Segment.
   */
  @tailrec
  private def persist(entry: MapEntry[K, V]): Unit = {
    val persisted =
      try
        currentMap writeNoSync entry
      catch {
        case exception: Throwable if self.closed =>
          throw swaydb.Exception.InvalidAccessException(Maps.closeErrorMessage, exception)

        case throwable: Throwable =>
          //If there is a failure writing an Entry to the Map. Start a new Map immediately! This ensures that
          //if the failure was due to a corruption in the current Map, all the new Entries do not value submitted
          //to the same Map file. They SHOULD be added to a new Map file that is not already unreadable.
          logger.error(s"FATAL: Failed to write Map entry of size ${entry.entryBytesSize}.byte(s). Initialising a new Map.", throwable)
          initNextMap(fileSize)
          throw throwable
      }

    val accelerate = acceleration(meter)
    if (accelerate.brake.isEmpty) {
      if (brakePedal != null && brakePedal.isReleased())
        brakePedal = null
    } else if (brakePedal == null) {
      val brake = accelerate.brake.get
      brakePedal =
        new BrakePedal(
          brakeFor = brake.brakeFor,
          releaseRate = brake.releaseRate,
          logAsWarning = brake.logAsWarning
        )
    }

    if (!persisted) {
      val nextMapSize = accelerate.nextMapSize max entry.totalByteSize
      logger.debug(s"Map full. Initialising next map of size: $nextMapSize.bytes.")
      initNextMap(nextMapSize)
      persist(entry)
    }
  }

  @inline final private def findFirst[B](nullResult: B,
                                         finder: Map[K, V, C] => B): B = {
    val iterator = queue.iterator

    @inline def getNext() = if (iterator.hasNext) iterator.next() else null

    @tailrec
    def find(next: Map[K, V, C]): B = {
      val foundOrNullResult = finder(next)
      if (foundOrNullResult == nullResult) {
        val next = getNext()
        if (next == null)
          nullResult
        else
          find(next)
      } else {
        foundOrNullResult
      }
    }

    val next = getNext()
    if (next == null)
      nullResult
    else
      find(next)
  }

  @inline final def reduce[A >: Null, B](nullResult: B,
                                         applier: Map[K, V, C] => B,
                                         reducer: (B, B) => B): B = {

    val iterator = queue.iterator

    @inline def getNextOrNull() = if (iterator.hasNext) iterator.next() else null

    @tailrec
    def find(nextOrNull: Map[K, V, C],
             previousResult: B): B =
      if (nextOrNull == null) {
        previousResult
      } else {
        val nextResult = applier(nextOrNull)
        if (nextResult == nullResult) {
          find(getNextOrNull(), previousResult)
        } else if (previousResult == nullResult) {
          find(getNextOrNull(), nextResult)
        } else {
          val result = reducer(previousResult, nextResult)
          find(getNextOrNull(), result)
        }
      }

    find(getNextOrNull(), nullResult)
  }

  def find[B](nullResult: B,
              matcher: Map[K, V, C] => B): B =
    findFirst[B](
      nullResult = nullResult,
      finder = matcher
    )

  def nextJob(): Option[Map[K, V, C]] = {
    val last = queue.lastOrNull()
    if (last == null || queue.size == 1)
      None
    else
      Some(last)
  }

  def removeLast(map: Map[K, V, C]): IO[Error.Map, Unit] =
    IO(queue.removeLast(map))
      .and {
        IO(map.delete) match {
          case IO.Right(_) =>
            IO.unit

          case IO.Left(error) =>
            //failed to delete file. Add it back to the queue.
            logger.error(s"Failed to delete map '${map.toString}'. Adding it back to the queue.", error.exception)
            queue.addLast(map)
            IO.Left(error)
        }
      }

  def keyValueCount: Int =
    reduce[Integer, Int](
      nullResult = 0,
      applier = _.cache.maxKeyValueCount,
      reducer = _ + _
    )

  def mapsCount =
    queue.size

  def isEmpty: Boolean =
    queue.isEmpty

  def map: Map[K, V, C] =
    currentMap

  def close(): IO[swaydb.Error.Map, Unit] =
    IO {
      closed = true
      timer.close
    }.onLeftSideEffect {
      failure =>
        logger.error("Failed to close timer file", failure.exception)
    }.and {
      self
        .queue
        .iterator
        .toList
        .foreachIO(map => IO(map.close()), failFast = false)
        .getOrElse(IO.unit)
    }

  def delete(): IO[Error.Map, Unit] =
    close()
      .and {
        self
          .queue
          .iterator
          .toList
          .foreachIO(map => IO(map.delete))
          .getOrElse(IO.unit)
      }

  def iterator: Iterator[Map[K, V, C]] =
    queue.iterator

  def dropIterator: DropIterator.Single[Null, Map[K, V, C]] =
    queue.dropIterator

  def mmap: MMAP =
    currentMap.mmap

  def stateId: Long =
    totalMapsCount

  def isClosed =
    closed
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy