Skip to content

Commit

Permalink
added load and store functions for persistence with tests
Browse files Browse the repository at this point in the history
  • Loading branch information
CharlieTap committed Nov 26, 2022
1 parent c0c8d0b commit abc165c
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 3 deletions.
4 changes: 3 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ plugins {
}

group = "com.tap.hlc"
version = "1.0.1"
version = "1.1.0"

kotlin {
targets {
Expand All @@ -15,6 +15,7 @@ kotlin {
sourceSets {
val commonMain by getting {
dependencies {
api(libs.okio.core)
api(libs.kotlinx.datetime)
api(libs.result)
api(libs.uuid)
Expand All @@ -24,6 +25,7 @@ kotlin {
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
implementation(libs.okio.fakefilesystem)
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ kotlinx-coroutines = "1.6.4"
kotlinx-datetime = "0.4.0"
kotlinx-serialization = "1.4.1"

okio="3.1.0"
result = "1.1.16"
uuid = "0.5.0"

Expand All @@ -24,6 +25,8 @@ kotlinter = { id = "org.jmailen.kotlinter", version.ref = "kotlinter" }

kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime"}
kotlinx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization"}
okio-core = { module = "com.squareup.okio:okio", version.ref = "okio"}
okio-fakefilesystem = { module = "com.squareup.okio:okio-fakefilesystem", version.ref = "okio"}
result = { module = "com.michael-bull.kotlin-result:kotlin-result", version.ref = "result" }
uuid = { module = "com.benasher44:uuid", version.ref = "uuid"}

Expand Down
48 changes: 46 additions & 2 deletions src/commonMain/kotlin/com/tap/hlc/HybridLogicalClock.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result
import com.github.michaelbull.result.flatMap
import com.github.michaelbull.result.getOr
import kotlinx.datetime.Clock
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.pow
import okio.FileSystem
import okio.Path

/**
* Implementation of a HLC [1][2]
Expand All @@ -26,7 +29,11 @@ data class HybridLogicalClock(

companion object {

// Call this every time a new event is generated on the node, set the local clock and the events timestamp equal to the result
private const val CLOCK_FILE = "clock.hlc"

/**
* This should be called every time a new event is generated locally, the result becomes the events timestamp and the new local time
*/
fun localTick(
local: HybridLogicalClock,
wallClockTime: Timestamp = Timestamp.now(Clock.System),
Expand All @@ -39,7 +46,9 @@ data class HybridLogicalClock(
}
}

// Call this on all events from external nodes to create a new local hlc which factors in the remote event
/**
* This should be called every time a new event is received from a remote node, the result becomes the new local time
*/
fun remoteTock(
local: HybridLogicalClock,
remote: HybridLogicalClock,
Expand Down Expand Up @@ -100,6 +109,41 @@ data class HybridLogicalClock(

return Ok(HybridLogicalClock(timestamp, node, counter))
}

/**
* Persists the clock to a disk file at the specified directory path
*
* This call is blocking
*
* Usage:
* val directory = "/Users/alice".toPath()
* HybridLogicalClock.store(hlc, path)
*/
fun store(hlc: HybridLogicalClock, directory: Path, fileSystem: FileSystem = FileSystem.SYSTEM, fileName : String = CLOCK_FILE) {
fileSystem.createDirectories(directory)
val filepath = directory / fileName
fileSystem.write(filepath) {
writeUtf8(hlc.toString())
}
}

/**
* Attempts to load a clock from a disk file at the specified directory path
*
* This call is blocking and will return null if no file is found
*
* Usage:
* val directory = "/Users/alice".toPath()
* val nullableClock = HybridLogicalClock.load(path)
*/
fun load(directory: Path, fileSystem: FileSystem = FileSystem.SYSTEM, fileName: String = CLOCK_FILE) : HybridLogicalClock? {
val filepath = directory / fileName
if(!fileSystem.exists(filepath)) {
return null
}
val encoded = fileSystem.read(filepath) { readUtf8() }
return decodeFromString(encoded).getOr(null)
}
}

override fun compareTo(other: HybridLogicalClock): Int {
Expand Down
85 changes: 85 additions & 0 deletions src/commonTest/kotlin/com/tap/hlc/HLCPersistTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.tap.hlc

import com.benasher44.uuid.uuid4
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import okio.Path.Companion.toPath
import okio.fakefilesystem.FakeFileSystem

class HLCPersistTest {

@Test
fun `can store the hlc into a file at a given path`() {

val fileSystem = FakeFileSystem()

val epochMillis = 943920000000L
val counter = 15
val node = uuid4()

val clock = HybridLogicalClock(Timestamp(epochMillis), NodeID.mint(node), counter)
val path = "/Users/alice".toPath()
val filename = "test.hlc"

HybridLogicalClock.store(clock, path, fileSystem, filename)

val expectedEncoded = "${epochMillis.toString().padStart(15, '0')}:${counter.toString(36).padStart(5, '0')}:${node.toString().replace("-", "").takeLast(16)}"
val result = fileSystem.read(path / filename) {
readUtf8()
}

assertEquals(expectedEncoded, result)
fileSystem.checkNoOpenFiles()
}

@Test
fun `can load a hlc from a given path`() {

val fileSystem = FakeFileSystem()
val path = "/Users/alice".toPath()
fileSystem.createDirectories(path)
val filename = "test.hlc"

val epochMillis = 943920000000L
val counter = 15
val node = uuid4().toString().replace("-", "").takeLast(16)

val encoded = "${epochMillis.toString().padStart(15, '0')}:${counter.toString(36).padStart(5, '0')}:$node"

fileSystem.write(path / filename) {
writeUtf8(encoded)
}

val result = HybridLogicalClock.load(path, fileSystem, filename)

assertNotNull(result)
assertEquals(result.timestamp.epochMillis, epochMillis)
assertEquals(result.counter, counter)
assertEquals(result.node.identifier, node)
fileSystem.checkNoOpenFiles()
}

@Test
fun `can store and load a hlc to and from a given path`() {

val fileSystem = FakeFileSystem()
val path = "/Users/alice".toPath()
fileSystem.createDirectories(path)
val filename = "test.hlc"

val epochMillis = 943920000000L
val counter = 15
val node = uuid4()

val clock = HybridLogicalClock(Timestamp(epochMillis), NodeID.mint(node), counter)
HybridLogicalClock.store(clock, path, fileSystem, filename)
val result = HybridLogicalClock.load(path, fileSystem, filename)

assertNotNull(result)
assertEquals(result.timestamp.epochMillis, epochMillis)
assertEquals(result.counter, counter)
assertEquals(result.node.identifier, NodeID.mint(node).identifier)
fileSystem.checkNoOpenFiles()
}
}

0 comments on commit abc165c

Please sign in to comment.