fix: add entry update logic with better mmap handling

This commit is contained in:
Akash Yadav
2024-07-28 16:14:39 +05:30
parent 23203c76cf
commit aebffd4ea6
9 changed files with 343 additions and 164 deletions
@@ -195,15 +195,14 @@ internal abstract class AbstractLevelHash<K : Any, V : Any?> internal constructo
}
override fun remove(key: K): V? {
return findSlot(key).let { slot ->
if (slot != null) {
val oldValue = slot.value
slot.reset(null, null)
return@let oldValue
}
return@let null
val slot = findSlot(key)
if (slot != null) {
val oldValue = slot.value
slot.reset(null, null)
return oldValue
}
return null
}
override fun findSlot(key: K): LevelSlot<K, V>? {
@@ -0,0 +1,78 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.levelhash.internal
import com.itsaky.androidide.levelhash.internal.PersistentKeymapIO.Companion.KEYMAP_HEADER_SIZE_BYTES
import com.itsaky.androidide.levelhash.internal.PersistentKeymapIO.Companion.KEYMAP_MAGIC_NUMBER
import org.slf4j.LoggerFactory
import java.io.DataInputStream
import java.io.File
import java.io.RandomAccessFile
/**
* @author Akash Yadav
*/
internal object BinaryFileUtils {
private val log = LoggerFactory.getLogger(BinaryFileUtils::class.java)
fun initSparseFile(file: File, magicNumber: Long, maxLength: Long,
conf: (raf: RandomAccessFile) -> Unit = {}
) {
val raf = RandomAccessFile(file, "rw")
if (!file.exists()) {
doInit(file, raf, magicNumber, maxLength, conf)
return
}
if (file.isDirectory) {
file.deleteRecursively()
} else if (file.isFile && file.length() < KEYMAP_HEADER_SIZE_BYTES) {
check(file.delete()) { "Failed to delete existing index file" }
} else {
DataInputStream(file.inputStream()).use { dis ->
val magic = dis.readLong()
if (magic != magicNumber) {
// mismatched magic number
// delete file and create new one
log.warn(
"Magic number mismatch for index: {}. Deleting existing file...",
file.canonicalPath)
check(file.delete()) { "Failed to delete existing index file" }
}
}
}
doInit(file, raf, magicNumber, maxLength, conf)
}
private inline fun doInit(file: File, raf: RandomAccessFile, magic: Long,
maxLength: Long,
conf: (raf: RandomAccessFile) -> Unit = {}
) {
check(file.createNewFile()) {
"Failed to create keymap file: ${file.canonicalPath}"
}
// raf.setLength(maxLength)
raf.seek(0)
raf.writeLong(magic)
raf.use(conf)
}
}
@@ -57,8 +57,8 @@ internal class InMemoryLevelSlot<K, V> private constructor(
*/
override fun reset(key: K?, value: V?) {
lock.writeLock().withLock {
_key = key ?: _key
_value = value ?: _value
_key = key
_value = value
}
}
}
@@ -37,7 +37,7 @@ internal class PersistentKeymapIO(indexFile: File
*/
private val keymapSegments = arrayOfNulls<KeymapSegment>(KEYMAP_MAX_SEGMENTS)
private val keymapFile = File(indexFile.parentFile, "${indexFile.name}._i")
private val raKeymapFile = RandomAccessFile(keymapFile, "rw")
private val raKeymapFile by lazy { RandomAccessFile(keymapFile, "rw") }
private val segmentCount by lazy {
if (!keymapFile.exists() || !keymapFile.isFile || keymapFile.length() <= KEYMAP_HEADER_SIZE_BYTES) {
@@ -45,25 +45,25 @@ internal class PersistentKeymapIO(indexFile: File
return@lazy 1
}
raKeymapFile.seek(KEYMAP_HEADER_SIZE_BYTES.toLong())
raKeymapFile.seek(KEYMAP_HEADER_SIZE_BYTES)
val count = raKeymapFile.readLong()
raKeymapFile.seek(0)
return@lazy count
}
override fun close() {
keymapSegments.forEach { segment ->
try {
segment?.unmap()
} catch (err: Throwable) {
log.error("Failed to unmap segment", err)
}
}
init {
BinaryFileUtils.initSparseFile(
file = keymapFile,
magicNumber = KEYMAP_MAGIC_NUMBER,
maxLength = KEYMAP_HEADER_SIZE_BYTES + KEYMAP_SEGMENT_SIZE_BYTES * KEYMAP_MAX_SEGMENTS
) { raf ->
var position = MAGIC_NUMBER_SIZE_BYTES
raf.seek(position)
raf.writeInt(0)
position += SIZE_INT
try {
raKeymapFile.close()
} catch (e: Throwable) {
log.error("Failed to close keymap file", e)
raf.seek(position)
raf.writeLong(KEYMAP_SEGMENT_SIZE_BYTES)
}
}
@@ -96,19 +96,31 @@ internal class PersistentKeymapIO(indexFile: File
return segment to positionInSegment
}
fun deallocate(
offset: Long,
len: Long
fun deallocate(offset: Long, len: Long
) {
FileUtils.deallocate(raKeymapFile, offset, len)
}
fun deallocate(
segment: KeymapSegment,
positionInSegment: Long,
len: Long
fun deallocate(segment: KeymapSegment, positionInSegment: Long, len: Long
) {
FileUtils.deallocate(raKeymapFile, segment.segmentOffset + positionInSegment, len)
FileUtils.deallocate(raKeymapFile,
segment.segmentOffset + positionInSegment, len)
}
override fun close() {
keymapSegments.forEach { segment ->
try {
segment?.unmap()
} catch (err: Throwable) {
log.error("Failed to unmap segment", err)
}
}
try {
raKeymapFile.close()
} catch (e: Throwable) {
log.error("Failed to close keymap file", e)
}
}
companion object {
@@ -57,8 +57,8 @@ internal class PersistentLevelHash<K : Any, V : Any?>(
override val key: K
get() = checkNotNull(io.readKey(levelIdx, bucketIdx, slotIdx))
override val value: V
get() = checkNotNull(io.readValue(levelIdx, bucketIdx, slotIdx))
override val value: V?
get() = io.readValue(levelIdx, bucketIdx, slotIdx)
override fun isOccupied(): Boolean {
return io.isOccupied(levelIdx, bucketIdx, slotIdx)
@@ -23,9 +23,11 @@ import com.itsaky.androidide.levelhash.internal.PersistentKeymapIO.Companion.KEY
import com.itsaky.androidide.levelhash.internal.PersistentKeymapIO.Companion.KEYMAP_SEGMENT_SIZE_BYTES
import com.itsaky.androidide.levelhash.seekBoolean
import com.itsaky.androidide.levelhash.seekInt
import com.itsaky.androidide.levelhash.seekShort
import com.itsaky.androidide.levelhash.util.DataExternalizers.SIZE_BOOLEAN
import com.itsaky.androidide.levelhash.util.DataExternalizers.SIZE_INT
import com.itsaky.androidide.levelhash.util.DataExternalizers.SIZE_LONG
import com.itsaky.androidide.levelhash.util.DataExternalizers.SIZE_SHORT
import com.itsaky.androidide.levelhash.util.FileUtils
import com.itsaky.androidide.levelhash.util.MappedRandomAccessIO
import org.slf4j.LoggerFactory
@@ -33,7 +35,6 @@ import java.io.File
import java.io.RandomAccessFile
import java.nio.channels.FileChannel
private val logger = LoggerFactory.getLogger(PersistentLevelHashIO::class.java)
/**
@@ -67,6 +68,7 @@ private val logger = LoggerFactory.getLogger(PersistentLevelHashIO::class.java)
*
* ```
* keymap {
* u64 magic_number;
* u32 segment_count;
* u64 segment_size;
* segment segments[segment_count];
@@ -100,6 +102,7 @@ private val logger = LoggerFactory.getLogger(PersistentLevelHashIO::class.java)
*
* value {
* u8 is_occupied;
* u16 entry_size;
* u32 key_size;
* u8 key[key_size];
* u32 value_size;
@@ -109,8 +112,8 @@ private val logger = LoggerFactory.getLogger(PersistentLevelHashIO::class.java)
* ```
* @author Akash Yadav
*/
class PersistentLevelHashIO<K : Any, V : Any?>(
private val indexFile: File,
internal class PersistentLevelHashIO<K : Any, V : Any?>(
indexFile: File,
private val keyExternalizer: DataExternalizer<K>,
private val valueExternalizer: DataExternalizer<V>,
) : AutoCloseable {
@@ -123,53 +126,51 @@ class PersistentLevelHashIO<K : Any, V : Any?>(
private val keymapIo = PersistentKeymapIO(indexFile)
private val raIndexFile by lazy {
RandomAccessFile(indexFile, "rw")
}
private val metaFile = File(indexFile.parentFile, "${indexFile.name}._meta")
private val raMetaFile by lazy { RandomAccessFile(metaFile, "rw") }
private val raIndexFile by lazy { RandomAccessFile(indexFile, "rw") }
private val metaFile by lazy {
File(indexFile.parentFile, "${indexFile.name}._meta")
}
private val raMetaFile by lazy {
RandomAccessFile(metaFile, "rw")
}
private var valuesFirstEntryAt = 0L
private var valuesLastEntryAt = 0L
private var valuesWriteAt = 0L
private var valuesMapSize = 0L
private var valuesIo = MappedRandomAccessIO()
private var valFirstEntryPos = 0L
private var valNxtEntryPos = 0L
private var valIdxSize = 0L
private var valIo = MappedRandomAccessIO()
init {
if (indexFile.exists()) {
if (indexFile.isDirectory) {
indexFile.deleteRecursively()
} else if (indexFile.isFile && indexFile.length() < VALUES_HEADER_SIZE_BYTES) {
indexFile.delete()
}
} else {
indexFile.createNewFile()
BinaryFileUtils.initSparseFile(
file = indexFile,
magicNumber = VALUES_MAGIC_NUMBER,
maxLength = VALUES_HEADER_SIZE_BYTES + valIdxSize
) { raf ->
var position = MAGIC_NUMBER_SIZE_BYTES
raf.seek(position)
raf.writeLong(0) // first entry
position += SIZE_LONG
raf.seek(position)
raf.writeLong(0) // next entry
}
// TODO(itsaky): Load the below values from file when the file already exists
// TODO: Load the below values from file when the file already exists
valuesLastEntryAt = -1
valuesWriteAt = 0
valuesFirstEntryAt = VALUES_HEADER_SIZE_BYTES
valuesMapSize = VALUES_MAX_MAP_SIZE_BYTES
valNxtEntryPos = 0
valFirstEntryPos = VALUES_HEADER_SIZE_BYTES
valIdxSize = VALUES_MAX_MAP_SIZE_BYTES
// the header region is not memory mapped
log.debug("mmap values file: offset={}, size={}", VALUES_HEADER_SIZE_BYTES,
valuesMapSize)
valIdxSize)
val buffer = raIndexFile.channel.map(FileChannel.MapMode.READ_WRITE,
VALUES_HEADER_SIZE_BYTES, valuesMapSize)
valuesIo.reset(buffer, 0L, valuesMapSize)
VALUES_HEADER_SIZE_BYTES, valIdxSize)
valIo.reset(buffer, 0L, valIdxSize)
}
private fun getValuesRealOffset(offset: Long) =
VALUES_HEADER_SIZE_BYTES + offset
private fun valuesDeallocate(offset: Long, len: Long) {
FileUtils.deallocate(raIndexFile, getValuesRealOffset(offset), len)
}
/**
* Get the address of the value entry in the values for the given slot
* position.
@@ -192,23 +193,6 @@ class PersistentLevelHashIO<K : Any, V : Any?>(
}
}
fun isOccupied(
levelNum: Int,
bucketIdx: Int,
slotIdx: Int,
): Boolean {
val valueAddr = valueAddressForPos(levelNum, bucketIdx, slotIdx)
if (valueAddr == 0L) {
// 0 == no value entry
return false
}
valuesIo.position(valueAddr - 1)
// token must be set and key size must be > 0
return valuesIo.readBoolean() && valuesIo.readInt() > 0
}
private fun moveToSlot(
levelNum: Int,
bucketIdx: Int,
@@ -220,10 +204,23 @@ class PersistentLevelHashIO<K : Any, V : Any?>(
return false
}
valuesIo.position(valueAddr - 1)
valIo.position(valueAddr - 1)
return true
}
fun isOccupied(
levelNum: Int,
bucketIdx: Int,
slotIdx: Int,
): Boolean {
if (!moveToSlot(levelNum, bucketIdx, slotIdx)) {
return false
}
// token must be set and key size must be > 0
return valIo.readBoolean() && valIo.readInt() > 0
}
/**
* Visits the key region for the given slot position.
*
@@ -238,24 +235,31 @@ class PersistentLevelHashIO<K : Any, V : Any?>(
return null
}
val isOccupied = valuesIo.readBoolean()
val isOccupied = valIo.readBoolean()
if (!isOccupied) {
return null
}
val keySize = valuesIo.readInt()
if (valIo.readUnsignedShort() <= 0L) {
// entry size is 0, so the slot is empty
return null
}
val keySize = valIo.readInt()
if (keySize == 0) {
if (readKey) {
logReadFailure("key", "key size is 0", levelNum, bucketIdx, slotIdx)
log.info(
"Cannot read 'key' at slot (key size is 0): level={}, bucket={}, slot={}",
levelNum, bucketIdx, slotIdx)
}
return null
}
if (readKey) {
val keyAddr = valuesIo.position()
val key = keyExternalizer.read(valuesIo)
val readBytes = (valuesIo.position() - keyAddr).toInt()
val keyAddr = valIo.position()
val key = keyExternalizer.read(valIo)
val readBytes = (valIo.position() - keyAddr).toInt()
check(readBytes == keySize) {
"Key size mismatch. Expected to read $keySize bytes, but $readBytes bytes were read."
}
@@ -264,7 +268,7 @@ class PersistentLevelHashIO<K : Any, V : Any?>(
}
if (keySize > 0) {
valuesIo.skipBytes(keySize)
valIo.skipBytes(keySize)
}
return null to keySize
@@ -297,15 +301,14 @@ class PersistentLevelHashIO<K : Any, V : Any?>(
return null
}
val valueSize = valuesIo.readInt()
val valueSize = valIo.readInt()
if (valueSize == 0) {
logReadFailure("value", "value size is 0", levelNum, bucketIdx, slotIdx)
return null
}
val valAddr = valuesIo.position()
val value = valueExternalizer.read(valuesIo)
val readBytes = (valuesIo.position() - valAddr).toInt()
val valAddr = valIo.position()
val value = valueExternalizer.read(valIo)
val readBytes = (valIo.position() - valAddr).toInt()
check(readBytes == valueSize) {
"Value size mismatch. Expected to read $valueSize bytes, but $readBytes bytes were read."
}
@@ -329,32 +332,59 @@ class PersistentLevelHashIO<K : Any, V : Any?>(
return
}
// TODO(itsaky): Punch holes in the region of the existing entry on update
// to ensure the disk space is released
segment.position(positionInSegment)
val existingValueAddr = segment.readLong()
val (isUpdate, existingEntrySize) = run {
if (existingValueAddr > 0L) {
valIo.position(existingValueAddr - 1)
valIo.readBoolean() to valIo.readUnsignedShort().toLong()
} else {
false to 0L
}
}
// leave space for the 'is_occupied' token
val tokenAddr = valuesWriteAt
valuesIo.position(tokenAddr)
valuesIo.seekBoolean()
val tokenAddr = valNxtEntryPos
valIo.position(tokenAddr)
// leave space for isOccupied token and entry size
valIo.seekBoolean()
valIo.seekShort()
val keyValPos = valIo.position()
// write key and value
writeSlotKeyOrVal(key, keyExternalizer)
writeSlotKeyOrVal(value, valueExternalizer)
val finalAddr = valuesIo.position()
val finalAddr = valIo.position()
val entrySize = finalAddr - keyValPos
check(entrySize <= Short.MAX_VALUE) {
"Entry size is too large: $entrySize"
}
// then set the token
valuesIo.position(tokenAddr)
valuesIo.writeBoolean(true)
valIo.position(tokenAddr)
valIo.writeBoolean(true)
valIo.writeShort((entrySize and 0xFFFF).toInt())
// reset to the final position
valuesIo.position(finalAddr)
valIo.position(finalAddr)
// then update the address in the keymap
segment.position(positionInSegment)
segment.writeLong(tokenAddr + 1)
valuesWriteAt = finalAddr
valNxtEntryPos = finalAddr
if (isUpdate) {
var size: Long = SIZE_BOOLEAN + SIZE_SHORT
if (existingEntrySize > 0L) {
size += existingEntrySize
}
// deallocate the region which contains the existing entry
valuesDeallocate(existingValueAddr - 1, size)
}
}
private fun deleteEntry(
@@ -369,47 +399,48 @@ class PersistentLevelHashIO<K : Any, V : Any?>(
return null
}
// reading this region again will return 0, which is considered
// a null pointer
keymapIo.deallocate(segment, positionInSegment, SIZE_LONG)
valuesIo.position(valueAddr - 1)
valIo.position(valueAddr - 1)
var entrySize = 0L
if (!valuesIo.readBoolean()) {
if (!valIo.readBoolean()) {
// slot is not occupied
return null
}
entrySize += SIZE_BOOLEAN // isOccupied
entrySize += SIZE_SHORT // entrySize
val keySize = valuesIo.readInt()
val keySize = valIo.readInt()
entrySize += SIZE_INT
entrySize += keySize
if (keySize > 0) {
valuesIo.seekRelative(keySize)
valIo.seekRelative(keySize)
}
val valueSize = valuesIo.readInt()
val valueSize = valIo.readInt()
entrySize += SIZE_INT
entrySize += valueSize
var value: V? = null
if (readValue && valueSize > 0) {
val valAddr = valuesIo.position()
value = valueExternalizer.read(valuesIo)
val valAddr = valIo.position()
value = valueExternalizer.read(valIo)
// ensure that the externalizer read exactly the expected bytes
check((valuesIo.position() - valAddr).toInt() == valueSize)
check((valIo.position() - valAddr).toInt() == valueSize)
} else if (valueSize > 0) {
valuesIo.seekRelative(valueSize)
valIo.seekRelative(valueSize)
}
// punch holes in the file
FileUtils.deallocate(raIndexFile, getValuesRealOffset(valueAddr - 1),
entrySize)
valuesDeallocate(valueAddr - 1, entrySize)
// TODO(itsaky): When deleting an entry, update the corresponding keymap entry with -1
// TODO(itsaky): When an entry is deleted, update valueFirstEntryAt and valueLastEntryAt accordingly
return value
}
@@ -417,10 +448,10 @@ class PersistentLevelHashIO<K : Any, V : Any?>(
fun clear() {
keymapIo.deallocate(KEYMAP_HEADER_SIZE_BYTES,
KEYMAP_SEGMENT_SIZE_BYTES * KEYMAP_MAX_SEGMENTS)
FileUtils.deallocate(raIndexFile, VALUES_HEADER_SIZE_BYTES, valuesMapSize)
valuesDeallocate(VALUES_HEADER_SIZE_BYTES, valIdxSize)
}
@Suppress("UNUSED", "UNUSED_PARAMETER")
fun tryMoveToInterim(
levelNum: Int,
bucketIdx: Int,
@@ -437,22 +468,22 @@ class PersistentLevelHashIO<K : Any, V : Any?>(
// obj is not null
// leave space for the obj size and write the key
// then write the number of bytes written for obj to the sizeAddr
val sizeAddr = valuesIo.position()
valuesIo.seekInt()
val sizeAddr = valIo.position()
valIo.seekInt()
val valueAddr = valuesIo.position()
externalizer.write(valuesIo, obj)
val valueAddr = valIo.position()
externalizer.write(valIo, obj)
val finalAddr = valuesIo.position()
valuesIo.position(sizeAddr)
valuesIo.writeInt((finalAddr - valueAddr).toInt().also {
val finalAddr = valIo.position()
valIo.position(sizeAddr)
valIo.writeInt((finalAddr - valueAddr).toInt().also {
check(it > 0) { "Key/Value must be at least 1 byte in size" }
})
valuesIo.position(finalAddr)
valIo.position(finalAddr)
} else {
// object is null, write the size of the obj as 0
// since obj size is 0, we don't need to write the obj
valuesIo.writeInt(0)
valIo.writeInt(0)
}
}
@@ -477,18 +508,6 @@ class PersistentLevelHashIO<K : Any, V : Any?>(
}
}
private fun logReadFailure(
what: String,
reason: String,
levelNum: Int,
bucketIdx: Int,
slotIdx: Int,
) {
log.info(
"Cannot read '$what' at slot ($reason): level={}, bucket={}, slot={}",
levelNum, bucketIdx, slotIdx)
}
companion object {
@@ -18,6 +18,7 @@
package com.itsaky.androidide.levelhash.util
import com.itsaky.androidide.levelhash.DataExternalizer
import com.itsaky.androidide.utils.uncheckedCast
import java.io.DataInput
import java.io.DataOutput
@@ -179,4 +180,33 @@ object DataExternalizers {
out.writeUTF(data)
}
}
}
}
/**
* Create a [DataExternalizer] for nullable type [T]. An extra byte before the
* actual data is used to represent the null state.
*/
fun <T> DataExternalizer<T>.nullable(): DataExternalizer<T?> {
if (this is NullableDataExternalizer<*>) {
return uncheckedCast(this)
}
return NullableDataExternalizer(this)
}
private class NullableDataExternalizer<T>(
private val self: DataExternalizer<T>
) : DataExternalizer<T?> {
override fun read(input: DataInput): T? {
if (input.readBoolean()) {
return null
}
return self.read(input)
}
override fun write(out: DataOutput, data: T?) {
out.writeBoolean(data == null)
if (data != null) {
self.write(out, data)
}
}
}
@@ -25,7 +25,6 @@ import com.itsaky.androidide.levelhash.seekFloat
import com.itsaky.androidide.levelhash.seekInt
import com.itsaky.androidide.levelhash.seekLong
import com.itsaky.androidide.levelhash.seekShort
import com.itsaky.androidide.levelhash.util.DataExternalizers.SIZE_BYTE
import java.io.DataInput
import java.io.DataInputStream
import java.io.DataOutput
@@ -46,16 +45,16 @@ internal class MappedRandomAccessIO : DataInput, DataOutput, RandomAccessIO {
private lateinit var buffer: MappedByteBuffer
fun reset(buffer: MappedByteBuffer, offset: Long, length: Long) {
this.buffer = buffer
this.position = offset
this.size = length
internal fun buf(): MappedByteBuffer = buf(this.position)
internal fun buf(position: Long): MappedByteBuffer {
return this.buffer.apply { position(position.toInt()) }
}
private fun buf(): MappedByteBuffer = buf(this.position)
private fun buf(position: Long): MappedByteBuffer {
return this.buffer.apply { position(position.toInt()) }
fun reset(buffer: MappedByteBuffer, position: Long, length: Long) {
this.buffer = buffer
this.position = position
this.size = length
}
override fun position(position: Long) {
@@ -101,9 +100,7 @@ internal class MappedRandomAccessIO : DataInput, DataOutput, RandomAccessIO {
}
override fun readByte(): Byte {
return buf(position).get().also {
position += SIZE_BYTE
}
return buf(position).get().also { seekByte() }
}
override fun readUnsignedByte(): Int {
@@ -20,6 +20,7 @@ package com.itsaky.androidide.levelhash
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.levelhash.LevelHash.OverflowException
import com.itsaky.androidide.levelhash.util.DataExternalizers
import com.itsaky.androidide.levelhash.util.nullable
import com.itsaky.androidide.testing.common.LinuxOnlyTestRule
import org.junit.Rule
import org.junit.Test
@@ -38,20 +39,21 @@ class PersistentLevelHashTest {
@Rule
val linuxOnlyTestRule = LinuxOnlyTestRule()
private fun persistentStringHash(
fileName: String,
conf: PersistentHashBuilder<String, String>.() -> Unit = {}
): LevelHash<String, String> {
private inline fun <V : String?> persistentStringHash(fileName: String,
valueExternalizer: DataExternalizer<V>,
conf: PersistentHashBuilder<String, V>.() -> Unit = {}
): LevelHash<String, V> {
val indexFile = File("./build/sample-index/${fileName}.storage")
if (indexFile.parentFile!!.exists()) {
indexFile.parentFile!!.deleteRecursively()
}
indexFile.parentFile!!.mkdirs()
indexFile.createNewFile()
val hash = LevelHash.persistentHashBuilder<String, String>()
val hash = LevelHash.persistentHashBuilder<String, V>()
.keyExternalizer(DataExternalizers.STRING)
.valueExternalizer(DataExternalizers.STRING)
.valueExternalizer(valueExternalizer)
.indexFile(indexFile)
.apply(conf)
.levelSize(2)
@@ -63,6 +65,12 @@ class PersistentLevelHashTest {
return hash
}
private inline fun persistentStringHash(fileName: String,
conf: PersistentHashBuilder<String, String>.() -> Unit = {}
): LevelHash<String, String> {
return persistentStringHash(fileName, DataExternalizers.STRING, conf)
}
@Test
fun testInsertion() {
val hash = persistentStringHash("insert")
@@ -109,4 +117,40 @@ class PersistentLevelHashTest {
assertThat(hash.findSlot("key")).isNull()
}
@Test(expected = LevelHash.EntryNotFoundException::class)
fun `test value update for non-existing entry`() {
val hash = persistentStringHash("update-non-existing-entry")
assertThat(hash.insert("k", "v")).isTrue()
val slot = hash.findSlot("k")
assertThat(slot).isNotNull()
assertThat(slot!!.key).isEqualTo("k")
assertThat(slot.value).isEqualTo("v")
assertThat(hash.set("kk", "newV")).isNull()
}
@Test
fun `test value update for existing entry with null value`() {
val hash = persistentStringHash(
fileName = "update-existing-with-null-value",
valueExternalizer = DataExternalizers.STRING.nullable()
)
assertThat(hash.insert("k", null)).isTrue()
var slot = hash.findSlot("k")
assertThat(slot).isNotNull()
assertThat(slot!!.key).isEqualTo("k")
assertThat(slot.value).isNull()
assertThat(hash.set("k", "newV")).isNull()
slot = hash.findSlot("k")
assertThat(slot).isNotNull()
assertThat(slot!!.key).isEqualTo("k")
assertThat(slot.value).isEqualTo("newV")
}
}