![JAR search and dependency download from the Maven repository](/logo.png)
swaydb.core.map.PersistentMap.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.IO._
import swaydb.core.actor.ByteBufferSweeper
import swaydb.core.actor.ByteBufferSweeper.ByteBufferSweeperActor
import swaydb.core.actor.FileSweeper.FileSweeperActor
import swaydb.core.io.file.Effect._
import swaydb.core.io.file.{DBFile, Effect, ForceSaveApplier}
import swaydb.core.map.serializer.{MapCodec, MapEntryReader, MapEntryWriter}
import swaydb.core.util.Extension
import swaydb.data.config.{IOStrategy, MMAP}
import swaydb.data.order.KeyOrder
import swaydb.data.slice.Slice
import scala.annotation.tailrec
private[map] object PersistentMap extends LazyLogging {
private[map] def apply[K, V, C <: MapCache[K, V]](folder: Path,
mmap: MMAP.Map,
flushOnOverflow: Boolean,
fileSize: Long,
dropCorruptedTailEntries: Boolean)(implicit keyOrder: KeyOrder[K],
fileSweeper: FileSweeperActor,
bufferCleaner: ByteBufferSweeperActor,
reader: MapEntryReader[MapEntry[K, V]],
writer: MapEntryWriter[MapEntry.Put[K, V]],
cacheBuilder: MapCacheBuilder[C],
forceSaveApplier: ForceSaveApplier): RecoveryResult[PersistentMap[K, V, C]] = {
Effect.createDirectoryIfAbsent(folder)
val cache = cacheBuilder.create()
val fileRecoveryResult =
recover[K, V, C](
folder = folder,
mmap = mmap,
fileSize = fileSize,
cache = cache,
dropCorruptedTailEntries = dropCorruptedTailEntries
)
val map =
new PersistentMap[K, V, C](
path = folder,
mmap = mmap,
fileSize = fileSize,
flushOnOverflow = flushOnOverflow,
cache = cache,
currentFile = fileRecoveryResult.item
)
RecoveryResult(
item = map,
result = fileRecoveryResult.result
)
}
private[map] def apply[K, V, C <: MapCache[K, V]](folder: Path,
mmap: MMAP.Map,
flushOnOverflow: Boolean,
fileSize: Long)(implicit keyOrder: KeyOrder[K],
fileSweeper: FileSweeperActor,
bufferCleaner: ByteBufferSweeperActor,
cacheBuilder: MapCacheBuilder[C],
writer: MapEntryWriter[MapEntry.Put[K, V]],
forceSaveApplier: ForceSaveApplier): PersistentMap[K, V, C] = {
Effect.createDirectoryIfAbsent(folder)
val file =
firstFile(
folder = folder,
memoryMapped = mmap,
fileSize = fileSize
)
new PersistentMap[K, V, C](
path = folder,
mmap = mmap,
fileSize = fileSize,
flushOnOverflow = flushOnOverflow,
currentFile = file,
cache = cacheBuilder.create()
)
}
private[map] def firstFile(folder: Path,
memoryMapped: MMAP.Map,
fileSize: Long)(implicit fileSweeper: FileSweeperActor,
bufferCleaner: ByteBufferSweeperActor,
forceSaveApplier: ForceSaveApplier): DBFile =
memoryMapped match {
case MMAP.On(deleteAfterClean, forceSave) =>
DBFile.mmapInit(
path = folder.resolve(0.toLogFileId),
fileOpenIOStrategy = IOStrategy.SynchronisedIO(true),
bufferSize = fileSize,
blockCacheFileId = 0,
autoClose = false,
deleteAfterClean = deleteAfterClean,
forceSave = forceSave
)(fileSweeper, None, bufferCleaner, forceSaveApplier)
case MMAP.Off(forceSave) =>
DBFile.channelWrite(
path = folder.resolve(0.toLogFileId),
fileOpenIOStrategy = IOStrategy.SynchronisedIO(true),
blockCacheFileId = 0,
autoClose = false,
forceSave = forceSave
)(fileSweeper, None, bufferCleaner, forceSaveApplier)
}
private[map] def recover[K, V, C <: MapCache[K, V]](folder: Path,
mmap: MMAP.Map,
fileSize: Long,
cache: C,
dropCorruptedTailEntries: Boolean)(implicit writer: MapEntryWriter[MapEntry.Put[K, V]],
mapReader: MapEntryReader[MapEntry[K, V]],
fileSweeper: FileSweeperActor,
bufferCleaner: ByteBufferSweeperActor,
forceSaveApplier: ForceSaveApplier): RecoveryResult[DBFile] = {
val files = folder.files(Extension.Log)
val recoveredFiles =
files mapRecover {
path =>
logger.info("{}: Recovering with dropCorruptedTailEntries = {}.", path, dropCorruptedTailEntries)
val file = DBFile.channelRead(path, IOStrategy.SynchronisedIO(true), autoClose = false, blockCacheFileId = 0)(fileSweeper, None, bufferCleaner, forceSaveApplier)
val bytes = file.readAll
val recovery = MapCodec.read[K, V](bytes, dropCorruptedTailEntries).get
logger.debug("{}: Recovered! Indexing recovered key-values.", path)
val entriesRecovered =
recovery.item.foldLeft(0) {
case (size, entry) =>
//when populating skipList do the same checks a PersistentMap does when writing key-values to the skipList.
//Use the merger to write key-values to skipList if the there a range, update or remove(with deadline).
//else simply write the key-values to the skipList. This logic should be abstracted out to a common function.
//See MapSpec for tests.
cache writeNonAtomic entry
size + entry.entriesCount
}
logger.info(s"{}: Recovered {} ${if (entriesRecovered == 0 || entriesRecovered > 1) "entries" else "entry"}.", path, entriesRecovered)
RecoveryResult(file, recovery.result)
}
val file =
nextFile[K, V, C](
oldFiles = recoveredFiles.map(_.item),
mmap = mmap,
fileSize = fileSize,
cache = cache
) getOrElse {
firstFile(
folder = folder,
memoryMapped = mmap,
fileSize = fileSize
)
}
//if there was a failure recovering any one of the files, return the recovery with the failure result.
RecoveryResult(
item = file,
result = recoveredFiles.find(_.result.isLeft).map(_.result) getOrElse IO.unit
)
}
/**
* Creates nextFile by persisting the entries in skipList to the new file. This function does not
* re-read oldFiles to apply the existing entries to skipList, skipList should already be populated with new entries.
* This is to ensure that before deleting any of the old entries, a new file is successful created.
*
* oldFiles value deleted after the recovery is successful. In case of a failure an error message is logged.
*/
private[map] def nextFile[K, V, C <: MapCache[K, V]](oldFiles: Slice[DBFile],
mmap: MMAP.Map,
fileSize: Long,
cache: C)(implicit writer: MapEntryWriter[MapEntry.Put[K, V]],
fileSweeper: FileSweeperActor,
bufferCleaner: ByteBufferSweeperActor,
forceSaveApplier: ForceSaveApplier): Option[DBFile] =
oldFiles.lastOption map {
lastFile =>
val file =
nextFile[K, V, C](
currentFile = lastFile,
mmap = mmap,
size = fileSize,
cache = cache
)
//Next file successfully created. delete all old files without the last which gets deleted by nextFile.
try {
oldFiles.dropRight(1).foreach(_.delete())
logger.debug(s"Recovery successful")
file
} catch {
case throwable: Throwable =>
logger.error(
"Recovery successful but failed to delete old log file. Delete this file manually and every other file except '{}' and reboot.",
file.path,
throwable
)
throw throwable
}
}
private[map] def nextFile[K, V, C <: MapCache[K, V]](currentFile: DBFile,
mmap: MMAP.Map,
size: Long,
cache: C)(implicit writer: MapEntryWriter[MapEntry.Put[K, V]],
fileSweeper: FileSweeperActor,
bufferCleaner: ByteBufferSweeperActor,
forceSaveApplier: ForceSaveApplier): DBFile = {
val nextPath = currentFile.path.incrementFileId
val bytes = MapCodec.write(cache.iterator)
val newFile =
mmap match {
case MMAP.On(deleteAfterClean, forceSave) =>
DBFile.mmapInit(
path = nextPath,
fileOpenIOStrategy = IOStrategy.SynchronisedIO(true),
bufferSize = bytes.size + size,
blockCacheFileId = 0,
autoClose = false,
deleteAfterClean = deleteAfterClean,
forceSave = forceSave
)(fileSweeper, None, bufferCleaner, forceSaveApplier)
case MMAP.Off(forceSave) =>
DBFile.channelWrite(
path = nextPath,
fileOpenIOStrategy = IOStrategy.SynchronisedIO(true),
blockCacheFileId = 0,
autoClose = false,
forceSave = forceSave
)(fileSweeper, None, bufferCleaner, forceSaveApplier)
}
newFile.append(bytes)
currentFile.delete()
newFile
}
}
protected case class PersistentMap[K, V, C <: MapCache[K, V]](path: Path,
mmap: MMAP.Map,
fileSize: Long,
flushOnOverflow: Boolean,
cache: C,
private var currentFile: DBFile)(implicit val fileSweeper: FileSweeperActor,
val bufferCleaner: ByteBufferSweeperActor,
val writer: MapEntryWriter[MapEntry.Put[K, V]],
val forceSaveApplier: ForceSaveApplier) extends Map[K, V, C] with LazyLogging {
// actualSize of the file can be different to fileSize when the entry's size is > fileSize.
// In this case a file is created just to fit those bytes (for that one entry).
// For eg: if fileSize is 4.mb and the entry size is 5.mb, a new file is created with 5.mb for that one entry.
// all the subsequent entries value added to 4.mb files, if it fits, or else the size is extended again.
private var actualFileSize: Long = fileSize
// does not account of flushed entries.
private var bytesWritten: Long = 0
//minimum of writes we should see before logging out warning messages that the fileSize is too small.
private val minimumNumberOfWritesAfterFlush = 10
//maintains allowed number of writes that can occurred after the last flush before warning.
private var allowedPostFlushEntriesBeforeWarn: Long = 0
override val uniqueFileNumber: Long =
Map.uniqueFileNumberGenerator.nextID
def typeName = productPrefix
def currentFilePath =
currentFile.path
override def writeSync(mapEntry: MapEntry[K, V]): Boolean =
synchronized(writeNoSync(mapEntry))
/**
* Before writing the Entry, check to ensure if the current [[MapEntry]] requires a merge write or direct write.
*
* Merge write should be used when
* - The entry contains a [[swaydb.core.data.Memory.Range]] key-value.
* - The entry contains a [[swaydb.core.data.Memory.Update]] Update key-value.
* - The entry contains a [[swaydb.core.data.Memory.Remove]] with deadline key-value. Removes without deadlines do not require merging.
*
* Note: These check are not required for Appendix writes because Appendix entries current do not use
* Range, Update or key-values with deadline.
*/
@tailrec
final def writeNoSync(entry: MapEntry[K, V]): Boolean = {
val entryTotalByteSize = entry.totalByteSize
if ((bytesWritten + entryTotalByteSize) <= actualFileSize) {
currentFile.append(MapCodec.write(entry))
//if this main contains range then use skipListMerge.
cache.writeAtomic(entry)
bytesWritten += entryTotalByteSize
allowedPostFlushEntriesBeforeWarn -= 1 //decrement the number on successful write
true
} else if (!flushOnOverflow && bytesWritten != 0) {
//flushOnOverflow is executed if the current file is empty, even if flushOnOverflow = false.
false
} else {
val nextFilesSize = entryTotalByteSize.toLong max fileSize
try {
val newFile =
PersistentMap.nextFile[K, V, C](
currentFile = currentFile,
mmap = mmap,
size = nextFilesSize,
cache = cache
)
/**
* If the fileSize is too small like 1.byte it will result in too many flushes. Log a warn message to increase
* file size.
*/
if (allowedPostFlushEntriesBeforeWarn <= 0) //if it was in negative then restart the count
allowedPostFlushEntriesBeforeWarn = minimumNumberOfWritesAfterFlush
else if (allowedPostFlushEntriesBeforeWarn > 0) //If the count did not get negative then warn that the fileSize is too small.
logger.warn(s"$typeName's file size of $fileSize.bytes is too small and would result in too many flushes. Please increase the default fileSize to at least ${newFile.fileSize}.bytes. Folder: $path.")
currentFile = newFile
actualFileSize = nextFilesSize
bytesWritten = 0
} catch {
case exception: Exception =>
logger.error("{}: Failed to replace with new file", currentFile.path, exception)
throw new Exception("Fatal exception", exception)
}
writeNoSync(entry)
}
}
override def close(): Unit =
currentFile.close()
override def exists: Boolean =
currentFile.existsOnDisk
override def delete: Unit =
if (mmap.deleteAfterClean) {
//if it's require deleteAfterClean then do not invoke delete directly
//instead invoke close (which will also call ByteBufferCleaner for closing)
// and then submit delete to ByteBufferCleaner actor.
currentFile.close()
bufferCleaner.actor send ByteBufferSweeper.Command.DeleteFolder(path, currentFile.path)
} else {
//else delete immediately.
currentFile.delete()
Effect.delete(path)
}
override def pathOption: Option[Path] =
Some(path)
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy