Collect fix 2

This commit is contained in:
Brage Skjønborg 2026-02-02 04:36:38 +01:00
parent 1116f80066
commit b8562f48e7
5 changed files with 200 additions and 65 deletions

View File

@ -1,30 +1,33 @@
package no.iktdev.mediaprocessing.coordinator.listeners.events
import mu.KotlinLogging
import no.iktdev.eventi.events.EventListener
import no.iktdev.eventi.models.Event
import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.CollectedEvent
import no.iktdev.mediaprocessing.shared.common.listeners.SummaryEventListener
import no.iktdev.mediaprocessing.shared.common.projection.CollectProjection
import no.iktdev.mediaprocessing.shared.database.stores.EventStore
import org.springframework.stereotype.Component
@Component
class CollectEventsListener : EventListener() {
class CollectEventsListener(eventStore: no.iktdev.eventi.stores.EventStore = EventStore) : SummaryEventListener(eventStore) {
private val log = KotlinLogging.logger {}
override fun shouldSummarize(fullHistory: List<Event>): Boolean {
val projection = CollectProjection(fullHistory)
if (projection.startedWith == null) return false
if (!projection.isWorkflowComplete()) return false
return true
}
override fun onEvent(event: Event, history: List<Event>): Event? {
// Avoid double-collection
if (event is CollectedEvent || history.any { it is CollectedEvent }) return null
val projection = CollectProjection(history)
// Must have a StartProcessingEvent
if (projection.startedWith == null) return null
override fun produceSummary(fullHistory: List<Event>): Event {
// Must have all relevant tasks completed
if (!projection.isWorkflowComplete()) return null
val eventIds = fullHistory.map { it.eventId }.toSet()
return CollectedEvent(history.map { it.eventId }.toSet()).derivedOf(event)
return CollectedEvent(eventIds).derivedOf(fullHistory.last())
}
override fun summaryAlreadyExists(fullHistory: List<Event>): Boolean {
return fullHistory.any { it is CollectedEvent }
}
}

View File

@ -1,14 +1,17 @@
package no.iktdev.mediaprocessing
import io.mockk.*
import no.iktdev.eventi.events.EventTypeRegistry
import no.iktdev.eventi.models.Event
import no.iktdev.eventi.models.Task
import no.iktdev.mediaprocessing.coordinator.*
import no.iktdev.mediaprocessing.ffmpeg.dsl.AudioCodec
import no.iktdev.mediaprocessing.ffmpeg.dsl.VideoCodec
import no.iktdev.mediaprocessing.shared.common.event_task_contract.EventRegistry
import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.OperationType
import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.StartData
import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.StartProcessingEvent
import no.iktdev.mediaprocessing.shared.database.InMemoryEventStore
import no.iktdev.mediaprocessing.shared.database.stores.TaskStore
import org.junit.jupiter.api.BeforeEach
@ -16,6 +19,9 @@ import java.io.File
import java.util.*
open class TestBase {
val eventStore = InMemoryEventStore()
class DummyEvent: Event()
class DummyTask: Task()
@ -35,14 +41,14 @@ open class TestBase {
every { coordinatorEnv.incomingContent } returns File("./tmp/input")
every { coordinatorEnv.cachedContent } returns File("./tmp/cached")
every { coordinatorEnv.streamitAddress } returns "http://streamit.lan"
EventRegistry.getEvents().let {
EventTypeRegistry.register(it)
}
eventStore.clear()
}
fun mockkIO() {
mockkConstructor(File::class)
every { anyConstructed<File>().exists() } returns true
}
fun defaultStartEvent(): StartProcessingEvent {
val start = StartProcessingEvent(
data = StartData(

View File

@ -17,13 +17,13 @@ import org.junit.jupiter.api.Test
class CollectEventsListenerTest : TestBase() {
private val listener = CollectEventsListener(eventStore)
private val listener = CollectEventsListener()
@Test
@DisplayName(
"""
Hvis historikken har alle påkrevde hendelser og alle oppgaver er i en gyldig tisltand
Hvis historikken har alle påkrevde hendelser og alle oppgaver er i en gyldig tilstand
Når onEvent kalles og projeksjonen tilsier gyldig status
:
Opprettes CollectEvent basert historikken
@ -54,23 +54,21 @@ class CollectEventsListenerTest : TestBase() {
*convert.toTypedArray(),
*cover.toTypedArray(),
)
eventStore.setHistory(history)
val result = listener.onEvent(history.last(), history)
assertThat(result).isNotNull()
assertThat {
result is CollectedEvent
}
assertThat(result).isInstanceOf(CollectedEvent::class.java)
}
@Test
@DisplayName(
"""
Hvis vi har kun encoded hendelse, men vi har sagt at vi også skal ha extract, men ikke har opprettet extract
Når encode result kommer inn
:
Opprettes CollectEvent basert historikken
Opprettes ikke CollectEvent
"""
)
fun success2() {
@ -101,6 +99,7 @@ class CollectEventsListenerTest : TestBase() {
metadata,
*encode.toTypedArray(),
)
eventStore.setHistory(history)
val result = listener.onEvent(history.last(), history)
@ -108,7 +107,6 @@ class CollectEventsListenerTest : TestBase() {
}
@Test
@DisplayName(
"""
@ -150,6 +148,7 @@ class CollectEventsListenerTest : TestBase() {
*metadata.toTypedArray(),
*convert.toTypedArray(),
)
eventStore.setHistory(history)
val result = listener.onEvent(history.last(), history)
@ -165,7 +164,7 @@ class CollectEventsListenerTest : TestBase() {
Når extract result kommer inn
:
Skal vi si pending convert
Listener skal returnerere null
Listener skal returnere null
"""
)
fun failure1() {
@ -186,6 +185,8 @@ class CollectEventsListenerTest : TestBase() {
*encode.toTypedArray(),
*extract.toTypedArray(),
)
eventStore.setHistory(history)
val result = listener.onEvent(history.last(), history)
assertThat(result).isNull()
}
@ -196,7 +197,7 @@ class CollectEventsListenerTest : TestBase() {
Hvis historikken har alle påkrevde media hendelser, men venter metadata
Når onEvent kalles og projeksjonen tilsier ugyldig tilstand
:
Returerer vi failure
Returnerer vi null
"""
)
fun failure2() {
@ -221,20 +222,20 @@ class CollectEventsListenerTest : TestBase() {
*extract.toTypedArray(),
*convert.toTypedArray(),
)
eventStore.setHistory(history)
val result = listener.onEvent(history.last(), history)
assertThat(result).isNull()
}
@Test
@DisplayName(
"""
Hvis historikken har alle påkrevde hendelser og encode feilet
Når onEvent kalles og projeksjonen tilsier ugyldig tilstand
:
Collect feiler
Collect returnerer null
"""
)
fun failure3() {
@ -262,11 +263,14 @@ class CollectEventsListenerTest : TestBase() {
*convert.toTypedArray(),
*cover.toTypedArray(),
)
eventStore.setHistory(history)
val result = listener.onEvent(history.last(), history)
assertThat(result).isNull()
}
@Test
@DisplayName(
"""
@ -287,17 +291,76 @@ class CollectEventsListenerTest : TestBase() {
mediaType = MediaType.Movie
).derivedOf(started)
val history = listOf(
started,
parsed,
)
eventStore.setHistory(history)
val result = listener.onEvent(history.last(), history)
assertThat(result).isNull()
}
@Test
@DisplayName(
"""
Summarizer skal være idempotent:
- Første kjøring skal produsere CollectedEvent
- Andre kjøring skal returnere null
- Re-feed skal ikke produsere flere CollectedEvent
"""
)
fun summarizerDoesNotGoHaywire() {
val started = defaultStartEvent()
val parsed = mediaParsedEvent(
collection = "MyCollection",
fileName = "MyCollection 1",
mediaType = MediaType.Movie
).derivedOf(started)
val metadata = metadataEvent(parsed)
val encode = encodeEvent("/tmp/video.mp4", parsed)
val extract = extractEvent("en", "/tmp/sub1.srt", encode.last())
val convert = convertEvent(
language = "en",
baseName = "sub1",
outputFiles = listOf("/tmp/sub1.vtt"),
derivedFrom = extract.last()
)
val cover = coverEvent("/tmp/cover.jpg", metadata.last())
val history = listOf(
started,
parsed,
*metadata.toTypedArray(),
*encode.toTypedArray(),
*extract.toTypedArray(),
*convert.toTypedArray(),
*cover.toTypedArray(),
)
// Gi summarizeren full historikk
eventStore.setHistory(history)
// Første kjøring: skal produsere CollectedEvent
val first = listener.onEvent(history.last(), history)
assertThat(first).isInstanceOf(CollectedEvent::class.java)
// Simuler at summarizer-eventet ble lagret i historikken
val collected = first as CollectedEvent
val newHistory = history + collected
eventStore.setHistory(newHistory)
// Andre kjøring: summarizer skal se at CollectedEvent finnes → returnere null
val second = listener.onEvent(collected, newHistory)
assertThat(second).isNull()
// Tredje kjøring: re-feed av siste event → fortsatt null
val third = listener.onEvent(collected, newHistory)
assertThat(third).isNull()
}
}

View File

@ -0,0 +1,26 @@
package no.iktdev.mediaprocessing.shared.common.listeners
import no.iktdev.eventi.ZDS.toEvent
import no.iktdev.eventi.events.EventListener
import no.iktdev.eventi.models.Event
import no.iktdev.eventi.stores.EventStore
abstract class SummaryEventListener(
private val eventStore: EventStore
) : EventListener() {
final override fun onEvent(event: Event, history: List<Event>): Event? {
val fullHistory = eventStore.getPersistedEventsFor(event.referenceId)
val events = fullHistory.map { it.toEvent() }.filterNotNull()
if (!shouldSummarize(events)) return null
if (summaryAlreadyExists(events)) return null
return produceSummary(events).derivedOf(event)
}
abstract fun shouldSummarize(fullHistory: List<Event>): Boolean
abstract fun produceSummary(fullHistory: List<Event>): Event
abstract fun summaryAlreadyExists(fullHistory: List<Event>): Boolean
}

View File

@ -0,0 +1,37 @@
package no.iktdev.mediaprocessing.shared.database
import no.iktdev.eventi.MyTime
import no.iktdev.eventi.ZDS.toPersisted
import no.iktdev.eventi.models.Event
import no.iktdev.eventi.models.store.PersistedEvent
import no.iktdev.eventi.stores.EventStore
import java.time.Instant
import java.util.*
class InMemoryEventStore : EventStore {
private val persisted = mutableListOf<PersistedEvent>()
private var nextId = 1L
override fun getPersistedEventsAfter(timestamp: Instant): List<PersistedEvent> =
persisted.filter { it.persistedAt > timestamp }
override fun getPersistedEventsFor(referenceId: UUID): List<PersistedEvent> =
persisted.filter { it.referenceId == referenceId }
override fun persist(event: Event) {
val persistedEvent = event.toPersisted(nextId++, MyTime.utcNow())
persisted += persistedEvent!!
}
fun persistAt(event: Event, persistedAt: Instant) {
val persistedEvent = event.toPersisted(nextId++, persistedAt)
persisted += persistedEvent!!
}
fun setHistory(events: List<Event>) {
events.forEach { persist(it) }
}
fun all(): List<PersistedEvent> = persisted
fun clear() { persisted.clear(); nextId = 1L }
}