PostgresEventLog

class PostgresEventLog(dataSource: DataSource, context: String, val partitions: Int, dispatcher: CoroutineDispatcher = VirtualThreads.dispatcher, pollInterval: Duration = DEFAULT_POLL_INTERVAL, tailBatch: Int = DEFAULT_TAIL_BATCH, maxPollInterval: Duration = DEFAULT_MAX_POLL_INTERVAL, ddl: Ddl = Ddl.AUTO) : EventLog, AccessWalks

The EventLog of one bounded context on PostgreSQL (documentation/designs/03-log-and-snapshot.md §2).

The log takes one table per aggregate type, wow_log__<context>__<aggregate>; a context-level table holds one counter row per partition, so partitions, their sequences, leases and checkpoints stay per context. A counter row also holds its partition's lineage, parent (null) and epoch (0): format 1 reserves them for the online splits of 10.x, and lineage reads them back. An append is one autocommitted statement (see AggregateLogTable.append): it bumps its partition's counter, which locks the row until the statement commits, and inserts the stream into its aggregate's table at that sequence only if the aggregate is exactly one version behind. Because the lock is held until commit, the sequence order is the order commits become visible, whichever table they land in; a failed append rolls its sequence back too, so a partition's sequences have no gaps. A group commit (appendAll) is the same statement for many streams: one round trip per group. The transport is stored in its own bytea column as the bytes the runtime encoded, never read, and given back only by tail and readEntries.

Aggregate tables. As the snapshot tables (me.ahoo.wow.postgres.snapshot.PostgresSnapshotStore), an aggregate type's table is created the first time this log touches the type, or up front by createTable; a new aggregate type needs no restart. Every aggregate table is recorded in a registry table (PostgresEventLogTables.REGISTRY) before its first entry is written, and tail reads the context's tables from it: a node tailing a partition finds the tables other nodes created, whether or not it knows their aggregate types.

Tail. tail merges the partition's entries of every aggregate table by sequence, in one statement and so one snapshot, which also reads the partition's counter. It emits only a contiguous run from the last sequence: entries the snapshot should hold but does not (a table registered after this log last read the registry) make it read the registry again and retry, and it never steps over a missing sequence.

Request ids. The request key is (tenant_id, request_id) within each aggregate table: a request id reused by two aggregate types commits twice (the reuse is the caller's error). The kernel's pre-check, the findByRequestId taking the aggregate type, reads that type's table alone.

Polling. A tail polls every pollInterval (50 ms by default) while it finds entries; each poll that finds none doubles the wait, up to maxPollInterval (1 s), so an idle cluster's partitions cost a statement a second each rather than twenty. A commit this log makes wakes the partition's tails at once, and a poll it woke that finds the partition caught up keeps the backoff, so a commit costs one read of the tail; an entry another node appends is read within maxPollInterval. LISTEN/NOTIFY would wake the tails of every node; it is not implemented: the bounded backoff and the wake on this log's commits keep an idle cluster cheap without a listening connection per node.

JDBC calls run on dispatcher, virtual threads by default (JDK 21).

Constructors

Link copied to clipboard
constructor(dataSource: DataSource, context: String, partitions: Int, dispatcher: CoroutineDispatcher = VirtualThreads.dispatcher, pollInterval: Duration = DEFAULT_POLL_INTERVAL, tailBatch: Int = DEFAULT_TAIL_BATCH, maxPollInterval: Duration = DEFAULT_MAX_POLL_INTERVAL, ddl: Ddl = Ddl.AUTO)

Properties

Link copied to clipboard
open override val appendsAcrossPartitions: Boolean

A group may hold streams of several partitions.

Link copied to clipboard
open override val partitions: Int

Functions

Link copied to clipboard
open suspend override fun advance(type: AggregateType, schema: String, cursor: String?, walked: Long, completed: Boolean): Boolean
Link copied to clipboard
open suspend override fun aggregates(type: AggregateType, after: String?, limit: Int): AggregatePage
Link copied to clipboard
open suspend override fun append(stream: EventStreamRecord, transport: ByteArray?): AppendOutcome
Link copied to clipboard
open suspend override fun appendAll(streams: List<Pair<EventStreamRecord, ByteArray?>>): List<AppendOutcome>
Link copied to clipboard
open suspend override fun claim(type: AggregateType, walk: AccessWalk, expected: String?): Boolean
Link copied to clipboard

Creates the partition counters and the registry when missing; refuses a different partition count, and a log recorded in another format version (PostgresFormat). The aggregate tables are created on first use, or by createTable.

Link copied to clipboard
fun createTable(aggregateName: String)

Creates and registers the log table of aggregateName when missing, once per log; refuses a table recorded in another format version (PostgresFormat), and an aggregate whose table name another aggregate has.

Link copied to clipboard
open suspend override fun findByRequestId(type: AggregateType, tenantId: String, requestId: String): EventStreamRecord?
Link copied to clipboard
open suspend override fun head(partition: Int): Long?
Link copied to clipboard
open suspend override fun holds(type: AggregateType): Boolean
Link copied to clipboard
open suspend override fun lineage(partition: Int): PartitionLineage
Link copied to clipboard
open suspend override fun positionOf(ref: AggregateRef, version: Int): LogPosition?
Link copied to clipboard
open override fun read(ref: AggregateRef, fromVersion: Int, toVersion: Int): Flow<EventStreamRecord>
Link copied to clipboard
open override fun readEntries(ref: AggregateRef, fromVersion: Int, toVersion: Int): Flow<LogEntry>
Link copied to clipboard
open suspend override fun readTail(ref: AggregateRef, fromVersion: Int, earlier: Int): AggregateTail
Link copied to clipboard
open suspend override fun stamp(ref: AggregateRef, access: Map<String, Any?>, asOf: Int, from: Int, until: Int): Int
Link copied to clipboard
open suspend override fun stampAll(stamps: List<AccessStamp>): Int

One statement of the stamps of distinct streams (PostgresEventLogTables stampRows), autocommitted, per aggregate type; stamps that overlap are written one at a time, so the later version of two wins.

Link copied to clipboard
open override fun tail(partition: Int, after: Long?): Flow<LogEntry>
Link copied to clipboard
open suspend override fun walk(type: AggregateType): AccessWalk?