refactor(event-store): split into EventStore (read-only) + MutableEventStore
ci / JVM build + tests (push) Failing after 1m49s
ci / JVM build + tests (push) Failing after 1m49s
Разделяет интерфейс на read-only (EventStore) и write (MutableEventStore).
EventStore (read-only, для consumer'ов):
- events(after: Instant?): Flow<CommonEvent>
- earliestEventDate(): Instant
- close()
MutableEventStore : EventStore (для producer'ов):
- + append(event: CommonEvent)
- suspend, не идемпотентный, может быть silently evicted
Зачем:
- Consumer'ы (server SSE, admin dashboard, parent agents) принимают
EventStore — compile-time гарантия что они не могут писать в store.
- Producer'ы (ChatAgent, sub-agents, A2A-bridge) принимают MutableEventStore.
- Тесты могут использовать EventStore без опасности случайной модификации.
Миграция:
- :event-store пока без implementations, поэтому ничего не сломалось.
- Когда добавим InMemoryEventStore — он будет реализовывать оба
(MutableEventStore = EventStore + append). Подписки получают только
read-only projection через приведение типа.
Также: импорт обновлён AllEvent → CommonEvent (по rename в :proto).
This commit is contained in:
@@ -2,7 +2,7 @@ package pw.binom.agentik.eventStore
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlin.time.Instant
|
||||
import pw.binom.agentik.proto.AllEvent
|
||||
import pw.binom.agentik.proto.CommonEvent
|
||||
|
||||
/**
|
||||
* Bounded-tail event log с автоматическим управлением TTL.
|
||||
@@ -30,11 +30,8 @@ import pw.binom.agentik.proto.AllEvent
|
||||
* - Позволяет impl выбирать retention strategy (TTL, size cap, sliding window).
|
||||
* - Сохраняет контракт clean: интерфейс только о put/get.
|
||||
*
|
||||
* **Append is NOT idempotent**: [AllEvent] не имеет уникального id,
|
||||
* поэтому retry с тем же logical event приведёт к дубликату в tail'е.
|
||||
* Для at-least-once → exactly-once нужна дедупликация на стороне
|
||||
* consumer'а (catchup через `:message-store-api` audit log имеет
|
||||
* монотонный `id` и работает как dedup anchor).
|
||||
* **Read-only**: этот интерфейс предоставляет только read-операции.
|
||||
* Для записи см. [MutableEventStore].
|
||||
*
|
||||
* **Подписки нереентрантные**: каждый вызов [events] создаёт **новую
|
||||
* подписку** (cold Flow). Один [events] НЕ видит события, добавленные до
|
||||
@@ -46,18 +43,6 @@ import pw.binom.agentik.proto.AllEvent
|
||||
*/
|
||||
interface EventStore : AutoCloseable {
|
||||
|
||||
/**
|
||||
* Положить event в log.
|
||||
*
|
||||
* - **Идемпотентно по [AllEvent.date]**: повторный append с тем же id — no-op.
|
||||
* - **Silently evicted**: implementation может выкинуть этот event сразу
|
||||
* после append (TTL/cap) без уведомления producer'а. Producer **не
|
||||
* должен** полагаться на то, что event дойдёт до клиента, если он
|
||||
* вне retention window.
|
||||
* - **Suspend**: для KMP I/O impl'ов (SQLite через JNI).
|
||||
*/
|
||||
suspend fun append(event: AllEvent)
|
||||
|
||||
/**
|
||||
* Subscribe на events.
|
||||
*
|
||||
@@ -78,7 +63,7 @@ interface EventStore : AutoCloseable {
|
||||
* полноты клиент обязан cross-check с [earliestEventDate] и fallback
|
||||
* в message store при gap'е (см. KDoc интерфейса).
|
||||
*/
|
||||
fun events(after: Instant?): Flow<AllEvent>
|
||||
fun events(after: Instant?): Flow<CommonEvent>
|
||||
|
||||
/**
|
||||
* Date **стартовой точки** буфера.
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package pw.binom.agentik.eventStore
|
||||
|
||||
import pw.binom.agentik.proto.CommonEvent
|
||||
|
||||
/**
|
||||
* Mutable вариант [EventStore] — добавляет producer-операцию [append].
|
||||
*
|
||||
* Этот интерфейс предназначен **только для producer'ов** (ChatAgent,
|
||||
* sub-agents, A2A-bridge). Consumer'ы (server SSE endpoints, admin
|
||||
* dashboards, parent agents) должны принимать **read-only** [EventStore]
|
||||
* — тогда невозможно случайно писать в store из observer'а.
|
||||
*
|
||||
* Типичное использование:
|
||||
* ```
|
||||
* // Producer
|
||||
* class ChatAgent(private val events: MutableEventStore) {
|
||||
* suspend fun doSomething() {
|
||||
* events.append(CommonEvent.Agent(date = now, event = AgentEvent.Created(...)))
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* // Consumer
|
||||
* class EventStreamEndpoint(private val events: EventStore) {
|
||||
* fun stream() = events.events(after = null)
|
||||
* // Ошибка компиляции если раскомментировать:
|
||||
* // events.append(...) // ← нельзя, MutableEventStore нет в типе
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* **Append НЕ идемпотентен**: [CommonEvent] не имеет уникального id,
|
||||
* поэтому retry с тем же logical event (например, после network failure
|
||||
* между producer и store) приведёт к дубликату в tail'е. Это OK для
|
||||
* use case'a bounded-tail — клиент, делающий catchup через [events](after),
|
||||
* получит свой диапазон ровно один раз при подключении, а последующие
|
||||
* retry producer'а просто насытят tail повторами, не задевая уже
|
||||
* обработанные. Для гарантированной exactly-once — dedup через
|
||||
* [message-store] (там есть монотонный `id`).
|
||||
*
|
||||
* **Silently evicted**: implementation может выкинуть этот event сразу
|
||||
* после append (TTL/cap) без уведомления producer'а. Producer **не
|
||||
* должен** полагаться на то, что event дойдёт до клиента, если он
|
||||
* вне retention window.
|
||||
*/
|
||||
interface MutableEventStore : EventStore {
|
||||
/**
|
||||
* Положить event в log.
|
||||
*
|
||||
* - **Не идемпотентно** — см. KDoc интерфейса.
|
||||
* - **Suspend** для KMP I/O impl'ов (SQLite через JNI).
|
||||
*/
|
||||
suspend fun append(event: CommonEvent)
|
||||
}
|
||||
Reference in New Issue
Block a user