From 7f475ec1bd9d9bffadb0a625060094796be1d273 Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Sat, 18 Mar 2017 17:16:01 -0500 Subject: [PATCH] add lockfile mechanism --- build.gradle | 2 +- .../com/github/sanity/shoebox/FileWatcher.kt | 45 ------------------- .../kotlin/com/github/sanity/shoebox/Store.kt | 36 +++++++++++++-- .../kotlin/com/github/sanity/shoebox/utils.kt | 3 +- .../github/sanity/shoebox/FileWatcherSpec.kt | 34 -------------- .../com/github/sanity/shoebox/StoreSpec.kt | 35 +++++++++++++++ 6 files changed, 71 insertions(+), 84 deletions(-) delete mode 100644 src/main/kotlin/com/github/sanity/shoebox/FileWatcher.kt delete mode 100644 src/test/kotlin/com/github/sanity/shoebox/FileWatcherSpec.kt diff --git a/build.gradle b/build.gradle index 52cd40d..cb6b3db 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ group 'com.github.sanity' -version '0.1.4' +version '0.1.5' buildscript { ext.kotlin_version = '1.1.0' diff --git a/src/main/kotlin/com/github/sanity/shoebox/FileWatcher.kt b/src/main/kotlin/com/github/sanity/shoebox/FileWatcher.kt deleted file mode 100644 index 6221bfe..0000000 --- a/src/main/kotlin/com/github/sanity/shoebox/FileWatcher.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.github.sanity.shoebox - -import java.nio.file.Path -import java.nio.file.StandardWatchEventKinds -import java.nio.file.WatchEvent -import java.util.concurrent.ConcurrentHashMap -import kotlin.concurrent.thread - -/** - * Created by ian on 3/12/17. - */ - -class FileWatcher(path : Path, listener : (WatchEvent.Kind, Path) -> Unit) { - private val listeners = ConcurrentHashMap, Path) -> Unit>>() - - - init { - TODO("Doesn't work, possibly because Java NIO doesn't work on OSX - possible solution: https://github.com/gjoseph/BarbaryWatchService") - val service = path.fileSystem.newWatchService() - println("Init service") - thread { - service.use { watchService -> - while (true) { - println("Waiting for key") - val key = watchService.take() - println("Found key") - println("Polled") - if (key != null) { - for (rawWatchEvent in key.pollEvents()) { - println("Watch event") - val watchEvent = rawWatchEvent as WatchEvent - val kind = rawWatchEvent.kind() - if (kind == StandardWatchEventKinds.OVERFLOW) continue - val eventPath = watchEvent.context() - listeners[eventPath]?.forEach { it(kind, eventPath) } - } - if (key.reset()) break - } - } - } - } - } - - -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/sanity/shoebox/Store.kt b/src/main/kotlin/com/github/sanity/shoebox/Store.kt index b634e04..752d0a9 100644 --- a/src/main/kotlin/com/github/sanity/shoebox/Store.kt +++ b/src/main/kotlin/com/github/sanity/shoebox/Store.kt @@ -6,7 +6,9 @@ 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.ConcurrentHashMap +import java.util.concurrent.TimeUnit import kotlin.reflect.KClass @@ -36,8 +38,30 @@ inline fun Store(directory : Path) = Store(directory, T::class */ class Store(val directory: Path, private val kc: KClass) { + 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 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) } internal val cache: LoadingCache = CacheBuilder.newBuilder().build( @@ -61,9 +85,10 @@ class Store(val directory: Path, private val kc: KClass) { * @return The keys and their corresponding values in this [Store] */ val entries: Iterable> get() = Files.newDirectoryStream(directory) - .mapNotNull { - val fileKey = it.fileName.toString() - KeyValue(fileKey, this[fileKey]!!) + .mapNotNull {it.fileName.toString()} + .filter {it != LOCK_FILENAME} + .map { + KeyValue(it, this[it]!!) } /** @@ -182,6 +207,11 @@ class Store(val directory: Path, private val kc: KClass) { } } } + + protected fun finalize() { + Files.delete(lockFilePath) + } + } /** diff --git a/src/main/kotlin/com/github/sanity/shoebox/utils.kt b/src/main/kotlin/com/github/sanity/shoebox/utils.kt index 482c0f0..2ad7ef9 100644 --- a/src/main/kotlin/com/github/sanity/shoebox/utils.kt +++ b/src/main/kotlin/com/github/sanity/shoebox/utils.kt @@ -4,13 +4,14 @@ import java.nio.file.Files import java.nio.file.OpenOption import java.nio.file.Path import java.util.* +import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicLong /** * Created by ian on 3/9/17. */ - +val scheduledExecutor = Executors.newScheduledThreadPool(1) fun Path.newBufferedReader() = Files.newBufferedReader(this) diff --git a/src/test/kotlin/com/github/sanity/shoebox/FileWatcherSpec.kt b/src/test/kotlin/com/github/sanity/shoebox/FileWatcherSpec.kt deleted file mode 100644 index d5d2b10..0000000 --- a/src/test/kotlin/com/github/sanity/shoebox/FileWatcherSpec.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.github.sanity.shoebox - -import io.kotlintest.specs.FreeSpec -import java.nio.file.Files -import java.nio.file.StandardWatchEventKinds - -/** - * Created by ian on 3/17/17. - */ -class FileWatcherSpec : FreeSpec() { - init { - "File watcher should detect a file creation" { - val dir = Files.createTempDirectory("ss-") - val one = dir.resolve("one") - one.mkdirIfAbsent() - println("Directory: $one") - var callCount = 0 - FileWatcher(one, { kind, path -> - callCount++ - kind shouldEqual StandardWatchEventKinds.ENTRY_CREATE - path.last().shouldEqual("dog") - }) - - Thread.sleep(1000) - println("Writing file") - Files.newOutputStream(one.resolve("dog")).use { - it.write(1) - } - println("File written") - Thread.sleep(1000) - callCount shouldEqual 1 - } - } -} diff --git a/src/test/kotlin/com/github/sanity/shoebox/StoreSpec.kt b/src/test/kotlin/com/github/sanity/shoebox/StoreSpec.kt index 5f78fc9..4b1f009 100644 --- a/src/test/kotlin/com/github/sanity/shoebox/StoreSpec.kt +++ b/src/test/kotlin/com/github/sanity/shoebox/StoreSpec.kt @@ -1,7 +1,9 @@ package com.github.sanity.shoebox +import io.kotlintest.matchers.be import io.kotlintest.specs.FreeSpec import java.nio.file.Files +import java.nio.file.attribute.FileTime import java.util.concurrent.atomic.AtomicInteger /** @@ -12,6 +14,39 @@ class StoreSpec : FreeSpec() { init { "A Shoebox store" - { + "locking mechanism" - { + val dir = Files.createTempDirectory("ss-") + val pm = Store(dir) + "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 { + Store(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)) + Store(dir) + } + + "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)) + Store(dir) + Files.getLastModifiedTime(lockFilePath).toMillis() should be gt(System.currentTimeMillis()-5000) + } + } val object1 = TestData(1, 2) val object2 = TestData(3, 4) "when an item is stored" - {