Skip to content

Commit

Permalink
missing files
Browse files Browse the repository at this point in the history
  • Loading branch information
sanity committed Mar 23, 2017
1 parent 3a80bc1 commit f43c6eb
Show file tree
Hide file tree
Showing 3 changed files with 240 additions and 0 deletions.
137 changes: 137 additions & 0 deletions src/main/kotlin/com/github/sanity/shoebox/stores/DirectoryStore.kt
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 src/main/kotlin/com/github/sanity/shoebox/stores/MemoryStore.kt
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
}
}
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
}
}
}
}
}

0 comments on commit f43c6eb

Please sign in to comment.