Instant
This commit is contained in:
parent
a0f1908a1a
commit
a9a06a41f9
@ -1,12 +1,14 @@
|
|||||||
package no.iktdev.eventi
|
package no.iktdev.eventi
|
||||||
|
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
import java.time.LocalDateTime
|
import java.time.Instant
|
||||||
|
|
||||||
object MyTime {
|
object MyTime {
|
||||||
private val clock: Clock = Clock.systemUTC()
|
private val clock: Clock = Clock.systemUTC()
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun UtcNow(): LocalDateTime =
|
fun utcNow(): Instant =
|
||||||
LocalDateTime.now(clock)
|
Instant.now(clock)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package no.iktdev.eventi
|
package no.iktdev.eventi
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.JsonDeserializationContext
|
import com.google.gson.JsonDeserializationContext
|
||||||
import com.google.gson.JsonDeserializer
|
import com.google.gson.JsonDeserializer
|
||||||
@ -15,13 +16,14 @@ import no.iktdev.eventi.models.store.PersistedTask
|
|||||||
import no.iktdev.eventi.models.store.TaskStatus
|
import no.iktdev.eventi.models.store.TaskStatus
|
||||||
import no.iktdev.eventi.tasks.TaskTypeRegistry
|
import no.iktdev.eventi.tasks.TaskTypeRegistry
|
||||||
import java.lang.reflect.Type
|
import java.lang.reflect.Type
|
||||||
|
import java.time.Instant
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
object ZDS {
|
object ZDS {
|
||||||
val gson = WGson.gson
|
val gson = WGson.gson
|
||||||
|
|
||||||
fun Event.toPersisted(id: Long, persistedAt: LocalDateTime = MyTime.UtcNow()): PersistedEvent? {
|
fun Event.toPersisted(id: Long, persistedAt: Instant = MyTime.utcNow()): PersistedEvent? {
|
||||||
val payloadJson = gson.toJson(this)
|
val payloadJson = gson.toJson(this)
|
||||||
val eventName = this::class.simpleName ?: run {
|
val eventName = this::class.simpleName ?: run {
|
||||||
throw IllegalStateException("Missing class name for event: $this")
|
throw IllegalStateException("Missing class name for event: $this")
|
||||||
@ -47,7 +49,7 @@ object ZDS {
|
|||||||
return gson.fromJson(data, clazz)
|
return gson.fromJson(data, clazz)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Task.toPersisted(id: Long, status: TaskStatus = TaskStatus.Pending, persistedAt: LocalDateTime = MyTime.UtcNow()): PersistedTask? {
|
fun Task.toPersisted(id: Long, status: TaskStatus = TaskStatus.Pending, persistedAt: Instant = MyTime.utcNow()): PersistedTask? {
|
||||||
val payloadJson = gson.toJson(this)
|
val payloadJson = gson.toJson(this)
|
||||||
val taskName = this::class.simpleName ?: run {
|
val taskName = this::class.simpleName ?: run {
|
||||||
throw IllegalStateException("Missing class name for task: $this")
|
throw IllegalStateException("Missing class name for task: $this")
|
||||||
@ -80,26 +82,47 @@ object ZDS {
|
|||||||
|
|
||||||
object WGson {
|
object WGson {
|
||||||
val gson = GsonBuilder()
|
val gson = GsonBuilder()
|
||||||
|
.registerTypeAdapter(Instant::class.java, InstantAdapter())
|
||||||
|
// hvis du fortsatt har LocalDateTime et sted:
|
||||||
.registerTypeAdapter(LocalDateTime::class.java, LocalDateTimeAdapter())
|
.registerTypeAdapter(LocalDateTime::class.java, LocalDateTimeAdapter())
|
||||||
.create()
|
.create()
|
||||||
fun toJson(data: Any?): String {
|
|
||||||
return gson.toJson(data)
|
fun toJson(data: Any?): String =
|
||||||
|
gson.toJson(data)
|
||||||
|
|
||||||
|
class InstantAdapter : JsonSerializer<Instant>, JsonDeserializer<Instant> {
|
||||||
|
override fun serialize(
|
||||||
|
src: Instant,
|
||||||
|
typeOfSrc: Type,
|
||||||
|
context: JsonSerializationContext
|
||||||
|
): JsonElement =
|
||||||
|
JsonPrimitive(src.toString()) // ISO-8601, UTC
|
||||||
|
|
||||||
|
override fun deserialize(
|
||||||
|
json: JsonElement,
|
||||||
|
typeOfT: Type,
|
||||||
|
context: JsonDeserializationContext
|
||||||
|
): Instant =
|
||||||
|
Instant.parse(json.asString)
|
||||||
}
|
}
|
||||||
|
|
||||||
class LocalDateTimeAdapter : JsonSerializer<LocalDateTime>, JsonDeserializer<LocalDateTime> {
|
class LocalDateTimeAdapter : JsonSerializer<LocalDateTime>, JsonDeserializer<LocalDateTime> {
|
||||||
private val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME
|
private val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME
|
||||||
|
|
||||||
override fun serialize(
|
override fun serialize(
|
||||||
src: LocalDateTime, typeOfSrc: Type, context: JsonSerializationContext
|
src: LocalDateTime,
|
||||||
): JsonElement {
|
typeOfSrc: Type,
|
||||||
return JsonPrimitive(src.format(formatter))
|
context: JsonSerializationContext
|
||||||
}
|
): JsonElement =
|
||||||
|
JsonPrimitive(src.format(formatter))
|
||||||
|
|
||||||
override fun deserialize(
|
override fun deserialize(
|
||||||
json: JsonElement, typeOfT: Type, context: JsonDeserializationContext
|
json: JsonElement,
|
||||||
): LocalDateTime {
|
typeOfT: Type,
|
||||||
return LocalDateTime.parse(json.asString, formatter)
|
context: JsonDeserializationContext
|
||||||
}
|
): LocalDateTime =
|
||||||
|
LocalDateTime.parse(json.asString, formatter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -6,7 +6,7 @@ import no.iktdev.eventi.MyTime
|
|||||||
import no.iktdev.eventi.ZDS.toEvent
|
import no.iktdev.eventi.ZDS.toEvent
|
||||||
import no.iktdev.eventi.stores.EventStore
|
import no.iktdev.eventi.stores.EventStore
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.LocalDateTime
|
import java.time.Instant
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.collections.iterator
|
import kotlin.collections.iterator
|
||||||
|
|
||||||
@ -16,10 +16,10 @@ abstract class EventPollerImplementation(
|
|||||||
private val dispatcher: EventDispatcher
|
private val dispatcher: EventDispatcher
|
||||||
) {
|
) {
|
||||||
// Erstatter ikke lastSeenTime, men supplerer den
|
// Erstatter ikke lastSeenTime, men supplerer den
|
||||||
protected val refWatermark = mutableMapOf<UUID, LocalDateTime>()
|
protected val refWatermark = mutableMapOf<UUID, Instant>()
|
||||||
|
|
||||||
// lastSeenTime brukes kun som scan hint
|
// lastSeenTime brukes kun som scan hint
|
||||||
var lastSeenTime: LocalDateTime = LocalDateTime.of(1970, 1, 1, 0, 0)
|
var lastSeenTime: Instant = Instant.EPOCH
|
||||||
|
|
||||||
open var backoff = Duration.ofSeconds(2)
|
open var backoff = Duration.ofSeconds(2)
|
||||||
protected set
|
protected set
|
||||||
@ -40,7 +40,7 @@ abstract class EventPollerImplementation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun pollOnce() {
|
suspend fun pollOnce() {
|
||||||
val pollStartedAt = MyTime.UtcNow()
|
val pollStartedAt = MyTime.utcNow()
|
||||||
log.debug { "🔍 Polling for new events" }
|
log.debug { "🔍 Polling for new events" }
|
||||||
|
|
||||||
// Global scan hint: kombiner refWatermark og lastSeenTime
|
// Global scan hint: kombiner refWatermark og lastSeenTime
|
||||||
@ -70,7 +70,7 @@ abstract class EventPollerImplementation(
|
|||||||
val maxPersistedThisRound = newPersisted.maxOf { it.persistedAt }
|
val maxPersistedThisRound = newPersisted.maxOf { it.persistedAt }
|
||||||
|
|
||||||
for ((ref, eventsForRef) in grouped) {
|
for ((ref, eventsForRef) in grouped) {
|
||||||
val refSeen = refWatermark[ref] ?: LocalDateTime.of(1970, 1, 1, 0, 0)
|
val refSeen = refWatermark[ref] ?: Instant.EPOCH
|
||||||
|
|
||||||
// Finn kun nye events for denne ref’en
|
// Finn kun nye events for denne ref’en
|
||||||
val newForRef = eventsForRef.filter { it.persistedAt > refSeen }
|
val newForRef = eventsForRef.filter { it.persistedAt > refSeen }
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
package no.iktdev.eventi.models
|
package no.iktdev.eventi.models
|
||||||
|
|
||||||
import no.iktdev.eventi.MyTime
|
import no.iktdev.eventi.MyTime
|
||||||
import java.time.LocalDateTime
|
import java.time.Instant
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class Metadata {
|
class Metadata {
|
||||||
val created: LocalDateTime = MyTime.UtcNow()
|
val created: Instant = MyTime.utcNow()
|
||||||
var derivedFromId: Set<UUID>? = null
|
var derivedFromId: Set<UUID>? = null
|
||||||
private set
|
private set
|
||||||
fun derivedFromEventId(vararg id: UUID) = apply {
|
fun derivedFromEventId(vararg id: UUID) = apply {
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
package no.iktdev.eventi.models
|
package no.iktdev.eventi.models
|
||||||
|
|
||||||
import java.time.LocalDateTime
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package no.iktdev.eventi.models.store
|
package no.iktdev.eventi.models.store
|
||||||
|
|
||||||
import java.time.LocalDateTime
|
import java.time.Instant
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
data class PersistedEvent(
|
data class PersistedEvent(
|
||||||
@ -9,5 +9,5 @@ data class PersistedEvent(
|
|||||||
val eventId: UUID,
|
val eventId: UUID,
|
||||||
val event: String,
|
val event: String,
|
||||||
val data: String,
|
val data: String,
|
||||||
val persistedAt: LocalDateTime
|
val persistedAt: Instant
|
||||||
)
|
)
|
||||||
@ -1,6 +1,6 @@
|
|||||||
package no.iktdev.eventi.models.store
|
package no.iktdev.eventi.models.store
|
||||||
|
|
||||||
import java.time.LocalDateTime
|
import java.time.Instant
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
data class PersistedTask(
|
data class PersistedTask(
|
||||||
@ -13,8 +13,8 @@ data class PersistedTask(
|
|||||||
val claimed: Boolean,
|
val claimed: Boolean,
|
||||||
val claimedBy: String? = null,
|
val claimedBy: String? = null,
|
||||||
val consumed: Boolean,
|
val consumed: Boolean,
|
||||||
val lastCheckIn: LocalDateTime? = null,
|
val lastCheckIn: Instant? = null,
|
||||||
val persistedAt: LocalDateTime
|
val persistedAt: Instant
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
enum class TaskStatus {
|
enum class TaskStatus {
|
||||||
|
|||||||
@ -2,11 +2,11 @@ package no.iktdev.eventi.stores
|
|||||||
|
|
||||||
import no.iktdev.eventi.models.Event
|
import no.iktdev.eventi.models.Event
|
||||||
import no.iktdev.eventi.models.store.PersistedEvent
|
import no.iktdev.eventi.models.store.PersistedEvent
|
||||||
import java.time.LocalDateTime
|
import java.time.Instant
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
interface EventStore {
|
interface EventStore {
|
||||||
fun getPersistedEventsAfter(timestamp: LocalDateTime): List<PersistedEvent>
|
fun getPersistedEventsAfter(timestamp: Instant): List<PersistedEvent>
|
||||||
fun getPersistedEventsFor(referenceId: UUID): List<PersistedEvent>
|
fun getPersistedEventsFor(referenceId: UUID): List<PersistedEvent>
|
||||||
fun persist(event: Event)
|
fun persist(event: Event)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,6 @@ import org.junit.jupiter.api.Assertions.assertTrue
|
|||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.DisplayName
|
import org.junit.jupiter.api.DisplayName
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import java.time.LocalDateTime
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class EventDispatcherTest: TestBase() {
|
class EventDispatcherTest: TestBase() {
|
||||||
@ -71,7 +70,7 @@ class EventDispatcherTest: TestBase() {
|
|||||||
val listener = ProducingListener()
|
val listener = ProducingListener()
|
||||||
|
|
||||||
val trigger = TriggerEvent()
|
val trigger = TriggerEvent()
|
||||||
val derived = DerivedEvent().derivedOf(trigger).toPersisted(1L, MyTime.UtcNow())
|
val derived = DerivedEvent().derivedOf(trigger).toPersisted(1L, MyTime.utcNow())
|
||||||
|
|
||||||
eventStore.persist(derived!!.toEvent()!!) // simulate prior production
|
eventStore.persist(derived!!.toEvent()!!) // simulate prior production
|
||||||
|
|
||||||
|
|||||||
@ -4,25 +4,25 @@ import no.iktdev.eventi.ZDS.toPersisted
|
|||||||
import no.iktdev.eventi.models.Event
|
import no.iktdev.eventi.models.Event
|
||||||
import no.iktdev.eventi.models.store.PersistedEvent
|
import no.iktdev.eventi.models.store.PersistedEvent
|
||||||
import no.iktdev.eventi.stores.EventStore
|
import no.iktdev.eventi.stores.EventStore
|
||||||
import java.time.LocalDateTime
|
import java.time.Instant
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class InMemoryEventStore : EventStore {
|
class InMemoryEventStore : EventStore {
|
||||||
private val persisted = mutableListOf<PersistedEvent>()
|
private val persisted = mutableListOf<PersistedEvent>()
|
||||||
private var nextId = 1L
|
private var nextId = 1L
|
||||||
|
|
||||||
override fun getPersistedEventsAfter(timestamp: LocalDateTime): List<PersistedEvent> =
|
override fun getPersistedEventsAfter(timestamp: Instant): List<PersistedEvent> =
|
||||||
persisted.filter { it.persistedAt > timestamp }
|
persisted.filter { it.persistedAt > timestamp }
|
||||||
|
|
||||||
override fun getPersistedEventsFor(referenceId: UUID): List<PersistedEvent> =
|
override fun getPersistedEventsFor(referenceId: UUID): List<PersistedEvent> =
|
||||||
persisted.filter { it.referenceId == referenceId }
|
persisted.filter { it.referenceId == referenceId }
|
||||||
|
|
||||||
override fun persist(event: Event) {
|
override fun persist(event: Event) {
|
||||||
val persistedEvent = event.toPersisted(nextId++, MyTime.UtcNow())
|
val persistedEvent = event.toPersisted(nextId++, MyTime.utcNow())
|
||||||
persisted += persistedEvent!!
|
persisted += persistedEvent!!
|
||||||
}
|
}
|
||||||
|
|
||||||
fun persistAt(event: Event, persistedAt: LocalDateTime) {
|
fun persistAt(event: Event, persistedAt: Instant) {
|
||||||
val persistedEvent = event.toPersisted(nextId++, persistedAt)
|
val persistedEvent = event.toPersisted(nextId++, persistedAt)
|
||||||
persisted += persistedEvent!!
|
persisted += persistedEvent!!
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import no.iktdev.eventi.models.store.PersistedTask
|
|||||||
import no.iktdev.eventi.models.store.TaskStatus
|
import no.iktdev.eventi.models.store.TaskStatus
|
||||||
import no.iktdev.eventi.stores.TaskStore
|
import no.iktdev.eventi.stores.TaskStore
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.LocalDateTime
|
import java.time.temporal.ChronoUnit
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.concurrent.atomics.AtomicReference
|
import kotlin.concurrent.atomics.AtomicReference
|
||||||
|
|
||||||
@ -30,13 +30,13 @@ open class InMemoryTaskStore : TaskStore {
|
|||||||
override fun claim(taskId: UUID, workerId: String): Boolean {
|
override fun claim(taskId: UUID, workerId: String): Boolean {
|
||||||
val task = findByTaskId(taskId) ?: return false
|
val task = findByTaskId(taskId) ?: return false
|
||||||
if (task.claimed && !isExpired(task)) return false
|
if (task.claimed && !isExpired(task)) return false
|
||||||
update(task.copy(claimed = true, claimedBy = workerId, lastCheckIn = MyTime.UtcNow()))
|
update(task.copy(claimed = true, claimedBy = workerId, lastCheckIn = MyTime.utcNow()))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun heartbeat(taskId: UUID) {
|
override fun heartbeat(taskId: UUID) {
|
||||||
val task = findByTaskId(taskId) ?: return
|
val task = findByTaskId(taskId) ?: return
|
||||||
update(task.copy(lastCheckIn = MyTime.UtcNow()))
|
update(task.copy(lastCheckIn = MyTime.utcNow()))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun markConsumed(taskId: UUID, status: TaskStatus) {
|
override fun markConsumed(taskId: UUID, status: TaskStatus) {
|
||||||
@ -45,7 +45,7 @@ open class InMemoryTaskStore : TaskStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun releaseExpiredTasks(timeout: Duration) {
|
override fun releaseExpiredTasks(timeout: Duration) {
|
||||||
val now = MyTime.UtcNow()
|
val now = MyTime.utcNow()
|
||||||
tasks.filter {
|
tasks.filter {
|
||||||
it.claimed && !it.consumed && it.lastCheckIn?.isBefore(now.minus(timeout)) == true
|
it.claimed && !it.consumed && it.lastCheckIn?.isBefore(now.minus(timeout)) == true
|
||||||
}.forEach {
|
}.forEach {
|
||||||
@ -60,8 +60,8 @@ open class InMemoryTaskStore : TaskStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun isExpired(task: PersistedTask): Boolean {
|
private fun isExpired(task: PersistedTask): Boolean {
|
||||||
val now = MyTime.UtcNow()
|
val now = MyTime.utcNow()
|
||||||
return task.lastCheckIn?.isBefore(now.minusMinutes(15)) == true
|
return task.lastCheckIn?.isBefore(now.minus(15, ChronoUnit.MINUTES)) == true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun serialize(data: Any?): String = data?.toString() ?: "{}"
|
private fun serialize(data: Any?): String = data?.toString() ?: "{}"
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import org.junit.jupiter.api.Assertions.assertNull
|
|||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.DisplayName
|
import org.junit.jupiter.api.DisplayName
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import java.time.LocalDateTime
|
|
||||||
|
|
||||||
class ZDSTest {
|
class ZDSTest {
|
||||||
|
|
||||||
|
|||||||
@ -22,7 +22,6 @@ import org.junit.jupiter.api.Assertions.assertTrue
|
|||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.LocalDateTime
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
@ -138,7 +137,7 @@ class EventPollerImplementationTest : TestBase() {
|
|||||||
|
|
||||||
val testPoller = object : EventPollerImplementation(eventStore, queue, dispatcher) {
|
val testPoller = object : EventPollerImplementation(eventStore, queue, dispatcher) {
|
||||||
init {
|
init {
|
||||||
lastSeenTime = MyTime.UtcNow().plusSeconds(1)
|
lastSeenTime = MyTime.utcNow().plusSeconds(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,12 +8,13 @@ import no.iktdev.eventi.InMemoryEventStore
|
|||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import java.time.LocalDateTime
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import no.iktdev.eventi.MyTime
|
||||||
import no.iktdev.eventi.ZDS.toPersisted
|
import no.iktdev.eventi.ZDS.toPersisted
|
||||||
import no.iktdev.eventi.models.Event
|
import no.iktdev.eventi.models.Event
|
||||||
import no.iktdev.eventi.models.Metadata
|
import no.iktdev.eventi.models.Metadata
|
||||||
|
import java.time.Instant
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
|
||||||
@ -59,9 +60,9 @@ class TestEvent : Event() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class FakeClock(var now: LocalDateTime) {
|
class FakeClock(var now: Instant) {
|
||||||
fun advanceSeconds(sec: Long) {
|
fun advanceSeconds(sec: Long) {
|
||||||
now = now.plusSeconds(sec)
|
now = MyTime.utcNow().plusSeconds(sec)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +89,7 @@ class RunSimulationTestTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun persistEvent(ref: UUID, time: LocalDateTime) {
|
private fun persistEvent(ref: UUID, time: Instant) {
|
||||||
val e = TestEvent().withReference(ref)
|
val e = TestEvent().withReference(ref)
|
||||||
store.persist(e.setMetadata(Metadata()))
|
store.persist(e.setMetadata(Metadata()))
|
||||||
}
|
}
|
||||||
@ -96,14 +97,14 @@ class RunSimulationTestTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `poller updates lastSeenTime when dispatch happens`() = runTest(testDispatcher) {
|
fun `poller updates lastSeenTime when dispatch happens`() = runTest(testDispatcher) {
|
||||||
val ref = UUID.randomUUID()
|
val ref = UUID.randomUUID()
|
||||||
val t = LocalDateTime.of(2026, 1, 22, 12, 0, 0)
|
val t = Instant.parse("2026-01-22T12:00:00Z")
|
||||||
|
|
||||||
persistEvent(ref, t)
|
persistEvent(ref, t)
|
||||||
|
|
||||||
poller.pollOnce()
|
poller.pollOnce()
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
assertThat(poller.lastSeenTime).isAfter(LocalDateTime.of(1970,1,1,0,0))
|
assertThat(poller.lastSeenTime).isGreaterThan(Instant.EPOCH)
|
||||||
assertThat(dispatcher.dispatched).hasSize(1)
|
assertThat(dispatcher.dispatched).hasSize(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,7 +117,7 @@ class RunSimulationTestTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `poller DOES update lastSeenTime even when queue is busy`() = runTest {
|
fun `poller DOES update lastSeenTime even when queue is busy`() = runTest {
|
||||||
val ref = UUID.randomUUID()
|
val ref = UUID.randomUUID()
|
||||||
val t = LocalDateTime.of(2026,1,22,12,0,0)
|
val t = Instant.parse("2026-01-22T12:00:00Z")
|
||||||
|
|
||||||
store.persistAt(TestEvent().withReference(ref), t)
|
store.persistAt(TestEvent().withReference(ref), t)
|
||||||
|
|
||||||
@ -129,7 +130,7 @@ class RunSimulationTestTest {
|
|||||||
|
|
||||||
// Etter livelock-fixen skal lastSeenTime være *etter* eventet
|
// Etter livelock-fixen skal lastSeenTime være *etter* eventet
|
||||||
assertThat(poller.lastSeenTime)
|
assertThat(poller.lastSeenTime)
|
||||||
.isAfter(t)
|
.isGreaterThan(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -138,7 +139,7 @@ class RunSimulationTestTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `poller does not double-dispatch`() = runTest(testDispatcher) {
|
fun `poller does not double-dispatch`() = runTest(testDispatcher) {
|
||||||
val ref = UUID.randomUUID()
|
val ref = UUID.randomUUID()
|
||||||
val t = LocalDateTime.of(2026, 1, 22, 12, 0, 0)
|
val t = Instant.parse("2026-01-22T12:00:00Z")
|
||||||
|
|
||||||
persistEvent(ref, t)
|
persistEvent(ref, t)
|
||||||
|
|
||||||
@ -155,7 +156,7 @@ class RunSimulationTestTest {
|
|||||||
fun `poller handles multiple referenceIds`() = runTest(testDispatcher) {
|
fun `poller handles multiple referenceIds`() = runTest(testDispatcher) {
|
||||||
val refA = UUID.randomUUID()
|
val refA = UUID.randomUUID()
|
||||||
val refB = UUID.randomUUID()
|
val refB = UUID.randomUUID()
|
||||||
val t = LocalDateTime.of(2026, 1, 22, 12, 0, 0)
|
val t = Instant.parse("2026-01-22T12:00:00Z")
|
||||||
|
|
||||||
persistEvent(refA, t)
|
persistEvent(refA, t)
|
||||||
persistEvent(refB, t.plusSeconds(1))
|
persistEvent(refB, t.plusSeconds(1))
|
||||||
@ -170,7 +171,7 @@ class RunSimulationTestTest {
|
|||||||
fun `poller handles identical timestamps`() = runTest(testDispatcher) {
|
fun `poller handles identical timestamps`() = runTest(testDispatcher) {
|
||||||
val refA = UUID.randomUUID()
|
val refA = UUID.randomUUID()
|
||||||
val refB = UUID.randomUUID()
|
val refB = UUID.randomUUID()
|
||||||
val t = LocalDateTime.of(2026, 1, 22, 12, 0, 0)
|
val t = Instant.parse("2026-01-22T12:00:00Z")
|
||||||
|
|
||||||
persistEvent(refA, t)
|
persistEvent(refA, t)
|
||||||
persistEvent(refB, t)
|
persistEvent(refB, t)
|
||||||
@ -212,7 +213,7 @@ class RunSimulationTestTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `poller processes events arriving while queue is busy`() = runTest(testDispatcher) {
|
fun `poller processes events arriving while queue is busy`() = runTest(testDispatcher) {
|
||||||
val ref = UUID.randomUUID()
|
val ref = UUID.randomUUID()
|
||||||
val t1 = LocalDateTime.of(2026, 1, 22, 12, 0, 0)
|
val t1 = Instant.parse("2026-01-22T12:00:00Z")
|
||||||
val t2 = t1.plusSeconds(5)
|
val t2 = t1.plusSeconds(5)
|
||||||
|
|
||||||
persistEvent(ref, t1)
|
persistEvent(ref, t1)
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package no.iktdev.eventi.events.poller
|
|||||||
|
|
||||||
import kotlinx.coroutines.test.*
|
import kotlinx.coroutines.test.*
|
||||||
import no.iktdev.eventi.InMemoryEventStore
|
import no.iktdev.eventi.InMemoryEventStore
|
||||||
|
import no.iktdev.eventi.MyTime
|
||||||
import no.iktdev.eventi.TestBase
|
import no.iktdev.eventi.TestBase
|
||||||
import no.iktdev.eventi.events.EventDispatcher
|
import no.iktdev.eventi.events.EventDispatcher
|
||||||
import no.iktdev.eventi.events.EventTypeRegistry
|
import no.iktdev.eventi.events.EventTypeRegistry
|
||||||
@ -13,12 +14,13 @@ import no.iktdev.eventi.models.Event
|
|||||||
import no.iktdev.eventi.models.Metadata
|
import no.iktdev.eventi.models.Metadata
|
||||||
import no.iktdev.eventi.models.store.PersistedEvent
|
import no.iktdev.eventi.models.store.PersistedEvent
|
||||||
import no.iktdev.eventi.stores.EventStore
|
import no.iktdev.eventi.stores.EventStore
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.DisplayName
|
import org.junit.jupiter.api.DisplayName
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import java.time.LocalDateTime
|
import java.time.Instant
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
|
||||||
|
|
||||||
class PollerStartLoopTest: TestBase() {
|
class PollerStartLoopTest: TestBase() {
|
||||||
|
|
||||||
@ -29,8 +31,8 @@ class PollerStartLoopTest: TestBase() {
|
|||||||
private lateinit var queue: RunSimulationTestTest.ControlledDispatchQueue
|
private lateinit var queue: RunSimulationTestTest.ControlledDispatchQueue
|
||||||
private lateinit var poller: TestablePoller
|
private lateinit var poller: TestablePoller
|
||||||
|
|
||||||
private fun t(seconds: Long): LocalDateTime =
|
private fun t(seconds: Long): Instant =
|
||||||
LocalDateTime.of(2024, 1, 1, 12, 0).plusSeconds(seconds)
|
Instant.parse("2024-01-01T12:00:00Z").plusSeconds(seconds)
|
||||||
|
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
@ -45,7 +47,7 @@ class PollerStartLoopTest: TestBase() {
|
|||||||
poller = TestablePoller(store, queue, dispatcher, scope)
|
poller = TestablePoller(store, queue, dispatcher, scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun persistAt(ref: UUID, time: LocalDateTime) {
|
private fun persistAt(ref: UUID, time: Instant) {
|
||||||
val e = TestEvent().withReference(ref).setMetadata(Metadata())
|
val e = TestEvent().withReference(ref).setMetadata(Metadata())
|
||||||
store.persistAt(e, time)
|
store.persistAt(e, time)
|
||||||
}
|
}
|
||||||
@ -80,7 +82,7 @@ class PollerStartLoopTest: TestBase() {
|
|||||||
val before = poller.backoff
|
val before = poller.backoff
|
||||||
|
|
||||||
val ref = UUID.randomUUID()
|
val ref = UUID.randomUUID()
|
||||||
persistAt(ref, LocalDateTime.now())
|
persistAt(ref, MyTime.utcNow())
|
||||||
|
|
||||||
poller.startFor(iterations = 1)
|
poller.startFor(iterations = 1)
|
||||||
|
|
||||||
@ -93,7 +95,7 @@ class PollerStartLoopTest: TestBase() {
|
|||||||
|
|
||||||
poller.startFor(iterations = 3)
|
poller.startFor(iterations = 3)
|
||||||
|
|
||||||
persistAt(ref, LocalDateTime.now())
|
persistAt(ref, MyTime.utcNow())
|
||||||
|
|
||||||
poller.startFor(iterations = 1)
|
poller.startFor(iterations = 1)
|
||||||
|
|
||||||
@ -108,7 +110,7 @@ class PollerStartLoopTest: TestBase() {
|
|||||||
queue.busyRefs += ref
|
queue.busyRefs += ref
|
||||||
|
|
||||||
// Legg inn et event
|
// Legg inn et event
|
||||||
val t = LocalDateTime.now()
|
val t = MyTime.utcNow()
|
||||||
persistAt(ref, t)
|
persistAt(ref, t)
|
||||||
|
|
||||||
// Første poll: ingen dispatch fordi ref er busy
|
// Første poll: ingen dispatch fordi ref er busy
|
||||||
@ -152,7 +154,7 @@ class PollerStartLoopTest: TestBase() {
|
|||||||
|
|
||||||
queue.busyRefs += ref
|
queue.busyRefs += ref
|
||||||
|
|
||||||
val t1 = LocalDateTime.now()
|
val t1 = MyTime.utcNow()
|
||||||
persistAt(ref, t1)
|
persistAt(ref, t1)
|
||||||
|
|
||||||
poller.startFor(iterations = 1)
|
poller.startFor(iterations = 1)
|
||||||
@ -223,7 +225,7 @@ class PollerStartLoopTest: TestBase() {
|
|||||||
assertThat(poller.watermarkFor(refA)).isEqualTo(wmA1)
|
assertThat(poller.watermarkFor(refA)).isEqualTo(wmA1)
|
||||||
|
|
||||||
// B skal ha flyttet watermark
|
// B skal ha flyttet watermark
|
||||||
assertThat(poller.watermarkFor(refB)).isAfter(wmB1)
|
assertThat(poller.watermarkFor(refB)).isGreaterThan(wmB1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@DisplayName("🍌 Bananastesten™ — stress-test av watermark, busy refs og dispatch-semantikk")
|
@DisplayName("🍌 Bananastesten™ — stress-test av watermark, busy refs og dispatch-semantikk")
|
||||||
@ -346,7 +348,7 @@ class PollerStartLoopTest: TestBase() {
|
|||||||
|
|
||||||
// Fake EventStore som alltid returnerer samme event
|
// Fake EventStore som alltid returnerer samme event
|
||||||
val fakeStore = object : EventStore {
|
val fakeStore = object : EventStore {
|
||||||
override fun getPersistedEventsAfter(ts: LocalDateTime): List<PersistedEvent> {
|
override fun getPersistedEventsAfter(ts: Instant): List<PersistedEvent> {
|
||||||
// Alltid returner én event som ligger før watermark
|
// Alltid returner én event som ligger før watermark
|
||||||
return listOf(
|
return listOf(
|
||||||
PersistedEvent(
|
PersistedEvent(
|
||||||
@ -392,8 +394,8 @@ class PollerStartLoopTest: TestBase() {
|
|||||||
poller.pollOnce()
|
poller.pollOnce()
|
||||||
|
|
||||||
// Fixen skal flytte lastSeenTime forbi eventen
|
// Fixen skal flytte lastSeenTime forbi eventen
|
||||||
assertThat(poller.lastSeenTime)
|
assertThat<Instant>(poller.lastSeenTime)
|
||||||
.isAfter(t(50))
|
.isGreaterThan(t(50))
|
||||||
|
|
||||||
// Andre poll: nå skal polleren IKKE spinne
|
// Andre poll: nå skal polleren IKKE spinne
|
||||||
val before = poller.lastSeenTime
|
val before = poller.lastSeenTime
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import no.iktdev.eventi.events.EventDispatcher
|
|||||||
import no.iktdev.eventi.events.EventPollerImplementation
|
import no.iktdev.eventi.events.EventPollerImplementation
|
||||||
import no.iktdev.eventi.events.SequenceDispatchQueue
|
import no.iktdev.eventi.events.SequenceDispatchQueue
|
||||||
import no.iktdev.eventi.stores.EventStore
|
import no.iktdev.eventi.stores.EventStore
|
||||||
import java.time.LocalDateTime
|
import java.time.Instant
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class TestablePoller(
|
class TestablePoller(
|
||||||
@ -31,19 +31,19 @@ class TestablePoller(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun watermarkFor(ref: UUID): LocalDateTime? {
|
override fun watermarkFor(ref: UUID): Instant? {
|
||||||
return refWatermark[ref]?.let {
|
return refWatermark[ref]?.let {
|
||||||
return it
|
return it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setWatermarkFor(ref: UUID, time: LocalDateTime) {
|
override fun setWatermarkFor(ref: UUID, time: Instant) {
|
||||||
refWatermark[ref] = time
|
refWatermark[ref] = time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
interface WatermarkDebugView {
|
interface WatermarkDebugView {
|
||||||
fun watermarkFor(ref: UUID): LocalDateTime?
|
fun watermarkFor(ref: UUID): Instant?
|
||||||
fun setWatermarkFor(ref: UUID, time: LocalDateTime)
|
fun setWatermarkFor(ref: UUID, time: Instant)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user