Skip to content

Commit

Permalink
Merge pull request #7 from spaceopen/master
Browse files Browse the repository at this point in the history
Preliminary LMDB backend support, tested with modified todo example.
  • Loading branch information
sanity authored Jul 21, 2019
2 parents 58f1091 + e6a5b84 commit 0c79782
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 9 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,13 @@ repositories {

dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
compile 'com.github.salomonbrys.kotson:kotson:2.5.0'
compile 'com.google.guava:guava:27.1-jre'
compile 'net.incongru.watchservice:barbary-watchservice:1.0'
compile 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.1'
implementation 'org.lmdbjava:lmdbjava:0.7.0'

testCompile 'io.kotlintest:kotlintest:2.0.7'
}
Expand Down
19 changes: 10 additions & 9 deletions src/main/kotlin/io/kweb/shoebox/Shoebox.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import java.util.concurrent.ConcurrentHashMap
import kotlin.reflect.KClass


/*
* TODO: 1) Add a lockfile mechanism to prevent multiple JVMs or threads from
* TODO: using the same directory
* TODO: 2) Handle changes that occur to the filesystem which aren't initiated here
* TODO: (then remove the previous lockfile mechanism)
*/
/*
* TODO: 1) Add a lockfile mechanism to prevent multiple JVMs or threads from
* TODO: using the same directory
* TODO: 2) Handle changes that occur to the filesystem which aren't initiated here
* TODO: (then remove the previous lockfile mechanism)
*/

/**
* Create a [Shoebox], use this in preference to the Shoebox constructor to avoid having to provide a `KClass`
Expand All @@ -28,7 +28,7 @@ import kotlin.reflect.KClass
inline fun <reified T : Any> Shoebox(store : Store<T>) = Shoebox(store, T::class)
inline fun <reified T : Any> Shoebox(dir : Path) = Shoebox(DirectoryStore(dir), T::class)
inline fun <reified T : Any> Shoebox() = Shoebox(MemoryStore(), T::class)

inline fun <reified T : Any> LmdbShoebox(name: String) = Shoebox(LmdbStore(name), T::class)

/**
* Can persistently store and retrieve objects, and notify listeners of changes to those objects
Expand Down Expand Up @@ -139,13 +139,13 @@ class Shoebox<T : Any>(val store: Store<T>, private val kc: KClass<T>) {
fun deleteRemoveListener(handle : Long) {
removeListeners.remove(handle)
}

fun onChange(listener: (T, KeyValue<T>, Source) -> Unit) : Long {
val handle = listenerHandleSource.incrementAndGet()
changeListeners.put(handle, listener)
return handle
}

fun onChange(key: String, listener: (T, T, Source) -> Unit) : Long {
val handle = listenerHandleSource.incrementAndGet()
keySpecificChangeListeners.computeIfAbsent(key, { ConcurrentHashMap() }).put(handle, listener)
Expand All @@ -170,6 +170,7 @@ class Shoebox<T : Any>(val store: Store<T>, private val kc: KClass<T>) {
is MemoryStore<T> -> MemoryStore<Reference>()
is DirectoryStore<T> ->
DirectoryStore<Reference>(store.directory.parent.resolve("${store.directory.fileName}-$name-view"))
is LmdbStore<T> -> LmdbStore<Reference>("${store.name}-$name-view")
else -> throw RuntimeException("Shoebox doesn't currently support creating a view for store type ${store::class.simpleName}")
}
return View<T>(Shoebox(store), this, verify, by)
Expand Down
150 changes: 150 additions & 0 deletions src/main/kotlin/io/kweb/shoebox/stores/LmdbStore.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package io.kweb.shoebox.stores

import com.fatboyindustrial.gsonjavatime.Converters
import com.google.gson.*
import com.google.gson.reflect.TypeToken
import io.kweb.shoebox.*
import java.nio.file.*
import java.time.*
import kotlin.reflect.KClass

import org.lmdbjava.*
import java.io.File
import java.nio.ByteBuffer
import java.nio.ByteBuffer.allocateDirect
import java.nio.charset.StandardCharsets.UTF_8
import kotlin.io.FileSystemException


/**
* TODO: remove dependence on gson
*/

inline fun <reified T : Any> LmdbStore(name: String) = LmdbStore(name, T::class)
/*
val defaultGson: Gson = Converters.registerAll(GsonBuilder()).let {
it.registerTypeAdapter(object : TypeToken<Duration>() {}.type, DurationConverter())
}.create()
*/
class LmdbStore<T : Any>(val name: String, private val kc: KClass<T>, val gson: Gson = defaultGson) : Store<T> {

companion object {
private val home: String = System.getProperty("user.dir")
var env: Env<ByteBuffer> = create("$home/data")

fun create(path: String): Env<ByteBuffer> {
println("LMDB database directory: $path")
val file = File(path)
if (!file.exists()) {
if (!file.mkdir()) {
throw FileSystemException(file, reason = "Failed to create LMDB database directory!")
}
} else {
if (!file.isDirectory) {
throw InvalidPathException("Not a directory", path)
}
}
return Env.create().setMapSize(10485760).setMaxDbs(100).open(file)
}

protected fun finalize() {
env.close()
}

}

private val dbi: Dbi<ByteBuffer> = env.openDbi(name, DbiFlags.MDB_CREATE)

/**
* Retrieve the entries in this store, similar to [Map.entries] but lazy
*
* @return The keys and their corresponding values in this [Shoebox]
*/
override val entries: Iterable<KeyValue<T>> get() {
val ret = mutableSetOf<KeyValue<T>>()
env.txnRead().use { txn ->
dbi.iterate(txn).use { c ->
c.forEach {
val k = UTF_8.decode(it.key()).toString()
val v = gson.fromJson(UTF_8.decode(it.`val`()).toString(), kc.javaObjectType)
ret.add(KeyValue(k, v))
}
}
txn.abort()
}
return ret
}

/**
* Retrieve a value, similar to [Map.get]
*
* @param key The key associated with the desired value
* @return The value associated with the key, or null if no value is associated
*/
override operator fun get(key: String): T? {
require(key.isNotBlank()) {"key(\"$key\") must not be blank"}
val k = allocateDirect(env.maxKeySize)
k.put(key.toByteArray(UTF_8)).flip()
var ret: T? = null
env.txnRead().use { txn ->
val v: ByteBuffer? = dbi.get(txn, k)
if (v != null) {
ret = gson.fromJson(UTF_8.decode(v).toString(), kc.javaObjectType)
}
txn.abort()
}
return ret
}

/**
* Remove a key-value pair
*
* @param key The key associated with the value to be removed, similar to [MutableMap.remove]
*/
override fun remove(key: String) : T? {
require(key.isNotBlank()) {"key(\"$key\") must not be blank"}
val k = allocateDirect(env.maxKeySize)
k.put(key.toByteArray(UTF_8)).flip()
var ret: T? = null
env.txnWrite().use { txn ->
// who needs the value?
val oldv: ByteBuffer? = dbi.get(txn, k)
if (oldv != null) {
ret = gson.fromJson(UTF_8.decode(oldv).toString(), kc.javaObjectType)
}
dbi.delete(txn, k)
txn.commit()
}
return ret
}

/**
* Set or change a value, simliar to [MutableMap.set]
*
* @param key The key associated with the value to be set or changed
* @param value The new value
*/
override operator fun set(key: String, value: T) : T? {
require(key.isNotBlank()) {"key(\"$key\") must not be blank"}
val k = allocateDirect(env.maxKeySize)
k.put(key.toByteArray(UTF_8)).flip()
val bytes = gson.toJson(value, kc.javaObjectType).toByteArray(UTF_8)
val v = allocateDirect(bytes.size)
v.put(bytes).flip()
var ret: T? = null
env.txnWrite().use { txn ->
// is the old value necessary?
val oldv: ByteBuffer? = dbi.get(txn, k)
if (oldv != null) {
ret = gson.fromJson(UTF_8.decode(oldv).toString(), kc.javaObjectType)
}
dbi.put(txn, k, v)
txn.commit()
}
return ret
}

protected fun finalize() {
dbi.close()
}
}

0 comments on commit 0c79782

Please sign in to comment.