readview

package module
v0.6.12 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 3, 2026 License: MIT Imports: 18 Imported by: 0

README

eventsourced Readview handler

GoReportCard GoDoc Build Statuscoverage report

Package readview provides materialized view projections built from event streams for use with the eventsourced framework.

Install

go get gitlab.com/unboundsoftware/eventsourced/readview

Prerequisites

Getting Started

Define an event handler and a reset handler, then create and start the readview:

import (
    "context"
    "database/sql"

    "github.com/jmoiron/sqlx"

    "gitlab.com/unboundsoftware/eventsourced/eventsourced"
    "gitlab.com/unboundsoftware/eventsourced/readview"
)

// Event handler: update read model within a transaction
func userEventHandler(ctx context.Context, tx *sql.Tx, event eventsourced.Event) error {
    switch e := event.(type) {
    case *UserCreated:
        _, err := tx.ExecContext(ctx,
            "INSERT INTO users (id, name) VALUES ($1, $2)",
            e.AggregateIdentity(), e.Name,
        )
        return err
    }
    return nil
}

// Reset handler: clear data before a full rebuild
func userResetHandler(ctx context.Context, name string, tx *sqlx.Tx) error {
    _, err := tx.ExecContext(ctx, "TRUNCATE TABLE users")
    return err
}

// Create and start readview
db := sqlx.NewDb(sqlDB, "postgres")
rv, err := readview.New(db, store,
    readview.Register("user_list", userEventHandler, userResetHandler),
)
if err != nil {
    log.Fatal(err)
}

if err := rv.Start(ctx); err != nil {
    log.Fatal(err)
}

Backfill

On startup, Start checks each readview's current position against the event store's maximum global sequence number. If behind, it replays historical events through the event handler.

Progress is committed in batches (configurable via WithBackFillBatchSize) and tracked with heartbeat timestamps to detect stale backfills.

AMQP Integration

After backfill completes, wire up the readview handlers with AMQP for live event processing:

handlers := rv.Handlers()

// Regular events (filters out external source events)
conn.Handle("user_list", rv.RegularMappingHandler(conn, "user_list", handlers["user_list"]))

// External events (no filtering)
conn.Handle("external_list", rv.ExternalMappingHandler(conn, handlers["external_list"]))

Distributed Coordination

Multiple application instances coordinate via PostgreSQL row-level locks (FOR UPDATE NOWAIT). Only one instance can backfill a given readview at a time. Others wait and retry.

The heartbeat mechanism detects stale backfills: if an instance crashes during backfill, another instance can take over after the heartbeat duration expires.

Resetting a Readview

Call Reset to clear a readview's data and re-backfill from scratch:

err := rv.Reset(ctx, "user_list")

This calls the registered ResetHandler, deletes the status row, and initiates a full backfill.

Database Schema

The readview creates and manages a readview_status table (migrations tracked in goose_db_version_readview):

Column Type Description
name text (PK) Readview name
current_seq_no int Last processed global sequence number
filler text Hostname of the instance performing backfill
started timestamp When the backfill started
updated timestamp Last heartbeat update

Configuration Options

Option Default Description
Register(name, handler, resetHandler) - Register a named readview (required, repeatable)
WithLogger(logger) slog.Default() Set custom logger
WithBackFillBatchSize(size) 10 Events per batch commit during backfill
WithHeartbeat(duration) 5s Stale backfill detection threshold

Error Handling

Error Condition
ErrStatusRowLocked Another instance holds the backfill lock
ErrNotReadyToProcessEvents Backfill not yet complete; live events are deferred

Documentation

Overview

Package readview provides materialized view projections built from event streams for use with the eventsourced framework.

A readview subscribes to events (either via backfill from the event store or live via AMQP) and maintains a denormalized read model in PostgreSQL. This implements the CQRS read side of an event-sourced system.

Usage

rv, err := readview.New(db, store,
    readview.Register("user_list", userEventHandler, userResetHandler),
    readview.WithBackFillBatchSize(100),
)
if err != nil {
    log.Fatal(err)
}

// Start backfill (blocks until complete)
if err := rv.Start(ctx); err != nil {
    log.Fatal(err)
}

// Wire up AMQP handlers for live events
for name, handler := range rv.Handlers() {
    conn.Handle(name, handler)
}

Backfill

On startup, each readview checks its current position against the event store's maximum global sequence number. If behind, it initiates a backfill that replays historical events through the event handler. Progress is committed in batches (configurable via WithBackFillBatchSize) and tracked via heartbeat timestamps.

Distributed Coordination

Multiple application instances coordinate via PostgreSQL row-level locks on the readview_status table. Only one instance can backfill a given readview at a time. Other instances wait and retry until the lock is released or the heartbeat expires.

Configuration

  • Register: Register a named readview with its event and reset handlers
  • WithLogger: Set a custom logger (default: slog.Default())
  • WithBackFillBatchSize: Set batch commit size during backfill (default: 10)
  • WithHeartbeat: Set heartbeat duration for stale backfill detection (default: 5s)

Index

Constants

This section is empty.

Variables

View Source
var ErrNotReadyToProcessEvents = fmt.Errorf("not ready to process events")
View Source
var ErrStatusRowLocked = errors.New("status row locked")

Functions

This section is empty.

Types

type EventHandler

type EventHandler func(ctx context.Context, tx *sql.Tx, event eventsourced.Event) error

EventHandler is a function that processes an event within a database transaction to update a read model. It receives the transaction to ensure atomicity with the readview status update.

type Logger

type Logger interface {
	Info(msg string, args ...any)
	Warn(msg string, args ...any)
	Error(msg string, args ...any)
}

Logger defines the logging interface used by readview for operational messages.

type MessageHandler

type MessageHandler func(msg any) (any, error)

MessageHandler is a function that handles incoming messages from AMQP. It wraps the event processing with readview coordination logic.

type Opt

type Opt func(r *Readview) error

Opt is a functional option for configuring a Readview.

func Register

func Register(name string, handler EventHandler, resetHandler ResetHandler) Opt

Register registers a named readview with its event handler and reset handler. The name must be unique across all registered readviews. The event handler processes each event, and the reset handler clears data before a full rebuild.

func WithBackFillBatchSize

func WithBackFillBatchSize(size int) Opt

WithBackFillBatchSize sets the number of events to process before committing during backfill. Smaller batches commit more frequently (safer for restarts), while larger batches are faster. Default: 10.

func WithHeartbeat

func WithHeartbeat(heartbeat time.Duration) Opt

WithHeartbeat sets the duration used to determine if a backfill is still active. If the last heartbeat update is older than this duration, the backfill is considered stale and another instance can take over. Default: 5s.

func WithLogger

func WithLogger(logger Logger) Opt

WithLogger sets the logger used by readview for operational messages. Defaults to slog.Default().

type Readview

type Readview struct {
	// contains filtered or unexported fields
}

Readview manages materialized view projections built from event streams. It coordinates backfilling historical events, processing live events, and distributed locking to ensure only one instance processes events at a time.

func New

func New(db *sqlx.DB, store eventsourced.EventStore, opts ...Opt) (*Readview, error)

New creates a Readview instance with the given database connection and event store. It runs schema migrations automatically and initializes all registered views. Default values: backfill batch size = 10, heartbeat = 5s, logger = slog.Default().

func (*Readview) ExternalMappingHandler

func (r *Readview) ExternalMappingHandler(typeMapper goamqp.TypeMapper, messageHandler MessageHandler) messaging.EventHandler[any]

ExternalMappingHandler creates an event handler for external events. Unlike RegularMappingHandler, it does not filter by event source, passing all messages to the readview's message handler.

func (*Readview) Handlers

func (r *Readview) Handlers() map[string]MessageHandler

Handlers returns a map of readview names to their MessageHandler functions. Use these handlers to wire up AMQP message consumption.

func (*Readview) RegularMappingHandler

func (r *Readview) RegularMappingHandler(name string, typeMapper goamqp.TypeMapper, messageHandler MessageHandler) messaging.EventHandler[any]

RegularMappingHandler creates an event handler for regular (non-external) events. It filters out non-Event messages and ExternalSource events, passing only regular events to the readview's message handler. The TypeMapper resolves each message's routing key to its concrete Go type before the handler runs.

func (*Readview) Reset

func (r *Readview) Reset(ctx context.Context, name string) error

Reset clears a readview's data using its ResetHandler and re-initiates a full backfill from sequence 0. Returns ErrStatusRowLocked if another instance is currently backfilling.

func (*Readview) Start

func (r *Readview) Start(ctx context.Context) error

Start initiates the backfill process for all registered readviews. It checks the current sequence number against the maximum global sequence and backfills any missing events. Multiple instances coordinate via database row locks; instances that cannot acquire the lock will wait and retry.

type ResetHandler

type ResetHandler func(ctx context.Context, name string, tx *sqlx.Tx) error

ResetHandler is called to clear and rebuild a readview's data. It receives the readview name and a transaction for atomic cleanup operations.

type ViewInfo

type ViewInfo struct {
	// contains filtered or unexported fields
}

ViewInfo holds the handlers and state for a registered readview.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL