From f43c6eb210e7fc0ac715a2f0870d4a964273e739 Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Wed, 22 Mar 2017 20:29:39 -0500 Subject: [PATCH] missing files --- .../sanity/shoebox/stores/DirectoryStore.kt | 137 ++++++++++++++++++ .../sanity/shoebox/stores/MemoryStore.kt | 29 ++++ .../shoebox/stores/DirectoryStoreSpec.kt | 74 ++++++++++ 3 files changed, 240 insertions(+) create mode 100644 src/main/kotlin/com/github/sanity/shoebox/stores/DirectoryStore.kt create mode 100644 src/main/kotlin/com/github/sanity/shoebox/stores/MemoryStore.kt create mode 100644 src/test/kotlin/com/github/sanity/shoebox/stores/DirectoryStoreSpec.kt diff --git a/src/main/kotlin/com/github/sanity/shoebox/stores/DirectoryStore.kt b/src/main/kotlin/com/github/sanity/shoebox/stores/DirectoryStore.kt new file mode 100644 index 0000000..b5538e9 --- /dev/null +++ b/src/main/kotlin/com/github/sanity/shoebox/stores/DirectoryStore.kt @@ -0,0 +1,137 @@ +package com.github.sanity.shoebox.stores + +import com.github.sanity.shoebox.* +import com.google.common.cache.CacheBuilder +import com.google.common.cache.CacheLoader +import com.google.common.cache.LoadingCache +import com.google.gson.GsonBuilder +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.attribute.FileTime +import java.util.concurrent.TimeUnit +import kotlin.reflect.KClass + +/** + * Created by ian on 3/22/17. + */ + +inline fun DirectoryStore(directory : Path) = DirectoryStore(directory, T::class) + +class DirectoryStore(private val directory : Path, private val kc : KClass) : Store { + companion object { + const private val LOCK_FILENAME = "shoebox.lock" + const private val LOCK_TOUCH_TIME_MS = 2000.toLong() + const private val LOCK_STALE_TIME = LOCK_TOUCH_TIME_MS * 2 + private val gson = GsonBuilder().create() + } + + internal val cache: LoadingCache = CacheBuilder.newBuilder().build( + object : CacheLoader() { + override fun load(key: String): T? { + return this@DirectoryStore.load(key) + } + } + ) + + private val lockFilePath = directory.resolve(LOCK_FILENAME) + + init { + Files.createDirectories(directory) + if (Files.exists(lockFilePath)) { + if (System.currentTimeMillis() - Files.getLastModifiedTime(lockFilePath).toMillis() < LOCK_STALE_TIME) { + throw RuntimeException("$directory locked by $lockFilePath") + } else { + Files.setLastModifiedTime(lockFilePath, FileTime.fromMillis(System.currentTimeMillis())) + } + } else { + Files.newBufferedWriter(lockFilePath).use { + it.appendln("locked") + } + } + scheduledExecutor.scheduleWithFixedDelay({ + Files.setLastModifiedTime(lockFilePath, FileTime.fromMillis(System.currentTimeMillis())) + }, LOCK_TOUCH_TIME_MS, LOCK_TOUCH_TIME_MS, TimeUnit.MILLISECONDS) + } + + /** + * 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> get() = Files.newDirectoryStream(directory) + .mapNotNull {it.fileName.toString()} + .filter {it != LOCK_FILENAME} + .map { + KeyValue(it, this[it]!!) + } + + /** + * 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? { + return load(key) + } + + /** + * 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? { + val cachedValue: T? = cache.getIfPresent(key) + if (cachedValue != null) { + cache.invalidate(key) + } + val filePath = directory.resolve(key) + if (Files.exists(filePath)) { + val oldValue = cachedValue ?: load(key) + if (oldValue != null) { + Files.delete(filePath) + return oldValue + } else { + return null + } + } else { + return null + } + } + + /** + * 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? { + val previousValue = get(key) + cache.put(key, value) + if (value != previousValue) { + if (!directory.exists()) throw RuntimeException("Parent directory doesn't exist") + val filePath = directory.resolve(key) + filePath.newBufferedWriter().use { + gson.toJson(value, kc.javaObjectType, it) + } + } + return previousValue + } + + private fun load(key: String): T? { + val filePath = directory.resolve(key) + if (Files.exists(filePath)) { + val o = filePath.newBufferedReader().use { + gson.fromJson(it, kc.javaObjectType) + } + cache.put(key, o) + return o + } else { + return null + } + } + + protected fun finalize() { + Files.delete(lockFilePath) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/sanity/shoebox/stores/MemoryStore.kt b/src/main/kotlin/com/github/sanity/shoebox/stores/MemoryStore.kt new file mode 100644 index 0000000..4d92bb7 --- /dev/null +++ b/src/main/kotlin/com/github/sanity/shoebox/stores/MemoryStore.kt @@ -0,0 +1,29 @@ +package com.github.sanity.shoebox.stores + +import com.github.sanity.shoebox.KeyValue +import com.github.sanity.shoebox.Store +import java.util.concurrent.ConcurrentHashMap + +/** + * Created by ian on 3/22/17. + */ +class MemoryStore : Store { + private val map = ConcurrentHashMap() + + override val entries: Iterable> + get() = map.entries.map {KeyValue(it.key, it.value)} + + override fun remove(key: String): T? { + return map.remove(key) + } + + override fun get(key: String): T? { + return map.get(key) + } + + override fun set(key: String, value: T): T? { + val previousVal = map.get(key) + map.set(key, value) + return previousVal + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/sanity/shoebox/stores/DirectoryStoreSpec.kt b/src/test/kotlin/com/github/sanity/shoebox/stores/DirectoryStoreSpec.kt new file mode 100644 index 0000000..84cda7e --- /dev/null +++ b/src/test/kotlin/com/github/sanity/shoebox/stores/DirectoryStoreSpec.kt @@ -0,0 +1,74 @@ +package com.github.sanity.shoebox.stores + +import com.github.sanity.shoebox.ShoeboxSpec +import io.kotlintest.matchers.be +import io.kotlintest.specs.FreeSpec +import java.nio.file.Files +import java.nio.file.attribute.FileTime + +/** + * Created by ian on 3/22/17. + */ +class DirectoryStoreSpec : FreeSpec() { + init { + "DirectoryStore" - { + "locking" - { + val dir = Files.createTempDirectory("ss-") + "should create a lockfile" { + Files.exists(dir.resolve("shoebox.lock")) shouldBe true + } + "should throw an exception if attempting to create a store for a directory that already has a store" { + shouldThrow { + DirectoryStore(dir) + } + } + "should disregard an old lock" { + val dir = Files.createTempDirectory("ss-") + val lockFilePath = dir.resolve("shoebox.lock") + Files.newBufferedWriter(lockFilePath).use { + it.appendln("lock") + } + Files.setLastModifiedTime(lockFilePath, FileTime.fromMillis(System.currentTimeMillis() - 60000)) + DirectoryStore(dir, ShoeboxSpec.TestData::class) + } + + "should update an old lock" { + val dir = Files.createTempDirectory("ss-") + val lockFilePath = dir.resolve("shoebox.lock") + Files.newBufferedWriter(lockFilePath).use { + it.appendln("lock") + } + Files.setLastModifiedTime(lockFilePath, FileTime.fromMillis(System.currentTimeMillis() - 60000)) + DirectoryStore(dir, ShoeboxSpec.TestData::class) + Files.getLastModifiedTime(lockFilePath).toMillis() should be gt (System.currentTimeMillis() - 5000) + } + } + val object1 = ShoeboxSpec.TestData(1, 2) + val object2 = ShoeboxSpec.TestData(3, 4) + "when an item is stored" - { + val object1 = ShoeboxSpec.TestData(1, 2) + val dir = Files.createTempDirectory("ss-") + val pm = DirectoryStore(dir) + pm["key1"] = object1 + "should cache the item that was stored" { + pm.cache.get("key1") shouldEqual object1 + } + } + "when an item is replaced" - { + val dir = Files.createTempDirectory("ss-") + val pm = DirectoryStore(dir) + pm["key1"] = object1 + pm["key1"] = object2 + + "should have cached the replaced data" { + pm.cache.get("key1") shouldEqual object2 + } + "should retrieve the replaced data without the cache" { + pm.cache.invalidate("key1") + pm["key1"] shouldEqual object2 + } + } + } + } +} +