-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
240 additions
and
0 deletions.
There are no files selected for viewing
137 changes: 137 additions & 0 deletions
137
src/main/kotlin/com/github/sanity/shoebox/stores/DirectoryStore.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <reified T : Any> DirectoryStore(directory : Path) = DirectoryStore(directory, T::class) | ||
|
||
class DirectoryStore<T : Any>(private val directory : Path, private val kc : KClass<T>) : Store<T> { | ||
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<String, T?> = CacheBuilder.newBuilder().build<String, T?>( | ||
object : CacheLoader<String, T?>() { | ||
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<KeyValue<T>> 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) | ||
} | ||
} |
29 changes: 29 additions & 0 deletions
29
src/main/kotlin/com/github/sanity/shoebox/stores/MemoryStore.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<T : Any> : Store<T> { | ||
private val map = ConcurrentHashMap<String, T>() | ||
|
||
override val entries: Iterable<KeyValue<T>> | ||
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 | ||
} | ||
} |
74 changes: 74 additions & 0 deletions
74
src/test/kotlin/com/github/sanity/shoebox/stores/DirectoryStoreSpec.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<RuntimeException> { | ||
DirectoryStore<ShoeboxSpec.TestData>(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<ShoeboxSpec.TestData>(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<ShoeboxSpec.TestData>(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<ShoeboxSpec.TestData>(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<ShoeboxSpec.TestData>(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 | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|